${b.l}
${b.fmt(b.az)}
vs ${b.fmt(b.sec)} settore
${better?'◎':'◈'} ${diffStr} rispetto alla media di settore
`;
}).join('');
setTimeout(() => {
document.querySelectorAll('.bench-bar-a').forEach(el => { el.style.width = el.dataset.w; });
}, 600);
// KPI strip
const kpis = [
{ l:'Fatturato', v:fmtE(fatt), s:'Anno corrente', b: null },
{ l:'EBITDA', v:fmtE(ebitda), s:'Op. lordo', b: ebitda>0?{c:'g',t:'Positivo'}:{c:'r',t:'Negativo'} },
{ l:'Margine %', v:fmtP(margine*100), s:'Su fatturato', b: margine>=.2?{c:'g',t:'Eccellente'}:margine>=.1?{c:'a',t:'Sufficiente'}:{c:'r',t:'Critico'} },
{ l:'Leva D/EBITDA', v:lev, s:'Sostenibilità deb.',b: lev==='—'?null:parseFloat(lev)<=2?{c:'g',t:'Sostenibile'}:parseFloat(lev)<=4?{c:'a',t:'Monitorare'}:{c:'r',t:'Elevata'} },
{ l:'Liquidità', v:fmtE(liq), s:'Cassa disponibile', b: liq>fatt*.15?{c:'g',t:'Adeguata'}:{c:'a',t:'Monitorare'} },
{ l:'Personale / Fatt',v:fmtP(d.persPct), s:'Incidenza', b: d.persPct>0&&d.persPct0?{c:'g',t:'Positivo'}:{c:'r',t:'Negativo'} },
];
document.getElementById('r-kpi').innerHTML = kpis.map(k => `
${k.l}
${k.v}
${k.s}
${k.b ? `
${k.b.t}` : ''}
`).join('');
// Trend
const hasPrev = d.pfatt > 0;
const pFmtDelta = (curr, prev) => {
if (!prev || !curr) return '';
const pct = (curr - prev) / Math.abs(prev) * 100;
const col = pct > 0 ? 'var(--green)' : pct < 0 ? 'var(--red)' : 'var(--muted)';
const arr = pct > 1 ? '↑' : pct < -1 ? '↓' : '→';
return `${arr} ${(pct > 0 ? '+' : '') + pct.toFixed(1)}%`;
};
document.getElementById('r-tr').innerHTML = `
${hasPrev ? 'Confronto Anno su Anno' : 'Dati anno precedente non inseriti'}
${hasPrev ? `
Fatturato${fmtE(d.fatt)}${pFmtDelta(d.fatt,d.pfatt)}
EBITDA${fmtE(d.ebitda)}${pFmtDelta(d.ebitda,d.pebitda)}
Margine %${fmtP(margine*100)}${pFmtDelta(margine,d.pmargine)}
Debiti fin.${fmtE(deb)}${pFmtDelta(d.pdeb,deb)}
` : '
Inserire i dati anno precedente nello Step 3 per il confronto quantitativo.
'}
Indicatori qualitativi
Trend fatturato${{crescita:'↗ Crescita',stabile:'→ Stabile',calo:'↘ Calo'}[S.tf]||'—'}
Trend margini${{aumento:'↑ In aumento',costanti:'↔ Costanti',calo:'↓ In calo'}[S.tm]||'—'}
Pressione competitiva${{bassa:'◦ Bassa',media:'◈ Media',alta:'◉ Alta'}[d.pc]||'—'}
Urgenza${{alta:'◉ Alta',media:'◈ Media',bassa:'◦ Bassa'}[d.urg]||'—'}
`;
// Strategy banner
document.getElementById('r-sacc').style.background = strat.color;
document.getElementById('r-sn').textContent = strat.label;
document.getElementById('r-so').textContent = strat.obj;
// Objective
const om = OBJ_META[d.obj] || { icon: '◈', pills: [] };
document.getElementById('r-obj').innerHTML = `
${om.icon}
Obiettivo dichiarato · Orizzonte ${d.oriz}
${d.obj}
${om.pills.map(p => `${p}`).join('')}
`;
// Piano skeleton (AI will fill)
const phases = [
{ tag:'Mesi 1–2', name:'Analisi & Diagnosi' },
{ tag:'Mesi 3–4', name:'Interventi Prioritari' },
{ tag:'Mesi 5–6', name:'Implementazione' },
{ tag:'Mesi 7–12', name:'Consolidamento & KPI' },
];
document.getElementById('r-piano').innerHTML = phases.map((ph, pi) => `
${ph.tag}
${ph.name}
▾
Elaborazione AI in corso…
`).join('');
// Impatto
document.getElementById('r-imp').innerHTML = `
Proiezione 12 Mesi
Impatto economico
stimato del piano operativo
${strat.impatto.map(i => `
${i.k}${i.v}
`).join('')}
`;
// Fiscal
const fLev = strat.fiscal;
const fIdx = FISCAL_ORDER.indexOf(fLev);
document.getElementById('r-fis').innerHTML = FISCAL_ORDER.map((k, i) => {
const f = FISCAL_DB[k]; const on = i <= fIdx;
return `
${f.tl}${on?' ✓':' — non applicabile'}
${f.title}
${f.items.map(it => `- ${it.i}${it.t}
`).join('')}
`;
}).join('');
// Raccomandazioni skeleton
document.getElementById('r-rac').innerHTML = Array.from({length:6},(_,i) => `
`).join('');
}
/* ══════════════════════════════════════════
AI STREAMING — FULL PROMPT
══════════════════════════════════════════ */
async function streamAIContent(d) {
const lev = d.ebitda > 0 ? (d.deb / d.ebitda).toFixed(1) + 'x' : 'n.d.';
const bench = d.bench;
const hasPrev = d.pfatt > 0;
const vrd = scoreVerdict(d.sc.total);
const prompt = `Sei un senior advisor di Horizon Group Srl, family office e società di intermediazione assicurativa e finanziaria con sede a Firenze. Hai un QI di 150 e sei specializzato in strategia d'impresa, finanza aziendale e assicurazioni per le PMI italiane.
Stai elaborando una relazione strategica riservata. Usa un tono autorevole, diretto e professionale. Cita sempre i numeri reali dell'azienda. Non essere generico.
═══════════════════════════════════════
DATI AZIENDALI
═══════════════════════════════════════
Ragione sociale: ${d.ragione}
Settore: ${d.settore}
Forma giuridica: ${d.forma}
Dipendenti: ${d.dip || 'n.d.'}
Provincia: ${d.prov || 'n.d.'}
${d.anni ? `Anni di attività: ${d.anni} anni` : ''}
═══════════════════════════════════════
DATI ECONOMICO-FINANZIARI (ANNO CORRENTE)
═══════════════════════════════════════
Fatturato: €${Math.round(d.fatt).toLocaleString('it-IT')}
Costi materie/merci: €${Math.round(d.cv).toLocaleString('it-IT')}
Costo del personale: €${Math.round(d.personale).toLocaleString('it-IT')} (${d.persPct.toFixed(1)}% del fatturato)
Affitti e leasing: €${Math.round(d.affitti).toLocaleString('it-IT')}
Altri costi fissi: €${Math.round(d.altri_cf).toLocaleString('it-IT')}
Ammortamenti: €${Math.round(d.amm).toLocaleString('it-IT')}
Oneri finanziari: €${Math.round(d.oneri_fin).toLocaleString('it-IT')}
EBITDA (calcolato): €${Math.round(d.ebitda).toLocaleString('it-IT')} (margine: ${(d.margine*100).toFixed(1)}%)
EBIT: €${Math.round(d.ebit).toLocaleString('it-IT')}
Risultato netto: €${Math.round(d.netto).toLocaleString('it-IT')}
Liquidità: €${Math.round(d.liq).toLocaleString('it-IT')}
Crediti clienti: €${Math.round(d.cred).toLocaleString('it-IT')}
Debiti finanziari: €${Math.round(d.deb).toLocaleString('it-IT')}
Debiti commerciali: €${Math.round(d.deb_comm).toLocaleString('it-IT')}
Patrimonio netto: €${Math.round(d.pn).toLocaleString('it-IT')}
Leva D/EBITDA: ${lev}
${d.dso ? `DSO giorni incasso: ${d.dso} giorni` : ''}
${hasPrev ? `═══════════════════════════════════════
DATI ANNO PRECEDENTE E TREND
═══════════════════════════════════════
Fatturato prec.: €${Math.round(d.pfatt).toLocaleString('it-IT')} (Δ ${((d.fatt-d.pfatt)/d.pfatt*100).toFixed(1)}%)
EBITDA prec.: €${Math.round(d.pebitda).toLocaleString('it-IT')} (margine: ${(d.pmargine*100).toFixed(1)}%)
Variazione margine: ${(d.margine*100-d.pmargine*100).toFixed(1)}pp
Debiti finanziari prec.: €${Math.round(d.pdeb).toLocaleString('it-IT')}
Trend fatturato (auto): ${S.tf || 'n.r.'}
Trend margini (auto): ${S.tm || 'n.r.'}` : 'Dati anno precedente: non inseriti.'}
═══════════════════════════════════════
SCORING HORIZON GROUP (${d.sc.total}/100 — ${vrd})
═══════════════════════════════════════
Margine operativo: ${d.sc.mPct} → ${d.sc.mL} (${d.sc.mS}/30 pt)
Leva finanziaria: ${d.sc.lev} → ${d.sc.lL} (${d.sc.lS}/25 pt)
Liquidità: ${d.sc.qr} → ${d.sc.qL} (${d.sc.qS}/25 pt)
Trend: ${d.sc.tL} (${d.sc.tS}/20 pt)
═══════════════════════════════════════
BENCHMARK SETTORE: ${d.settore}
═══════════════════════════════════════
Margine EBITDA medio settore: ${bench.margine}% → azienda: ${(d.margine*100).toFixed(1)}% (${d.margine*100 >= bench.margine ? 'SUPERIORE' : 'INFERIORE'} di ${Math.abs(d.margine*100-bench.margine).toFixed(1)}pp)
Incid. personale media settore: ${bench.personale_pct}% → azienda: ${d.persPct.toFixed(1)}% (${d.persPct <= bench.personale_pct ? 'IN LINEA/MEGLIO' : 'SUPERIORE, ATTENZIONE'})
Leva media settore: ${bench.leva}x → azienda: ${lev} (${d.ebitda>0 && d.deb/d.ebitda <= bench.leva ? 'IN LINEA/MEGLIO' : 'SUPERIORE'})
Multipli valutazione settore: ${bench.multiplo} EBITDA
═══════════════════════════════════════
SCENARIO STRATEGICO: ${d.stratKey}
OBIETTIVO IMPRENDITORE: ${d.obj}
ORIZZONTE: ${d.oriz}
URGENZA: ${d.urg}
PRESSIONE COMPETITIVA: ${d.pc}
${d.note ? `NOTE AGGIUNTIVE: ${d.note}` : ''}
═══════════════════════════════════════
KNOWLEDGE BASE HORIZON GROUP
═══════════════════════════════════════
CASO 1 RISTRUTTURAZIONE (margine <10%, EBITDA basso/negativo, cash flow fragile):
- Obiettivo: ripristinare equilibrio eco-fin, margine >10%, stabilizzare liquidità
- Azioni: taglio costi immediato (P/S/N), revisione pricing, ristrutturazione finanziaria, protezione rischio
- KPI: margine >10%, EBITDA positivo, cash flow operativo positivo, riduzione costi fissi
CASO 2 CONSOLIDAMENTO (margine 10-20%, EBITDA positivo, struttura discreta):
- Obiettivo: incrementare efficienza, margine 15-20%, mantenere stabilità
- Azioni: ottimizzazione marginalità, miglioramento cash flow (riduzione DSO), sviluppo commerciale controllato, welfare+protezione
- KPI: margine 15-20%, DSO in riduzione, ticket medio in aumento, EBITDA in crescita
CASO 3 CRESCITA (margine >20%, EBITDA alto, liquidità buona):
- Obiettivo: scalare business, aumentare fatturato e valore aziendale
- Azioni: espansione commerciale, investimenti (marketing/persone/tech), strutturazione organizzativa, ottimizzazione fiscale avanzata (holding)
- KPI: crescita fatturato, margine >20%, ROI marketing, nuovi clienti
CASO 4 EXIT (EBITDA stabile, azienda strutturata, interesse imprenditore a uscire):
- Obiettivo: massimizzare valore, preparare vendita
- Azioni: pulizia bilancio (normalizzazione EBITDA), stabilizzazione ricavi ricorrenti, riduzione dipendenza cliente, coperture assicurative
- KPI: EBITDA normalizzato, ricavi ricorrenti %, dipendenza top clienti, multiplo valutazione
STRATEGIA FISCALE:
- Base: deducibilità costi, ammortamenti
- Intermedia: welfare aziendale, premi produttività detassati
- Avanzata: holding, protezione patrimonio, dividendi ottimizzati
CLASSIFICAZIONE COSTI P/S/N:
- P (Produttivo): genera direttamente fatturato/margine
- S (Supporto): necessario ma non direttamente produttivo
- N (Non produttivo): eliminabile senza impatto sul business core
═══════════════════════════════════════
PARAMETRI ESPLICITI PER LE RACCOMANDAZIONI
═══════════════════════════════════════
Ogni raccomandazione DEVE seguire questi criteri:
1. SPECIFICITÀ: citare i numeri reali (es. "con un margine del ${(d.margine*100).toFixed(1)}% vs ${bench.margine}% di settore")
2. IMPLEMENTABILITÀ: descrivere azioni concrete, non principi astratti
3. IMPATTO ECONOMICO: stimare sempre l'impatto quantitativo atteso
4. TIMELINE: specificare entro quando implementare
5. PRIORITÀ: valutare in base a urgenza (${d.urg}), scenario (${d.stratKey}) e obiettivo (${d.obj})
6. SETTORE-SPECIFICO: tenere conto delle dinamiche del settore ${d.settore}
Regole per l'assegnazione della priorità:
- ALTA: impatto immediato su liquidità/sopravvivenza, o prerequisito per tutto il resto
- MEDIA: impatto significativo entro 6 mesi, dipende da altre azioni
- BASSA: ottimizzazione strutturale, orizzonte 6-12 mesi
═══════════════════════════════════════
ISTRUZIONI OUTPUT — USA ESATTAMENTE QUESTI MARCATORI
═══════════════════════════════════════
---SECTION:EXEC---
Scrivi un executive summary analitico in 3 paragrafi distinti separati da riga vuota:
PARAGRAFO 1 — Valutazione dei risultati economici: commenta in modo diretto e specifico margine ${(d.margine*100).toFixed(1)}% vs benchmark ${bench.margine}%, EBITDA €${Math.round(d.ebitda).toLocaleString('it-IT')}, leva ${lev}, liquidità €${Math.round(d.liq).toLocaleString('it-IT')}. Esprimi un giudizio netto sulla solidità finanziaria.
PARAGRAFO 2 — Fattori critici: identifica 2-3 driver specifici per ${d.settore} e per questa azienda che spiegano la situazione. Collega trend e numeri.
PARAGRAFO 3 — Direzione strategica: anticipa la raccomandazione principale e perché è quella giusta per ${d.ragione} in questa fase.
---SECTION:DIAGNOSI---
Diagnosi strategica in 2 paragrafi:
PARAGRAFO 1: Forze e debolezze strutturali rilevate dai dati. Cita numeri specifici. Confronta con il benchmark settore ${d.settore}.
PARAGRAFO 2: Rischi specifici nei prossimi 12 mesi e implicazioni operative per ${d.ragione}.
---SECTION:OBJ---
2-3 frasi: come l'obiettivo "${d.obj}" si inserisce nel contesto attuale di ${d.ragione}. Condizioni favorevoli e rischi specifici per raggiungerlo.
---SECTION:PIANO_0---
Fase 1 (Mesi 1-2): genera 3 azioni dettagliate per ${d.ragione} scenario ${d.stratKey}. Per ogni azione usa formato:
TITOLO||DESCRIZIONE IMPLEMENTAZIONE (3 frasi specifiche con numeri dove possibile)
Separa le 3 azioni con "|||"
---SECTION:PIANO_1---
Fase 2 (Mesi 3-4): stessa struttura, 3 azioni.
---SECTION:PIANO_2---
Fase 3 (Mesi 5-6): stessa struttura, 3 azioni.
---SECTION:PIANO_3---
Fase 4 (Mesi 7-12): stessa struttura, 3 azioni.
---SECTION:RAC1---
Formato: PRIORITA||TITOLO (max 10 parole)||DETTAGLIO IMPLEMENTAZIONE (2-3 frasi con numeri)
PRIORITA deve essere: ALTA, MEDIA o BASSA
---SECTION:RAC2---
Stessa struttura.
---SECTION:RAC3---
Stessa struttura.
---SECTION:RAC4---
Stessa struttura.
---SECTION:RAC5---
Stessa struttura.
---SECTION:RAC6---
Stessa struttura.
---SECTION:END---`;
try {
const resp = await fetch('https://horizon-proxy.serjjf.workers.dev/api', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
model: 'claude-sonnet-4-20250514',
max_tokens: 4000,
stream: true,
messages: [{ role: 'user', content: prompt }]
})
});
if (!resp.ok) throw new Error('API ' + resp.status);
await parseStream(resp);
} catch(e) {
console.error('AI error:', e);
fallbackContent(d);
}
}
/* ══════════════════════════════════════════
STREAM PARSER
══════════════════════════════════════════ */
async function parseStream(resp) {
const reader = resp.body.getReader();
const dec = new TextDecoder();
let sseBuffer = ''; // raw SSE line buffer
let accumulated = ''; // full AI text accumulated
// Process accumulated text: split by section markers, flush completed sections
function processAccumulated(isFinal) {
const MARKER_RE = /---SECTION:([A-Z0-9_]+)---/g;
const markers = [];
let m;
while ((m = MARKER_RE.exec(accumulated)) !== null) {
markers.push({ sec: m[1], idx: m.index, end: m.index + m[0].length });
}
if (markers.length === 0) return;
for (let i = 0; i < markers.length; i++) {
const cur = markers[i];
const next = markers[i + 1];
const contentStart = cur.end;
const contentEnd = next ? next.idx : (isFinal ? accumulated.length : -1);
if (contentEnd === -1) {
// Section not yet complete — stream live preview
const partial = accumulated.slice(contentStart).replace(/---SECTION:[A-Z0-9_]+---/g, '').trim();
renderStreamChunk(cur.sec, partial);
continue;
}
// Section complete — flush it
const text = accumulated.slice(contentStart, contentEnd).replace(/---SECTION:[A-Z0-9_]+---/g, '').trim();
flushSection(cur.sec, text);
}
}
while (true) {
const { done, value } = await reader.read();
if (done) break;
sseBuffer += dec.decode(value, { stream: true });
const lines = sseBuffer.split('\n');
sseBuffer = lines.pop();
for (const line of lines) {
if (!line.startsWith('data: ')) continue;
const data = line.slice(6).trim();
if (data === '[DONE]') continue;
try {
const ev = JSON.parse(data);
if (ev.type === 'content_block_delta' && ev.delta?.text) {
accumulated += ev.delta.text;
processAccumulated(false);
}
} catch(e) {}
}
}
// Final flush
processAccumulated(true);
}
function renderStreamChunk(section, text) {
const clean = text.replace(/---SECTION:[A-Z0-9_]+---/g, '').trim();
if (section === 'EXEC') {
const el = document.getElementById('r-eb');
if (el) el.innerHTML = formatParagraphs(clean) + '';
} else if (section === 'DIAGNOSI') {
const el = document.getElementById('r-diag');
if (el) el.innerHTML = formatParagraphs(clean) + '';
} else if (section === 'OBJ') {
const el = document.getElementById('r-obj-desc');
if (el) el.innerHTML = clean + '';
}
}
function flushSection(section, text) {
if (!section || section === 'END') return;
const clean = text.replace(/---SECTION:[A-Z0-9_]+---/g, '').trim();
if (!clean) return;
if (section === 'EXEC') {
const el = document.getElementById('r-eb');
if (el) el.innerHTML = formatParagraphs(clean);
} else if (section === 'DIAGNOSI') {
const el = document.getElementById('r-diag');
if (el) el.innerHTML = formatParagraphs(clean);
} else if (section === 'OBJ') {
const el = document.getElementById('r-obj-desc');
if (el) el.textContent = clean;
} else if (section.startsWith('PIANO_')) {
const pi = parseInt(section.replace('PIANO_', ''));
renderPhase(pi, clean);
} else if (section.match(/^RAC\d$/)) {
const idx = parseInt(section.replace('RAC', '')) - 1;
renderRac(idx, clean);
}
}
function renderPhase(pi, text) {
const container = document.getElementById('phase-actions-' + pi);
if (!container) return;
const actions = text.split('|||').map(s => s.trim()).filter(Boolean);
const icons = ['◎','◈','◇'];
container.innerHTML = actions.map((ac, ai) => {
const parts = ac.split('||');
const title = (parts[0] || '').trim();
const detail = (parts[1] || '').trim();
return ``;
}).join('');
}
function renderRac(idx, text) {
const parts = text.split('||');
const prio = (parts[0] || 'MEDIA').trim().toUpperCase();
const title = (parts[1] || '').trim();
const detail = (parts[2] || '').trim();
const prioMap = { 'ALTA':'h', 'MEDIA':'m', 'BASSA':'l' };
const prioClass = prioMap[prio] || 'm';
const tEl = document.getElementById('rac-t-' + idx);
const dEl = document.getElementById('rac-d-' + idx);
const pEl = document.getElementById('rac-p-' + idx);
if (tEl) tEl.textContent = title;
if (dEl) dEl.textContent = detail;
if (pEl) { pEl.textContent = prio; pEl.className = 'rac-pill ' + prioClass; }
}
function formatParagraphs(text) {
return text.split(/\n\n+/).filter(Boolean).map(p => `${p.trim()}
`).join('');
}
/* ══════════════════════════════════════════
FALLBACK (quando API non disponibile)
══════════════════════════════════════════ */
function fallbackContent(d) {
const lev = d.ebitda > 0 ? (d.deb / d.ebitda).toFixed(1) + 'x' : 'n.d.';
const vrd = scoreVerdict(d.sc.total);
document.getElementById('r-eb').innerHTML = `
${d.ragione} opera nel settore ${d.settore} con un fatturato di ${fmtE(d.fatt)} e un EBITDA di ${fmtE(d.ebitda)}, corrispondente a un margine del ${fmtP(d.margine*100)}. Il benchmark di settore indica un margine medio del ${d.bench.margine}%: l'azienda risulta ${d.margine*100 >= d.bench.margine ? 'al di sopra' : 'al di sotto'} della media. Lo scoring Horizon è di ${d.sc.total}/100 — ${vrd}.
La leva finanziaria di ${lev} e la liquidità disponibile di ${fmtE(d.liq)} definiscono il profilo di rischio attuale. Il costo del personale incide per il ${fmtP(d.persPct)} del fatturato, vs una media di settore del ${d.bench.personale_pct}%.
Lo scenario identificato è ${d.stratKey}. ${d.strat.obj} L'obiettivo dichiarato — ${d.obj} — con orizzonte ${d.oriz} guiderà le priorità di intervento.
`;
document.getElementById('r-diag').innerHTML = `La diagnosi evidenzia i principali punti di attenzione per ${d.ragione} nel contesto del settore ${d.settore}. Il confronto con i benchmark di riferimento indica aree di miglioramento prioritarie che il piano operativo affronterà sistematicamente.
Le raccomandazioni operative nelle sezioni successive sono calibrate sull'obiettivo dichiarato e sui dati economici analizzati. Si raccomanda di avviare subito le azioni ad alta priorità identificate nella sezione 12.
`;
// Fill piano with static fallback
const staticActions = [
[['Audit costi P/S/N||Mappare ogni voce di costo classificandola come Produttiva, di Supporto o Non produttiva. Definire subito le azioni sui costi N.','Analisi clienti per marginalità||Calcolare il margine per singolo cliente o segmento. Identificare i clienti con marginalità negativa o sotto il 5%.','Analisi debito e liquidità||Costruire il quadro completo dell\'esposizione bancaria e del fabbisogno di cassa per i prossimi 90 giorni.']],
[['Interventi su costi prioritari||Avviare le azioni sui costi identificati nella fase di audit. Prioritizzare quelle con impatto immediato sul conto economico.','Rinegoziazione fornitori||Aprire trattative con i top 5 fornitori per riduzione prezzi o dilazione termini. Obiettivo: -10/15% sulla spesa.','Revisione pricing e marginalità||Rivedere i listini prezzi sui clienti a bassa marginalità. Implementare scontistica basata sul valore, non sul volume.']],
[['Implementazione interventi commerciali||Avviare le azioni commerciali pianificate: upselling, cross-selling, acquisizione nuovi clienti nel target identificato.','Strutturazione cash flow||Implementare un report di tesoreria rolling a 13 settimane. Alert automatici sotto soglia critica.','Coperture assicurative||Verificare adeguatezza delle polizze esistenti. Attivare le coperture mancanti in base al profilo di rischio rilevato.']],
[['Monitoraggio KPI mensile||Istituire una review mensile dei KPI con il management. Definire soglie di allerta e piani di azione predefiniti per le deviazioni.','Ottimizzazione struttura||Consolidare i miglioramenti ottenuti. Standardizzare i processi più efficienti. Eliminare le attività residue a basso valore aggiunto.','Pianificazione fase successiva||Sulla base dei risultati ottenuti, definire gli obiettivi e il piano per il prossimo esercizio.']],
];
staticActions[0].forEach((actions, pi) => {
actions.forEach((ac, ai) => {
// Already rendered as skeleton
});
});
[0,1,2,3].forEach(pi => {
const actions = pi < staticActions.length ? staticActions[pi][0] : [];
renderPhase(pi, actions.join('|||'));
});
const racFallback = [
{ p:'ALTA', t:'Avviare audit costi P/S/N completo', d:`Con un margine del ${fmtP(d.margine*100)} vs ${d.bench.margine}% di settore, l'audit costi è il punto di partenza obbligato. Classificare ogni voce come Produttiva, di Supporto o Non produttiva entro 30 giorni.` },
{ p:'ALTA', t:'Analizzare marginalità per singolo cliente', d:'Calcolare il margine netto per cliente o segmento di clientela. Identificare immediatamente i clienti in perdita e pianificare la revisione dei termini commerciali.' },
{ p:'MEDIA', t:'Ottimizzare il ciclo del capitale circolante', d:`Con ${d.dso ? d.dso + ' giorni di DSO' : 'crediti commerciali da ottimizzare'}, una riduzione del 20% nei giorni di incasso libera liquidità immediata. Implementare politiche di incasso strutturate.` },
{ p:'MEDIA', t:'Rivedere la struttura dei costi fissi', d:`I costi fissi incidono significativamente sulla struttura. Identificare i contratti in scadenza e le opportunità di rinegoziazione. Focus su affitti, leasing e consulenze ricorrenti.` },
{ p:'BASSA', t:'Valutare ottimizzazione fiscale appropriata', d:`In base allo scenario ${d.stratKey}, la strategia fiscale ${d.strat.fiscal} è quella più adatta. Pianificare con il commercialista le azioni da implementare nel prossimo esercizio.` },
{ p:'BASSA', t:'Implementare dashboard KPI mensile', d:'Istituire una reportistica mensile con i KPI chiave del business. La misurazione sistematica è il prerequisito per qualsiasi miglioramento strutturale.' },
];
racFallback.forEach((r, i) => renderRac(i, `${r.p}||${r.t}||${r.d}`));
}
/* ══════════════════════════════════════════
SAVE / LOAD / RESTART
══════════════════════════════════════════ */
function saveHTML() {
const ragione = document.getElementById('r-co')?.textContent || 'report';
const styles = Array.from(document.styleSheets).map(s => {
try { return Array.from(s.cssRules).map(r => r.cssText).join(''); } catch(e) { return ''; }
}).join('');
const fullHtml = `
Report Horizon — ${ragione}
${document.getElementById('report-page').outerHTML}`;
const blob = new Blob([fullHtml], { type: 'text/html' });
const a = document.createElement('a');
a.href = URL.createObjectURL(blob);
a.download = `Horizon_Report_${ragione.replace(/[^a-zA-Z0-9]/g,'_')}_${new Date().toISOString().slice(0,10)}.html`;
a.click();
URL.revokeObjectURL(a.href);
}
function loadHTML(file) {
const reader = new FileReader();
reader.onload = ev => {
try {
const html = ev.target.result;
// Extract body content between tags
const bodyMatch = html.match(/]*>([\s\S]*)<\/body>/i);
const bodyContent = bodyMatch ? bodyMatch[1] : html;
// Parse it
const parser = new DOMParser();
const doc = parser.parseFromString(html, 'text/html');
const rptEl = doc.getElementById('report-page');
if (!rptEl) { alert('File non riconosciuto.'); return; }
// Inject content
const target = document.getElementById('report-page');
target.innerHTML = rptEl.innerHTML;
// Force visible with !important via setAttribute
// Use body class to override CSS cleanly
document.body.classList.add('report-loaded');
target.removeAttribute('style');
document.getElementById('wizard').removeAttribute('style');
// Show FABs
document.getElementById('fab-group').classList.add('on');
// Force all sections visible
target.querySelectorAll('.rs').forEach(s => {
s.setAttribute('style', 'opacity:1;transform:none');
});
// Re-attach accordion
target.querySelectorAll('.phase-hdr').forEach(hdr => {
hdr.addEventListener('click', () => hdr.closest('.phase-block')?.classList.toggle('open'));
});
// Re-attach restart
const rb = document.getElementById('btn-restart');
if (rb) rb.addEventListener('click', restartWizard);
window.scrollTo({ top:0 });
} catch(e) { alert('Errore: ' + e.message); }
};
reader.onerror = () => alert('Errore lettura file.');
reader.readAsText(file);
}
function restartWizard() {
document.getElementById('report-page').style.display = 'none';
document.getElementById('fab-group').classList.remove('on');
document.getElementById('wizard').style.display = 'block';
for (let i = 1; i <= 4; i++) {
const ps = document.getElementById('ps' + i);
ps.classList.remove('active', 'done');
}
curStep = 1;
for (let i = 1; i <= 4; i++) document.getElementById('step' + i).classList.remove('active');
document.getElementById('step1').classList.add('active');
document.getElementById('ps1').classList.add('active');
document.querySelectorAll('.rs').forEach(s => s.classList.remove('in'));
S = { pc: null, obj: null, tf: null, tm: null };
document.querySelectorAll('.card-opt,.press-opt').forEach(e => e.classList.remove('sel'));
document.getElementById('btn-gen').disabled = false;
document.getElementById('import-zone').classList.remove('success');
document.getElementById('import-status').textContent = '';
window.scrollTo({ top: 0 });
}
/* ══════════════════════════════════════════
DOM READY — EVENT LISTENERS
══════════════════════════════════════════ */
document.addEventListener('DOMContentLoaded', function() {
// Auto-calc on input
['f_fatt','f_cv','f_personale','f_affitti','f_altri_cf','f_amm','f_deb'].forEach(id => {
const el = document.getElementById(id);
if (el) el.addEventListener('input', recalcEbitda);
});
['f_fatt','f_cv','f_personale','f_affitti','f_altri_cf','f_pfatt','f_pcv','f_ppersonale','f_paffitti','f_paltri','f_pdeb'].forEach(id => {
const el = document.getElementById(id);
if (el) el.addEventListener('input', recalcTrend);
});
// Wizard nav
const bindings = [
['btn-next-1', () => goStep(2)],
['btn-back-2', () => goStep(1)],
['btn-next-2', () => goStep(3)],
['btn-back-3', () => goStep(2)],
['btn-next-3', () => goStep(4)],
['btn-back-4', () => goStep(3)],
['btn-gen', () => startGeneration()],
['btn-restart',() => restartWizard()],
['btn-print', () => window.print()],
['btn-save-html', () => saveHTML()],
['logo-btn', () => { if (confirm('Tornare alla home?')) location.reload(); }],
];
bindings.forEach(([id, fn]) => {
const el = document.getElementById(id);
if (el) el.addEventListener('click', fn);
});
// Card opts (objective)
document.addEventListener('click', e => {
const card = e.target.closest('.card-opt');
if (card) {
document.querySelectorAll('.card-opt').forEach(c => c.classList.remove('sel'));
card.classList.add('sel');
S.obj = card.dataset.v;
}
const press = e.target.closest('.press-opt');
if (press) {
document.querySelectorAll('.press-opt').forEach(p => p.classList.remove('sel'));
press.classList.add('sel');
S.pc = press.dataset.v;
}
const hdr = e.target.closest('.phase-hdr');
if (hdr) {
const block = hdr.closest('.phase-block');
if (block) block.classList.toggle('open');
}
});
// Import zone
const importZone = document.getElementById('import-zone');
const importFile = document.getElementById('import-file');
if (importZone) {
importZone.addEventListener('click', () => importFile.click());
importZone.addEventListener('dragover', e => { e.preventDefault(); importZone.classList.add('drag'); });
importZone.addEventListener('dragleave', () => importZone.classList.remove('drag'));
importZone.addEventListener('drop', e => {
e.preventDefault(); importZone.classList.remove('drag');
const file = e.dataTransfer.files[0];
if (file) handleImportFile(file);
});
}
if (importFile) {
importFile.addEventListener('change', e => {
const file = e.target.files[0];
if (file) handleImportFile(file);
e.target.value = '';
});
}
// Load HTML report
const loadInput = document.getElementById('load-html-input');
if (loadInput) {
loadInput.addEventListener('change', e => {
const file = e.target.files[0];
if (file) loadHTML(file);
// Reset after reader starts, not before
setTimeout(() => { e.target.value = ''; }, 100);
});
}
});
function handleImportFile(file) {
const ext = file.name.split('.').pop().toLowerCase();
if (ext === 'pdf') importPDF(file);
else if (ext === 'xlsx' || ext === 'xls') importExcel(file);
else setImportStatus('err', 'Formato non supportato. Usa PDF o Excel.');
}