Allinea la dashboard alla rifinitura del mock (finezza bordi/colori/badge):
- Card su border-border-light (slate-100) per il bordo sottile
- KPI: valore verde su Incassato Reale, delta verde "+N questo mese" su
Clienti Attivi (nuova metrica getDashboardStats.clientiNuoviMese)
- Redditività: badge "Ottimo" con tint emerald soft (non brand primary)
- Follow-up: righe come box bordati (no avatar), link "Apri →" primary
- Forecast: selettore anno (pill interattiva) spostato nell'header della card,
barre non-picco grigie (bg-border), rimosso il numero "mese prossimo"
- Offerte Più Richieste: rimossa la pill totale nell'header
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- 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>
- 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>
- 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>
Adds a "Modello AI" toggle (Opus default, Sonnet option) styled like the
existing Soggetto toggle. The server action validates the keyword and maps
it to the real model id; the agent receives the resolved id and uses it in
the Anthropic messages.create call. The proposals DB record stores the
actual model used.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- client-view.ts: read cumulative_price + services list from offer_tier_services→services
(Phase 12 catalog); legacy fallback to offer_micro_services→offer_services when no
new-style rows exist. Extend activeOffers type with services[] in both ProjectView and ClientView.
- OffersSection.tsx: extract OfferCard client component with useState accordion;
hide "Valore incluso" row when price is 0; add "Cosa è compreso" chevron toggle
listing included services (name + description when present).
- client-dashboard.tsx: hide progress bar and PhaseViewToggle when hasRetainer;
render polished empty-state placeholder with RefreshCw icon instead.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
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>
- 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>
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>
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>
- 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>
- 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>
- getOfferListCards() for Plan 04 list page (category + archive status)
- getOfferEditorData(macroId) returns macro fields, A/B/C tiers with
assignedServiceIds + computed servicesTotal, category-filtered
availableServices, and tipoTags/obiettivoTags
- getOfferFieldOptions() for categoria/ticket/tipo/obiettivo select pools
- scripts/verify-12-03-queries.ts documents the 5 spec'd test cases
(typecheck-only, no test runner configured, prod DB not mutated)
- LeadWithTags type + getLeadsWithTags() left-joins leads with tags
scoped to entity_type="leads", Map-reduce pattern mirroring
getAllServices (Phase 11)
- LeadFieldOptions type + getLeadFieldOptions() returns fixed
LEAD_STAGES for status and distinct sorted lead tag names
- desc import added to drizzle-orm import; LEAD_STAGES imported from
lib/lead-validators
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>
- Add ServiceWithTags type = Service & { tags: string[] }
- getAllServices() now left-joins tags scoped to entity_type=services
- Tags aggregated per service, ordered by service name then tag name
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>
- Add getOfferPhases(microId) — get all phases for an offer micro
- Add getOfferPhaseServices(phaseId) — get all services assigned to a phase
- Add getQuoteById(quoteId) — get full quote header by ID
- Add getQuoteByToken(token) — get quote by public token
- Add getQuotesByClient(clientId) — get all quotes for a client
- Update imports to include new tables and types
- All queries properly typed with Drizzle ORM
- Build verified with npm run build
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>
- KPI queries: clienti attivi, revenue totale, pagamenti in sospeso, progetti in corso
- Activity feed: last 10 events across clients, projects, deliverables, time entries
- Uses isNotNull for nullable timestamp filters, Promise.all for parallel fetch
- Add activeOffers field to ProjectView and ClientView interfaces (public_name only — never internal_name)
- Add offer queries to getProjectView(): project_offers JOIN offer_micros (explicit public_name selection), cumulative price via offer_micro_services JOIN offer_services
- Create src/lib/forecast-queries.ts with ForecastMonth type and getRevenueForecast12Months() 12-bucket JS algorithm
- Forecast spreads accepted_total evenly across duration_months from start_date
- 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
- 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
Root cause: INTERNAL_SECRET was on same line as HOST= in .env (no trailing
newline when appended), so proxy middleware got empty secret and rejected
all /api/internal/validate-* calls → rewrote every /client/* request to
/not-found without running the page component.
- 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>
- 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>
Provides getAnalyticsByYear, getMonthlyCollected, and getAvailableYears
to power the admin dashboard analytics view (not yet wired to a page).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- src/lib/admin-queries.ts: getAllClientsWithPayments() and getClientById() for admin DB reads
- src/components/admin/NavBar.tsx: minimal nav with Clienti link and Esci (logout) button
- src/app/admin/layout.tsx: wraps all /admin/* pages with NavBar + centered main content area