Python 64.6%
TypeScript 33.7%
CSS 0.8%
1import type { ToolCallView } from '@/lib/types';2import { fmtCAD, fmtNum, fmtPct } from '@/lib/format';34const LABELS: Record<string, string> = {5 value: 'Valeur', land_value: 'Valeur du terrain', cost_new: 'Coût neuf', total_depreciation: 'Dépréciation totale',6 depreciated_cost: 'Coût déprécié', depreciation_ratio: 'Taux de dépréciation', ratio: 'Ratio', noi: 'RNE', cap_rate: 'TGA',7 gim: 'MRB', land_value_pv: 'Valeur du terrain (VA)', per_lot: 'Par lot', gross_sales: 'Recettes brutes', costs: 'Coûts',8 profit: 'Profit', net_undiscounted: 'Net non actualisé', building_income: 'Revenu attribuable au bâtiment',9 residual_income: 'Revenu résiduel (terrain)', index_ratio: 'Ratio d’indices', direct_costs: 'Coûts directs', extras: 'Extras',10 indirect_costs: 'Coûts indirects', entrepreneur_profit: 'Profit de l’entrepreneur', cost_per_unit_all_in: 'Coût unitaire tout compris',11 effective_age: 'Âge effectif', annual_rate: 'Taux annuel', depreciation: 'Dépréciation', land_ratio: 'Ratio terrain',12 curable_physical: 'Physique récupérable', short_lived: 'Courte vie', long_lived: 'Longue vie', functional: 'Fonctionnelle', external: 'Externe',13 min: 'Minimum', max: 'Maximum', mean: 'Moyenne (indicatif)', median: 'Médiane', weighted: 'Pondération réconciliée',14 least_adjusted: 'Comparable le moins ajusté', least_adjusted_price: 'Prix ajusté (moins ajusté)',15};16const PCT = new Set(['depreciation_ratio', 'ratio', 'cap_rate', 'annual_rate', 'land_ratio', 'time_pct', 'net_pct', 'gross_pct']);17const PLAIN = new Set(['gim', 'index_ratio', 'effective_age', 'unit']);1819function fmt(k: string, v: unknown): string {20 if (v === null || v === undefined) return '—';21 if (typeof v === 'number') {22 if (PCT.has(k)) return fmtPct(v, 2);23 if (PLAIN.has(k)) return fmtNum(v, 2);24 return Math.abs(v) >= 1000 ? fmtCAD(v, 0) : fmtNum(v, 2);25 }26 return String(v);27}2829interface Comp { name: string; price: number; time_pct: number; time_adjusted: number; adjustments: Record<string, number>; net: number; adjusted_price: number; net_pct: number; gross_pct: number; reliable: boolean }3031export function AppraisalCalcCard({ call }: { call: ToolCallView }) {32 const p = call.payload as { function?: string; result?: Record<string, unknown> };33 const r = p.result || {};34 const table = r.table as unknown[][] | undefined;35 const comps = r.comparables as Comp[] | undefined;36 const stats = r.stats as Record<string, unknown> | undefined;37 const components = r.components as Record<string, number> | undefined;38 const scalarKeys = Object.keys(r).filter((k) => !['formula', 'table', 'comparables', 'stats', 'components', 'note'].includes(k) && typeof r[k] !== 'object');39 const adjKeys = comps ? [...new Set(comps.flatMap((c) => Object.keys(c.adjustments)))] : [];40 return (41 <div className="space-y-3 text-sm">42 {r.formula ? <div className="rounded-lg bg-uqo-blue-light/60 px-3 py-2 font-mono text-xs text-uqo-blue-dark">{String(r.formula)}</div> : null}43 {scalarKeys.length > 0 && (44 <dl className="grid grid-cols-2 sm:grid-cols-3 gap-2">45 {scalarKeys.map((k) => (46 <div key={k} className={`rounded-xl border px-3 py-2 ${k === 'value' || k === 'land_value' || k === 'land_value_pv' ? 'border-uqo-green bg-[#f2f9ef]' : 'border-neutral-line bg-white'}`}>47 <dt className="text-[11px] text-neutral-muted">{LABELS[k] || k.replace(/_/g, ' ')}</dt>48 <dd className="font-semibold tabular-nums">{fmt(k, r[k])}</dd>49 </div>50 ))}51 </dl>52 )}53 {components && (54 <dl className="grid grid-cols-2 sm:grid-cols-5 gap-2">55 {Object.entries(components).map(([k, v]) => <div key={k} className="rounded-xl border border-neutral-line bg-white px-3 py-2"><dt className="text-[11px] text-neutral-muted">{LABELS[k] || k}</dt><dd className="font-semibold tabular-nums">{fmtCAD(v, 0)}</dd></div>)}56 </dl>57 )}58 {table && (59 <div className="rounded-xl border border-neutral-line overflow-x-auto scroll-thin bg-white">60 <table className="text-xs min-w-full">61 <tbody>62 {table.map((row, i) => {63 const last = i === table.length - 1;64 return (65 <tr key={i} className={`border-b border-neutral-line/60 last:border-0 ${last ? 'font-semibold text-uqo-blue-dark bg-uqo-blue-light/40' : ''}`}>66 {row.map((c, j) => <td key={j} className={`px-3 py-1.5 ${typeof c === 'number' ? 'text-right tabular-nums whitespace-nowrap' : ''}`}>{typeof c === 'number' ? fmtCAD(c, 0) : String(c ?? '')}</td>)}67 </tr>68 );69 })}70 </tbody>71 </table>72 </div>73 )}74 {comps && (75 <div className="rounded-xl border border-neutral-line overflow-x-auto scroll-thin bg-white">76 <table className="text-xs min-w-full">77 <thead className="bg-uqo-blue-light text-uqo-blue-dark">78 <tr><th className="px-2 py-1.5 text-left">Comparable</th><th className="px-2 py-1.5 text-right">Prix</th><th className="px-2 py-1.5 text-right">Temps</th>{adjKeys.map((k) => <th key={k} className="px-2 py-1.5 text-right capitalize">{k}</th>)}<th className="px-2 py-1.5 text-right">Prix ajusté</th><th className="px-2 py-1.5 text-right">Net %</th><th className="px-2 py-1.5 text-right">Brut %</th><th /></tr>79 </thead>80 <tbody>81 {comps.map((c) => (82 <tr key={c.name} className="border-t border-neutral-line/60">83 <td className="px-2 py-1.5 font-medium whitespace-nowrap">{c.name}</td>84 <td className="px-2 py-1.5 text-right tabular-nums">{fmtCAD(c.price, 0)}</td>85 <td className="px-2 py-1.5 text-right tabular-nums">{fmtPct(c.time_pct, 1)}</td>86 {adjKeys.map((k) => <td key={k} className={`px-2 py-1.5 text-right tabular-nums ${(c.adjustments[k] || 0) < 0 ? 'text-semantic-error' : (c.adjustments[k] || 0) > 0 ? 'text-[#1f7a1f]' : 'text-neutral-muted'}`}>{c.adjustments[k] !== undefined ? fmtCAD(c.adjustments[k], 0) : '—'}</td>)}87 <td className="px-2 py-1.5 text-right tabular-nums font-semibold">{fmtCAD(c.adjusted_price, 0)}</td>88 <td className="px-2 py-1.5 text-right tabular-nums">{fmtPct(c.net_pct, 1)}</td>89 <td className="px-2 py-1.5 text-right tabular-nums">{fmtPct(c.gross_pct, 1)}</td>90 <td className="px-2 py-1.5">{c.reliable ? <span className="text-[10px] rounded bg-[#e9f6d9] text-[#3f7a0a] px-1.5 py-0.5">fiable</span> : <span className="text-[10px] rounded bg-amber-50 text-amber-700 px-1.5 py-0.5">à pondérer</span>}</td>91 </tr>92 ))}93 </tbody>94 </table>95 </div>96 )}97 {stats && (98 <dl className="grid grid-cols-2 sm:grid-cols-4 gap-2">99 {Object.entries(stats).map(([k, v]) => <div key={k} className={`rounded-xl border px-3 py-2 ${k === 'weighted' ? 'border-uqo-green bg-[#f2f9ef]' : 'border-neutral-line bg-white'}`}><dt className="text-[11px] text-neutral-muted">{LABELS[k] || k}</dt><dd className="font-semibold tabular-nums">{typeof v === 'number' ? fmtCAD(v, 0) : String(v)}</dd></div>)}100 </dl>101 )}102 {r.note ? <p className="text-xs text-neutral-muted">{String(r.note)}</p> : null}103 </div>104 );105}106