Commit Graph

65 Commits

Author SHA1 Message Date
simone 6d5e04bf9c style: refine Pipeline fidelity to Quiet Luxury reference
- Sidebar: revert to white palette (drop emerald pass), remove right border
- Table: softer slate-100 borders, remove min-height gap, "Mostrando N di M
  lead" footer, empty Tag cell = dashed-circle add button
- SegmentedToggle: more visible track (slate-200/60), neutral active text
- Kanban: roomy fixed-width columns (280px, horizontal scroll), sober column
  headers (no colored dots), airy cards matching mock (name+badge/email/
  company+"Tag +"); 6 stages + @dnd-kit intact

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-10 22:33:26 +02:00
simone 26d752892a style: align light palette to Quiet Luxury reference
- Page background now #F8F9FA (was white); tokens matched to reference:
  foreground slate-900, muted slate-50/slate-500, border slate-200,
  card pure white, tertiary slate-400, border-light slate-100
- Sidebar colors matched to mock: emerald-200/70 inactive, white/8 active
  with emerald-400 active icon, emerald-50 logo, red-300 logout, right border
- Content wrapped in max-w-[1400px] centered container (px-8 lg:px-10)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-10 16:33:00 +02:00
simone 9eb2d45c67 fix: sidebar width/centering + kanban card badge & table details
- Sidebar: expanded 256->200px, collapsed 80->64px; collapsed state now
  renders icon-only (label removed from flow) so icons are centered, not
  pushed left by an invisible text span
- Kanban cards: add StatusBadge + email line to match design reference
- Lead table: add "Mostrando N lead" footer and center/right-align
  Stato/Tag/Azioni headers per reference

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-10 16:28:43 +02:00
simone 86e1499e8f feat: Quiet Luxury design system + Pipeline (ex-Lead) redesign
- Design system foundations: Plus Jakarta Sans font, shadow-card/radius
  tokens in @theme, design-reference/DESIGN-SYSTEM.md with component inventory
- Collapsible admin shell (AdminShell): smooth w-64<->w-20 sidebar, new top
  header with "Admin" + avatar placeholder, localStorage-persisted state
- Rename route /admin/leads -> /admin/pipeline (redirect stubs preserved),
  nav label Lead -> Pipeline; DB table unchanged
- Reusable primitives: StatusBadge, SearchInput, SegmentedToggle (dual-theme)
- Luxury restyle of lead table, kanban (6 stages + @dnd-kit intact), PageHeader
- Tokenize editable-cell / option-multi-select for dark-mode legibility

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-10 16:16:44 +02:00
simone 43cb7e7469 feat: dark/light theming system + design tokens (OMC design system port)
- Restructure design tokens into :root/.dark raw vars mapped via @theme inline
- Add light/dark/system theming: useTheme hook + FOUC guard + ThemeToggle
- Keep iamcavalli palette (primary #1A463C, accent #DEF168) and Geist fonts
- Derive brand-consistent dark palette (verde-nero bg, lightened primary)
- Add scrollbar utilities (.no-scrollbar/.thin-scrollbar)
- Install tailwindcss-animate, register via @plugin (Tailwind v4)
- Mount ThemeToggle in admin sidebar footer

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-03 08:08:56 +01:00
simone add2176a6b feat: tier names + per-tier prices on offer cards, unified admin UI
- offer editor: per-tier name input (mirrors public_name/internal_name)
- offer list cards: show 3-tier services total + manual public price
- shared PageHeader component, full-width layout across all admin pages
- UI-RULES.md design conventions doc

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-26 16:58:18 +02:00
simone 9abe1fe4bb fix: offer name in client detail, retainer forecast, LTV with offers, offers-sold chart
- Client detail offers: show offer macro name ("Mantenimento") instead of tier letter
- Forecast: retainers project the monthly fee across every month from start_date
  (no longer capped by duration_months); una_tantum unchanged
- Clients list LTV: per project max(accepted_total, sum of assigned offers) so a
  retainer with unset quote still counts
- Dashboard: new "Offerte vendute" chart (count by offer + tier)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-26 14:14:13 +02:00
simone ae355c33a6 feat: offer badges, unified dashboard, income forecast, Pipedrive-style follow-up
- Client detail: category + tier badges on active offers
- Dashboard: remove top KPI cards + recent activity; fold current KPIs into
  bottom MetricCard style; add 12-month income forecast chart
- Forecast query: branch by offer_type (retainer = monthly fee, una_tantum =
  spread over duration), filter archived projects
- Payments: set the month a payment was collected (paid_at) from PaymentsTab
- Redesign FollowUpWidget in clean Pipedrive style (brand green, no orange)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-25 22:28:12 +02:00
simone 64030afef3 feat: sidebar CTA removal, delete phase/task, public dashboard UX, chat panel
1. AdminSidebar: removed yellow "Genera preventivo" CTA (duplicated Preventivi tab nav item)
2. Delete phase/task: new deletePhase/deleteTask server actions with FK cascade and phase-status recompute; DeletePhaseTaskButton client component with window.confirm; trash icons in PhasesTab per phase and per task
3. Public dashboard: extend activeOffers query with offer_macros join (offer_name, offer_type); reorder sidebar 1°Offerte 2°Pagamenti 3°Documenti; OffersSection shows macro public_name as heading (never tier letter/internal_name); PaymentStatus gains totalLabel/overrideAmount/hideRows props — retainer=monthly label + no Acconto/Saldo rows
4. Offer editor: Descrizione textarea (nota interna) and Modalità toggle moved directly under title; Tags section now contains only Categoria/Ticket/Tipo/Obiettivo
5. Basecamp chat: API route supports entity_type="phase" with authorization scoping; comments scope in client-view broadened to include phaseIds and client_id (general); CommentsTab updated with phase/general entities; ChatProvider React context; ChatPanel fixed slide-in panel with FAB (bottom-right) and composer tag selector (Generale + phases); PhaseCard gets chat-bubble icon pre-tagging that phase; inline ChatSection block removed from dashboard main column

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-25 14:11:55 +02:00
simone 186bb9ea19 feat: payments plans, phase auto-cascade, transcripts, manual timer
- Pagamenti: totale ereditato dalla somma accepted_total delle offerte
  attive (con override) + selettore schema rate 1/2/3 step; nuova colonna
  payments.percent per rescalare gli importi preservando label/stato
- Fasi & Task: recomputePhaseStatus auto-cascade (task -> fase) su
  updateTaskStatus/addTask, fix UI stale, etichette Da iniziare/In corso/Completata
- Pagina pubblica: colori fasi (grigio/blu/#1A463C) + fasi collassabili
- Transcript del cliente visibili in tab Documenti admin e dashboard pubblica
- Timer: inserimento manuale (+30/+60, minuti liberi, data) + elimina entry
- CLAUDE.md: procedura Deploy & DB Access; push su main automatico

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-23 10:38:56 +02:00
simone f98828f75e feat: offer_type (una tantum/retainer), dedup tiers, redesigned Offerte tab
- offer_macros.offer_type ('una_tantum'|'retainer') + editor "Modalità" toggle
  (migration 0012, applied to prod)
- migration 0012 also adds UNIQUE(macro_id, tier_letter) — prevents duplicate
  tiers; ran after a one-time dedup of Web Domination's duplicate A/B/C tiers
- OffersTab redesigned: two-step assign (Offer → Tier cards with price), type
  badge "Una tantum/Ricorrente" instead of misleading "X mesi", no redundant
  "· A" when public_name == tier letter, cleaner active-offers cards
- getProjectFullDetail: availableMicros grouped by macro + defensive dedup;
  projectOffers/availableMicros carry offer_type/category/price
- proposal deck PricingSection shows offer type (fallback to duration for
  pre-existing proposals)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-22 14:32:59 +02:00
simone 1824cb643f feat: wire offer→project phases/tasks, redo Offerte tab, cleanup project tabs
- importOfferIntoProject(): materializes project phases/tasks from the assigned
  tier's services grouped by services.fase (merge-by-title, dedup tasks)
- assignOfferToProject: optional import_phases flag triggers the import
- getProjectFullDetail: projectOffers/availableMicros now include macro name +
  tier_letter (dropdown shows "Offerta — Tier X"); availableMicros filters
  non-archived macros
- OffersTab redone: shows macro + tier badge, "Importa fasi e task" checkbox
- removed project "Preventivo" tab + deleted QuoteTab.tsx (legacy quote_items)
- sidebar: Lead before Clienti
- no DB migration (reuses existing tables)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-22 09:08:28 +02:00
simone f5f90cd643 feat: lead → client conversion + client contact fields (email/phone)
Blocco A+B (milestone v2.3). Migration 0011 is additive (ADD COLUMN only):
clients.email, clients.phone, leads.archived.

- createClientCore() extracted from createClient and reused by conversion
- clients.email/phone added to create + edit forms and shown on client header
  (never exposed on the public /client/[token] page)
- convertLeadToClient(): reuses core, carries over lead transcripts
  (client_transcripts.client_id), links project.created_from_lead_id,
  archives the lead while keeping its "won" status; idempotent
- "Converti in cliente" button on won leads; "Convertito → Apri cliente"
  once converted (clientId resolved via created_from_lead_id)
- archived leads hidden from list/kanban; detail page fetches by id incl. archived

Migration must be applied to the prod DB BEFORE this is deployed.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-22 08:18:41 +02:00
simone e1b3e8c3d5 style: refine settings taxonomy UI (field cells, icon headers, 2-col grid)
- PoolManager: defined field cells with value count, refined chips,
  inline ghost + button, focus ring, pending/empty states
- TaxonomyManager: icon-badged section headers, ordered 2-col grid,
  sync explainer note
- Settings page: wider container (max-w-4xl) for the grid to breathe

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-21 14:46:13 +02:00
simone e80c95f838 feat: centralized Notion-style taxonomy management in settings
Single persistent option pool per taxonomy (7 fields: offer
categoria/ticket/tipo/obiettivo + catalog fase/offerta/pacchetto),
stored as JSON in the settings table (no DB migration).

- src/lib/taxonomy.ts: pools with lazy seed from in-use values,
  add/remove(cascade)/rename helpers
- Inline creation anywhere registers into the pool (save offer,
  addServiceOption, updateServiceField fase, quickAdd, create macro)
- Deselecting on a row never touches the pool; global delete only
  from settings (cascade-strips tags / nulls columns)
- getOfferFieldOptions + getCatalogFieldOptions read from pools
- Settings: TaxonomyManager (offer + catalog groups), confirm on delete

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-21 14:24:47 +02:00
simone 320827e13a fix: flag A/B/C persistence, catalog sort+reorder, offer category pools
- Fix duplicate tier INSERT bug (useState not syncing after router.refresh)
- Add column sort by clicking headers in service catalog
- Add drag-and-drop column reordering (persisted in localStorage)
- Add Categorie Offerta section in Impostazioni (tipo/obiettivo/categoria pools)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-21 12:12:49 +02:00
simone fdcc938252 feat(21-22): agente AI generazione preventivo + pagina pubblica deck
- Migration 0010: tabella proposals (id, slug, lead_id, client_id,
  offer_macro_id, content jsonb, state, selected_tier, accepted_at)
  applicata a prod via SSH tunnel
- @anthropic-ai/sdk@0.105.0 installato; ANTHROPIC_API_KEY in .env.local
- src/lib/proposal/: schema Zod ProposalContent, agente Claude Opus 4.8,
  assemble (AI + offerta DB + config consulente), queries, profile.ts
- Admin: /admin/preventivi lista + /genera (pre-fill ?lead_id=X) + /[id] review
- Sidebar: voce Preventivi + CTA globale lime "Genera preventivo"
- LeadDetail: pulsante "Genera preventivo" → /admin/preventivi/genera?lead_id=X
- Pagina pubblica /preventivo/[slug]: deck 20+ slide light-mode iamcavalli,
  navigazione frecce + dot + keyboard, accept/reject con guard immutabilità
- STATE.md aggiornato (80%), 21-PLAN.md scritto nel formato GSD

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-20 14:37:05 +02:00
simone 9f5a6dcbe5 feat(20-03): add transcript UI — modal, list, expand/collapse, delete
TranscriptModal.tsx: new component — call_date input, optional title,
content textarea (min-h-48). Calls addTranscript server action.

LeadDetail.tsx: adds transcripts prop (ClientTranscript[]), TranscriptModal
button in header, "Transcript Call" Card after Storico Attività with
call_date formatted in Italian, 3-line preview with expand/collapse toggle,
Elimina button via deleteTranscript. border-l-4 border-purple-300.

page.tsx: getTranscripts(id) added to Promise.all, transcripts prop
passed to LeadDetail. Build: clean, TypeScript: no errors.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-20 08:58:06 +02:00
simone b0dbd341a9 fix(19-01): add min-h-[300px] to LeadTable scroll container to prevent dropdown clipping 2026-06-19 18:04:28 +02:00
simone 9f8eafffb0 fix(19-01): remove overflow-hidden from LeadTable to allow dropdown overflow 2026-06-19 18:00:15 +02:00
simone 607c2578f0 feat(19-01): add LeadsViewToggle with integrated search + list/kanban pill toggle
- LeadsViewToggle: client wrapper with useState<list|kanban>, useMemo filtered
- Search input (left) + pill toggle Lista/Kanban (right) on single row
- Filters leads by name/email/company/status/tags across both views
- Search state preserved when switching between Lista and Kanban
- LeadsSearch.tsx: replaced with re-export of LeadsViewToggle (backward compat)
2026-06-19 17:48:31 +02:00
simone 2c67e6fd72 feat(19-01): add LeadsKanbanBoard with @dnd-kit drag-drop between 6 stage columns
- 6-column Kanban board: contacted, qualified, proposal_sent, negotiating, won, lost
- DroppableColumn with useDroppable, isOver highlight (border-[#1A463C])
- DraggableLeadCard with useDraggable, opacity-30 on drag, name/company/next_action
- Optimistic update via setLeadStatuses + startTransition + updateLeadField + router.refresh
- Client-side guard: VALID_STAGES check before calling server action (T-19-01)
- DragOverlay dropAnimation={null} ghost card with rotate-1 shadow-xl
- overflow-x-auto wrapper with grid-cols-6 min-w-[1080px]
2026-06-19 17:48:00 +02:00
simone 268f56ccd2 chore(18-01): remove quote builder route, exclusive components, and SendQuoteModal entry point (CLEAN-02)
- Delete src/app/admin/quotes/new/page.tsx and actions.ts re-export
- Delete QuoteBuilderForm, OfferSelector, PriceOverrideInput, QuotePreview components
- Rewrite SendQuoteModal: remove "Crea Nuovo" tab and window.location navigation to quotes/new
- Remove stale JSDoc comment referencing /admin/quotes/new from admin-queries.ts
2026-06-19 12:07:48 +02:00
simone 14bdbab880 feat(phase-18): plan 02 — consolidate analytics into admin dashboard
- Merge /admin/analytics content (MetricCard, MonthlyChart, time tracking) into /admin
- AdminDashboard now accepts searchParams and year query param
- Add getAnalyticsByYear, getMonthlyCollected, getAvailableYears, getTimeByClient, getTotalTrackedHours queries
- Unify fmtEur to accept number (more robust); update KPI card callers with parseFloat()
- Add MetricCard and fmtSeconds helpers to admin/page.tsx
- YearSelector navigates to /admin?year=Y (was /admin/analytics?year=Y)
- Delete src/app/admin/analytics/page.tsx — /admin/analytics now returns 404
2026-06-19 12:07:31 +02:00
simone 7d884099aa chore(18-01): remove forecast route, sidebar entry, and revalidatePath calls (CLEAN-01)
- Remove Forecast NAV_ITEMS entry and TrendingUp import from AdminSidebar
- Delete src/app/admin/forecast/page.tsx and empty directory
- Remove revalidatePath("/admin/forecast") from assignOfferToProject, removeProjectOffer, updateProjectOfferTotal
2026-06-19 12:05:05 +02:00
simone 43238341c1 feat: service offer tags, catalog cleanup, offer editor filter
- Import 58 offer membership tags for all 55 services (entity_type="services")
  via scripts/import-service-offer-tags.ts (already run on prod)
- ServiceTable: remove Categoria column, rename Tag → Offerta; QuickAddRow
  no longer has a category field (offer tags set post-creation)
- offer-queries: getOfferEditorData fetches offerTags per service (join on
  tags WHERE entity_type="services") and exposes them in OfferEditorData
- OfferEditorClient: filter chips above "Servizi Inclusi" — Tutti / Entry
  Offer / Signature Offer / Retainer Offer, derived from actual tag data

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-18 21:58:34 +02:00
simone 27e8706e39 fix(offer-editor): show all services regardless of offer category
offer_macros.category ("Entry Offer") and services.category are different
taxonomies — filtering one by the other always returns empty. Remove the
client-side category filter; all active services are always shown.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-18 18:35:37 +02:00
simone 4f7e0033c4 fix(offer-editor): fix category bug, add ripristina, always-saveable
- Bug: remove server-side category pre-filter from getOfferEditorData;
  services now loaded unfiltered, client-side filter handles display →
  changing category no longer wipes the service list
- Fix tierTotals to use full service map (not filtered subset) so totals
  stay correct when category is edited mid-session
- Add Ripristina button for archived offers (calls toggleOfferArchived false)
- Remove canSave gate: always allow saving; button is "Salva Bozza"
  (secondary style, stays on page) when no services assigned, "Salva
  Offerta" (primary, redirects to list) when at least one tier has services
- Show "Bozza salvata." inline feedback after draft save

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-18 18:25:50 +02:00
simone 791a6e64b1 chore: merge executor worktree (worktree-agent-a44f854f45ac76582) 2026-06-15 10:28:07 +02:00
simone 7df4b9c74a feat(12-04): build OfferListClient with category filter, archive toggle, card grid
- Category filter chips ("Tutti" + dynamic categoryOptions), active/inactive
  styling per UI-SPEC (#1A463C / #e5e7eb)
- "Mostra offerte archiviate" toggle (default off), filters via useMemo
- Responsive card grid (1/2/3 cols) linking to /admin/offers/[id]/edit,
  with category badge and "Archiviata" indicator
- "+ Nuova Offerta" inline form calling createOfferMacro via useTransition
  + router.refresh()
- Empty state with "Nessuna offerta" / "Inizia creando la tua prima offerta"
2026-06-15 10:25:15 +02:00
simone 8c5c918304 feat(12-05): build offer editor client component
- Add OfferEditorClient: full editor for /admin/offers/[id]/edit
- Categoria/Ticket single-select (OptionSelect) + Tipo/Obiettivo
  multi-select with on-the-fly creation (OptionMultiSelect)
- Services matrix (A/B/C checkboxes) pre-filtered by macro.category,
  with live "Totale Servizi" recalculation via useMemo (OFFER-11)
- Independent manual "Prezzo Pubblico" per tier (OFFER-16)
- 5-field "Promessa di Trasformazione" block via EditableCell (OFFER-17)
- Salva Offerta / Annulla / Archivia actions wired to saveOfferEditor,
  toggleOfferArchived and renameOfferOption (OFFER-15)
2026-06-15 10:25:02 +02:00
simone c8d53134ba fix(14): remove dead generate_new branch from assignQuoteToLead (CRM-12)
assignQuoteToLead's generate_new check was unreachable since
SendQuoteModal always passed false. Removes the field from both
the server schema and the modal's local schema/defaultValues/call site.
2026-06-14 15:17:48 +02:00
simone ab7fa62059 feat(14-02): add LeadsSearch, rewire leads pages, surface tags in LeadDetail (CRM-08/09)
- New LeadsSearch.tsx: client-side instant filter across name/email/
  company/status/tags (mirrors CatalogSearch.tsx)
- page.tsx: fetch via getLeadsWithTags()/getLeadFieldOptions(), render
  LeadsSearch + CreateLeadModal, revalidate=0
- [id]/page.tsx: fetch getLeadsWithTags()/getLeadFieldOptions(), find
  lead by id, pass tags/tagOptions to LeadDetail
- LeadDetail.tsx: add OptionMultiSelect for lead tags in the "Profilo"
  card, wired to addLeadTag/removeLeadTag/renameLeadTag with error display
2026-06-14 13:21:55 +02:00
simone 4887a319f7 feat(14-02): rewrite LeadTable as raw-table database view (CRM-08/09)
Replace shadcn Table-based read-only LeadTable with an Attio-style raw
table database view (1:1 structural port of ServiceTable.tsx):
- EditableCell for name/email/phone/company/next_action, persisted via
  updateLeadField
- Local StatusCell: closed click-to-open dropdown over the 6 fixed
  LEAD_STAGES values, preserving semantic STAGE_COLOR badges (won=green,
  lost=red, etc.) without modifying shared option-select.tsx
- OptionMultiSelect for lead tags, wired to addLeadTag/removeLeadTag/
  renameLeadTag
- Per-row error display and "Nessun lead trovato" empty state
2026-06-14 13:21:23 +02:00
simone a495d84511 fix(14-03): remove dead onSubmit branch in SendQuoteModal (CRM-12)
- Remove unreachable if (tab === "new") branch in onSubmit - the new-tab button has its own onClick navigation and never triggers form submit
- Escape unescaped double quotes in JSX text (react/no-unescaped-entities)
- generate_new field left as-is per plan (optional cleanup, out of scope)
- Log pre-existing useForm<any>/onSubmit(data: any) no-explicit-any lint errors to deferred-items.md (out of scope: fixing requires resolving a zodResolver/Resolver generic incompatibility with assignQuoteSchema's optional/default fields)
2026-06-14 12:46:55 +02:00
simone ee509cd5fb fix(14-03): type LeadForm useForm with CreateLeadInput (CRM-11)
- CreateLeadModal and EditLeadModal use useForm<CreateLeadInput>() instead of useForm<any>()
- Import CreateLeadInput from lead-validators, remove local z.infer redeclaration
- Narrow EditLeadModal status default value to CreateLeadInput["status"], remove 'as any'
- Fix FormField generic signature in shared ui/form.tsx (was ControllerProps<any, any>, broke Control<T> assignability once useForm<T> became concrete) - Rule 3 blocking issue
2026-06-14 12:43:00 +02:00
simone 272e363f4d fix(14-03): translate FollowUpWidget to Italian (CRM-10)
- Replace 6 hardcoded English strings with Italian equivalents
- Remove pluralization ternary (Italian "lead" is invariant)
2026-06-14 12:38:12 +02:00
simone e858a8f577 feat(catalog): Notion-style shared select fields (tag/pacchetto/categoria/fase)
Turn the catalog into Notion/Airtable select properties:
- Tag + Pacchetto: multi-select with a SHARED pool — created values persist and
  are selectable from a dropdown across all services (no more re-typing)
- Categoria + Fase: single-select chips with the same dropdown + create-on-the-fly
- Rename an option once -> propagates to every row (renameServiceOption)
- Deterministic colors per value (shared option-colors util)
- Quick-add row now sets name+description+categoria+fase+prezzo then Enter (active)
- Search broadened to name/categoria/fase/tag/pacchetto

Data model (additive): tag/pacchetto in polymorphic tags table (entity_type
services / services.pacchetto); categoria/fase as single-select columns on
services (new: services.fase). Pools derived from distinct values.

New: OptionSelect, OptionMultiSelect, option-colors. Removed tag-multi-select.
Migration 0007_add_services_fase.sql must be applied before runtime.

tsc + eslint + next build clean. CSV bulk import (OFFER-12) stays Phase 12.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-13 21:40:34 +02:00
simone 42c16e1bab fix(11): address code-review findings in catalog inline-edit
- WR-01: remove blur-commit on EditableCell toggle (onChange already saves+closes; prevents double updateServiceField)
- WR-02: skip server action when cell value unchanged (commit + new commitOnBlur dirty-check)
- WR-03: on blur, revert required-empty field via cancel() instead of leaving the cell stuck in error state
- WR-04: normalize it-IT price input (1.234,50) before parse; Number() rejects trailing garbage
- IN-01: render row error in its own <tr> instead of an invalid 7th <td> in a 6-column row

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-13 21:02:50 +02:00
simone 912a892ba5 feat(11-04): add client-side search bar to catalog page, remove ServiceForm
- New CatalogSearch client component: filters ServiceWithTags[] by name/tag, case-insensitive, instant (no reload)
- page.tsx now passes filtered services through CatalogSearch -> ServiceTable
- ServiceForm.tsx deleted — quick-add row in ServiceTable replaces its create-service UX
2026-06-13 16:01:17 +02:00
simone c0bedf300e feat(11-04): rewrite ServiceTable as database-view (inline edit, tags, quick-add, active/inactive split)
- 6-column table (Nome, Descrizione, Categoria, Prezzo, Tag, Stato), no Azioni column (D-14)
- Every field cell uses EditableCell (text/textarea/number/toggle), saving via updateServiceField
- Tags column uses TagMultiSelect, wired to addTagToService/removeTagFromService
- QuickAddRow creates new services (unit_price=0) via quickAddService on Enter (D-12)
- Inactive services sink below a "Servizi disattivati" divider with opacity-50 (D-13)
- All mutations trigger router.refresh()
2026-06-13 15:58:36 +02:00
simone 008a43469d feat(10-redo-C): CRM leads module — schema, services, UI (Phase 10 redo)
Stage C of staged Phase 10 redo. Restores dangling phase10-wip work:
- schema.ts: leads expansion + activities/reminders tables + relations
- lead-service.ts / lead-validators.ts service layer
- /admin/leads list + detail + actions, LeadTable/LeadDetail/LeadForm
- LogActivityModal, SendQuoteModal, FollowUpWidget on dashboard
- migration 0005 (applied to prod DB in Stage B, along with
  previously-missing 0001/0003/0004 — root cause of all post-deploy
  500s: prod DB had no migrations applied since 0000)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-11 20:58:14 +02:00
simone 614cf0114f feat(09-02): implement admin quote builder UI with server actions
- src/lib/quote-actions.ts: createQuote server action with nanoid tokens and atomic DB transaction
- src/components/admin/quotes/QuoteBuilderForm.tsx: Two-column form (left: client/offer/price inputs, right: live preview)
- src/components/admin/quotes/OfferSelector.tsx: Grouped dropdown for macro/micro offer selection
- src/components/admin/quotes/PriceOverrideInput.tsx: Price input with server validation feedback
- src/components/admin/quotes/QuotePreview.tsx: Live preview card showing offer details and calculated total
- src/app/admin/quotes/new/page.tsx: Quote builder page rendering form with data from DB
- src/app/admin/quotes/new/actions.ts: Re-export of createQuote for page-level action imports
- src/lib/admin-queries.ts: Added getAllOfferMacrosWithMicros() query for form population

Features:
- Admin selects client and offer (required fields)
- Form validates on blur and displays success/error states
- Server action validates data and rejects if total mismatch
- On success, displays public /quote/[token] link with copy button
- Quote saved as draft state with immutable accepted_total

Completes Phase 9 Plan 2 Wave 1: Admin UI Layer

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
2026-06-11 07:34:16 +02:00
simone 4ed2f8b105 feat(07-unified-service-catalog): rewire /admin/catalog CRUD + query layer to services table
- Task 1: Update getAllServices() and activeServices queries to read from services table instead of service_catalog
  - Both ClientFullDetail and ProjectFullDetail now return Service[] (not ServiceCatalog[])
  - Quote items label join remains unchanged — still references service_catalog for historical quote_items.service_id FK integrity

- Task 2: Rewire /admin/catalog actions and components to operate on services table
  - src/app/admin/catalog/actions.ts: createService/updateService/toggleServiceActive now write to services with optional category field
  - ServiceTable.tsx: Updated to Service[] type, added category column rendering
  - ServiceForm.tsx: Added optional category input field
  - QuoteTab.tsx: Updated activeServices type annotation to Service[]

- New services created via /admin/catalog have migrated_from=null, migrated_id=null (not migrated)
- TypeScript compiles successfully
- npm run build: PASS

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
2026-06-11 06:26:03 +02:00
simone 4d52d87341 chore(06-01): delete NavBar.tsx — replaced by AdminSidebar
- NavBar is no longer imported anywhere (layout.tsx updated in prior task)
- tsc --noEmit and npm run build both pass clean
2026-05-31 20:09:32 +02:00
simone 2283740d20 feat(06-01): create AdminSidebar component with 7 nav links + logout
- Client component using usePathname for active route highlighting
- 7 nav links: Dashboard, Clienti, Progetti, Offerte, Forecast, Catalogo, Impostazioni
- Logout button at bottom calls signOut with /admin/login callback
- Green sidebar bg #1A463C matching brand
2026-05-31 20:08:33 +02:00
simone 0c09d44b68 feat(05-03): add OffersTab component + Offerte tab in project workspace
- Create OffersTab.tsx client component with assign form and offer list
- Inline accepted_total editing via onBlur handler
- Add Offerte tab trigger and content to project workspace page
- Destructure projectOffers and availableMicros from getProjectFullDetail result
2026-05-30 16:27:07 +02:00
simone ce8f95a163 feat(05-02): /admin/offers RSC page + ServiceCheckboxList + NavBar update
- Create src/app/admin/offers/page.tsx — RSC with macro/micro/service CRUD sections
- Create src/components/admin/offers/ServiceCheckboxList.tsx — client checkbox UI for service-to-micro assignment
- Update src/components/admin/NavBar.tsx — add Forecast and Offerte links (order: Statistiche | Forecast | Catalogo | Offerte | Impostazioni)
2026-05-30 16:22:19 +02:00
simone 75097ccfa4 feat: init payments + split payment in project workspace
- initProjectPayments: crea Acconto 50%/Saldo 50% se il progetto non ha pagamenti
- splitPayment: divide un pagamento in due rate con importi custom
- SplitPaymentForm: form client interattivo con anteprima live della rata 2
- PaymentsTab: stato vuoto con CTA + pulsante Dividi per ogni riga
- projectId prop opzionale per attivare le feature solo nel contesto progetto
2026-05-23 10:18:06 +02:00
simone eab88c9f63 fix(04-06): five post-deploy fixes
- Middleware (proxy.ts): switch internal API calls to localhost instead of
  request.url — avoids Docker hairpin NAT issues where the container can't
  reach its own external hostname via Traefik
- ProjectRow timer: pass projectId={project.id} to TimerCell so it calls
  startTimer(projectId) directly instead of startTimerForClient(project.id)
- Admin client list: add slug field to ClientWithPayments + show slug link
  when available (falls back to token) — so admins see the human-readable URL
- Analytics contracted: sum projects.accepted_total (authoritative) instead
  of clients.accepted_total (always 0 in multi-project architecture)
- createClient: auto-generate slug from client name at creation time
  (e.g. "Mario Rossi" → "mario-rossi"), with -2/-3 suffix on conflict

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-22 13:42:38 +02:00