Files
clienthub/.planning/phases/03-service-catalog-quote-builder/03-CONTEXT.md
T

4.2 KiB

phase, title, status, date
phase title status date
3 Service Catalog & Quote Builder discussed 2026-05-16

Phase 3 — Decisions & Context

Phase Goal

L'admin può costruire un catalogo servizi riutilizzabile e comporre preventivi da esso; il cliente vede solo il totale accettato (accepted_total).

Key Decisions (LOCKED)

1. Service Catalog — Location: /admin/catalog

  • Pagina dedicata /admin/catalog con link aggiunto in NavBar (Clienti | Statistiche | Catalogo).
  • Tabella con colonne: Nome, Descrizione, Prezzo unitario, Stato (Attivo/Disattivato).
  • CRUD completo: aggiungi, modifica inline, disattiva (soft delete via active = false).
  • Items disattivati restano visibili in elenco (filtro toggle) ma non appaiono nel selettore quote.

2. Quote Builder — Location: Tab "Preventivo" in /admin/clients/[id]

  • Nuovo tab nell'admin client detail page, accanto a Fasi, Pagamenti, Documenti.
  • Mostra le voci preventivo del cliente con totale calcolato.
  • L'admin può aggiungere voci dal catalogo (dropdown con active = true) o voci libere (nome + prezzo custom).
  • Nessun blocco dopo la finalizzazione — voci sempre editabili.

3. Voci Preventivo — Catalogo + Free-form

  • Da catalogo: seleziona voce, inserisce quantità; unit_price viene snapshotato al momento dell'aggiunta (non segue futuri cambi al catalogo).

  • Voce libera: nome testo libero, prezzo unitario, quantità. service_id sarà NULL in quote_items.

    Schema change needed: service_id in quote_items deve diventare nullable (attualmente notNull()). Aggiungere campo custom_label text a quote_items per le voci libere.

4. Accepted Total — Admin-controlled, not auto-calculated

  • Il builder mostra la somma calcolata delle voci come riferimento.
  • Esiste un campo separato "Totale accettato dal cliente" (editable input) con pulsante "Salva".
  • Il pulsante scrive il valore (che l'admin può modificare liberamente) su clients.accepted_total.
  • Rationale: il cliente accetta una cifra commerciale (es. €1.500 tondo) che può differire dalla somma analitica interna. Il preventivo interno è solo uno strumento di stima.

5. Pagamenti — Nessun aggiornamento automatico

  • Finalizzare il preventivo NON tocca i record payments.
  • L'admin aggiorna manualmente gli importi di acconto e saldo nella tab Pagamenti.

6. Constraint già in vigore (IMMUTABLE)

  • quote_items non vengono mai esposti dalle API client-facing.
  • clients.accepted_total è l'unico valore economico che il cliente vede.

Schema Changes Required

-- quote_items.service_id diventa nullable
ALTER TABLE quote_items ALTER COLUMN service_id DROP NOT NULL;

-- aggiunta colonna per voci libere
ALTER TABLE quote_items ADD COLUMN custom_label text;

In Drizzle schema.ts:

service_id: text("service_id").references(() => service_catalog.id, { onDelete: "restrict" }), // removed .notNull()
custom_label: text("custom_label"), // new field

Reusable Assets

  • service_catalog e quote_items tables già presenti in schema.ts con relazioni e TS types.
  • Pattern Server Actions già stabilito (vedi clients/[id]/actions.ts).
  • Pattern tab UI già stabilito (tabs/PhasesTab.tsx, tabs/PaymentsTab.tsx, etc.).
  • Pattern inline edit già stabilito (DocumentRow.tsx).
  • fmtEur() già definita in analytics/page.tsx — estrarre in lib/utils o duplicare.

Pages & Routes to Create

Route Type Purpose
/admin/catalog Server Component page Lista + CRUD catalogo servizi
/admin/catalog/actions.ts Server Actions createService, updateService, toggleActive
src/components/admin/tabs/QuoteTab.tsx Client Component Quote builder UI
src/app/admin/clients/[id]/quote-actions.ts Server Actions addQuoteItem, removeQuoteItem, updateAcceptedTotal

UI Notes

  • Stile coerente con tab esistenti (border-b tabs navigation nell'admin client page).
  • Catalogo: tabella simile a admin clients list (bg-white rounded-xl border border-[#e5e7eb]).
  • Quote builder: due colonne su desktop (catalogo disponibile | voci selezionate) o lista unica con selettore.
  • Totale calcolato mostrato in bold come sommario; campo accepted_total separato con label chiara.
  • Colore brand: #1A463C per accent, #DEF168 per highlight.