Compare commits
8 Commits
43238341c1
...
044a056480
| Author | SHA1 | Date | |
|---|---|---|---|
| 044a056480 | |||
| afe4274de9 | |||
| c00ec25e48 | |||
| 268f56ccd2 | |||
| 14bdbab880 | |||
| 7d884099aa | |||
| b3670d164b | |||
| 10a390239a |
+37
-24
@@ -8,15 +8,20 @@ Suite operativa per un consulente di personal branding, live su hub.iamcavalli.n
|
||||
|
||||
Il cliente apre il link e vede esattamente a che punto è il suo progetto, cosa deve ancora succedere e cosa ha già approvato — senza dover scrivere email per chiedere aggiornamenti.
|
||||
|
||||
## Current Milestone: v2.1 Offer Studio + Proposal AI
|
||||
## Current Milestone: v2.2 Sales Loop *(reset 2026-06-19)*
|
||||
|
||||
**Goal:** Trasformare catalogo/offerte in una UX rapida stile Notion/Airtable (inline edit, tag, drag-to-compose), ristrutturare i tab del workspace progetto, ricostruire il CRM in stile Attio, poi chiudere con l'AI proposal builder → pagina pubblica → link pagamento.
|
||||
**Contesto reset:** v2.1 ha consegnato Offer Studio (catalogo + offer editor) e il CRM Attio. Dopo aver articolato il flusso commerciale reale, le fasi residue v2.1 (13/15/16/17) sono congelate/abbandonate/ri-scopate. Piano completo: `.claude/plans/glittery-sprouting-pudding.md`.
|
||||
|
||||
**Target features:**
|
||||
- **Catalogo & Offerte (Offer Studio)** — vista database stile Notion/Airtable per servizi e offerte: inline editing, tag multi-select con creazione al volo, quick-add, filtri/ricerca istantanei, composizione offerta via drag&drop (`@dnd-kit`) con totale live, import CSV one-shot per seed
|
||||
- **Workspace progetto** — rimuovere tab "Preventivo" (UI only, `quote_items` intatto — `accepted_total` resta editabile via Payments, zero perdita funzionale verificata); nuova tab "Servizi attivi" che assorbe "Offerte" (one-shot + ricorrenti mensili, schema per tracking incassi mese-per-mese); rimuovere pagina "Forecast", statistiche revenue mensile nella Dashboard
|
||||
- **CRM custom Attio-style** — tabelle/viste con inline editing, tag, filtri; assorbe i fix minori noti (FollowUpWidget IT, LeadForm types, SendQuoteModal flow)
|
||||
- **Proposal AI** — preventivo builder AI (pesca cliente + offerta + info extra) → pagina pubblica HTML/CSS redesign → link pagamento (Stripe Payment Link su offerta); assorbe ex-Phase 11 (auto-provisioning al "Vinto": crea cliente+progetto+fasi+pagamenti) ed ex-Phase 12 (notifiche email Resend per invio link)
|
||||
**Goal:** Chiudere il **loop di vendita** end-to-end: lead in pipeline Kanban (Pipedrive-style) → transcript datati delle call per cliente → agente AI (io scelgo l'offerta, l'AI personalizza) che genera il preventivo → pagina pubblica `/preventivo/[slug]` + invio email → vinto/perso manuale nel CRM. Il portale cliente post-vendita resta invariato.
|
||||
|
||||
**Target features (Phase 18→22 / R1→R5):**
|
||||
- **R1 Cleanup** — rimuovere Forecast e quote builder manuale; fondere `/admin/analytics` nella Dashboard (no doppioni)
|
||||
- **R2 Pipeline Kanban** — board lead stile Pipedrive con drag-drop tra stage; vinto/perso = cambio-colonna manuale
|
||||
- **R3 Knowledge Base** — schema + UI per transcript datati multipli per lead/cliente
|
||||
- **R4 Agente AI** — builder: admin sceglie cliente+offerta, l'AI (Claude) legge transcript + offerta → bozza preventivo; admin revisiona
|
||||
- **R5 Pagina pubblica** — `/preventivo/[slug]` HTML/CSS, accept/reject → CRM, invio link via email (Resend)
|
||||
|
||||
**Backlog post-R5** (ex Proposal AI v2.1): Stripe Payment Link su offerta (PROP-03) + auto-provisioning cliente/progetto/fasi/pagamenti al "Vinto" (PROP-04). Phase 13 (Servizi attivi/ricorrenti post-vendita) congelata, ripescabile.
|
||||
|
||||
## Requirements
|
||||
|
||||
@@ -43,29 +48,37 @@ Shipped in v2.0 (Phases 7–10, in produzione su hub.iamcavalli.net):
|
||||
- ✓ Preventivo pubblico `/quote/[token]`: stati draft→sent→viewed→accepted/rejected, `accepted_at` immutabile, raccolta email/note cliente — Phase 9
|
||||
- ✓ CRM pipeline lead: stati, activity log, reminder follow-up in dashboard — Phase 10
|
||||
|
||||
Validated in v2.1 (in corso):
|
||||
Validated in v2.1 (consegnato, in prod):
|
||||
|
||||
- ✓ Catalogo `services` come vista database (inline edit, tag multi-select, quick-add, ricerca istantanea) + consolidamento legacy — Phase 11 (OFFER-07..10, OFFER-13)
|
||||
- ✓ Offer Editor: lista filtrabile/archiviabile + 3 tier A/B/C via matrice checkbox, totale live, prezzo pubblico manuale, tag 4-dimensioni, promessa di trasformazione — Phase 12 (OFFER-11, OFFER-15..18). 55 servizi reali + tag offerta caricati.
|
||||
- ✓ CRM custom Attio-style: `/admin/leads` ridisegnata come tabella inline-edit (status, next_action, ecc.) + tag multi-select con creazione al volo; FollowUpWidget interamente in italiano; `LeadForm` tipizzato senza `useForm<any>`; `SendQuoteModal`/`assignQuoteToLead` senza rami di codice irraggiungibili — Phase 14 (CRM-08, CRM-09, CRM-10, CRM-11, CRM-12)
|
||||
|
||||
### Active
|
||||
### Active — v2.2 Sales Loop (reset 2026-06-19)
|
||||
|
||||
**Catalogo & Offerte — Offer Studio (priorità 1):**
|
||||
- [ ] Vista database stile Notion/Airtable per servizi e offerte: inline editing, tag multi-select con creazione al volo, quick-add, filtri/ricerca istantanei
|
||||
- [ ] Composizione offerta via drag&drop servizi dal catalogo (`@dnd-kit`) con totale live
|
||||
- [ ] Import CSV one-shot per seed pacchetti esistenti (porta d'ingresso, non fonte di verità)
|
||||
- [ ] Consolidamento finale `service_catalog` + `offer_services` legacy → `services` (carry-over da v2.0)
|
||||
**R1 Cleanup & Consolidamento (Phase 18):**
|
||||
- [ ] Rimuovere pagina Forecast (`/admin/forecast`) + voce sidebar (CLEAN-01)
|
||||
- [ ] Rimuovere quote builder manuale (`/admin/quotes/new`) (CLEAN-02)
|
||||
- [ ] Fondere `/admin/analytics` nella Dashboard `/admin`, eliminare la rotta duplicata (CLEAN-03)
|
||||
|
||||
**Workspace progetto (priorità 2):**
|
||||
- [ ] Rimuovere tab "Preventivo" (solo UI — `quote_items` resta intatto, `accepted_total` già editabile via Payments)
|
||||
- [ ] Nuova tab "Servizi attivi" che assorbe "Offerte": offerte one-shot + ricorrenti mensili
|
||||
- [ ] Schema per tracking incassi mese-per-mese sulle offerte ricorrenti (UI può arrivare dopo)
|
||||
- [ ] Rimuovere pagina "Forecast"; statistiche revenue mensile nella Dashboard (UI dopo mockup utente)
|
||||
**R2 Pipeline CRM Kanban (Phase 19):**
|
||||
- [ ] Board lead stile Pipedrive con colonne per stage + drag-drop (PIPE-01)
|
||||
- [ ] Vinto/perso come cambio-colonna manuale (PIPE-02)
|
||||
|
||||
**Proposal AI (priorità 3, ultimo miglio):**
|
||||
- [ ] Preventivo Builder AI: genera proposta da cliente + offerta + info extra fornite dall'utente
|
||||
- [ ] Redesign pagina pubblica preventivo (HTML/CSS) con link pagamento (Stripe Payment Link su offerta)
|
||||
- [ ] Auto-provisioning al "Vinto"/accettazione: crea cliente + progetto + fasi copiate + 1-4 pagamenti (ex-Phase 11)
|
||||
- [ ] Notifiche email per invio link preventivo/proposta (ex-Phase 12, Resend)
|
||||
**R3 Knowledge Base Cliente (Phase 20):**
|
||||
- [ ] Schema additivo transcript datati multipli per lead/cliente (KB-01)
|
||||
- [ ] UI per incollare/elencare transcript nel profilo (KB-02)
|
||||
|
||||
**R4 Agente AI — generazione preventivo (Phase 21):**
|
||||
- [ ] Builder: admin sceglie cliente+offerta, l'AI (Claude) legge transcript + offerta → bozza preventivo (AI-01)
|
||||
- [ ] Admin rivede/edita la bozza prima di pubblicare (AI-02)
|
||||
|
||||
**R5 Pagina pubblica `/preventivo/[slug]` + email (Phase 22):**
|
||||
- [ ] Pagina pubblica HTML/CSS della proposta (PUB-01)
|
||||
- [ ] Accept/reject → riflesso nel CRM (PUB-02)
|
||||
- [ ] Invio link proposta via email Resend (PUB-03)
|
||||
|
||||
**Backlog (post-R5):** Stripe Payment Link su offerta (PROP-03), auto-provisioning al "Vinto" (PROP-04), Servizi attivi/ricorrenti post-vendita (ex Phase 13).
|
||||
|
||||
### Out of Scope
|
||||
|
||||
|
||||
+91
-28
@@ -1,10 +1,12 @@
|
||||
# Requirements — ClientHub v2.1 Offer Studio + Proposal AI
|
||||
# Requirements — ClientHub (v2.1 chiuso → v2.2 Sales Loop)
|
||||
|
||||
**Defined:** 2026-06-13
|
||||
**Defined:** 2026-06-13 · **Reset:** 2026-06-19
|
||||
**Core Value:** Il cliente apre il link e vede esattamente a che punto è il suo progetto, cosa deve ancora succedere e cosa ha già approvato — senza dover scrivere email per chiedere aggiornamenti.
|
||||
|
||||
v2.0 (Phases 7–10) ha consegnato catalogo unificato, offerte con fasi, preventivo pubblico e CRM pipeline (vedi MILESTONES.md, REQUIREMENTS.md archiviato in `.planning/milestones/v2.0-REQUIREMENTS.md`). v2.1 rifà la UX di catalogo/offerte in stile database-view, ristruttura il workspace progetto, ricostruisce il CRM in stile Attio e chiude il loop commerciale con un builder preventivi AI.
|
||||
|
||||
> **RESET 2026-06-19 — milestone v2.2 "Sales Loop".** v2.1 ha consegnato Offer Studio (Phase 11+12) e CRM Attio (Phase 14). Le fasi residue v2.1 — **13 (Servizi Attivi), 15 (Dashboard Revenue), 16/17 (Proposal AI originali)** — sono state **congelate/abbandonate/ri-scopate** dopo che l'utente ha articolato il flusso commerciale reale (vedi piano `.claude/plans/glittery-sprouting-pudding.md`). Il nuovo north-star è il **loop di vendita**: lead in pipeline Kanban → transcript call → agente AI genera preventivo → pagina pubblica `/preventivo/[slug]` → vinto/perso. Requirements attivi: sezione **v2.2** più sotto.
|
||||
|
||||
## Offer Studio — Catalogo & Offerte (v1)
|
||||
|
||||
| ID | Requirement | Status |
|
||||
@@ -20,15 +22,17 @@ v2.0 (Phases 7–10) ha consegnato catalogo unificato, offerte con fasi, prevent
|
||||
| OFFER-18 | La lista offerte è filtrabile per categoria e supporta l'archiviazione (mostra/nascondi archiviate) | Complete |
|
||||
| OFFER-13 | I dati legacy di `service_catalog` e `offer_services` sono consolidati in `services` senza perdita (migration additiva) | Complete |
|
||||
|
||||
## Workspace Progetto — Servizi Attivi (v1)
|
||||
## Workspace Progetto — Servizi Attivi (ex Phase 13 — CONGELATO v2.2)
|
||||
|
||||
> Congelato dal reset 2026-06-19: tocca il portale post-vendita che l'utente vuole lasciare invariato. Ripescabile in futuro se serve il tracking ricavi ricorrenti. (PROJ-10 "rimuovi Forecast" è confluito in CLEAN-01 v2.2.)
|
||||
|
||||
| ID | Requirement | Status |
|
||||
|----|-------------|--------|
|
||||
| PROJ-06 | La tab "Preventivo" è rimossa dal workspace progetto (solo UI — `quote_items` resta intatta, `accepted_total` resta editabile via tab Pagamenti) | Pending |
|
||||
| PROJ-07 | La tab "Offerte" diventa "Servizi attivi": mostra tutte le offerte assegnate al progetto (`project_offers`) | Pending |
|
||||
| PROJ-08 | L'utente marca ogni "servizio attivo" come one-shot o ricorrente mensile | Pending |
|
||||
| PROJ-09 | Per i servizi ricorrenti, l'utente registra in quali mesi è stata incassata la fattura (schema nuovo, additivo — UI può essere minimale) | Pending |
|
||||
| PROJ-10 | La pagina "Forecast" e la relativa voce di sidebar sono rimosse | Pending |
|
||||
| PROJ-06 | La tab "Preventivo" è rimossa dal workspace progetto (solo UI — `quote_items` resta intatta, `accepted_total` resta editabile via tab Pagamenti) | Congelato (v2.2) |
|
||||
| PROJ-07 | La tab "Offerte" diventa "Servizi attivi": mostra tutte le offerte assegnate al progetto (`project_offers`) | Congelato (v2.2) |
|
||||
| PROJ-08 | L'utente marca ogni "servizio attivo" come one-shot o ricorrente mensile | Congelato (v2.2) |
|
||||
| PROJ-09 | Per i servizi ricorrenti, l'utente registra in quali mesi è stata incassata la fattura (schema nuovo, additivo — UI può essere minimale) | Congelato (v2.2) |
|
||||
| PROJ-10 | La pagina "Forecast" e la relativa voce di sidebar sono rimosse | → CLEAN-01 (v2.2) |
|
||||
|
||||
## CRM Custom Attio-style (v1)
|
||||
|
||||
@@ -40,21 +44,67 @@ v2.0 (Phases 7–10) ha consegnato catalogo unificato, offerte con fasi, prevent
|
||||
| CRM-11 | Il form lead (`LeadForm`) valida i campi senza workaround di type-relaxation su react-hook-form | Complete |
|
||||
| CRM-12 | Il flusso `SendQuoteModal` (preventivo esistente vs nuovo) non contiene rami di codice irraggiungibili | Complete |
|
||||
|
||||
## Dashboard — Revenue Stats (v1, bloccato su mockup utente)
|
||||
## Dashboard — Revenue Stats (ex Phase 15 — ABBANDONATO v2.2)
|
||||
|
||||
> Abbandonato dal reset 2026-06-19: mockup mai arrivato, valore dubbio nel nuovo flusso. L'esigenza analytics è coperta da CLEAN-03 (fusione `/admin/analytics` nella dashboard, no fase dedicata).
|
||||
|
||||
| ID | Requirement | Status |
|
||||
|----|-------------|--------|
|
||||
| DASH-11 | La Dashboard admin mostra le statistiche di revenue mensile (ex `/admin/forecast`, calcolate da `project_offers`) | Blocked — attesa mockup dashboard utente |
|
||||
| DASH-11 | La Dashboard admin mostra le statistiche di revenue mensile (ex `/admin/forecast`, calcolate da `project_offers`) | Dropped (v2.2 → CLEAN-03) |
|
||||
|
||||
## Proposal AI (v1)
|
||||
## Proposal AI — versione v2.1 originale (SUPERSEDED da v2.2)
|
||||
|
||||
> Ri-scopata dal reset 2026-06-19 nelle fasi v2.2 (PIPE/KB/AI/PUB) con il flusso chiaro + l'aggiunta dei transcript (che mancava). PROP-03 (Stripe Payment Link) e PROP-04 (auto-provisioning al "Vinto") restano backlog: non nel loop minimo, da rivalutare dopo R5.
|
||||
|
||||
| ID | Requirement | Status |
|
||||
|----|-------------|--------|
|
||||
| PROP-01 | L'admin avvia un builder AI che genera una proposta a partire da cliente + offerta(e) selezionata(e) + note extra fornite manualmente | Pending |
|
||||
| PROP-02 | La pagina pubblica `/quote/[token]` è ridisegnata (HTML/CSS) per presentare la proposta generata | Pending |
|
||||
| PROP-03 | Ogni offerta/preventivo ha un campo "link di pagamento" (Stripe Payment Link) mostrato come CTA nella pagina pubblica | Pending |
|
||||
| PROP-04 | All'accettazione del preventivo, il sistema crea automaticamente cliente + progetto, copia le fasi dall'offerta e configura 1-4 pagamenti (ex-Phase 11 WIN-01..06) | Pending |
|
||||
| PROP-05 | Il sistema invia via email (Resend) il link della proposta pubblica al lead (ex-Phase 12) | Pending |
|
||||
| PROP-01 | L'admin avvia un builder AI che genera una proposta a partire da cliente + offerta(e) selezionata(e) + note extra fornite manualmente | Superseded → AI-01/AI-02 (v2.2) |
|
||||
| PROP-02 | La pagina pubblica `/quote/[token]` è ridisegnata (HTML/CSS) per presentare la proposta generata | Superseded → PUB-01 (v2.2) |
|
||||
| PROP-03 | Ogni offerta/preventivo ha un campo "link di pagamento" (Stripe Payment Link) mostrato come CTA nella pagina pubblica | Backlog (post-R5) |
|
||||
| PROP-04 | All'accettazione del preventivo, il sistema crea automaticamente cliente + progetto, copia le fasi dall'offerta e configura 1-4 pagamenti (ex-Phase 11 WIN-01..06) | Backlog (post-R5) |
|
||||
| PROP-05 | Il sistema invia via email (Resend) il link della proposta pubblica al lead (ex-Phase 12) | Superseded → PUB-03 (v2.2) |
|
||||
|
||||
## v2.2 — Sales Loop: Lead → AI → Preventivo (ATTIVO)
|
||||
|
||||
> Reset 2026-06-19. Il flusso commerciale articolato dall'utente sostituisce le fasi residue v2.1. Fasi eseguite in ordine R1→R5 (numerazione GSD continua: Phase 18→22). Le fasi 11/12/14 restano completate.
|
||||
|
||||
### Cleanup & Consolidamento (Phase 18 / R1)
|
||||
|
||||
| ID | Requirement | Status |
|
||||
|----|-------------|--------|
|
||||
| CLEAN-01 | La pagina Forecast (`/admin/forecast`) e la sua voce di sidebar sono rimosse | Complete |
|
||||
| CLEAN-02 | Il quote builder manuale (`/admin/quotes/new`) e il suo entry point sono rimossi (superato dall'agente AI) | Complete |
|
||||
| CLEAN-03 | Le statistiche di `/admin/analytics` sono fuse nella Dashboard `/admin` (vista unica); la rotta duplicata è eliminata | Complete |
|
||||
| CLEAN-04 | Le fasi v2.1 residue (13/15/16/17) sono archiviate/ri-scopate nel planning | Complete |
|
||||
|
||||
### Pipeline CRM Kanban (Phase 19 / R2)
|
||||
|
||||
| ID | Requirement | Status |
|
||||
|----|-------------|--------|
|
||||
| PIPE-01 | I lead sono visualizzabili in una board Kanban stile Pipedrive con colonne per stage e drag-drop per cambiare stage | Pending |
|
||||
| PIPE-02 | Spostare un lead nelle colonne "Vinto"/"Perso" è il cambio-stato manuale dell'esito | Pending |
|
||||
|
||||
### Knowledge Base Cliente — Transcript (Phase 20 / R3)
|
||||
|
||||
| ID | Requirement | Status |
|
||||
|----|-------------|--------|
|
||||
| KB-01 | Schema additivo per memorizzare transcript datati e multipli per lead/cliente | Pending |
|
||||
| KB-02 | L'admin incolla ed elenca i transcript (storico ordinato per data) nel dettaglio lead/cliente | Pending |
|
||||
|
||||
### Agente AI — Generazione preventivo (Phase 21 / R4)
|
||||
|
||||
| ID | Requirement | Status |
|
||||
|----|-------------|--------|
|
||||
| AI-01 | L'admin seleziona cliente + offerta; l'agente AI (Claude) legge i transcript + i dati dell'offerta e genera una bozza di preventivo personalizzata | Pending |
|
||||
| AI-02 | L'admin rivede ed edita la bozza generata prima di pubblicarla | Pending |
|
||||
|
||||
### Pagina pubblica preventivo + email (Phase 22 / R5)
|
||||
|
||||
| ID | Requirement | Status |
|
||||
|----|-------------|--------|
|
||||
| PUB-01 | La proposta generata è pubblicata come pagina pubblica HTML/CSS a `/preventivo/[slug]` (evolve la meccanica di `/quote/[token]`) | Pending |
|
||||
| PUB-02 | Il cliente accetta/rifiuta dalla pagina; l'esito si riflette nel CRM (conferma vinto/perso manuale lato admin) | Pending |
|
||||
| PUB-03 | Il link della proposta viene inviato al lead via email (Resend) | Pending |
|
||||
|
||||
## v2 Requirements (Deferred)
|
||||
|
||||
@@ -101,23 +151,36 @@ Popolata dal roadmapper durante la creazione della roadmap.
|
||||
| OFFER-17 | Phase 12 | Complete |
|
||||
| OFFER-18 | Phase 12 | Complete |
|
||||
| OFFER-12 | Deferred (v2) | Rimandato 2026-06-14 |
|
||||
| PROJ-06 | Phase 13 | Pending |
|
||||
| PROJ-07 | Phase 13 | Pending |
|
||||
| PROJ-08 | Phase 13 | Pending |
|
||||
| PROJ-09 | Phase 13 | Pending |
|
||||
| PROJ-10 | Phase 13 | Pending |
|
||||
| PROJ-06 | Phase 13 | Congelato (v2.2) |
|
||||
| PROJ-07 | Phase 13 | Congelato (v2.2) |
|
||||
| PROJ-08 | Phase 13 | Congelato (v2.2) |
|
||||
| PROJ-09 | Phase 13 | Congelato (v2.2) |
|
||||
| PROJ-10 | Phase 13 | → CLEAN-01 |
|
||||
| CRM-08 | Phase 14 | Complete |
|
||||
| CRM-09 | Phase 14 | Complete |
|
||||
| CRM-10 | Phase 14 | Complete |
|
||||
| CRM-11 | Phase 14 | Complete |
|
||||
| CRM-12 | Phase 14 | Complete |
|
||||
| DASH-11 | Phase 15 | Blocked |
|
||||
| PROP-03 | Phase 16 | Pending |
|
||||
| PROP-04 | Phase 16 | Pending |
|
||||
| PROP-01 | Phase 17 | Pending |
|
||||
| PROP-02 | Phase 17 | Pending |
|
||||
| PROP-05 | Phase 17 | Pending |
|
||||
| DASH-11 | Phase 15 | Dropped (→ CLEAN-03) |
|
||||
| PROP-03 | Phase 16 | Backlog (post-R5) |
|
||||
| PROP-04 | Phase 16 | Backlog (post-R5) |
|
||||
| PROP-01 | Phase 17 | Superseded → AI-01/02 |
|
||||
| PROP-02 | Phase 17 | Superseded → PUB-01 |
|
||||
| PROP-05 | Phase 17 | Superseded → PUB-03 |
|
||||
| CLEAN-01 | Phase 18 (R1) | Complete |
|
||||
| CLEAN-02 | Phase 18 (R1) | Complete |
|
||||
| CLEAN-03 | Phase 18 (R1) | Complete |
|
||||
| CLEAN-04 | Phase 18 (R1) | Complete |
|
||||
| PIPE-01 | Phase 19 (R2) | Pending |
|
||||
| PIPE-02 | Phase 19 (R2) | Pending |
|
||||
| KB-01 | Phase 20 (R3) | Pending |
|
||||
| KB-02 | Phase 20 (R3) | Pending |
|
||||
| AI-01 | Phase 21 (R4) | Pending |
|
||||
| AI-02 | Phase 21 (R4) | Pending |
|
||||
| PUB-01 | Phase 22 (R5) | Pending |
|
||||
| PUB-02 | Phase 22 (R5) | Pending |
|
||||
| PUB-03 | Phase 22 (R5) | Pending |
|
||||
|
||||
---
|
||||
*Requirements defined: 2026-06-13*
|
||||
*Last updated: 2026-06-18 — Phase 12 complete: OFFER-11, OFFER-15, OFFER-16, OFFER-17, OFFER-18 → Complete*
|
||||
*Last updated: 2026-06-19 — RESET v2.2 "Sales Loop": Phase 13 congelata, 15 abbandonata, 16/17 ri-scopate; nuovi requirements CLEAN/PIPE/KB/AI/PUB (Phase 18→22)*
|
||||
|
||||
+93
-13
@@ -1,23 +1,32 @@
|
||||
# Roadmap: ClientHub v2.1 Offer Studio + Proposal AI
|
||||
# Roadmap: ClientHub — v2.1 (chiuso) → v2.2 Sales Loop
|
||||
|
||||
## Overview
|
||||
|
||||
**v2.0 (Phases 1–10, Complete)**: ClientHub è una suite operativa completa — catalogo servizi unificato, offerte con fasi, preventivo pubblico (`/quote/[token]`), CRM pipeline lead con activity log. Archiviato in `.planning/milestones/v2.0-ROADMAP.md`.
|
||||
|
||||
**v2.1 (Phases 11–17, Planning)**: Rifare la UX di catalogo/offerte in stile database-view (Notion-like), ristrutturare il workspace progetto (Preventivo → Servizi Attivi, rimozione Forecast), ricostruire il CRM in stile Attio/Pipedrive (inline edit, tag, fix bug residui), e chiudere il loop commerciale con un builder preventivi AI + invio email + auto-provisioning. Numerazione fasi: v2.0 ha chiuso a Phase 10; v2.1 parte da Phase 11.
|
||||
**v2.1 (Phases 11–17) — PARZIALE / CHIUSO dal reset 2026-06-19**: ha consegnato Offer Studio (Phase 11 catalogo database-view + Phase 12 offer editor tier A/B/C) e CRM Attio-style (Phase 14). Le fasi residue **13/15/16/17 sono congelate/abbandonate/ri-scopate** (vedi sotto).
|
||||
|
||||
**v2.2 (Phases 18–22, Sales Loop) — ATTIVO**: dopo che l'utente ha articolato il flusso commerciale reale (piano `.claude/plans/glittery-sprouting-pudding.md`), il focus diventa il **loop di vendita**: cleanup dei doppioni → pipeline CRM Kanban (Pipedrive-style) → knowledge base transcript per cliente → agente AI che genera il preventivo → pagina pubblica `/preventivo/[slug]` + email. Numerazione GSD continua: v2.2 parte da Phase 18. Le fasi R1–R5 mappano su Phase 18–22.
|
||||
|
||||
## Phases
|
||||
|
||||
- [x] **Phase 11: Catalog Database-View UX & Legacy Consolidation** - Catalogo `services` come tabella con inline edit, tag multi-select, quick-add, ricerca istantanea; consolidamento `service_catalog`/`offer_services` → `services`
|
||||
- [x] **Phase 12: Offer Editor — Tier A/B/C, Tag & Prezzo Pubblico** - Editor offerte completo: lista filtrabile + 3 tier (A/B/C) via matrice checkbox sui servizi `services`, totale live per tier, prezzo pubblico manuale, tag multi-dimensione, promessa di trasformazione (import CSV/Notion rimandato) (completed 2026-06-15)
|
||||
- [ ] **Phase 13: Workspace — Servizi Attivi** - Tab "Preventivo" rimossa (UI-only), "Offerte" → "Servizi attivi" (one-shot/ricorrenti + tracking incassi mensili), rimozione pagina Forecast
|
||||
- [x] **Phase 12: Offer Editor — Tier A/B/C, Tag & Prezzo Pubblico** - Editor offerte completo: lista filtrabile + 3 tier (A/B/C) via matrice checkbox sui servizi `services`, totale live per tier, prezzo pubblico manuale, tag multi-dimensione, promessa di trasformazione (completed 2026-06-18)
|
||||
- [~] **Phase 13: Workspace — Servizi Attivi** - ❌ CONGELATA (reset 2026-06-19): tocca il post-vendita lasciato invariato; ripescabile in futuro
|
||||
- [x] **Phase 14: CRM Attio-style & Fix** - Redesign `/admin/leads` stile Attio/Pipedrive (inline edit + tag), fix FollowUpWidget IT / LeadForm types / SendQuoteModal rami irraggiungibili (completed 2026-06-14)
|
||||
- [ ] **Phase 15: Dashboard Revenue Stats** - Dashboard admin mostra statistiche revenue mensile (ex `/admin/forecast`) da `project_offers` — **BLOCCATA**, attesa mockup utente
|
||||
- [ ] **Phase 16: Proposal AI — Data Foundations & Auto-Provisioning** - Campo Stripe Payment Link su offerte/preventivi + auto-provisioning (client+progetto+fasi+pagamenti) all'accettazione
|
||||
- [ ] **Phase 17: Proposal AI — Builder, Pagina Pubblica & Email** - Builder AI proposta da cliente+offerta+note, redesign `/quote/[token]`, invio email Resend con link proposta
|
||||
- [~] **Phase 15: Dashboard Revenue Stats** - ❌ ABBANDONATA (reset 2026-06-19): mockup mai arrivato; analytics confluisce nella dashboard via CLEAN-03
|
||||
- [~] **Phase 16: Proposal AI — Data Foundations & Auto-Provisioning** - ❌ RI-SCOPATA in v2.2; Stripe Payment Link + auto-provisioning → backlog post-R5
|
||||
- [~] **Phase 17: Proposal AI — Builder, Pagina Pubblica & Email** - ❌ RI-SCOPATA in v2.2 (Phase 21/22 con transcript + flusso chiaro)
|
||||
- [ ] **Phase 18: Cleanup & Consolidamento** - Rimuovi Forecast + quote builder manuale; fondi `/admin/analytics` nella dashboard; archivia fasi residue v2.1
|
||||
- [ ] **Phase 19: Pipeline CRM Kanban** - Board lead stile Pipedrive con colonne per stage + drag-drop; vinto/perso come cambio-colonna manuale
|
||||
- [ ] **Phase 20: Knowledge Base Cliente** - Schema additivo + UI per incollare transcript datati multipli per lead/cliente
|
||||
- [ ] **Phase 21: Agente AI — generazione preventivo** - Builder: admin sceglie cliente+offerta, l'AI (Claude) legge transcript + offerta e genera bozza preventivo; admin revisiona
|
||||
- [ ] **Phase 22: Pagina pubblica preventivo + email** - Pagina pubblica HTML/CSS, accept/reject → CRM, invio link via email (Resend)
|
||||
|
||||
## Phase Details
|
||||
|
||||
> **Reset 2026-06-19**: i dettagli delle fasi 13/15/16/17 sotto sono conservati per storico ma quelle fasi sono **cancellate/congelate** (vedi checklist Phases sopra e Progress sotto). Le fasi attive sono **18–22 (R1–R5)**, in fondo a questa sezione.
|
||||
|
||||
### Phase 11: Catalog Database-View UX & Legacy Consolidation
|
||||
**Goal**: L'utente vede ed edita il catalogo servizi come una tabella stile database (Notion-like) — inline editing, tag multi-select, quick-add, ricerca istantanea — su un'unica tabella `services` consolidata senza perdita di dati legacy
|
||||
**Mode:** migration + ui
|
||||
@@ -137,14 +146,85 @@ Plans:
|
||||
**UI hint**: yes
|
||||
**Status**: Pending planning
|
||||
|
||||
---
|
||||
|
||||
## v2.2 — Sales Loop (fasi attive)
|
||||
|
||||
### Phase 18: Cleanup & Consolidamento
|
||||
**Goal**: Sgomberare il campo dai doppioni e dal peso morto prima di costruire il loop di vendita, senza perdere dati.
|
||||
**Mode:** ui (solo rimozioni/rotte, nessun drop di tabelle)
|
||||
**Depends on**: nessuna (può partire subito)
|
||||
**Requirements**: CLEAN-01, CLEAN-02, CLEAN-03, CLEAN-04
|
||||
**Success Criteria** (what must be TRUE):
|
||||
1. La pagina Forecast (`/admin/forecast`) e la sua voce di sidebar non esistono più
|
||||
2. Il quote builder manuale (`/admin/quotes/new`) e il suo entry point non esistono più
|
||||
3. Le statistiche annuali di `/admin/analytics` sono visibili nella Dashboard `/admin` come vista unica; la rotta `/admin/analytics` è eliminata (nessun doppione)
|
||||
4. Le fasi v2.1 residue (13/15/16/17) sono marcate cancellate/congelate nel planning
|
||||
**Plans**: 3 plans
|
||||
**Plan list**:
|
||||
- [ ] 18-01-PLAN.md — Rimozione Forecast + Quote Builder manuale (CLEAN-01, CLEAN-02)
|
||||
- [ ] 18-02-PLAN.md — Fusione analytics nella Dashboard, eliminazione rotta /admin/analytics (CLEAN-03)
|
||||
- [ ] 18-03-PLAN.md — Verifica CLEAN-04 planning docs + build check + checkpoint visuale
|
||||
**Status**: Planned — ready for `/gsd-execute-phase 18`
|
||||
|
||||
### Phase 19: Pipeline CRM Kanban
|
||||
**Goal**: I lead si gestiscono in una board Kanban stile Pipedrive con drag-drop tra gli stage; vinto/perso è un cambio-colonna manuale.
|
||||
**Mode:** ui
|
||||
**Depends on**: Phase 14 (CRM lead esistente). Riusa `@dnd-kit` (pattern KanbanBoard progetti).
|
||||
**Requirements**: PIPE-01, PIPE-02
|
||||
**Success Criteria** (what must be TRUE):
|
||||
1. I lead sono mostrati in colonne per stage (contacted → qualified → proposal_sent → negotiating → won/lost) con drag-drop che aggiorna `leads.status`
|
||||
2. La tabella inline-edit esistente resta disponibile come vista alternativa
|
||||
3. Spostare un lead su "Vinto"/"Perso" è l'azione manuale che ne registra l'esito
|
||||
**Status**: Pending planning — `/gsd-plan-phase 19`
|
||||
|
||||
### Phase 20: Knowledge Base Cliente
|
||||
**Goal**: Memorizzare i transcript datati delle call (multipli per lead/cliente) e mostrarli nel profilo, pronti per essere dati in pasto all'AI.
|
||||
**Mode:** schema + ui
|
||||
**Depends on**: Phase 14 (lead) / clienti. Migration a prod via SSH PRIMA del codice dipendente.
|
||||
**Requirements**: KB-01, KB-02
|
||||
**Success Criteria** (what must be TRUE):
|
||||
1. Esiste uno schema additivo `client_transcripts` (lead_id/client_id, testo, data, titolo/tipo, created_at)
|
||||
2. L'admin incolla un transcript con data dal dettaglio lead/cliente
|
||||
3. I transcript sono elencati in ordine cronologico nel profilo
|
||||
**Status**: Pending planning — `/gsd-plan-phase 20`
|
||||
|
||||
### Phase 21: Agente AI — generazione preventivo
|
||||
**Goal**: L'admin seleziona cliente + offerta; l'agente AI (Claude) legge i transcript + i dati dell'offerta e genera una bozza di preventivo personalizzata, che l'admin può rivedere ed editare.
|
||||
**Mode:** ai + ui
|
||||
**Depends on**: Phase 20 (transcript) + DB Offerte (Phase 11/12, fatto)
|
||||
**Requirements**: AI-01, AI-02
|
||||
**Success Criteria** (what must be TRUE):
|
||||
1. Dal builder l'admin sceglie cliente + offerta e lancia la generazione
|
||||
2. L'AI produce una bozza di preventivo che riflette i transcript del cliente e i contenuti dell'offerta
|
||||
3. L'admin rivede/edita la bozza prima di pubblicarla
|
||||
**Note**: decisioni di design AI (modello Claude, prompt, storage output) in fase di planning dedicata
|
||||
**Status**: Pending planning — `/gsd-plan-phase 21`
|
||||
|
||||
### Phase 22: Pagina pubblica preventivo + email
|
||||
**Goal**: Pubblicare la proposta come pagina pubblica a `/preventivo/[slug]`, con accept/reject che si riflette nel CRM, e inviare il link via email.
|
||||
**Mode:** ui
|
||||
**Depends on**: Phase 21 (modello dati proposta). Evolve la meccanica di `/quote/[token]`.
|
||||
**Requirements**: PUB-01, PUB-02, PUB-03
|
||||
**Success Criteria** (what must be TRUE):
|
||||
1. La proposta generata è visibile come pagina pubblica HTML/CSS a `/preventivo/[slug]`
|
||||
2. Il cliente accetta/rifiuta dalla pagina; l'esito è disponibile lato admin per la conferma vinto/perso nel CRM
|
||||
3. Il link della proposta viene inviato al lead via email (Resend)
|
||||
**Status**: Pending planning — `/gsd-plan-phase 22`
|
||||
|
||||
## Progress
|
||||
|
||||
| Phase | Plans | Status | Completed |
|
||||
|-------|-------|--------|-----------|
|
||||
| 11. Catalog Database-View UX & Legacy Consolidation | 4/4 | ✅ Done | 2026-06-13 |
|
||||
| 12. Offer Editor — Tier A/B/C, Tag & Prezzo Pubblico | 5/5 | Complete | 2026-06-18 |
|
||||
| 13. Workspace — Servizi Attivi | — | Pending planning | — |
|
||||
| 14. CRM Attio-style & Fix | 3/3 | Complete | 2026-06-14 |
|
||||
| 15. Dashboard Revenue Stats | — | Blocked (mockup utente) | — |
|
||||
| 16. Proposal AI — Data Foundations & Auto-Provisioning | — | Pending planning | — |
|
||||
| 17. Proposal AI — Builder, Pagina Pubblica & Email | — | Pending planning | — |
|
||||
| 12. Offer Editor — Tier A/B/C, Tag & Prezzo Pubblico | 5/5 | ✅ Done | 2026-06-18 |
|
||||
| 13. Workspace — Servizi Attivi | — | ❌ Congelata (v2.2) | — |
|
||||
| 14. CRM Attio-style & Fix | 3/3 | ✅ Done | 2026-06-14 |
|
||||
| 15. Dashboard Revenue Stats | — | ❌ Abbandonata (v2.2) | — |
|
||||
| 16. Proposal AI — Data Foundations & Auto-Provisioning | — | ❌ Ri-scopata (v2.2) | — |
|
||||
| 17. Proposal AI — Builder, Pagina Pubblica & Email | — | ❌ Ri-scopata (v2.2) | — |
|
||||
| 18 (R1). Cleanup & Consolidamento | — | Pending planning | — |
|
||||
| 19 (R2). Pipeline CRM Kanban | — | Pending planning | — |
|
||||
| 20 (R3). Knowledge Base Cliente (transcript) | — | Pending planning | — |
|
||||
| 21 (R4). Agente AI — generazione preventivo | — | Pending planning | — |
|
||||
| 22 (R5). Pagina pubblica preventivo + email | — | Pending planning | — |
|
||||
|
||||
+30
-22
@@ -1,17 +1,17 @@
|
||||
---
|
||||
gsd_state_version: 1.0
|
||||
milestone: v2.1
|
||||
milestone_name: milestone
|
||||
status: completed
|
||||
stopped_at: "Phase 12 complete — Offer Editor (Tier A/B/C, Tag & Prezzo Pubblico) eseguita e verificata. Prossima: Phase 13 (Workspace Servizi Attivi) o Phase 14 (CRM)."
|
||||
last_updated: "2026-06-18T15:23:00.000Z"
|
||||
last_activity: 2026-06-18 -- Phase 12 execution complete (5/5 piani, build gate passed, OFFER-11/15/16/17/18 verified)
|
||||
milestone: v2.2
|
||||
milestone_name: — Sales Loop
|
||||
status: executing
|
||||
stopped_at: "Phase 12 re-scoped (mockup utente → editor offerte) e pianificata: 5 piani / 4 wave, planner + plan-checker pass. Pronta per /gsd-execute-phase 12"
|
||||
last_updated: "2026-06-19T10:25:00.000Z"
|
||||
last_activity: 2026-06-19 -- Phase 18 Plan 02 complete (analytics merged into /admin, /admin/analytics deleted)
|
||||
progress:
|
||||
total_phases: 7
|
||||
total_phases: 12
|
||||
completed_phases: 3
|
||||
total_plans: 12
|
||||
total_plans: 15
|
||||
completed_plans: 12
|
||||
percent: 100
|
||||
percent: 80
|
||||
---
|
||||
|
||||
# Project State
|
||||
@@ -22,16 +22,20 @@ See: .planning/PROJECT.md (updated 2026-06-13)
|
||||
|
||||
**Core value:** Il cliente apre il link e vede esattamente a che punto è il suo progetto, cosa deve ancora succedere e cosa ha già approvato — senza dover scrivere email per chiedere aggiornamenti.
|
||||
|
||||
**Current focus:** Phase 13 (Workspace Servizi Attivi) o Phase 14 (CRM Attio-style) — entrambe eseguibili. Phase 15 resta bloccata su mockup utente. Phase 12 (Offer Editor) completata e verificata.
|
||||
**Current focus:** Milestone **v2.2 "Sales Loop"** (reset 2026-06-19). North-star: lead in pipeline Kanban → transcript call → agente AI genera preventivo → pagina pubblica `/preventivo/[slug]` → vinto/perso. Piano: `.claude/plans/glittery-sprouting-pudding.md`. Prossimo passo: pianificare Phase 18 (R1) Cleanup.
|
||||
|
||||
## Current Position
|
||||
|
||||
Phase: 13 (next) — ready to plan/execute
|
||||
Plan: —
|
||||
Status: Phase 12 complete. Pronto per /gsd-execute-phase 13 o /gsd-execute-phase 14.
|
||||
Last activity: 2026-06-18 -- Phase 12 execution complete (OFFER-11/15/16/17/18 all verified PASS)
|
||||
Phase: 18 (R1) Cleanup & Consolidamento
|
||||
Plan: 02 complete (03 next)
|
||||
Status: Executing
|
||||
Last activity: 2026-06-19 -- Phase 18 Plan 02 complete (analytics merged into /admin, /admin/analytics deleted)
|
||||
|
||||
Progress: [██████████] 100%
|
||||
Progress (v2.2): [██░░░░░░░░] 20% — Phase 18 in progress (01, 02 done)
|
||||
|
||||
### Fasi completate (v2.1, storico)
|
||||
|
||||
Phase 11 (catalogo), Phase 12 (offer editor), Phase 14 (CRM Attio) — consegnate e in prod. 55 servizi reali + tag offerta caricati.
|
||||
|
||||
## Performance Metrics
|
||||
|
||||
@@ -66,7 +70,8 @@ Progress: [██████████] 100%
|
||||
Decisions are logged in PROJECT.md Key Decisions table.
|
||||
Recent decisions affecting current work:
|
||||
|
||||
- v2.1 roadmap: Offer Studio (Phases 11-15) sequenced before Proposal AI (Phases 16-17) — clean/fast data UX before the AI builder
|
||||
- **[RESET 2026-06-19] Milestone v2.2 "Sales Loop"** sostituisce le fasi residue v2.1. Decisioni bloccate: (1) URL preventivo = `/preventivo/[slug]` pubblico; (2) tagliare Forecast + quote builder manuale + Phase 15, fondere `/admin/analytics` nella dashboard; (3) portale post-vendita resta core, non si tocca (Phase 13 congelata); (4) agente AI = "io scelgo l'offerta, l'AI personalizza" leggendo i transcript, provider Claude. Piano: `.claude/plans/glittery-sprouting-pudding.md`
|
||||
- [SUPERSEDED dal reset] v2.1 roadmap: Offer Studio (Phases 11-15) sequenced before Proposal AI (Phases 16-17) — clean/fast data UX before the AI builder
|
||||
- Phase 11 bundles catalog database-view UX (OFFER-07..10) with legacy consolidation (OFFER-13) since the new UX should be built on a single unified `services` table, not on top of legacy `service_catalog`/`offer_services`
|
||||
- Phase 13 (Workspace — Servizi Attivi) is independent of Phases 11/12 — can execute in parallel order if useful, but numbered after for narrative flow
|
||||
- Phase 15 (Dashboard Revenue Stats / DASH-11) is isolated and BLOCKED on user-provided mockup; no other phase depends on it — can be deferred/skipped without blocking Phase 16/17
|
||||
@@ -75,6 +80,8 @@ Recent decisions affecting current work:
|
||||
- [Phase 11]: Phase 11 Plan 02: onConflictDoNothing() without explicit target compiles cleanly for tags table (single unique index tags_entity_name_unique) — used as written in plan, no fallback needed — Avoids unnecessary deviation; Drizzle's no-target ON CONFLICT DO NOTHING is correct given the single unique constraint from Plan 01
|
||||
- [Phase 11]: Phase 11 Plan 03: removed the plan's prescribed value-sync useEffect (and a follow-up render-time ref-read attempt) from EditableCell — both violate this project's react-hooks lint rules (set-state-in-effect, refs-during-render / React Compiler). tempValue is now only (re)initialized in startEdit()/cancel(), and the toggle display branch reads `value` directly instead of `tempValue` — Rule 1 lint fix, no behavioral change to the 8 spec'd test behaviors
|
||||
- [Phase 11]: Phase 11 Plan 04: left `createService`/`serviceSchema` in `src/app/admin/catalog/actions.ts` as unused dead code after deleting `ServiceForm.tsx` (its only consumer) — `actions.ts` was outside this plan's `files_modified` scope and `updateService` still depends on `serviceSchema`; logged to deferred-items.md for future cleanup
|
||||
- [Phase 18-02]: fmtEur unified to number version (analytics/page.tsx variant); KPI card callers using DB string values wrapped with parseFloat() — cleaner than maintaining two named variants
|
||||
- [Phase 18-02]: /admin/analytics route deleted; YearSelector now routes to /admin?year=Y — single admin entry point for statistics (CLEAN-03)
|
||||
|
||||
### Pending Todos
|
||||
|
||||
@@ -84,9 +91,10 @@ None yet.
|
||||
|
||||
### Blockers/Concerns
|
||||
|
||||
- **Phase 15 (DASH-11)**: Blocked pending dashboard mockup from user. Do not start until mockup provided; safe to skip in execution order without impacting Phase 16/17.
|
||||
- **Migrations**: Per CLAUDE.md, every phase touching schema (11 legacy consolidation, 13 recurring-revenue tracking, 16 payment link + provisioning) MUST have its migration applied to prod via SSH+docker exec BEFORE pushing dependent code.
|
||||
- Phase 11 Plan 1 (11-01): DB-execution steps blocked — DATABASE_URL (178.104.27.55:54321) firewalled, reachable only via SSH+docker exec on the production host (out of scope for this agent's authorization). Required before OFFER-13/D-02 are satisfied: run scripts/push-11-tags-migration.ts, scripts/migrate-services.ts, scripts/validate-services-migration.ts, scripts/migrate-tags.ts, scripts/validate-tags-migration.ts via an SSH-accessible session (see 11-01-SUMMARY.md 'Issues Encountered' for exact commands). OFFER-13 requirement NOT yet marked complete.
|
||||
- **Migrations (sempre valido)**: ogni fase con schema (es. Phase 20 transcript) DEVE avere la migration applicata a prod via tunnel SSH (`ssh -L 54321:localhost:54321 root@178.104.27.55`, `DATABASE_URL` riscritto a `127.0.0.1:54321`) PRIMA di pushare il codice dipendente. `drizzle-kit generate` rotto → SQL a mano.
|
||||
- **Phase 21 (AI)**: nessuna integrazione AI ancora nel codice. Servirà chiave Anthropic API in env (Coolify) + decisioni modello/prompt in fase di planning.
|
||||
- **Debito tecnico (non bloccante)**: tabelle legacy `service_catalog`/`offer_services`/`offer_micro_services` restano come deadweight; `createService`/`serviceSchema` dead code in `catalog/actions.ts`. Script di validazione consolidamento Phase 11 (migrate/validate) mai eseguiti ma OFFER-13 è di fatto soddisfatto (catalogo `services` in uso, 55 servizi reali caricati).
|
||||
- **[RISOLTO]** ~~Phase 15 bloccata su mockup~~ → fase abbandonata dal reset 2026-06-19.
|
||||
|
||||
## Deferred Items
|
||||
|
||||
@@ -100,6 +108,6 @@ Items acknowledged and carried forward from previous milestone close:
|
||||
|
||||
## Session Continuity
|
||||
|
||||
Last session: 2026-06-14T19:04:45.211Z
|
||||
Stopped at: Phase 12 re-scoped (mockup utente → editor offerte) e pianificata: 5 piani / 4 wave, planner + plan-checker pass. Pronta per /gsd-execute-phase 12
|
||||
Resume file: .planning/phases/12-offer-composition-drag-drop-csv-import/12-01-PLAN.md
|
||||
Last session: 2026-06-19T10:25:00.000Z
|
||||
Stopped at: Phase 18 Plan 02 complete — CLEAN-03 done. Next: 18-03-PLAN.md
|
||||
Resume file: .planning/phases/18-cleanup-consolidamento/18-03-PLAN.md
|
||||
|
||||
@@ -0,0 +1,233 @@
|
||||
---
|
||||
phase: 18-cleanup-consolidamento
|
||||
plan: 01
|
||||
type: execute
|
||||
wave: 1
|
||||
depends_on: []
|
||||
files_modified:
|
||||
- src/components/admin/AdminSidebar.tsx
|
||||
- src/app/admin/forecast/page.tsx
|
||||
- src/app/admin/quotes/new/page.tsx
|
||||
- src/components/admin/leads/SendQuoteModal.tsx
|
||||
- src/app/admin/projects/project-actions.ts
|
||||
autonomous: true
|
||||
requirements:
|
||||
- CLEAN-01
|
||||
- CLEAN-02
|
||||
|
||||
must_haves:
|
||||
truths:
|
||||
- "La rotta /admin/forecast non è più raggiungibile (404 o redirect)"
|
||||
- "La voce 'Forecast' non appare più nella sidebar admin"
|
||||
- "La rotta /admin/quotes/new non è più raggiungibile"
|
||||
- "SendQuoteModal non ha più il bottone che naviga a /admin/quotes/new"
|
||||
- "project-actions.ts non chiama più revalidatePath('/admin/forecast')"
|
||||
artifacts:
|
||||
- path: "src/components/admin/AdminSidebar.tsx"
|
||||
provides: "Sidebar senza voce Forecast"
|
||||
contains: "NON deve contenere href: \"/admin/forecast\""
|
||||
- path: "src/app/admin/forecast/page.tsx"
|
||||
provides: "File eliminato (rotta rimossa)"
|
||||
- path: "src/app/admin/quotes/new/page.tsx"
|
||||
provides: "File eliminato (rotta rimossa)"
|
||||
- path: "src/components/admin/leads/SendQuoteModal.tsx"
|
||||
provides: "Modal senza bottone navigazione a quotes/new"
|
||||
- path: "src/app/admin/projects/project-actions.ts"
|
||||
provides: "Server actions senza revalidatePath forecast"
|
||||
key_links:
|
||||
- from: "AdminSidebar.tsx NAV_ITEMS"
|
||||
to: "/admin/forecast"
|
||||
via: "href nel NAV_ITEMS array"
|
||||
pattern: "forecast"
|
||||
- from: "SendQuoteModal.tsx"
|
||||
to: "/admin/quotes/new"
|
||||
via: "window.location.href"
|
||||
pattern: "quotes/new"
|
||||
---
|
||||
|
||||
<objective>
|
||||
Rimuovere le rotte /admin/forecast e /admin/quotes/new e tutti i loro entry point.
|
||||
|
||||
Purpose: CLEAN-01 e CLEAN-02 — eliminare feature obsolete prima del Sales Loop. Il forecast è sostituito dal loop AI; il quote builder manuale è sostituito dall'agente AI in Phase 21.
|
||||
Output: Due rotte rimosse, sidebar pulita, SendQuoteModal senza entry point obsoleto, project-actions.ts senza revalidatePath forecast.
|
||||
</objective>
|
||||
|
||||
<execution_context>
|
||||
@$HOME/.claude/get-shit-done/workflows/execute-plan.md
|
||||
@$HOME/.claude/get-shit-done/templates/summary.md
|
||||
</execution_context>
|
||||
|
||||
<context>
|
||||
@.planning/PROJECT.md
|
||||
@.planning/ROADMAP.md
|
||||
@.planning/STATE.md
|
||||
</context>
|
||||
|
||||
<tasks>
|
||||
|
||||
<task type="auto">
|
||||
<name>Task 1: Rimuovi rotta Forecast — file, sidebar, revalidatePath (CLEAN-01)</name>
|
||||
|
||||
<read_first>
|
||||
- src/components/admin/AdminSidebar.tsx — array NAV_ITEMS, riga con href: "/admin/forecast"
|
||||
- src/app/admin/forecast/page.tsx — conferma contenuto prima di eliminare
|
||||
- src/app/admin/projects/project-actions.ts — righe 138, 145, 161 con revalidatePath("/admin/forecast")
|
||||
</read_first>
|
||||
|
||||
<files>
|
||||
src/components/admin/AdminSidebar.tsx,
|
||||
src/app/admin/projects/project-actions.ts
|
||||
</files>
|
||||
|
||||
<action>
|
||||
1. In AdminSidebar.tsx: rimuovi la riga dell'oggetto Forecast dall'array NAV_ITEMS:
|
||||
`{ href: "/admin/forecast", label: "Forecast", icon: TrendingUp },`
|
||||
Rimuovi anche l'import `TrendingUp` da lucide-react se non è usato altrove nel file.
|
||||
|
||||
2. In project-actions.ts: rimuovi le tre chiamate `revalidatePath("/admin/forecast")` alle righe ~138, ~145, ~161.
|
||||
Ogni funzione che la contiene (addProjectOffer, removeProjectOffer, updateProjectOfferTotal)
|
||||
mantiene le proprie altre revalidatePath (es. `/admin/projects/${projectId}`) — togli SOLO le righe forecast.
|
||||
|
||||
3. Elimina il file della rotta forecast:
|
||||
- src/app/admin/forecast/page.tsx
|
||||
Elimina anche la directory se è rimasta vuota:
|
||||
- src/app/admin/forecast/ (directory)
|
||||
|
||||
NON toccare: tabelle DB, lib/forecast-queries.ts (deadweight tollerabile, non blocca nulla),
|
||||
nessun'altra pagina o componente.
|
||||
</action>
|
||||
|
||||
<verify>
|
||||
<automated>
|
||||
grep -r "admin/forecast" /Users/simonecavalli/Vault/IAMCAVALLI/src --include="*.tsx" --include="*.ts" | grep -v "node_modules" | grep -v ".next" | wc -l
|
||||
</automated>
|
||||
Risultato atteso: 0 righe (nessun riferimento a /admin/forecast rimasto nel codice sorgente).
|
||||
|
||||
Verifica aggiuntiva sidebar:
|
||||
grep "TrendingUp\|forecast" /Users/simonecavalli/Vault/IAMCAVALLI/src/components/admin/AdminSidebar.tsx
|
||||
Risultato atteso: nessun output (zero match).
|
||||
</verify>
|
||||
|
||||
<acceptance_criteria>
|
||||
- grep -r "admin/forecast" src/ produce 0 risultati (esclusi node_modules e .next)
|
||||
- Il file src/app/admin/forecast/page.tsx non esiste più
|
||||
- AdminSidebar.tsx non contiene la stringa "forecast" né "TrendingUp" (se rimosso)
|
||||
- project-actions.ts non contiene revalidatePath("/admin/forecast")
|
||||
</acceptance_criteria>
|
||||
|
||||
<done>
|
||||
La voce Forecast è assente dalla sidebar, la rotta /admin/forecast è 404, nessun server action
|
||||
chiama revalidatePath su quella rotta.
|
||||
</done>
|
||||
</task>
|
||||
|
||||
<task type="auto">
|
||||
<name>Task 2: Rimuovi rotta Quote Builder manuale e il suo entry point (CLEAN-02)</name>
|
||||
|
||||
<read_first>
|
||||
- src/app/admin/quotes/new/page.tsx — conferma contenuto prima di eliminare
|
||||
- src/components/admin/leads/SendQuoteModal.tsx — riga ~120 con window.location.href = /admin/quotes/new
|
||||
- src/components/admin/quotes/QuoteBuilderForm.tsx — identifica che è usato solo da quotes/new
|
||||
- src/components/admin/quotes/OfferSelector.tsx — identifica che è usato solo da QuoteBuilderForm
|
||||
- src/components/admin/quotes/PriceOverrideInput.tsx — stesso
|
||||
- src/components/admin/quotes/QuotePreview.tsx — stesso
|
||||
</read_first>
|
||||
|
||||
<files>
|
||||
src/app/admin/quotes/new/page.tsx,
|
||||
src/components/admin/leads/SendQuoteModal.tsx,
|
||||
src/components/admin/quotes/QuoteBuilderForm.tsx,
|
||||
src/components/admin/quotes/OfferSelector.tsx,
|
||||
src/components/admin/quotes/PriceOverrideInput.tsx,
|
||||
src/components/admin/quotes/QuotePreview.tsx
|
||||
</files>
|
||||
|
||||
<action>
|
||||
1. In SendQuoteModal.tsx: trova il blocco che naviga a /admin/quotes/new:
|
||||
`window.location.href = /admin/quotes/new?lead_id=${leadId};`
|
||||
Rimuovi il pulsante / branch che lo contiene. Se il modal ha una logica "crea nuovo preventivo"
|
||||
che porta a quella rotta, rimuovi quella branch. Mantieni tutto il resto del modal intatto
|
||||
(il flusso "preventivo esistente" non va toccato).
|
||||
Prima di modificare, leggi il file per capire la struttura esatta del branch da eliminare.
|
||||
|
||||
2. Elimina i file della rotta e i componenti esclusivi:
|
||||
- src/app/admin/quotes/new/page.tsx
|
||||
- src/components/admin/quotes/QuoteBuilderForm.tsx
|
||||
- src/components/admin/quotes/OfferSelector.tsx
|
||||
- src/components/admin/quotes/PriceOverrideInput.tsx
|
||||
- src/components/admin/quotes/QuotePreview.tsx
|
||||
Elimina le directory rimaste vuote:
|
||||
- src/app/admin/quotes/new/ (directory)
|
||||
- src/components/admin/quotes/ (directory, solo se completamente vuota dopo la rimozione)
|
||||
|
||||
NON toccare: src/app/admin/quotes/ se esistono altre sotto-rotte (verifica prima),
|
||||
lib/admin-queries.ts (getAllOfferMacrosWithMicros — usata altrove), DB.
|
||||
|
||||
ATTENZIONE — verifica preventiva: prima di eliminare src/components/admin/quotes/,
|
||||
esegui: grep -r "quotes/" src/components --include="*.tsx" --include="*.ts"
|
||||
per confermare che i quattro componenti sopra non sono importati da altri file al di fuori di quotes/new.
|
||||
Se ci sono altri consumer, NON eliminare il componente e segnala in SUMMARY.
|
||||
</action>
|
||||
|
||||
<verify>
|
||||
<automated>
|
||||
grep -r "quotes/new" /Users/simonecavalli/Vault/IAMCAVALLI/src --include="*.tsx" --include="*.ts" | grep -v "node_modules" | grep -v ".next" | wc -l
|
||||
</automated>
|
||||
Risultato atteso: 0 righe.
|
||||
|
||||
Verifica esistenza file:
|
||||
test -f /Users/simonecavalli/Vault/IAMCAVALLI/src/app/admin/quotes/new/page.tsx && echo "ESISTE_ANCORA" || echo "OK_RIMOSSO"
|
||||
Risultato atteso: OK_RIMOSSO
|
||||
|
||||
Verifica SendQuoteModal:
|
||||
grep "quotes/new" /Users/simonecavalli/Vault/IAMCAVALLI/src/components/admin/leads/SendQuoteModal.tsx
|
||||
Risultato atteso: nessun output.
|
||||
</verify>
|
||||
|
||||
<acceptance_criteria>
|
||||
- grep -r "quotes/new" src/ produce 0 risultati
|
||||
- src/app/admin/quotes/new/page.tsx non esiste più
|
||||
- src/components/admin/quotes/QuoteBuilderForm.tsx non esiste più
|
||||
- SendQuoteModal.tsx non contiene la stringa "quotes/new"
|
||||
</acceptance_criteria>
|
||||
|
||||
<done>
|
||||
La rotta /admin/quotes/new è 404, il pulsante di navigazione nel SendQuoteModal è rimosso,
|
||||
i componenti esclusivi del quote builder sono eliminati.
|
||||
</done>
|
||||
</task>
|
||||
|
||||
</tasks>
|
||||
|
||||
<threat_model>
|
||||
## Trust Boundaries
|
||||
|
||||
| Boundary | Description |
|
||||
|----------|-------------|
|
||||
| admin UI → file system | Operazioni di eliminazione file irreversibili |
|
||||
|
||||
## STRIDE Threat Register
|
||||
|
||||
| Threat ID | Category | Component | Disposition | Mitigation Plan |
|
||||
|-----------|----------|-----------|-------------|-----------------|
|
||||
| T-18-01 | Tampering | project-actions.ts | accept | Rimozione di sole righe revalidatePath inerte — nessun path di dati toccato; le funzioni restano funzionali |
|
||||
| T-18-02 | Denial of Service | SendQuoteModal.tsx | mitigate | Leggere il file prima di modificare per evitare di rompere il flusso "preventivo esistente" che resta necessario |
|
||||
</threat_model>
|
||||
|
||||
<verification>
|
||||
Al termine del piano:
|
||||
1. `npm run build` deve completare senza errori TypeScript (nessun import rotto)
|
||||
2. `grep -r "admin/forecast" src/` → 0 risultati
|
||||
3. `grep -r "quotes/new" src/` → 0 risultati
|
||||
4. La sidebar admin non mostra la voce "Forecast"
|
||||
</verification>
|
||||
|
||||
<success_criteria>
|
||||
- CLEAN-01: /admin/forecast → 404, sidebar senza voce Forecast, project-actions.ts senza revalidatePath forecast
|
||||
- CLEAN-02: /admin/quotes/new → 404, SendQuoteModal senza navigazione a quotes/new, componenti quotes/ rimossi
|
||||
- Build TypeScript pulita (nessun import broken)
|
||||
</success_criteria>
|
||||
|
||||
<output>
|
||||
After completion, create `.planning/phases/18-cleanup-consolidamento/18-01-SUMMARY.md`
|
||||
</output>
|
||||
@@ -0,0 +1,156 @@
|
||||
---
|
||||
phase: 18-cleanup-consolidamento
|
||||
plan: "01"
|
||||
subsystem: ui
|
||||
tags: [next.js, admin, cleanup, sidebar, server-actions]
|
||||
|
||||
requires: []
|
||||
provides:
|
||||
- Forecast route removed (/admin/forecast → 404)
|
||||
- Admin sidebar without Forecast entry
|
||||
- project-actions.ts without revalidatePath forecast calls
|
||||
- Quote builder route removed (/admin/quotes/new → 404)
|
||||
- SendQuoteModal without navigation to quotes/new
|
||||
- QuoteBuilderForm, OfferSelector, PriceOverrideInput, QuotePreview components deleted
|
||||
affects: [phase-21-ai-quote-agent, any future admin nav changes]
|
||||
|
||||
tech-stack:
|
||||
added: []
|
||||
patterns:
|
||||
- "Dead-code deletion: remove page files + directories + all entry points atomically before Sales Loop phase"
|
||||
|
||||
key-files:
|
||||
created: []
|
||||
modified:
|
||||
- src/components/admin/AdminSidebar.tsx
|
||||
- src/app/admin/projects/project-actions.ts
|
||||
- src/components/admin/leads/SendQuoteModal.tsx
|
||||
- src/lib/admin-queries.ts
|
||||
deleted:
|
||||
- src/app/admin/forecast/page.tsx
|
||||
- src/app/admin/quotes/new/page.tsx
|
||||
- src/app/admin/quotes/new/actions.ts
|
||||
- src/components/admin/quotes/QuoteBuilderForm.tsx
|
||||
- src/components/admin/quotes/OfferSelector.tsx
|
||||
- src/components/admin/quotes/PriceOverrideInput.tsx
|
||||
- src/components/admin/quotes/QuotePreview.tsx
|
||||
|
||||
key-decisions:
|
||||
- "Kept lib/forecast-queries.ts and lib/quote-actions.ts as tolerable deadweight — no broken imports, no consumer"
|
||||
- "SendQuoteModal Tabs UI replaced with flat single-form layout (only 'existing' flow remains)"
|
||||
- "quotes/new/actions.ts (re-export shim) deleted along with page since its only consumer was QuoteBuilderForm"
|
||||
- "Stale JSDoc comment in admin-queries.ts referencing /admin/quotes/new cleaned up (Rule 1 auto-fix)"
|
||||
|
||||
patterns-established:
|
||||
- "Cleanup tasks: always grep for all entry points (revalidatePath, navigation, JSDoc) before marking done"
|
||||
|
||||
requirements-completed: [CLEAN-01, CLEAN-02]
|
||||
|
||||
duration: 12min
|
||||
completed: 2026-06-19
|
||||
---
|
||||
|
||||
# Phase 18 Plan 01: Cleanup — Remove Forecast and Manual Quote Builder Routes Summary
|
||||
|
||||
**Deleted /admin/forecast and /admin/quotes/new routes with all entry points (sidebar, revalidatePath, navigation button, exclusive components) to clear the codebase for the Sales Loop AI phase.**
|
||||
|
||||
## Performance
|
||||
|
||||
- **Duration:** ~12 min
|
||||
- **Started:** 2026-06-19T10:04:00Z
|
||||
- **Completed:** 2026-06-19T10:16:00Z
|
||||
- **Tasks:** 2
|
||||
- **Files modified/deleted:** 11
|
||||
|
||||
## Accomplishments
|
||||
|
||||
- Removed Forecast route: deleted `src/app/admin/forecast/page.tsx`, removed sidebar nav entry and `TrendingUp` import, stripped 3 `revalidatePath("/admin/forecast")` calls from project-actions.ts
|
||||
- Removed Quote Builder route: deleted `quotes/new/page.tsx`, `quotes/new/actions.ts`, and all 4 exclusive components (QuoteBuilderForm, OfferSelector, PriceOverrideInput, QuotePreview)
|
||||
- Simplified SendQuoteModal from two-tab layout to single flat form — "Crea Nuovo" tab and `window.location` navigation removed entirely; "existing quote" flow unchanged
|
||||
- Build passes clean: 0 TypeScript errors, 0 broken imports
|
||||
|
||||
## Task Commits
|
||||
|
||||
1. **Task 1: Rimuovi rotta Forecast** - `7d88409` (chore)
|
||||
2. **Task 2: Rimuovi rotta Quote Builder** - `268f56c` (chore)
|
||||
|
||||
**Plan metadata:** (see final commit below)
|
||||
|
||||
## Files Created/Modified
|
||||
|
||||
- `src/components/admin/AdminSidebar.tsx` - Removed Forecast NAV_ITEMS entry and TrendingUp import
|
||||
- `src/app/admin/projects/project-actions.ts` - Removed 3 revalidatePath("/admin/forecast") calls
|
||||
- `src/components/admin/leads/SendQuoteModal.tsx` - Removed "Crea Nuovo" tab and quotes/new navigation
|
||||
- `src/lib/admin-queries.ts` - Removed stale JSDoc comment referencing /admin/quotes/new
|
||||
|
||||
**Deleted:**
|
||||
- `src/app/admin/forecast/page.tsx`
|
||||
- `src/app/admin/quotes/new/page.tsx`
|
||||
- `src/app/admin/quotes/new/actions.ts`
|
||||
- `src/components/admin/quotes/QuoteBuilderForm.tsx`
|
||||
- `src/components/admin/quotes/OfferSelector.tsx`
|
||||
- `src/components/admin/quotes/PriceOverrideInput.tsx`
|
||||
- `src/components/admin/quotes/QuotePreview.tsx`
|
||||
|
||||
## Decisions Made
|
||||
|
||||
- Kept `lib/forecast-queries.ts` and `lib/quote-actions.ts` intact — they have no broken imports and are tolerable deadweight; removing them was explicitly out of scope per the plan
|
||||
- Deleted `quotes/new/actions.ts` (not listed in plan) because it was a server re-export shim with `QuoteBuilderForm` as its only consumer — Rule 3 auto-fix (would have blocked clean directory removal)
|
||||
- Stale JSDoc comment in `admin-queries.ts` updated — Rule 1 auto-fix (stale reference counts as dead code)
|
||||
|
||||
## Deviations from Plan
|
||||
|
||||
### Auto-fixed Issues
|
||||
|
||||
**1. [Rule 1 - Bug] Removed stale JSDoc comment referencing deleted route**
|
||||
- **Found during:** Task 2 (final grep verification)
|
||||
- **Issue:** `admin-queries.ts` line 862 had a `* Used by: /admin/quotes/new form (OfferSelector dropdown)` comment — the route no longer exists
|
||||
- **Fix:** Removed the stale line from the JSDoc block
|
||||
- **Files modified:** src/lib/admin-queries.ts
|
||||
- **Verification:** `grep -r "quotes/new" src/` → 0 results
|
||||
- **Committed in:** 268f56c (Task 2 commit)
|
||||
|
||||
**2. [Rule 3 - Blocking] Deleted quotes/new/actions.ts (unlisted file)**
|
||||
- **Found during:** Task 2 — directory was non-empty after deleting page.tsx
|
||||
- **Issue:** `src/app/admin/quotes/new/actions.ts` was a re-export shim (`export { createQuote } from "@/lib/quote-actions"`) with QuoteBuilderForm as its only consumer. It would leave a broken directory if not removed.
|
||||
- **Fix:** Confirmed no other consumers via grep, then deleted the file
|
||||
- **Files modified:** src/app/admin/quotes/new/actions.ts (deleted)
|
||||
- **Verification:** `grep -r "createQuote" src/` shows only lib/quote-actions.ts (the source) — no broken references
|
||||
- **Committed in:** 268f56c (Task 2 commit)
|
||||
|
||||
---
|
||||
|
||||
**Total deviations:** 2 auto-fixed (1 Rule 1 stale comment, 1 Rule 3 blocking unlisted file)
|
||||
**Impact on plan:** Both auto-fixes necessary for completeness and clean directory state. No scope creep.
|
||||
|
||||
## Issues Encountered
|
||||
|
||||
- A linter hook reverted `SendQuoteModal.tsx` after the first Write. On re-read, the linter had actually applied the intended changes (removed Tabs imports, removed tab state, flattened to single form). Confirmed the file was correct before staging.
|
||||
|
||||
## User Setup Required
|
||||
|
||||
None — no external service configuration required.
|
||||
|
||||
## Next Phase Readiness
|
||||
|
||||
- CLEAN-01 and CLEAN-02 complete: codebase is clear of forecast and manual quote builder dead routes
|
||||
- Ready for Phase 21 (AI Quote Agent) which will provide the replacement quote creation flow
|
||||
- `lib/forecast-queries.ts` and `lib/quote-actions.ts` remain as inert deadweight — can be cleaned in a future pass if desired
|
||||
|
||||
---
|
||||
*Phase: 18-cleanup-consolidamento*
|
||||
*Completed: 2026-06-19*
|
||||
|
||||
## Self-Check: PASSED
|
||||
|
||||
- `src/components/admin/AdminSidebar.tsx` — FOUND (modified)
|
||||
- `src/app/admin/projects/project-actions.ts` — FOUND (modified)
|
||||
- `src/components/admin/leads/SendQuoteModal.tsx` — FOUND (modified)
|
||||
- `src/lib/admin-queries.ts` — FOUND (modified)
|
||||
- `src/app/admin/forecast/page.tsx` — MISSING (deleted as intended)
|
||||
- `src/app/admin/quotes/new/page.tsx` — MISSING (deleted as intended)
|
||||
- Commit 7d88409 — FOUND
|
||||
- Commit 268f56c — FOUND
|
||||
- `grep -r "admin/forecast" src/` → 0 results
|
||||
- `grep -r "quotes/new" src/` → 0 results
|
||||
- Build: 0 errors
|
||||
@@ -0,0 +1,345 @@
|
||||
---
|
||||
phase: 18-cleanup-consolidamento
|
||||
plan: 02
|
||||
type: execute
|
||||
wave: 1
|
||||
depends_on: []
|
||||
files_modified:
|
||||
- src/app/admin/page.tsx
|
||||
- src/app/admin/analytics/page.tsx
|
||||
- src/components/admin/YearSelector.tsx
|
||||
autonomous: true
|
||||
requirements:
|
||||
- CLEAN-03
|
||||
|
||||
must_haves:
|
||||
truths:
|
||||
- "La Dashboard /admin mostra le statistiche annuali (MetricCard fatturato, grafico mensile, time tracking)"
|
||||
- "La rotta /admin/analytics non esiste più (404)"
|
||||
- "Il selettore anno naviga a /admin?year=X, non a /admin/analytics?year=X"
|
||||
- "Non esiste più il file src/app/admin/analytics/page.tsx"
|
||||
artifacts:
|
||||
- path: "src/app/admin/page.tsx"
|
||||
provides: "Dashboard con sezione Statistiche integrata"
|
||||
contains: "getAnalyticsByYear"
|
||||
- path: "src/app/admin/analytics/page.tsx"
|
||||
provides: "File eliminato"
|
||||
- path: "src/components/admin/YearSelector.tsx"
|
||||
provides: "YearSelector che naviga a /admin?year=Y"
|
||||
contains: "router.push(`/admin?year=${y}`)"
|
||||
key_links:
|
||||
- from: "src/app/admin/page.tsx"
|
||||
to: "lib/analytics-queries"
|
||||
via: "import getAnalyticsByYear, getMonthlyCollected, getAvailableYears, getTimeByClient, getTotalTrackedHours"
|
||||
pattern: "analytics-queries"
|
||||
- from: "YearSelector"
|
||||
to: "/admin"
|
||||
via: "router.push"
|
||||
pattern: "router.push.*admin"
|
||||
---
|
||||
|
||||
<objective>
|
||||
Fondere le statistiche di /admin/analytics nella Dashboard /admin ed eliminare la rotta duplicata.
|
||||
|
||||
Purpose: CLEAN-03 — unica vista admin per statistiche. Elimina il doppione /admin/analytics e porta
|
||||
tutto il suo contenuto (MetricCard economiche + MonthlyChart + time tracking per cliente) nella dashboard
|
||||
esistente, sotto le KPI card e il feed attività già presenti.
|
||||
Output: /admin mostra statistiche annuali, /admin/analytics → 404, YearSelector aggiornato.
|
||||
</objective>
|
||||
|
||||
<execution_context>
|
||||
@$HOME/.claude/get-shit-done/workflows/execute-plan.md
|
||||
@$HOME/.claude/get-shit-done/templates/summary.md
|
||||
</execution_context>
|
||||
|
||||
<context>
|
||||
@.planning/PROJECT.md
|
||||
@.planning/ROADMAP.md
|
||||
@.planning/STATE.md
|
||||
</context>
|
||||
|
||||
<interfaces>
|
||||
<!-- Estratto da src/app/admin/analytics/page.tsx — contratti da portare nella dashboard -->
|
||||
|
||||
Imports da copiare in admin/page.tsx:
|
||||
```typescript
|
||||
import {
|
||||
getAnalyticsByYear,
|
||||
getMonthlyCollected,
|
||||
getAvailableYears,
|
||||
getTimeByClient,
|
||||
getTotalTrackedHours,
|
||||
} from "@/lib/analytics-queries";
|
||||
import { YearSelector, MonthlyChart } from "@/components/admin/YearSelector";
|
||||
```
|
||||
|
||||
Funzioni helper da copiare in admin/page.tsx:
|
||||
```typescript
|
||||
function fmtEur(n: number) { /* formatta in EUR it-IT */ }
|
||||
function fmtSeconds(s: number): string { /* h m format */ }
|
||||
```
|
||||
ATTENZIONE: admin/page.tsx ha già una funzione `fmtEur` (ma accetta `string`, non `number`).
|
||||
Rinominare una delle due o unificarle — preferire la versione che accetta `number` (più robusta)
|
||||
e aggiornare i consumer nel file.
|
||||
|
||||
Componente MetricCard (da analytics/page.tsx) — da spostare in admin/page.tsx:
|
||||
```typescript
|
||||
function MetricCard({ label, value, sub, accent }: {
|
||||
label: string; value: string; sub?: string; accent?: boolean;
|
||||
}) { ... }
|
||||
```
|
||||
Questo componente è diverso da KpiCard già presente in admin/page.tsx:
|
||||
KpiCard ha icona + colore; MetricCard ha stile accent verde o bianco con bordo.
|
||||
Mantieni ENTRAMBI (scopi diversi): KpiCard per le 4 metriche operative in cima,
|
||||
MetricCard per le statistiche economiche annuali nella nuova sezione.
|
||||
|
||||
Firma del componente analytics/page.tsx (async, searchParams):
|
||||
```typescript
|
||||
export default async function AnalyticsPage({
|
||||
searchParams,
|
||||
}: {
|
||||
searchParams: Promise<{ year?: string }>;
|
||||
}) {
|
||||
const { year: yearParam } = await searchParams;
|
||||
const year = parseInt(yearParam ?? "") || new Date().getFullYear();
|
||||
...
|
||||
}
|
||||
```
|
||||
La stessa logica `year` va aggiunta al componente AdminDashboard in admin/page.tsx.
|
||||
</interfaces>
|
||||
|
||||
<tasks>
|
||||
|
||||
<task type="auto">
|
||||
<name>Task 1: Integra statistiche annuali nella Dashboard (CLEAN-03)</name>
|
||||
|
||||
<read_first>
|
||||
- src/app/admin/page.tsx — struttura attuale (KpiCard, FollowUpWidget, activity feed, fmtEur esistente)
|
||||
- src/app/admin/analytics/page.tsx — tutto il contenuto da portare (già in contesto da discovery)
|
||||
- src/components/admin/YearSelector.tsx — YearSelector e MonthlyChart (già in contesto da discovery)
|
||||
</read_first>
|
||||
|
||||
<files>
|
||||
src/app/admin/page.tsx,
|
||||
src/components/admin/YearSelector.tsx
|
||||
</files>
|
||||
|
||||
<action>
|
||||
STEP 1 — Aggiorna AdminDashboard in src/app/admin/page.tsx:
|
||||
|
||||
a) Aggiungi alla firma del componente il parametro searchParams (come in analytics/page.tsx):
|
||||
```typescript
|
||||
export default async function AdminDashboard({
|
||||
searchParams,
|
||||
}: {
|
||||
searchParams: Promise<{ year?: string }>;
|
||||
}) {
|
||||
const { year: yearParam } = await searchParams;
|
||||
const year = parseInt(yearParam ?? "") || new Date().getFullYear();
|
||||
```
|
||||
|
||||
b) Aggiungi gli import mancanti in cima al file:
|
||||
```typescript
|
||||
import {
|
||||
getAnalyticsByYear,
|
||||
getMonthlyCollected,
|
||||
getAvailableYears,
|
||||
getTimeByClient,
|
||||
getTotalTrackedHours,
|
||||
} from "@/lib/analytics-queries";
|
||||
import { YearSelector, MonthlyChart } from "@/components/admin/YearSelector";
|
||||
```
|
||||
|
||||
c) Aggiungi le query analytics al Promise.all esistente (o crea un secondo await per separare
|
||||
le query di diversa frequenza — è accettabile avere due await distinti per chiarezza):
|
||||
```typescript
|
||||
const [data, monthly, availableYears, timeByClient, totalHours] = await Promise.all([
|
||||
getAnalyticsByYear(year),
|
||||
getMonthlyCollected(year),
|
||||
getAvailableYears(),
|
||||
getTimeByClient(year),
|
||||
getTotalTrackedHours(year),
|
||||
]);
|
||||
```
|
||||
|
||||
d) Copia la funzione MetricCard da analytics/page.tsx nel file (accanto a KpiCard).
|
||||
|
||||
e) Risolvi il conflitto fmtEur:
|
||||
- La versione attuale in admin/page.tsx accetta `string` e fa `parseFloat(val)`.
|
||||
- Quella in analytics/page.tsx accetta `number`.
|
||||
- Unifica in una sola funzione che accetta `number`, aggiorna i consumer esistenti
|
||||
nel file che passavano una stringa (es. `fmtEur(kpi.revenueTotale)` e
|
||||
`fmtEur(kpi.pagamentiInSospeso)`) convertendo a number dove necessario,
|
||||
oppure mantieni entrambe con nomi distinti (`fmtEurStr` / `fmtEurNum`).
|
||||
Scegli l'approccio più pulito senza rompere i KPI card esistenti.
|
||||
|
||||
f) Aggiungi la sezione Statistiche DOPO il feed attività esistente (mantieni tutto quello
|
||||
che c'era prima intatto — FollowUpWidget, KPI card, activity feed):
|
||||
```tsx
|
||||
{/* ── SEZIONE STATISTICHE ANNUALI ── */}
|
||||
<div className="mt-10 space-y-10">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<h2 className="text-xl font-bold text-gray-900">Statistiche</h2>
|
||||
<p className="text-sm text-gray-400 mt-0.5">Panoramica per anno</p>
|
||||
</div>
|
||||
<YearSelector currentYear={year} availableYears={availableYears} />
|
||||
</div>
|
||||
|
||||
{/* Sezione economica */}
|
||||
<div className="space-y-4">
|
||||
<h3 className="text-sm font-bold text-[#71717a] uppercase tracking-wider">Fatturato</h3>
|
||||
<div className="grid grid-cols-2 lg:grid-cols-4 gap-4">
|
||||
<MetricCard label="Contrattualizzato" value={fmtEur(data.contracted)}
|
||||
sub={`${data.clientsAcquired} client${data.clientsAcquired === 1 ? "e" : "i"}`} accent />
|
||||
<MetricCard label="Incassato" value={fmtEur(data.collected)}
|
||||
sub={`${collectedPct}% del contrattualizzato`} />
|
||||
<MetricCard label="Da incassare" value={fmtEur(data.pending)} sub="Tutti gli anni" />
|
||||
<MetricCard label="Clienti acquisiti" value={String(data.clientsAcquired)} sub={`Anno ${year}`} />
|
||||
</div>
|
||||
<MonthlyChart data={monthly} year={year} />
|
||||
</div>
|
||||
|
||||
{/* Sezione time tracking */}
|
||||
<div className="space-y-4">
|
||||
<h3 className="text-sm font-bold text-[#71717a] uppercase tracking-wider">Tempo tracciato</h3>
|
||||
<div className="grid grid-cols-2 lg:grid-cols-4 gap-4">
|
||||
<MetricCard label="Ore totali" value={`${totalHours}h`} sub={`Anno ${year}`} accent />
|
||||
</div>
|
||||
{/* ... tabella ore per cliente (copia da analytics/page.tsx) ... */}
|
||||
</div>
|
||||
</div>
|
||||
```
|
||||
Ricopia fedelmente il blocco "ore per cliente" da analytics/page.tsx (righe 127-160)
|
||||
nella sezione time tracking della dashboard.
|
||||
|
||||
Calcola collectedPct prima del return:
|
||||
```typescript
|
||||
const collectedPct = data.contracted > 0
|
||||
? Math.round((data.collected / data.contracted) * 100) : 0;
|
||||
const maxClientSeconds = timeByClient[0]?.totalSeconds ?? 1;
|
||||
```
|
||||
|
||||
STEP 2 — Aggiorna YearSelector per navigare verso /admin:
|
||||
In src/components/admin/YearSelector.tsx, riga ~18:
|
||||
```typescript
|
||||
// PRIMA:
|
||||
router.push(`/admin/analytics?year=${y}`);
|
||||
// DOPO:
|
||||
router.push(`/admin?year=${y}`);
|
||||
```
|
||||
|
||||
NON modificare: MonthlyChart (rimane nello stesso file), struttura esistente della dashboard
|
||||
(FollowUpWidget, KPI card, activity feed — tutto invariato sopra la nuova sezione).
|
||||
</action>
|
||||
|
||||
<verify>
|
||||
<automated>
|
||||
grep "getAnalyticsByYear\|getMonthlyCollected\|getAvailableYears" /Users/simonecavalli/Vault/IAMCAVALLI/src/app/admin/page.tsx | wc -l
|
||||
</automated>
|
||||
Risultato atteso: 3 (tutte e tre le query importate e usate).
|
||||
|
||||
Verifica YearSelector:
|
||||
grep "router.push" /Users/simonecavalli/Vault/IAMCAVALLI/src/components/admin/YearSelector.tsx
|
||||
Risultato atteso: contiene "/admin?year=" e NON "/admin/analytics".
|
||||
</verify>
|
||||
|
||||
<acceptance_criteria>
|
||||
- src/app/admin/page.tsx importa da @/lib/analytics-queries
|
||||
- src/app/admin/page.tsx importa YearSelector e MonthlyChart
|
||||
- YearSelector.tsx naviga a /admin?year=Y (non a /admin/analytics)
|
||||
- Il file compila senza errori TypeScript
|
||||
</acceptance_criteria>
|
||||
|
||||
<done>
|
||||
La Dashboard mostra le statistiche annuali con selettore anno; il selettore naviga
|
||||
correttamente a /admin?year=X.
|
||||
</done>
|
||||
</task>
|
||||
|
||||
<task type="auto">
|
||||
<name>Task 2: Elimina rotta /admin/analytics (CLEAN-03)</name>
|
||||
|
||||
<read_first>
|
||||
- src/app/admin/analytics/page.tsx — conferma che non è più necessario (Task 1 ha già portato
|
||||
tutto il contenuto nella dashboard)
|
||||
</read_first>
|
||||
|
||||
<files>
|
||||
src/app/admin/analytics/page.tsx
|
||||
</files>
|
||||
|
||||
<action>
|
||||
1. Verifica preventiva — assicurati che nessun altro file nel progetto importi da
|
||||
src/app/admin/analytics/page.tsx direttamente (è una page, non un modulo, quindi
|
||||
non dovrebbe avere consumer diretti — ma controlla):
|
||||
grep -r "admin/analytics" src/ --include="*.tsx" --include="*.ts"
|
||||
Se trovi riferimenti diversi da YearSelector (già aggiornato in Task 1), risolvili prima.
|
||||
|
||||
2. Elimina:
|
||||
- src/app/admin/analytics/page.tsx
|
||||
- Directory src/app/admin/analytics/ (se vuota dopo la rimozione del file)
|
||||
|
||||
NON eliminare: lib/analytics-queries.ts (ancora importata dalla dashboard),
|
||||
src/components/admin/YearSelector.tsx (ancora usata dalla dashboard).
|
||||
</action>
|
||||
|
||||
<verify>
|
||||
<automated>
|
||||
test -f /Users/simonecavalli/Vault/IAMCAVALLI/src/app/admin/analytics/page.tsx && echo "ESISTE_ANCORA" || echo "OK_RIMOSSO"
|
||||
</automated>
|
||||
Risultato atteso: OK_RIMOSSO
|
||||
|
||||
Verifica nessun riferimento rimasto:
|
||||
grep -r "admin/analytics" /Users/simonecavalli/Vault/IAMCAVALLI/src --include="*.tsx" --include="*.ts" | grep -v "node_modules" | grep -v ".next"
|
||||
Risultato atteso: 0 righe.
|
||||
</verify>
|
||||
|
||||
<acceptance_criteria>
|
||||
- src/app/admin/analytics/page.tsx non esiste più
|
||||
- grep -r "admin/analytics" src/ produce 0 risultati
|
||||
- npm run build completa senza errori
|
||||
</acceptance_criteria>
|
||||
|
||||
<done>
|
||||
La rotta /admin/analytics è eliminata. Navigarci restituisce 404.
|
||||
Tutte le statistiche sono accessibili alla Dashboard /admin.
|
||||
</done>
|
||||
</task>
|
||||
|
||||
</tasks>
|
||||
|
||||
<threat_model>
|
||||
## Trust Boundaries
|
||||
|
||||
| Boundary | Description |
|
||||
|----------|-------------|
|
||||
| admin/page.tsx → analytics-queries | Aggiunta di query server-side già esistenti — nessuna nuova superficie |
|
||||
|
||||
## STRIDE Threat Register
|
||||
|
||||
| Threat ID | Category | Component | Disposition | Mitigation Plan |
|
||||
|-----------|----------|-----------|-------------|-----------------|
|
||||
| T-18-03 | Information Disclosure | admin/page.tsx | accept | Le statistiche erano già accessibili a /admin/analytics — stessa autenticazione Auth.js, nessuna nuova esposizione |
|
||||
| T-18-04 | Tampering | YearSelector.tsx | accept | Il cambio da /admin/analytics a /admin nella navigazione è una semplice redirect di path — nessun dato mutato |
|
||||
</threat_model>
|
||||
|
||||
<verification>
|
||||
Al termine del piano:
|
||||
1. `npm run build` completa senza errori TypeScript
|
||||
2. `grep -r "admin/analytics" src/` → 0 risultati
|
||||
3. La Dashboard /admin mostra le sezioni Statistiche con selettore anno
|
||||
4. Navigare a /admin/analytics restituisce 404
|
||||
5. YearSelector porta a /admin?year=X quando si cambia anno
|
||||
</verification>
|
||||
|
||||
<success_criteria>
|
||||
- CLEAN-03: /admin mostra le statistiche annuali (MetricCard + MonthlyChart + time tracking)
|
||||
- /admin/analytics → 404 (rotta eliminata)
|
||||
- YearSelector naviga a /admin?year=X
|
||||
- Build TypeScript pulita
|
||||
</success_criteria>
|
||||
|
||||
<output>
|
||||
After completion, create `.planning/phases/18-cleanup-consolidamento/18-02-SUMMARY.md`
|
||||
</output>
|
||||
@@ -0,0 +1,113 @@
|
||||
---
|
||||
phase: 18-cleanup-consolidamento
|
||||
plan: "02"
|
||||
subsystem: ui
|
||||
tags: [nextjs, analytics, dashboard, react, tailwind]
|
||||
|
||||
# Dependency graph
|
||||
requires:
|
||||
- phase: analytics-queries
|
||||
provides: getAnalyticsByYear, getMonthlyCollected, getAvailableYears, getTimeByClient, getTotalTrackedHours
|
||||
provides:
|
||||
- Admin dashboard at /admin integrates annual statistics (MetricCard, MonthlyChart, time tracking per client)
|
||||
- /admin/analytics route deleted — returns 404
|
||||
- YearSelector navigates to /admin?year=Y
|
||||
affects: [18-cleanup-consolidamento, admin-ui, analytics]
|
||||
|
||||
# Tech tracking
|
||||
tech-stack:
|
||||
added: []
|
||||
patterns:
|
||||
- Server page accepts searchParams for year filtering
|
||||
- Analytics queries co-located with dashboard queries in same async page
|
||||
|
||||
key-files:
|
||||
created: []
|
||||
modified:
|
||||
- src/app/admin/page.tsx
|
||||
- src/components/admin/YearSelector.tsx
|
||||
deleted:
|
||||
- src/app/admin/analytics/page.tsx
|
||||
|
||||
key-decisions:
|
||||
- "Unified fmtEur to accept number (analytics version); KPI card callers wrapped with parseFloat() for string DB values"
|
||||
- "Two separate awaits in AdminDashboard: getDashboardStats() first, then analytics Promise.all — acceptable for clarity"
|
||||
- "Stale .next cache (OfferSelector.tsx reference) cleared before final build — pre-existing issue, not introduced by this plan"
|
||||
|
||||
patterns-established:
|
||||
- "Statistics section appended after activity feed — existing dashboard structure preserved above new section"
|
||||
|
||||
requirements-completed:
|
||||
- CLEAN-03
|
||||
|
||||
# Metrics
|
||||
duration: 15min
|
||||
completed: 2026-06-19
|
||||
---
|
||||
|
||||
# Phase 18 Plan 02: Consolidate Analytics Dashboard Summary
|
||||
|
||||
**Merged /admin/analytics statistics (MetricCard, MonthlyChart, per-client time tracking) into /admin dashboard and deleted the duplicate route**
|
||||
|
||||
## Performance
|
||||
|
||||
- **Duration:** ~15 min
|
||||
- **Started:** 2026-06-19T10:04:00Z
|
||||
- **Completed:** 2026-06-19T10:19:00Z
|
||||
- **Tasks:** 2
|
||||
- **Files modified:** 3 (2 modified, 1 deleted)
|
||||
|
||||
## Accomplishments
|
||||
- AdminDashboard now shows annual statistics section below activity feed: MetricCard economic metrics, MonthlyChart, and per-client time tracking bars
|
||||
- /admin/analytics route deleted — navigating there returns 404
|
||||
- YearSelector updated to navigate to /admin?year=Y instead of /admin/analytics?year=Y
|
||||
- fmtEur unified to number-accepting version (more robust); existing KPI card string values wrapped with parseFloat()
|
||||
|
||||
## Task Commits
|
||||
|
||||
1. **Task 1: Integra statistiche annuali nella Dashboard** — included in `14bdbab` (feat)
|
||||
2. **Task 2: Elimina rotta /admin/analytics** — included in `14bdbab` (feat)
|
||||
|
||||
**Plan metadata:** (docs commit below)
|
||||
|
||||
## Files Created/Modified
|
||||
- `src/app/admin/page.tsx` — Added analytics imports, searchParams, analytics queries, MetricCard, fmtSeconds, fmtEur(number), statistics section
|
||||
- `src/components/admin/YearSelector.tsx` — Updated router.push from /admin/analytics to /admin
|
||||
- `src/app/admin/analytics/page.tsx` — Deleted
|
||||
|
||||
## Decisions Made
|
||||
- Unified `fmtEur` to the `number` version from analytics (cleaner, no `parseFloat` internally). Updated the two KPI card callers that received string values from DB to pass `parseFloat(kpi.revenueTotale)` and `parseFloat(kpi.pagamentiInSospeso)`.
|
||||
- Used two sequential awaits (getDashboardStats first, then analytics Promise.all) rather than one large combined Promise.all — avoids mixing query concerns and follows plan guidance for acceptable two-await pattern.
|
||||
- Preserved all existing dashboard content (FollowUpWidget, KPI cards, activity feed) exactly; statistics section appended at the bottom with `mt-10`.
|
||||
|
||||
## Deviations from Plan
|
||||
|
||||
### Auto-fixed Issues
|
||||
|
||||
**1. [Rule 3 - Blocking] Cleared stale .next cache referencing deleted OfferSelector.tsx**
|
||||
- **Found during:** Build verification after Task 2
|
||||
- **Issue:** Stale `.next/tsconfig.tsbuildinfo` incremental cache listed `src/components/admin/quotes/OfferSelector.tsx` as a root file. That file had been deleted in a prior session. TypeScript errored with "File not found — root file specified for compilation." The pre-existing cache was masking this because prior builds hit cached output.
|
||||
- **Fix:** Ran `rm -rf .next` to clear the stale incremental cache, then rebuilt clean.
|
||||
- **Files modified:** .next/ (cache only — not tracked in git)
|
||||
- **Verification:** Clean build passes with TypeScript check and all routes compiled correctly.
|
||||
- **Committed in:** n/a (cache not committed)
|
||||
|
||||
---
|
||||
|
||||
**Total deviations:** 1 auto-fixed (Rule 3 - blocking)
|
||||
**Impact on plan:** Required to get a clean build. Pre-existing issue unrelated to this plan's scope. .next/ is gitignored so no source changes.
|
||||
|
||||
## Issues Encountered
|
||||
- Stale `.next` incremental TypeScript cache referenced `OfferSelector.tsx` (deleted in a prior session). Cleared cache, rebuilt clean. Build passes correctly.
|
||||
|
||||
## User Setup Required
|
||||
None - no external service configuration required.
|
||||
|
||||
## Next Phase Readiness
|
||||
- /admin is the single analytics entry point — ready for 18-03 and any future admin UI work
|
||||
- No stale routes remain in the analytics path
|
||||
- Build is clean
|
||||
|
||||
---
|
||||
*Phase: 18-cleanup-consolidamento*
|
||||
*Completed: 2026-06-19*
|
||||
@@ -0,0 +1,197 @@
|
||||
---
|
||||
phase: 18-cleanup-consolidamento
|
||||
plan: 03
|
||||
type: execute
|
||||
wave: 2
|
||||
depends_on:
|
||||
- "18-01"
|
||||
- "18-02"
|
||||
autonomous: false
|
||||
files_modified:
|
||||
- .planning/ROADMAP.md
|
||||
- .planning/REQUIREMENTS.md
|
||||
requirements:
|
||||
- CLEAN-04
|
||||
|
||||
must_haves:
|
||||
truths:
|
||||
- "Le fasi 13/15/16/17 sono marcate esplicitamente come cancellate/congelate/ri-scopate nel ROADMAP.md"
|
||||
- "REQUIREMENTS.md riflette lo stato aggiornato dei requirement corrispondenti"
|
||||
- "CLEAN-04 è marcato Done in REQUIREMENTS.md"
|
||||
- "La build del progetto passa dopo le rimozioni dei piani precedenti"
|
||||
artifacts:
|
||||
- path: ".planning/ROADMAP.md"
|
||||
provides: "Fasi 13/15/16/17 con status esplicito cancellate/congelate"
|
||||
contains: "CONGELATA\|ABBANDONATA\|RI-SCOPATA"
|
||||
- path: ".planning/REQUIREMENTS.md"
|
||||
provides: "CLEAN-04 marcato Complete"
|
||||
key_links:
|
||||
- from: "ROADMAP.md fasi 13/15/16/17"
|
||||
to: "STATUS field"
|
||||
via: "checklist e label espliciti"
|
||||
pattern: "Congelata|Abbandonata|Ri-scopata"
|
||||
---
|
||||
|
||||
<objective>
|
||||
Verificare formalmente il completamento di CLEAN-04 e validare la build post-cleanup.
|
||||
|
||||
Purpose: CLEAN-04 — le fasi v2.1 residue sono già state archiviate/marcate nel reset 2026-06-19.
|
||||
Questo piano le verifica, le sigla formalmente nel planning e valida che i piani 01/02
|
||||
non abbiano introdotto errori di build.
|
||||
Output: CLEAN-04 marcato Done, build TypeScript verificata, check umano che la dashboard funziona.
|
||||
</objective>
|
||||
|
||||
<execution_context>
|
||||
@$HOME/.claude/get-shit-done/workflows/execute-plan.md
|
||||
@$HOME/.claude/get-shit-done/templates/summary.md
|
||||
</execution_context>
|
||||
|
||||
<context>
|
||||
@.planning/ROADMAP.md
|
||||
@.planning/REQUIREMENTS.md
|
||||
@.planning/STATE.md
|
||||
</context>
|
||||
|
||||
<tasks>
|
||||
|
||||
<task type="auto">
|
||||
<name>Task 1: Verifica CLEAN-04 e aggiorna planning docs</name>
|
||||
|
||||
<read_first>
|
||||
- .planning/ROADMAP.md — sezione fasi 13/15/16/17, verifica che abbiano label/status esplicito
|
||||
- .planning/REQUIREMENTS.md — sezione CLEAN-04, verifica stato attuale
|
||||
</read_first>
|
||||
|
||||
<files>
|
||||
.planning/ROADMAP.md,
|
||||
.planning/REQUIREMENTS.md
|
||||
</files>
|
||||
|
||||
<action>
|
||||
STEP 1 — Verifica ROADMAP.md:
|
||||
Leggi le voci delle fasi 13, 15, 16, 17 nel ROADMAP.md.
|
||||
Devono avere tutte uno status esplicito. Controlla che la checklist mostri:
|
||||
- Phase 13: [~] con label "❌ CONGELATA (reset 2026-06-19)"
|
||||
- Phase 15: [~] con label "❌ ABBANDONATA (reset 2026-06-19)"
|
||||
- Phase 16: [~] con label "❌ RI-SCOPATA in v2.2"
|
||||
- Phase 17: [~] con label "❌ RI-SCOPATA in v2.2"
|
||||
Se uno di questi status manca o è incompleto, aggiungilo/completalo.
|
||||
Aggiungi anche una nota nella tabella Progress in fondo con la data di chiusura 2026-06-19
|
||||
dove mancante.
|
||||
|
||||
STEP 2 — Aggiorna REQUIREMENTS.md:
|
||||
Nella tabella "Cleanup & Consolidamento (Phase 18 / R1)":
|
||||
- CLEAN-01: cambia Status da "Pending" a "Complete"
|
||||
- CLEAN-02: cambia Status da "Pending" a "Complete"
|
||||
- CLEAN-03: cambia Status da "Pending" a "Complete"
|
||||
- CLEAN-04: cambia Status da "In corso (questo reset)" a "Complete"
|
||||
|
||||
Nella tabella Traceability in fondo:
|
||||
- CLEAN-01 | Phase 18 (R1) | Complete
|
||||
- CLEAN-02 | Phase 18 (R1) | Complete
|
||||
- CLEAN-03 | Phase 18 (R1) | Complete
|
||||
- CLEAN-04 | Phase 18 (R1) | Complete
|
||||
|
||||
STEP 3 — Build verification:
|
||||
Esegui: cd /Users/simonecavalli/Vault/IAMCAVALLI && npm run build
|
||||
Se la build fallisce:
|
||||
- Leggi l'output dell'errore
|
||||
- Identifica il file con import rotto
|
||||
- Risolvi (probabilmente un import di componente eliminato o una funzione non trovata)
|
||||
- Ri-esegui npm run build fino a successo
|
||||
Registra nel SUMMARY se ci sono stati fix necessari.
|
||||
</action>
|
||||
|
||||
<verify>
|
||||
<automated>
|
||||
grep "CLEAN-04" /Users/simonecavalli/Vault/IAMCAVALLI/.planning/REQUIREMENTS.md
|
||||
</automated>
|
||||
Risultato atteso: riga contenente "CLEAN-04" e "Complete".
|
||||
|
||||
Verifica fasi archiviate:
|
||||
grep -E "Phase 13|Phase 15|Phase 16|Phase 17" /Users/simonecavalli/Vault/IAMCAVALLI/.planning/ROADMAP.md | grep -E "CONGELATA|ABBANDONATA|RI-SCOPATA" | wc -l
|
||||
Risultato atteso: 4 (tutte e quattro le fasi con label esplicito).
|
||||
</verify>
|
||||
|
||||
<acceptance_criteria>
|
||||
- REQUIREMENTS.md: CLEAN-04 è "Complete"
|
||||
- REQUIREMENTS.md: CLEAN-01, CLEAN-02, CLEAN-03 sono "Complete"
|
||||
- ROADMAP.md: fasi 13/15/16/17 hanno label di stato esplicito (CONGELATA/ABBANDONATA/RI-SCOPATA)
|
||||
- npm run build completa senza errori
|
||||
</acceptance_criteria>
|
||||
|
||||
<done>
|
||||
Tutti i requisiti CLEAN-01..04 sono marcati Complete nei planning docs.
|
||||
La build TypeScript è pulita. Phase 18 è formalmente chiusa.
|
||||
</done>
|
||||
</task>
|
||||
|
||||
<task type="checkpoint:human-verify" gate="blocking">
|
||||
<name>Checkpoint: Verifica visuale Dashboard e assenza rotte rimosse</name>
|
||||
|
||||
<what-built>
|
||||
Piano 01 ha rimosso /admin/forecast e /admin/quotes/new.
|
||||
Piano 02 ha fuso le statistiche di /admin/analytics nella Dashboard /admin
|
||||
e rimosso la rotta /admin/analytics.
|
||||
Il selettore anno nella dashboard naviga a /admin?year=X.
|
||||
</what-built>
|
||||
|
||||
<how-to-verify>
|
||||
1. Avvia il dev server: `npm run dev` (o `npx next dev`)
|
||||
2. Vai su http://localhost:3000/admin — verifica:
|
||||
- Sidebar: NON compare la voce "Forecast"
|
||||
- Dashboard: in fondo alla pagina compare la sezione "Statistiche" con selettore anno,
|
||||
MetricCard economiche (Contrattualizzato / Incassato / Da incassare / Clienti acquisiti),
|
||||
grafico mensile a barre, sezione ore per cliente
|
||||
- KPI card esistenti (Clienti attivi, Revenue totale, Progetti in corso, Pagamenti in sospeso)
|
||||
sono ancora presenti in cima
|
||||
- FollowUpWidget e feed attività sono ancora presenti
|
||||
3. Vai su http://localhost:3000/admin/forecast → deve restituire 404
|
||||
4. Vai su http://localhost:3000/admin/analytics → deve restituire 404
|
||||
5. Vai su http://localhost:3000/admin/quotes/new → deve restituire 404
|
||||
6. Clicca sul selettore anno nella sezione Statistiche → cambia anno e verifica che
|
||||
l'URL diventa /admin?year=XXXX (non /admin/analytics)
|
||||
7. Nel dettaglio di un lead, apri il modal "Invia preventivo" → verifica che NON
|
||||
compare più il bottone per creare un nuovo preventivo manuale
|
||||
</how-to-verify>
|
||||
|
||||
<resume-signal>
|
||||
Scrivi "ok" se tutto funziona correttamente.
|
||||
Se trovi problemi, descrivi cosa non funziona e l'esecuzione correggerà prima di chiudere la fase.
|
||||
</resume-signal>
|
||||
</task>
|
||||
|
||||
</tasks>
|
||||
|
||||
<threat_model>
|
||||
## Trust Boundaries
|
||||
|
||||
| Boundary | Description |
|
||||
|----------|-------------|
|
||||
| planning docs → stato del progetto | Aggiornamento documenti di tracking — nessuna superficie di attacco |
|
||||
|
||||
## STRIDE Threat Register
|
||||
|
||||
| Threat ID | Category | Component | Disposition | Mitigation Plan |
|
||||
|-----------|----------|-----------|-------------|-----------------|
|
||||
| T-18-05 | Repudiation | ROADMAP.md / REQUIREMENTS.md | accept | I planning docs sono in git — ogni modifica è tracciata e reversibile |
|
||||
</threat_model>
|
||||
|
||||
<verification>
|
||||
Al termine del piano:
|
||||
1. REQUIREMENTS.md: CLEAN-01, CLEAN-02, CLEAN-03, CLEAN-04 → tutti "Complete"
|
||||
2. ROADMAP.md: fasi 13/15/16/17 con status esplicito
|
||||
3. npm run build → 0 errori
|
||||
4. Checkpoint umano superato (Dashboard mostra statistiche, rotte rimosse → 404)
|
||||
</verification>
|
||||
|
||||
<success_criteria>
|
||||
- CLEAN-04: fasi v2.1 residue formalmente chiuse nei planning docs
|
||||
- Planning docs aggiornati con stati finali Phase 18
|
||||
- Build pulita confermata
|
||||
- Verifica visuale umana completata
|
||||
</success_criteria>
|
||||
|
||||
<output>
|
||||
After completion, create `.planning/phases/18-cleanup-consolidamento/18-03-SUMMARY.md`
|
||||
</output>
|
||||
@@ -1,163 +0,0 @@
|
||||
import {
|
||||
getAnalyticsByYear,
|
||||
getMonthlyCollected,
|
||||
getAvailableYears,
|
||||
getTimeByClient,
|
||||
getTotalTrackedHours,
|
||||
} from "@/lib/analytics-queries";
|
||||
import { YearSelector, MonthlyChart } from "@/components/admin/YearSelector";
|
||||
|
||||
export const revalidate = 0;
|
||||
|
||||
function fmtEur(n: number) {
|
||||
return n.toLocaleString("it-IT", { style: "currency", currency: "EUR", minimumFractionDigits: 2 });
|
||||
}
|
||||
|
||||
function fmtSeconds(s: number): string {
|
||||
const h = Math.floor(s / 3600);
|
||||
const m = Math.floor((s % 3600) / 60);
|
||||
if (h > 0) return `${h}h ${m}m`;
|
||||
return `${m}m`;
|
||||
}
|
||||
|
||||
function MetricCard({
|
||||
label, value, sub, accent,
|
||||
}: {
|
||||
label: string; value: string; sub?: string; accent?: boolean;
|
||||
}) {
|
||||
return (
|
||||
<div className={`rounded-xl border p-5 ${accent ? "bg-[#1A463C] border-[#1A463C] text-white" : "bg-white border-[#e5e7eb]"}`}>
|
||||
<p className={`text-xs font-bold uppercase tracking-wider mb-2 ${accent ? "text-white/60" : "text-[#71717a]"}`}>
|
||||
{label}
|
||||
</p>
|
||||
<p className={`text-2xl font-bold tracking-tight ${accent ? "text-white" : "text-[#1a1a1a]"}`}>
|
||||
{value}
|
||||
</p>
|
||||
{sub && (
|
||||
<p className={`text-xs mt-1 ${accent ? "text-white/60" : "text-[#71717a]"}`}>{sub}</p>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default async function AnalyticsPage({
|
||||
searchParams,
|
||||
}: {
|
||||
searchParams: Promise<{ year?: string }>;
|
||||
}) {
|
||||
const { year: yearParam } = await searchParams;
|
||||
const year = parseInt(yearParam ?? "") || new Date().getFullYear();
|
||||
|
||||
const [data, monthly, availableYears, timeByClient, totalHours] = await Promise.all([
|
||||
getAnalyticsByYear(year),
|
||||
getMonthlyCollected(year),
|
||||
getAvailableYears(),
|
||||
getTimeByClient(year),
|
||||
getTotalTrackedHours(year),
|
||||
]);
|
||||
|
||||
const collectedPct =
|
||||
data.contracted > 0 ? Math.round((data.collected / data.contracted) * 100) : 0;
|
||||
|
||||
const maxClientSeconds = timeByClient[0]?.totalSeconds ?? 1;
|
||||
|
||||
return (
|
||||
<div className="space-y-10">
|
||||
{/* Header */}
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<h1 className="text-2xl font-bold text-[#1a1a1a]">Statistiche</h1>
|
||||
<p className="text-sm text-[#71717a] mt-0.5">Panoramica per anno</p>
|
||||
</div>
|
||||
<YearSelector currentYear={year} availableYears={availableYears} />
|
||||
</div>
|
||||
|
||||
{/* ── SEZIONE ECONOMICA ── */}
|
||||
<div className="space-y-4">
|
||||
<h2 className="text-sm font-bold text-[#71717a] uppercase tracking-wider">Fatturato</h2>
|
||||
|
||||
<div className="grid grid-cols-2 lg:grid-cols-4 gap-4">
|
||||
<MetricCard
|
||||
label="Contrattualizzato"
|
||||
value={fmtEur(data.contracted)}
|
||||
sub={`${data.clientsAcquired} client${data.clientsAcquired === 1 ? "e" : "i"}`}
|
||||
accent
|
||||
/>
|
||||
<MetricCard
|
||||
label="Incassato"
|
||||
value={fmtEur(data.collected)}
|
||||
sub={`${collectedPct}% del contrattualizzato`}
|
||||
/>
|
||||
<MetricCard
|
||||
label="Da incassare"
|
||||
value={fmtEur(data.pending)}
|
||||
sub="Tutti gli anni"
|
||||
/>
|
||||
<MetricCard
|
||||
label="Clienti acquisiti"
|
||||
value={String(data.clientsAcquired)}
|
||||
sub={`Anno ${year}`}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<MonthlyChart data={monthly} year={year} />
|
||||
|
||||
{data.contracted === 0 && (
|
||||
<p className="text-sm text-[#71717a] italic text-center py-2">
|
||||
Nessun cliente registrato nel {year}.
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* ── SEZIONE TIME TRACKING ── */}
|
||||
<div className="space-y-4">
|
||||
<h2 className="text-sm font-bold text-[#71717a] uppercase tracking-wider">
|
||||
Tempo tracciato
|
||||
</h2>
|
||||
|
||||
<div className="grid grid-cols-2 lg:grid-cols-4 gap-4">
|
||||
<MetricCard
|
||||
label="Ore totali"
|
||||
value={`${totalHours}h`}
|
||||
sub={`Anno ${year}`}
|
||||
accent
|
||||
/>
|
||||
</div>
|
||||
|
||||
{timeByClient.length === 0 ? (
|
||||
<div className="bg-white rounded-xl border border-[#e5e7eb] p-8 text-center">
|
||||
<p className="text-sm text-[#71717a] italic">
|
||||
Nessuna sessione registrata nel {year}.
|
||||
Usa il timer ▶ nella lista clienti per iniziare.
|
||||
</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="bg-white rounded-xl border border-[#e5e7eb] p-6 space-y-4">
|
||||
<h3 className="text-sm font-bold text-[#1a1a1a]">Ore per cliente — {year}</h3>
|
||||
<div className="space-y-3">
|
||||
{timeByClient.map((row) => {
|
||||
const pct = Math.round((row.totalSeconds / maxClientSeconds) * 100);
|
||||
return (
|
||||
<div key={row.clientId}>
|
||||
<div className="flex items-center justify-between mb-1">
|
||||
<span className="text-sm font-medium text-[#1a1a1a]">{row.clientName}</span>
|
||||
<span className="text-sm tabular-nums text-[#71717a]">
|
||||
{fmtSeconds(row.totalSeconds)}
|
||||
</span>
|
||||
</div>
|
||||
<div className="h-2 rounded-full bg-[#f4f4f5] overflow-hidden">
|
||||
<div
|
||||
className="h-full rounded-full bg-[#1A463C] transition-all"
|
||||
style={{ width: `${pct}%` }}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,75 +0,0 @@
|
||||
import { getRevenueForecast12Months } from "@/lib/forecast-queries";
|
||||
|
||||
export const revalidate = 0;
|
||||
|
||||
export default async function ForecastPage() {
|
||||
const forecast = await getRevenueForecast12Months();
|
||||
const totalForecast = forecast.reduce((sum, m) => sum + m.total, 0);
|
||||
|
||||
return (
|
||||
<div className="max-w-3xl mx-auto py-8 px-4">
|
||||
<h1 className="text-2xl font-bold text-[#1a1a1a] mb-2">Revenue Forecast</h1>
|
||||
<p className="text-sm text-[#71717a] mb-6">
|
||||
Proiezione 12 mesi basata sulle offerte attive, i loro accepted_total e le durate in mesi.
|
||||
Ogni offerta distribuisce il suo totale equamente per i mesi di durata a partire dalla data di inizio.
|
||||
</p>
|
||||
|
||||
<div className="bg-white rounded-lg border border-[#e5e7eb] overflow-hidden">
|
||||
<table className="w-full text-sm">
|
||||
<thead className="bg-[#f9f9f9] border-b border-[#e5e7eb]">
|
||||
<tr>
|
||||
<th className="px-4 py-3 text-left text-xs font-semibold text-[#71717a] uppercase tracking-wider">
|
||||
Mese
|
||||
</th>
|
||||
<th className="px-4 py-3 text-right text-xs font-semibold text-[#71717a] uppercase tracking-wider">
|
||||
Fatturato previsto
|
||||
</th>
|
||||
<th className="px-4 py-3 text-right text-xs font-semibold text-[#71717a] uppercase tracking-wider">
|
||||
Barra
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y divide-[#e5e7eb]">
|
||||
{forecast.map((month) => {
|
||||
const maxTotal = Math.max(...forecast.map((m) => m.total), 1);
|
||||
const pct = Math.round((month.total / maxTotal) * 100);
|
||||
return (
|
||||
<tr key={`${month.year}-${month.month}`} className="hover:bg-[#f9f9f9]">
|
||||
<td className="px-4 py-3 text-[#1a1a1a] capitalize">{month.label}</td>
|
||||
<td className="px-4 py-3 text-right font-mono text-[#1a1a1a]">
|
||||
{month.total > 0 ? `€${month.total.toFixed(2)}` : <span className="text-[#71717a]">—</span>}
|
||||
</td>
|
||||
<td className="px-4 py-3">
|
||||
{month.total > 0 && (
|
||||
<div className="flex justify-end items-center">
|
||||
<div
|
||||
className="bg-[#1A463C] h-2 rounded"
|
||||
style={{ width: `${pct}%`, minWidth: "4px", maxWidth: "120px" }}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
})}
|
||||
</tbody>
|
||||
<tfoot className="border-t-2 border-[#e5e7eb] bg-[#f9f9f9]">
|
||||
<tr>
|
||||
<td className="px-4 py-3 text-sm font-semibold text-[#1a1a1a]">Totale 12 mesi</td>
|
||||
<td className="px-4 py-3 text-right font-mono font-bold text-[#1a1a1a]">
|
||||
€{totalForecast.toFixed(2)}
|
||||
</td>
|
||||
<td />
|
||||
</tr>
|
||||
</tfoot>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
{forecast.every((m) => m.total === 0) && (
|
||||
<p className="text-sm text-[#71717a] mt-4">
|
||||
Nessuna offerta con accepted_total trovata. Assegna micro-offerte ai progetti e imposta il totale accettato.
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
+173
-7
@@ -1,6 +1,14 @@
|
||||
import { Suspense } from "react";
|
||||
import { getDashboardStats } from "@/lib/dashboard-queries";
|
||||
import { FollowUpWidget } from "@/components/admin/dashboard/FollowUpWidget";
|
||||
import {
|
||||
getAnalyticsByYear,
|
||||
getMonthlyCollected,
|
||||
getAvailableYears,
|
||||
getTimeByClient,
|
||||
getTotalTrackedHours,
|
||||
} from "@/lib/analytics-queries";
|
||||
import { YearSelector, MonthlyChart } from "@/components/admin/YearSelector";
|
||||
import { Users, FolderOpen, Euro, Clock } from "lucide-react";
|
||||
|
||||
export const revalidate = 0;
|
||||
@@ -32,6 +40,44 @@ function KpiCard({
|
||||
);
|
||||
}
|
||||
|
||||
function MetricCard({
|
||||
label,
|
||||
value,
|
||||
sub,
|
||||
accent,
|
||||
}: {
|
||||
label: string;
|
||||
value: string;
|
||||
sub?: string;
|
||||
accent?: boolean;
|
||||
}) {
|
||||
return (
|
||||
<div
|
||||
className={`rounded-xl border p-5 ${
|
||||
accent ? "bg-[#1A463C] border-[#1A463C] text-white" : "bg-white border-[#e5e7eb]"
|
||||
}`}
|
||||
>
|
||||
<p
|
||||
className={`text-xs font-bold uppercase tracking-wider mb-2 ${
|
||||
accent ? "text-white/60" : "text-[#71717a]"
|
||||
}`}
|
||||
>
|
||||
{label}
|
||||
</p>
|
||||
<p
|
||||
className={`text-2xl font-bold tracking-tight ${
|
||||
accent ? "text-white" : "text-[#1a1a1a]"
|
||||
}`}
|
||||
>
|
||||
{value}
|
||||
</p>
|
||||
{sub && (
|
||||
<p className={`text-xs mt-1 ${accent ? "text-white/60" : "text-[#71717a]"}`}>{sub}</p>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const ACTIVITY_ICONS: Record<string, string> = {
|
||||
nuovo_cliente: "👤",
|
||||
nuovo_progetto: "📁",
|
||||
@@ -48,15 +94,44 @@ function fmt(ts: Date) {
|
||||
}).format(new Date(ts));
|
||||
}
|
||||
|
||||
function fmtEur(val: string) {
|
||||
return new Intl.NumberFormat("it-IT", { style: "currency", currency: "EUR" }).format(
|
||||
parseFloat(val) || 0
|
||||
);
|
||||
function fmtEur(n: number) {
|
||||
return n.toLocaleString("it-IT", {
|
||||
style: "currency",
|
||||
currency: "EUR",
|
||||
minimumFractionDigits: 2,
|
||||
});
|
||||
}
|
||||
|
||||
export default async function AdminDashboard() {
|
||||
function fmtSeconds(s: number): string {
|
||||
const h = Math.floor(s / 3600);
|
||||
const m = Math.floor((s % 3600) / 60);
|
||||
if (h > 0) return `${h}h ${m}m`;
|
||||
return `${m}m`;
|
||||
}
|
||||
|
||||
export default async function AdminDashboard({
|
||||
searchParams,
|
||||
}: {
|
||||
searchParams: Promise<{ year?: string }>;
|
||||
}) {
|
||||
const { year: yearParam } = await searchParams;
|
||||
const year = parseInt(yearParam ?? "") || new Date().getFullYear();
|
||||
|
||||
const { kpi, activity } = await getDashboardStats();
|
||||
|
||||
const [data, monthly, availableYears, timeByClient, totalHours] = await Promise.all([
|
||||
getAnalyticsByYear(year),
|
||||
getMonthlyCollected(year),
|
||||
getAvailableYears(),
|
||||
getTimeByClient(year),
|
||||
getTotalTrackedHours(year),
|
||||
]);
|
||||
|
||||
const collectedPct =
|
||||
data.contracted > 0 ? Math.round((data.collected / data.contracted) * 100) : 0;
|
||||
|
||||
const maxClientSeconds = timeByClient[0]?.totalSeconds ?? 1;
|
||||
|
||||
return (
|
||||
<div className="max-w-5xl">
|
||||
<h1 className="text-2xl font-bold text-gray-900 mb-6">Dashboard</h1>
|
||||
@@ -78,7 +153,7 @@ export default async function AdminDashboard() {
|
||||
/>
|
||||
<KpiCard
|
||||
label="Revenue totale"
|
||||
value={fmtEur(kpi.revenueTotale)}
|
||||
value={fmtEur(parseFloat(kpi.revenueTotale))}
|
||||
sub="progetti non archiviati"
|
||||
icon={Euro}
|
||||
color="bg-emerald-600"
|
||||
@@ -91,7 +166,7 @@ export default async function AdminDashboard() {
|
||||
/>
|
||||
<KpiCard
|
||||
label="Pagamenti in sospeso"
|
||||
value={fmtEur(kpi.pagamentiInSospeso)}
|
||||
value={fmtEur(parseFloat(kpi.pagamentiInSospeso))}
|
||||
sub="da_saldare + inviata"
|
||||
icon={Clock}
|
||||
color="bg-amber-500"
|
||||
@@ -122,6 +197,97 @@ export default async function AdminDashboard() {
|
||||
</ul>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* ── SEZIONE STATISTICHE ANNUALI ── */}
|
||||
<div className="mt-10 space-y-10">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<h2 className="text-xl font-bold text-gray-900">Statistiche</h2>
|
||||
<p className="text-sm text-gray-400 mt-0.5">Panoramica per anno</p>
|
||||
</div>
|
||||
<YearSelector currentYear={year} availableYears={availableYears} />
|
||||
</div>
|
||||
|
||||
{/* Sezione economica */}
|
||||
<div className="space-y-4">
|
||||
<h3 className="text-sm font-bold text-[#71717a] uppercase tracking-wider">Fatturato</h3>
|
||||
<div className="grid grid-cols-2 lg:grid-cols-4 gap-4">
|
||||
<MetricCard
|
||||
label="Contrattualizzato"
|
||||
value={fmtEur(data.contracted)}
|
||||
sub={`${data.clientsAcquired} client${data.clientsAcquired === 1 ? "e" : "i"}`}
|
||||
accent
|
||||
/>
|
||||
<MetricCard
|
||||
label="Incassato"
|
||||
value={fmtEur(data.collected)}
|
||||
sub={`${collectedPct}% del contrattualizzato`}
|
||||
/>
|
||||
<MetricCard
|
||||
label="Da incassare"
|
||||
value={fmtEur(data.pending)}
|
||||
sub="Tutti gli anni"
|
||||
/>
|
||||
<MetricCard
|
||||
label="Clienti acquisiti"
|
||||
value={String(data.clientsAcquired)}
|
||||
sub={`Anno ${year}`}
|
||||
/>
|
||||
</div>
|
||||
<MonthlyChart data={monthly} year={year} />
|
||||
{data.contracted === 0 && (
|
||||
<p className="text-sm text-[#71717a] italic text-center py-2">
|
||||
Nessun cliente registrato nel {year}.
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Sezione time tracking */}
|
||||
<div className="space-y-4">
|
||||
<h3 className="text-sm font-bold text-[#71717a] uppercase tracking-wider">
|
||||
Tempo tracciato
|
||||
</h3>
|
||||
<div className="grid grid-cols-2 lg:grid-cols-4 gap-4">
|
||||
<MetricCard label="Ore totali" value={`${totalHours}h`} sub={`Anno ${year}`} accent />
|
||||
</div>
|
||||
|
||||
{timeByClient.length === 0 ? (
|
||||
<div className="bg-white rounded-xl border border-[#e5e7eb] p-8 text-center">
|
||||
<p className="text-sm text-[#71717a] italic">
|
||||
Nessuna sessione registrata nel {year}. Usa il timer ▶ nella lista clienti per
|
||||
iniziare.
|
||||
</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="bg-white rounded-xl border border-[#e5e7eb] p-6 space-y-4">
|
||||
<h3 className="text-sm font-bold text-[#1a1a1a]">Ore per cliente — {year}</h3>
|
||||
<div className="space-y-3">
|
||||
{timeByClient.map((row) => {
|
||||
const pct = Math.round((row.totalSeconds / maxClientSeconds) * 100);
|
||||
return (
|
||||
<div key={row.clientId}>
|
||||
<div className="flex items-center justify-between mb-1">
|
||||
<span className="text-sm font-medium text-[#1a1a1a]">
|
||||
{row.clientName}
|
||||
</span>
|
||||
<span className="text-sm tabular-nums text-[#71717a]">
|
||||
{fmtSeconds(row.totalSeconds)}
|
||||
</span>
|
||||
</div>
|
||||
<div className="h-2 rounded-full bg-[#f4f4f5] overflow-hidden">
|
||||
<div
|
||||
className="h-full rounded-full bg-[#1A463C] transition-all"
|
||||
style={{ width: `${pct}%` }}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -135,14 +135,12 @@ export async function assignOfferToProject(formData: FormData) {
|
||||
: null,
|
||||
});
|
||||
revalidatePath(`/admin/projects/${parsed.data.project_id}`);
|
||||
revalidatePath("/admin/forecast");
|
||||
}
|
||||
|
||||
export async function removeProjectOffer(projectOfferId: string, projectId: string) {
|
||||
await requireAdmin();
|
||||
await db.delete(project_offers).where(eq(project_offers.id, projectOfferId));
|
||||
revalidatePath(`/admin/projects/${projectId}`);
|
||||
revalidatePath("/admin/forecast");
|
||||
}
|
||||
|
||||
export async function updateProjectOfferTotal(
|
||||
@@ -158,5 +156,4 @@ export async function updateProjectOfferTotal(
|
||||
.set({ accepted_total: amount.toFixed(2) })
|
||||
.where(eq(project_offers.id, projectOfferId));
|
||||
revalidatePath(`/admin/projects/${projectId}`);
|
||||
revalidatePath("/admin/forecast");
|
||||
}
|
||||
@@ -1,3 +0,0 @@
|
||||
"use server";
|
||||
|
||||
export { createQuote } from "@/lib/quote-actions";
|
||||
@@ -1,28 +0,0 @@
|
||||
import { Card } from "@/components/ui/card";
|
||||
import { QuoteBuilderForm } from "@/components/admin/quotes/QuoteBuilderForm";
|
||||
import { getAllClientsWithPayments } from "@/lib/admin-queries";
|
||||
import { getAllOfferMacrosWithMicros } from "@/lib/admin-queries";
|
||||
|
||||
export const revalidate = 0;
|
||||
|
||||
export default async function QuoteBuilderPage() {
|
||||
const [clientsData, offersData] = await Promise.all([
|
||||
getAllClientsWithPayments(),
|
||||
getAllOfferMacrosWithMicros(),
|
||||
]);
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<div>
|
||||
<h1 className="text-3xl font-bold text-gray-900">Crea Preventivo</h1>
|
||||
<p className="text-gray-600 mt-1">
|
||||
Componi un preventivo per il cliente selezionando l'offerta e il prezzo
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<Card className="p-6">
|
||||
<QuoteBuilderForm clients={clientsData} offerMacros={offersData} />
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -8,7 +8,6 @@ import {
|
||||
Users,
|
||||
FolderOpen,
|
||||
Tag,
|
||||
TrendingUp,
|
||||
BookOpen,
|
||||
Settings,
|
||||
LogOut,
|
||||
@@ -21,7 +20,6 @@ const NAV_ITEMS = [
|
||||
{ href: "/admin/leads", label: "Lead", icon: Zap },
|
||||
{ href: "/admin/projects", label: "Progetti", icon: FolderOpen },
|
||||
{ href: "/admin/offers", label: "Offerte", icon: Tag },
|
||||
{ href: "/admin/forecast", label: "Forecast", icon: TrendingUp },
|
||||
{ href: "/admin/catalog", label: "Catalogo", icon: BookOpen },
|
||||
{ href: "/admin/impostazioni", label: "Impostazioni", icon: Settings },
|
||||
];
|
||||
|
||||
@@ -15,7 +15,7 @@ export function YearSelector({
|
||||
const thisYear = new Date().getFullYear();
|
||||
|
||||
function go(y: number) {
|
||||
router.push(`/admin/analytics?year=${y}`);
|
||||
router.push(`/admin?year=${y}`);
|
||||
}
|
||||
|
||||
return (
|
||||
|
||||
@@ -22,7 +22,6 @@ import {
|
||||
} from "@/components/ui/form";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
||||
import { Loader2 } from "lucide-react";
|
||||
|
||||
const assignQuoteSchema = z.object({
|
||||
@@ -33,7 +32,6 @@ const assignQuoteSchema = z.object({
|
||||
export function SendQuoteModal({ leadId }: { leadId: string }) {
|
||||
const [open, setOpen] = useState(false);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [tab, setTab] = useState<"existing" | "new">("existing");
|
||||
|
||||
const form = useForm<any>({
|
||||
resolver: zodResolver(assignQuoteSchema),
|
||||
@@ -72,58 +70,36 @@ export function SendQuoteModal({ leadId }: { leadId: string }) {
|
||||
<DialogHeader>
|
||||
<DialogTitle>Invia Preventivo al Lead</DialogTitle>
|
||||
</DialogHeader>
|
||||
<Tabs value={tab} onValueChange={(v) => setTab(v as "existing" | "new")}>
|
||||
<TabsList className="grid w-full grid-cols-2">
|
||||
<TabsTrigger value="existing">Preventivo Esistente</TabsTrigger>
|
||||
<TabsTrigger value="new">Crea Nuovo</TabsTrigger>
|
||||
</TabsList>
|
||||
|
||||
<TabsContent value="existing" className="mt-4">
|
||||
<Form {...form}>
|
||||
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="quote_token"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Token Preventivo</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
placeholder="Incolla il token del preventivo"
|
||||
{...field}
|
||||
value={field.value || ""}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<p className="text-xs text-gray-600">
|
||||
Copia il token dal preventivo e incollalo qui. Il lead sarà aggiornato a
|
||||
"Proposal Sent".
|
||||
</p>
|
||||
<Button type="submit" disabled={loading} className="w-full">
|
||||
{loading && <Loader2 className="mr-2 h-4 w-4 animate-spin" />}
|
||||
Invia
|
||||
</Button>
|
||||
</form>
|
||||
</Form>
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="new" className="mt-4">
|
||||
<p className="text-sm text-gray-600 mb-4">
|
||||
Crea un nuovo preventivo per questo lead. Ti reindirizzeremo al builder.
|
||||
<Form {...form}>
|
||||
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="quote_token"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Token Preventivo</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
placeholder="Incolla il token del preventivo"
|
||||
{...field}
|
||||
value={field.value || ""}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<p className="text-xs text-gray-600">
|
||||
Copia il token dal preventivo e incollalo qui. Il lead sarà aggiornato a
|
||||
"Proposal Sent".
|
||||
</p>
|
||||
<Button
|
||||
className="w-full"
|
||||
onClick={() => {
|
||||
window.location.href = `/admin/quotes/new?lead_id=${leadId}`;
|
||||
}}
|
||||
>
|
||||
Apri Quote Builder
|
||||
<Button type="submit" disabled={loading} className="w-full">
|
||||
{loading && <Loader2 className="mr-2 h-4 w-4 animate-spin" />}
|
||||
Invia
|
||||
</Button>
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
</form>
|
||||
</Form>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
|
||||
@@ -1,46 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import { Label } from "@/components/ui/label";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectGroup,
|
||||
SelectLabel,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/components/ui/select";
|
||||
import { OfferMacro, OfferMicro } from "@/db/schema";
|
||||
|
||||
interface OfferSelectorProps {
|
||||
macros: (OfferMacro & { micros: OfferMicro[] })[];
|
||||
value: string;
|
||||
onValueChange: (value: string) => void;
|
||||
}
|
||||
|
||||
export function OfferSelector({ macros, value, onValueChange }: OfferSelectorProps) {
|
||||
return (
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="offer">Offerta*</Label>
|
||||
<Select value={value} onValueChange={onValueChange}>
|
||||
<SelectTrigger id="offer">
|
||||
<SelectValue placeholder="Seleziona un'offerta" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{macros.map((macro) => (
|
||||
<SelectGroup key={macro.id}>
|
||||
<SelectLabel className="text-xs font-semibold text-gray-600">
|
||||
{macro.public_name}
|
||||
</SelectLabel>
|
||||
{macro.micros.map((micro) => (
|
||||
<SelectItem key={micro.id} value={micro.id}>
|
||||
{micro.public_name}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectGroup>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,63 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { Check, X } from "lucide-react";
|
||||
import { useState } from "react";
|
||||
|
||||
interface PriceOverrideInputProps {
|
||||
value: string;
|
||||
onChange: (value: string) => void;
|
||||
calculatedTotal?: number;
|
||||
}
|
||||
|
||||
export function PriceOverrideInput({ value, onChange, calculatedTotal }: PriceOverrideInputProps) {
|
||||
const [isTouched, setIsTouched] = useState(false);
|
||||
|
||||
const numValue = parseFloat(value) || 0;
|
||||
const matches = calculatedTotal !== undefined && Math.abs(numValue - calculatedTotal) < 0.01;
|
||||
|
||||
return (
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="total">Totale preventivo*</Label>
|
||||
<div className="relative">
|
||||
<Input
|
||||
id="total"
|
||||
type="number"
|
||||
step="0.01"
|
||||
min="0"
|
||||
placeholder="0.00"
|
||||
value={value}
|
||||
onChange={(e) => onChange(e.target.value)}
|
||||
onBlur={() => setIsTouched(true)}
|
||||
className="pr-10"
|
||||
required
|
||||
/>
|
||||
{isTouched && value && (
|
||||
<div className="absolute right-3 top-1/2 -translate-y-1/2">
|
||||
{matches ? (
|
||||
<Check size={18} className="text-green-500" />
|
||||
) : (
|
||||
<X size={18} className="text-red-500" />
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{calculatedTotal !== undefined && (
|
||||
<p className="text-xs text-gray-500">
|
||||
Prezzo calcolato: €{calculatedTotal.toLocaleString("it-IT", {
|
||||
minimumFractionDigits: 2,
|
||||
maximumFractionDigits: 2,
|
||||
})}
|
||||
</p>
|
||||
)}
|
||||
|
||||
{isTouched && value && !matches && (
|
||||
<p className="text-xs text-yellow-600">
|
||||
Attenzione: il valore inserito non corrisponde al totale calcolato (server verificherà)
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,210 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import { useState, useCallback, useTransition } from "react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/components/ui/select";
|
||||
import { Card } from "@/components/ui/card";
|
||||
import { QuotePreview } from "./QuotePreview";
|
||||
import { OfferSelector } from "./OfferSelector";
|
||||
import { PriceOverrideInput } from "./PriceOverrideInput";
|
||||
import { createQuote, getOfferWithPhases } from "@/lib/quote-actions";
|
||||
import { OfferMacro, OfferMicro } from "@/db/schema";
|
||||
import { Check, Copy } from "lucide-react";
|
||||
|
||||
interface ClientOption {
|
||||
id: string;
|
||||
name: string;
|
||||
}
|
||||
|
||||
interface QuoteBuilderFormProps {
|
||||
clients: ClientOption[];
|
||||
offerMacros: (OfferMacro & { micros: OfferMicro[] })[];
|
||||
}
|
||||
|
||||
export function QuoteBuilderForm({ clients, offerMacros }: QuoteBuilderFormProps) {
|
||||
const [isPending, startTransition] = useTransition();
|
||||
const [selectedClient, setSelectedClient] = useState<string>("");
|
||||
const [selectedOffer, setSelectedOffer] = useState<string>("");
|
||||
const [selectedTotal, setSelectedTotal] = useState<string>("");
|
||||
const [currentOffer, setCurrentOffer] = useState<any>(null);
|
||||
const [successLink, setSuccessLink] = useState<string | null>(null);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [copied, setCopied] = useState(false);
|
||||
|
||||
// Fetch offer details when selection changes
|
||||
const handleOfferChange = useCallback((offerId: string) => {
|
||||
setSelectedOffer(offerId);
|
||||
setError(null);
|
||||
startTransition(async () => {
|
||||
const offer = await getOfferWithPhases(offerId);
|
||||
setCurrentOffer(offer);
|
||||
});
|
||||
}, []);
|
||||
|
||||
const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
|
||||
e.preventDefault();
|
||||
setError(null);
|
||||
setSuccessLink(null);
|
||||
|
||||
startTransition(async () => {
|
||||
const result = await createQuote({
|
||||
client_id: selectedClient,
|
||||
offer_micro_id: selectedOffer,
|
||||
accepted_total: selectedTotal,
|
||||
});
|
||||
|
||||
if (result.success) {
|
||||
setSuccessLink(result.publicLink || "");
|
||||
// Reset form
|
||||
setSelectedClient("");
|
||||
setSelectedOffer("");
|
||||
setSelectedTotal("");
|
||||
setCurrentOffer(null);
|
||||
} else {
|
||||
setError(result.error || "Errore sconosciuto");
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const handleCopyLink = async () => {
|
||||
if (successLink) {
|
||||
const fullUrl = `${window.location.origin}${successLink}`;
|
||||
await navigator.clipboard.writeText(fullUrl);
|
||||
setCopied(true);
|
||||
setTimeout(() => setCopied(false), 2000);
|
||||
}
|
||||
};
|
||||
|
||||
if (successLink) {
|
||||
return (
|
||||
<Card className="p-6 space-y-4">
|
||||
<div className="text-center space-y-3">
|
||||
<div className="inline-flex items-center justify-center w-12 h-12 bg-green-100 rounded-full">
|
||||
<Check size={24} className="text-green-600" />
|
||||
</div>
|
||||
<h3 className="text-lg font-semibold text-gray-900">Preventivo creato</h3>
|
||||
<p className="text-sm text-gray-600">
|
||||
Il preventivo è stato salvato e il link è pronto per essere condiviso
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="bg-gray-50 rounded-lg p-4">
|
||||
<p className="text-xs text-gray-500 mb-2">Link pubblico</p>
|
||||
<div className="flex gap-2">
|
||||
<code className="flex-1 bg-white border border-gray-200 rounded px-3 py-2 text-xs text-gray-900 font-mono">
|
||||
{window.location.origin}
|
||||
{successLink}
|
||||
</code>
|
||||
<Button
|
||||
type="button"
|
||||
size="sm"
|
||||
variant={copied ? "default" : "outline"}
|
||||
onClick={handleCopyLink}
|
||||
className="flex items-center gap-2"
|
||||
>
|
||||
{copied ? (
|
||||
<>
|
||||
<Check size={16} /> Copiato
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Copy size={16} /> Copia
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
className="w-full"
|
||||
onClick={() => {
|
||||
setSuccessLink(null);
|
||||
setSelectedClient("");
|
||||
setSelectedOffer("");
|
||||
setSelectedTotal("");
|
||||
setCurrentOffer(null);
|
||||
}}
|
||||
>
|
||||
Crea un altro preventivo
|
||||
</Button>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="grid grid-cols-2 gap-6">
|
||||
{/* LEFT COLUMN: FORM */}
|
||||
<div>
|
||||
<form onSubmit={handleSubmit} className="space-y-4">
|
||||
<h2 className="text-lg font-semibold text-gray-900">Crea Preventivo</h2>
|
||||
|
||||
{/* Client Select */}
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="client">Cliente*</Label>
|
||||
<Select value={selectedClient} onValueChange={setSelectedClient}>
|
||||
<SelectTrigger id="client">
|
||||
<SelectValue placeholder="Seleziona un cliente" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{clients.map((client) => (
|
||||
<SelectItem key={client.id} value={client.id}>
|
||||
{client.name}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
{/* Offer Select */}
|
||||
{selectedClient && (
|
||||
<OfferSelector
|
||||
macros={offerMacros}
|
||||
value={selectedOffer}
|
||||
onValueChange={handleOfferChange}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Price Input */}
|
||||
{selectedOffer && (
|
||||
<PriceOverrideInput
|
||||
value={selectedTotal}
|
||||
onChange={setSelectedTotal}
|
||||
calculatedTotal={currentOffer?.duration_months ? currentOffer.duration_months * 1000 : undefined}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Error Message */}
|
||||
{error && (
|
||||
<div className="p-3 bg-red-50 border border-red-200 rounded-lg">
|
||||
<p className="text-sm text-red-800">{error}</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Submit Button */}
|
||||
<Button
|
||||
type="submit"
|
||||
disabled={!selectedClient || !selectedOffer || !selectedTotal || isPending}
|
||||
className="w-full bg-[#1A463C] text-white hover:bg-[#1A463C]/90"
|
||||
>
|
||||
{isPending ? "Salvataggio…" : "Salva Preventivo"}
|
||||
</Button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
{/* RIGHT COLUMN: PREVIEW */}
|
||||
<div>
|
||||
<QuotePreview offer={currentOffer} selectedTotal={parseFloat(selectedTotal) || 0} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,61 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import { Card } from "@/components/ui/card";
|
||||
import { OfferMicro, OfferPhase } from "@/db/schema";
|
||||
|
||||
interface QuotePreviewProps {
|
||||
offer: (OfferMicro & { phases: OfferPhase[] }) | null;
|
||||
selectedTotal: number;
|
||||
}
|
||||
|
||||
export function QuotePreview({ offer, selectedTotal }: QuotePreviewProps) {
|
||||
if (!offer) {
|
||||
return (
|
||||
<div className="h-full flex items-center justify-center text-gray-400">
|
||||
<p className="text-sm">Seleziona un'offerta per visualizzare l'anteprima</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Card className="p-6 space-y-6">
|
||||
<div className="border-b pb-6">
|
||||
<h3 className="text-lg font-semibold text-gray-900">{offer.public_name}</h3>
|
||||
{offer.transformation_promise && (
|
||||
<p className="text-sm text-gray-600 mt-2">{offer.transformation_promise}</p>
|
||||
)}
|
||||
<p className="text-xs text-gray-500 mt-3">
|
||||
Durata: {offer.duration_months} mese{offer.duration_months !== 1 ? "i" : ""}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="space-y-4">
|
||||
<h4 className="font-medium text-gray-900">Fasi incluse</h4>
|
||||
{offer.phases && offer.phases.length > 0 ? (
|
||||
offer.phases.map((phase) => (
|
||||
<div key={phase.id} className="space-y-2">
|
||||
<p className="text-sm font-medium text-gray-800">{phase.title}</p>
|
||||
{phase.description && (
|
||||
<p className="text-xs text-gray-600 ml-2">{phase.description}</p>
|
||||
)}
|
||||
</div>
|
||||
))
|
||||
) : (
|
||||
<p className="text-xs text-gray-400">Nessuna fase configurata</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="border-t pt-6 space-y-3">
|
||||
<div className="flex justify-between items-center">
|
||||
<span className="text-sm text-gray-600">Totale preventivo</span>
|
||||
<span className="text-2xl font-bold text-gray-900">
|
||||
€{selectedTotal.toLocaleString("it-IT", { minimumFractionDigits: 2, maximumFractionDigits: 2 })}
|
||||
</span>
|
||||
</div>
|
||||
<p className="text-xs text-gray-500">
|
||||
Questo preventivo sarà visibile al cliente tramite link riservato
|
||||
</p>
|
||||
</div>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
@@ -859,7 +859,6 @@ export async function getQuotesByClient(clientId: string): Promise<Quote[]> {
|
||||
|
||||
/**
|
||||
* Get all offer macros with their micros (ordered by sort_order)
|
||||
* Used by: /admin/quotes/new form (OfferSelector dropdown)
|
||||
*/
|
||||
export async function getAllOfferMacrosWithMicros(): Promise<
|
||||
(OfferMacro & { micros: OfferMicro[] })[]
|
||||
|
||||
Reference in New Issue
Block a user