- 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)
- Add src/app/admin/offers/[id]/edit/page.tsx as async server component
- Fetches getOfferEditorData(id) + getOfferFieldOptions() via Promise.all
- Calls notFound() when offer id does not exist
- Delegates rendering to OfferEditorClient
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.
- 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
- requireAdmin() guard (mirrors catalog/actions.ts pattern), throws
"Non autorizzato" before any DB access
- updateLeadField: EDITABLE_FIELDS allowlist, name/status validated
server-side (LEAD_STAGES.includes), nullable fields cleared on empty
- addLeadTag/removeLeadTag/renameLeadTag: polymorphic tags table scoped
to entity_type="leads" (LEADS_TAG_ENTITY), never touches
entity_type="services" rows
- All four actions call revalidatePath for /admin/leads (+ detail page
where a single leadId applies)
- Log pre-existing lint issues (unrelated to this task) to
deferred-items.md per scope-boundary rule
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>
- 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>
- 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
- updateServiceField: single-field inline edit (name, description,
category, unit_price, active) with per-field validation
- addTagToService / removeTagFromService: tag assignment scoped to
entity_type="services" via onConflictDoNothing on the unique index
- quickAddService: creates a unit_price=0.00 row from name only (D-12)
- All actions admin-gated via requireAdmin() and revalidate /admin/catalog
Lead detail 500'd because params was accessed synchronously;
Next 16 App Router passes params as a Promise. Same pattern
already used correctly in /quote/[token].
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
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>
- 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>
- 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>
- 4 KPI cards: clienti attivi, revenue totale, progetti in corso, pagamenti in sospeso
- Activity feed: last 10 events with icon, label, detail, timestamp
- Page is RSC (async server component, no use client)
- revalidate = 0 for fresh data on each request
- New /admin/clients/page.tsx contains full client list with getAllClientsWithPayments
- Toggle archived link updated from /admin?archived=1 to /admin/clients?archived=1
- /admin/page.tsx replaced with redirect() to /admin/clients
- AdminLayout now uses flex-row: AdminSidebar (left) + main (right)
- Removed NavBar import and max-w-5xl centered constraint
- Main content area takes remaining width with px-8 py-8 padding
- Add ClientActiveOfferSummary type to admin-queries.ts
- Add getClientActiveOffers() function joining project_offers with offer_micros
- Only selects public_name (never internal_name) per CLAUDE.md constraint
- Update /admin/clients/[id] to fetch active offers in parallel with project data
- Add Offerte Attive section at bottom of client detail page
- 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
- Add offer_micros, project_offers imports to admin-queries.ts
- Add ProjectOfferWithMicro type to admin-queries.ts
- Extend ProjectFullDetail type with projectOffers + availableMicros fields
- Extend getProjectFullDetail() to query project offers and available micros in parallel
- Add z import and project_offers import to project-actions.ts
- Add assignOfferToProject, removeProjectOffer, updateProjectOfferTotal server actions
- All three actions call requireAdmin() and revalidate /admin/forecast
- 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
- Add requireAdmin() to all unprotected admin server actions
(clients/new, clients/[id], timer-actions — 17 functions total)
- Protect /api/internal/* endpoints with X-Internal-Secret header
(proxy.ts sends it; routes reject requests without it)
- Randomize auto-generated client slugs with 4-char suffix
to prevent enumeration via predictable name-based slugs
- Add in-memory rate limiting to /api/client/approve (20/min)
and /api/client/comment (10/min) per IP
- Add security headers: X-Frame-Options, X-Content-Type-Options,
Referrer-Policy, Permissions-Policy, X-DNS-Prefetch-Control
- Reduce JWT session from 30 days to 7 days with daily rotation
- Remove hardcoded NEXTAUTH_URL from Dockerfile (pass via Coolify env)
- Genericize client API error messages to not leak data structure
- Update .env.example with all required variables including INTERNAL_SECRET
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- 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>
- Admin client list: remove Timer column, rename Acconto→Importo incassato
and Saldo→Importo da saldare (calculated from payment amounts, not badges)
- Project workspace timer: pass projectId prop to TimerCell so it calls
startTimer(projectId) directly instead of startTimerForClient(projectId)
which was failing with "nessun progetto trovato"
- Public client page: pass client.token (real DB token) not the URL path
segment (which may be a slug) to ClientDashboard → fixes approve/chat APIs
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Create src/app/admin/catalog/actions.ts with createService, updateService, toggleServiceActive
- Each action includes requireAdmin() guard via getServerSession
- Zod validation: name (min 1), unit_price (coerce.number min 0.01)
- Add getAllServices() to src/lib/admin-queries.ts ordered by name
- Import service_catalog and ServiceCatalog types in admin-queries.ts
Schema:
- clients.archived boolean (default false)
- time_entries table (client_id, started_at, ended_at, duration_seconds)
Client management:
- /admin/clients/[id]/edit — form pre-compilato con nome, brand, brief
- ClientActions: Modifica / Archivia / Elimina con doppia conferma
- setClientArchived: toggle archiviazione senza perdere dati
- deleteClient: elimina con cascade, redirect a /admin
- Admin list: toggle "Mostra archiviati" via ?archived=1, righe archiviate opache
Time tracker:
- startTimer: crea sessione, ferma automaticamente quella precedente
- stopTimer: chiude sessione, calcola duration_seconds
- TimerCell: ▶/⏹ per ogni cliente, contatore live in secondi, totale cumulativo
- Una sola sessione attiva alla volta su tutta la lista
Analytics:
- Sezione "Fatturato" (invariata) + sezione "Tempo tracciato" separata
- Ore totali per anno + barre orizzontali per cliente
- getTotalTrackedHours, getTimeByClient queries
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Chat revisioni: rimuovi commenti inline da timeline/kanban, aggiungi
ChatSection con feed cronologico + selector task opzionale (Invio per inviare)
Bolle stile chat: Tu (destra, giallo) / iamcavalli (sinistra, verde)
Tag task su ogni messaggio quando il messaggio non è generale
- API /api/client/comment: supporto entity_type "general" (entity_id = clientId)
- Pagina /admin/analytics: year selector ←→, 4 metric card (contrattualizzato,
incassato, da incassare, clienti acquisiti), bar chart mensile incassato via CSS
- NavBar: link "Statistiche"
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Fix button contrast: add all missing shadcn tokens (primary-foreground,
ring, input, muted, destructive) aligned to iamcavalli brand
- NavBar: #1A463C green bar with white text
- Login page: clean brand layout with iamcavalli wordmark
- Admin pages: brand colors on headings, borders, links
- Admin ClientRow: semantic payment badges (green/yellow/red)
- Admin phases tab: Lista ↔ Kanban toggle with @dnd-kit drag & drop
between Da fare / In corso / Fatto columns (optimistic updates)
- Client dashboard: Timeline ↔ Kanban toggle, expandable task cards
with approve button + comment form inline
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>