// Company detail view // Forecast chart with confidence band const ForecastChart = ({ history, forecast, w = 680, h = 220 }) => { const all = [...history.map(h => ({ ...h, type: 'actual' })), ...forecast.map(f => ({ ...f, type: 'forecast' }))]; const padL = 32, padR = 16, padT = 12, padB = 24; const cw = w - padL - padR, ch = h - padT - padB; const vals = [...history.map(h => h.actual), ...forecast.map(f => f.upper)]; const min = 0, max = Math.max(...vals) * 1.1; const step = cw / (all.length - 1); const y = v => padT + ch - ((v - min) / (max - min)) * ch; const x = i => padL + i * step; // Confidence band const splitIdx = history.length; const bandUpper = forecast.map((f, i) => [x(splitIdx + i), y(f.upper)]); const bandLower = forecast.map((f, i) => [x(splitIdx + i), y(f.lower)]); // Connect to last actual point const lastActual = history[history.length - 1].actual; const connectX = x(splitIdx - 1), connectY = y(lastActual); const bandD = `M${connectX},${connectY} ` + bandUpper.map(p => `L${p[0]},${p[1]}`).join(' ') + ' ' + bandLower.slice().reverse().map(p => `L${p[0]},${p[1]}`).join(' ') + ` L${connectX},${connectY} Z`; const actualLine = 'M' + history.map((d, i) => `${x(i)},${y(d.actual)}`).join(' L'); const forecastLine = `M${connectX},${connectY} ` + forecast.map((d, i) => `L${x(splitIdx + i)},${y(d.forecast)}`).join(' '); const yTicks = [0, max * 0.25, max * 0.5, max * 0.75, max].map(v => Math.round(v * 10) / 10); return ( ); }; // Revenue bar chart const RevenueBars = ({ data, w = 340, h = 180 }) => { const padL = 28, padR = 8, padT = 8, padB = 22; const cw = w - padL - padR, ch = h - padT - padB; const max = Math.max(...data.map(d => Math.max(d.actual, d.budget))) * 1.1; const bw = cw / data.length * 0.6; const step = cw / data.length; const y = v => padT + ch - (v / max) * ch; return ( ); }; const CompanyDetail = ({ company, onBack }) => { const [tab, setTab] = React.useState('panoramica'); const D = window.REGULA_DATA; // Use MetalTech detail data as the canonical detail view (demo) const det = D.metaltech; // Context-aware KPIs const contextKPIs = { metaltech: [ { label: "Ricavo medio mensile", value: "4.02 M€", delta: "+6.3%" }, { label: "Margine EBITDA", value: "14.8%", delta: "+0.9 p.p." }, { label: "DSO", value: "62 gg", delta: "−4 gg" }, { label: "PFN / EBITDA", value: "1.82×", delta: "−0.14×" } ], aquaworld: [ { label: "Ricavo per visitatore", value: "28.40 €", delta: "+4.2%" }, { label: "Visitatori YTD", value: "654.900", delta: "+6.8%" }, { label: "Occupazione media", value: "68%", delta: "+3 p.p." }, { label: "DSCR", value: "1.24×", delta: "−0.08×", bad: true } ], lumiere: [ { label: "Ricavo per serata", value: "12.8 k€", delta: "−8.1%", bad: true }, { label: "Presenze medie", value: "412", delta: "−11%", bad: true }, { label: "Scontrino medio", value: "31.10 €", delta: "+2.4%" }, { label: "Serate aperte", value: "138", delta: "+4" } ], fondazione: [ { label: "Copertura da donazioni", value: "108%", delta: "+6 p.p." }, { label: "Donatori attivi", value: "1.284", delta: "+14%" }, { label: "Visitatori mostre", value: "42.100", delta: "+9%" }, { label: "Rapporto costi / entrate", value: "0.94", delta: "−0.04" } ] }; const kpis = contextKPIs[company.id] || contextKPIs.metaltech; return (
| Voce | Actual | Budget | Prev. | Δ vs Bdg |
|---|---|---|---|---|
| {row.label} | {row.actual > 0 ? '' : '−'}{Math.abs(row.actual).toFixed(1)} | {row.budget > 0 ? '' : '−'}{Math.abs(row.budget).toFixed(1)} | {row.prev > 0 ? '' : '−'}{Math.abs(row.prev).toFixed(1)} | 0 ? 'var(--green)' : delta < 0 ? 'var(--red)' : 'var(--muted)' }}> {delta > 0 ? '+' : ''}{delta.toFixed(1)}% |