// Vrai-Prix — méthode du coût : 03 Coût à neuf (tableau par catégorie, indirects, FG/profit/contingence, RCN, fourchette), // 04 Assemblages (lignes + surcharges + détail du calcul), 05 Main-d'œuvre. "use client"; import { useState } from "react"; import Link from "next/link"; import type { CostEstimate, CostInput, CostParams, EstimateLine, IndirectParams } from "@/lib/cost/types"; import { INDIRECT_LABELS, categoryLabel, tradeLabel } from "@/lib/cost/taxonomy"; import { UNIT_LABELS } from "@/lib/cost/units"; import { money, num, pct } from "@/components/avendre/fmt"; import { AttrBadge, KindIcon, kindLabel, LayerBadge } from "./Provenance"; import { Drawer, PriceSourceCard } from "./SourceDrawer"; import { Block, PctField } from "./fields"; import { RangeDist, StackBar } from "./Charts"; const unitL = (u: string, fr: boolean) => UNIT_LABELS[u as keyof typeof UNIT_LABELS]?.[fr ? "fr" : "en"] ?? u; export function CostTablePanel({ est, params, fr, patchP, hide }: { est: CostEstimate | null; params: CostParams; fr: boolean; patchP: (p: Partial) => void; hide: boolean }) { const lang = fr ? "fr" : "en"; const f = (v: number | null | undefined) => (hide ? "•••" : money(v, lang)); const base = est?.directCost ?? 0; const indirectBase = base + (est?.indirectCost ?? 0); return ( {fr ? "prix au" : "prices as of"} {est.priceDate} · {fr ? est.location.nameFr : est.location.nameEn}}> {!est ?

{fr ? "Calcul en cours…" : "Computing…"}

: ( <>
{est.categories.map((c) => ( ))}
{fr ? "Catégorie" : "Category"}{fr ? "Matériaux" : "Materials"}{fr ? "Main-d'œuvre" : "Labour"}{fr ? "Équipement" : "Equipment"}{fr ? "Direct" : "Direct"}{fr ? "Ajust. localisation" : "Location adj."}{fr ? "Ajusté" : "Adjusted"}%
{fr ? c.labelFr : c.labelEn} {f(c.material)} {f(c.labour)} {f(c.equipment)} {f(c.direct)} {hide ? "•••" : `${c.adjusted - c.direct >= 0 ? "+" : "−"}${money(Math.abs(c.adjusted - c.direct), lang)}`} {f(c.adjusted)} {num(c.sharePct, lang, 1)}
{fr ? "Coûts directs" : "Direct costs"} {f(est.directMaterial)} {f(est.directLabour)} {f(est.directEquipment)} {f(est.categories.reduce((s, c) => s + c.direct, 0))} {hide ? "•••" : `${est.directCost - est.categories.reduce((s, c) => s + c.direct, 0) >= 0 ? "+" : "−"}${money(Math.abs(est.directCost - est.categories.reduce((s, c) => s + c.direct, 0)), lang)}`} {f(est.directCost)} 100
{!hide && }

{fr ? "Coûts indirects — % du coût direct (hypothèses modifiables)" : "Indirect costs — % of direct cost (editable assumptions)"}

{(Object.keys(INDIRECT_LABELS) as (keyof IndirectParams)[]).map((k) => ( patchP({ indirect: { ...params.indirect, [k]: v } })} /> ))}
{fr ? "Total indirects" : "Total indirects"}{num(Object.values(params.indirect).reduce((s, v) => s + v, 0), lang, 1)} %{f(est.indirectCost)}

{fr ? "Entrepreneur — frais généraux et profit séparés" : "Contractor — overhead and profit kept separate"}

patchP({ overheadPct: v })} /> patchP({ profitPct: v })} />
{!hide && } )}
); } /* ------------------------------------------------------------ 04 assemblages */ export function AssemblyLinesPanel({ est, input, fr, setQty, toggleExclude, hide }: { est: CostEstimate | null; input: CostInput; fr: boolean; setQty: (code: string, q: number | null) => void; toggleExclude: (code: string) => void; hide: boolean }) { const lang = fr ? "fr" : "en"; const [open, setOpen] = useState(null); const [src, setSrc] = useState<{ comp: EstimateLine["unitDetail"]["components"][number]; line: EstimateLine } | null>(null); const f = (v: number) => (hide ? "•••" : money(v, lang)); const excluded = input.excludedAssemblies; return ( {est.lines.length} {fr ? "lignes · cliquez pour le détail du calcul" : "lines · click for calculation detail"}}> {!est ? null : (
{est.lines.map((l) => ( ))} {excluded.filter((c) => !est.lines.some((l) => l.assemblyCode === c)).map((c) => ( ))}
{fr ? "Assemblage" : "Assembly"}{fr ? "Quantité" : "Quantity"}{fr ? "Formule" : "Formula"}{fr ? "Coût unit." : "Unit cost"}MMOÉ{fr ? "Ajusté" : "Adjusted"}

{l.assemblyCode} · {categoryLabel(l.category, fr)} · {fr ? "part observée" : "observed share"} {Math.round(l.observedShare * 100)} %
setQty(l.assemblyCode, e.target.value === "" ? null : Number(e.target.value))} className="vp-input vp-mono !min-h-0 w-[88px] !py-0.5 text-right text-[12.5px]" aria-label={fr ? "Quantité" : "Quantity"} /> {unitL(l.unit, fr)}
{input.quantityOverrides[l.assemblyCode] != null && }
{l.quantityFormula} {hide ? "•••" : `${num(l.unitCost, lang, 2)} $`}
/ {unitL(l.unit, fr)}
{f(l.material)} {f(l.labour)} {f(l.equipment)} {f(l.adjusted)}
{c} — {fr ? "exclu" : "excluded"}
)} setOpen(null)} title={fr ? "Détails du calcul" : "Calculation details"} fr={fr}> {open && (

{open.assemblyCode}

{fr ? open.nameFr : open.nameEn}

{fr ? "Fiche complète de l'assemblage" : "Full assembly card"} →

{fr ? "Quantité" : "Quantity"}

{num(open.quantity, lang, 2)} {unitL(open.unit, fr)}

{fr ? "Formule de quantité" : "Quantity formula"}

{open.quantityFormula}

{fr ? "Coût unitaire direct" : "Unit direct cost"}

{num(open.unitCost, lang, 2)} $ / {unitL(open.unit, fr)}

{fr ? "Ligne (avant / après localisation)" : "Line (before / after location)"}

{money(open.direct, lang)} → {money(open.adjusted, lang)}

{fr ? "Écart-type de la ligne" : "Line standard deviation"}

± {money(open.sigma, lang)}

{fr ? "Vie économique · groupe" : "Economic life · group"}

{open.economicLife ?? "—"} {fr ? "ans" : "yrs"} · {open.conditionGroup ?? "—"}

{fr ? "matériau = qté × (1 + pertes) × prix · main-d'œuvre = heures × taux employeur · par unité d'assemblage" : "material = qty × (1 + waste) × price · labour = hours × employer rate · per assembly unit"}

{open.unitDetail.components.map((c, i) => ( setSrc({ comp: c, line: open })}> ))}
{fr ? "Composant" : "Component"}{fr ? "Qté / unité" : "Qty / unit"}{fr ? "Pertes" : "Waste"}{fr ? "Prix unit." : "Unit price"}{fr ? "Prov." : "Prov."}{fr ? "Matériau" : "Material"}{fr ? "Heures" : "Hours"}{fr ? "Taux" : "Rate"}MOÉ
{fr ? c.nameFr : c.nameEn}{c.itemCode && <>
{c.itemCode}}{c.trade && c.labourHours > 0 && <>
{tradeLabel(c.trade, fr)}}
{c.itemCode ? `${num(c.quantity, lang, 4)} ${unitL(c.unit, fr)}` : "—"} {c.itemCode ? `${Math.round(c.wasteFactor * 100)} %` : "—"} {c.itemCode ? `${num(c.unitPrice, lang, 2)} $` : "—"} {c.itemCode ? : } {c.materialCost ? `${num(c.materialCost, lang, 2)} $` : "—"} {c.labourHours ? num(c.labourHours, lang, 3) : "—"} {c.hourlyRate != null ? {num(c.hourlyRate, lang, 2)} $/h : "—"} {c.labourCost ? `${num(c.labourCost, lang, 2)} $` : "—"} {c.equipmentCost ? `${num(c.equipmentCost, lang, 2)} $` : "—"}
{fr ? "Par unité d'assemblage" : "Per assembly unit"}{num(open.unitDetail.material, lang, 2)} ${num(open.unitDetail.labour, lang, 2)} ${num(open.unitDetail.equipment, lang, 2)} $

{fr ? "Cliquez un composant pour sa fiche source." : "Click a component for its source card."}

)}
setSrc(null)} title={fr ? "Source" : "Source"} fr={fr}> {src && (
{src.comp.itemCode ? : null} {src.comp.rateProvenance && (

{fr ? "Taux de main-d'œuvre" : "Labour rate"}

{tradeLabel(src.comp.trade, fr)} — {num(src.comp.hourlyRate ?? 0, lang, 2)} $/h

{kindLabel(src.comp.rateProvenance.kind, fr)} · {src.comp.rateProvenance.source}{src.comp.rateProvenance.effectiveDate ? ` · ${fr ? "en vigueur le" : "effective"} ${src.comp.rateProvenance.effectiveDate}` : ""}

{src.comp.rateProvenance.note &&

{src.comp.rateProvenance.note}

} {src.comp.rateProvenance.sourceUrl && {fr ? "Document source" : "Source document"} ↗}

{fr ? "Coût employeur complet : taux horaire + vacances (13 %) + avantages sociaux + cotisations (AE, RQAP, FSS, CCQ, CNESST…) — jamais le seul salaire." : "Full employer cost: hourly wage + vacation (13 %) + benefits + contributions (EI, QPIP, HSF, CCQ, CNESST…) — never the bare wage."}

)}
)}
); } /* ------------------------------------------------------------ 05 main-d'œuvre */ export function LabourPanel({ est, fr, hide }: { est: CostEstimate | null; fr: boolean; hide: boolean }) { const lang = fr ? "fr" : "en"; if (!est) return null; const byTrade = new Map(); for (const l of est.lines) for (const c of l.unitDetail.components) { if (!(c.labourHours > 0)) continue; const k = c.trade ?? "?"; const cur = byTrade.get(k) ?? { hours: 0, cost: 0, rate: c.hourlyRate, official: c.rateProvenance?.kind === "official", source: c.rateProvenance?.source ?? null, date: c.rateProvenance?.effectiveDate ?? null }; cur.hours += c.labourHours * l.quantity; cur.cost += c.labourCost * l.quantity * est.location.labourFactor; byTrade.set(k, cur); } const rows = [...byTrade.entries()].sort((a, b) => b[1].cost - a[1].cost); const totalH = rows.reduce((s, [, r]) => s + r.hours, 0); const officialShare = est.directLabour > 0 ? rows.filter(([, r]) => r.official).reduce((s, [, r]) => s + r.cost, 0) / est.directLabour : 0; return ( {fr ? "part des taux officiels" : "official-rate share"} : {Math.round(officialShare * 100)} %}>

{fr ? "Heures totales" : "Total hours"}

{hide ? "•••" : num(totalH, lang)} h

{fr ? "Coût de main-d'œuvre (localisé)" : "Labour cost (localised)"}

{hide ? "•••" : money(est.directLabour, lang)}

{fr ? "Facteur régional MO" : "Regional labour factor"}

× {num(est.location.labourFactor, lang, 2)}

{rows.map(([t, r]) => ( ))}
{fr ? "Métier" : "Trade"}{fr ? "Heures" : "Hours"}{fr ? "Taux employeur" : "Employer rate"}{fr ? "Source · date" : "Source · date"}{fr ? "Coût" : "Cost"}%
{tradeLabel(t, fr)} {hide ? "•••" : num(r.hours, lang)} {r.rate != null ? `${num(r.rate, lang, 2)} $/h` : "—"} {r.official ? `${r.source}${r.date ? ` · ${r.date}` : ""}` : (fr ? "Taux de repli (hypothèse) — aucune grille" : "Fallback rate (assumption) — no grid")} {hide ? "•••" : money(r.cost, lang)} {est.directLabour ? pct((r.cost / est.directLabour) * 100, lang, 1, false) : "—"}

{fr ? "Heures = Σ (heures par unité d'assemblage × quantité). Taux = coût employeur complet des grilles APCHQ (conventions CCQ, secteur résidentiel léger, compagnon). Les heures de productivité sont des hypothèses Vrai-Prix." : "Hours = Σ (hours per assembly unit × quantity). Rate = full employer cost from APCHQ grids (CCQ agreements, light residential sector, journeyperson). Productivity hours are Vrai-Prix assumptions."}

); }