- ProposalDeck: outer div h-screen overflow-hidden, content wrapper h-full
- Rimosso il div interno ridondante min-h-screen
- useEffect blocca body overflow mentre la deck è aperta
- Tutti i 20 componenti slide: min-h-screen → h-full
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
DDL for client_transcripts table: id (nanoid PK), lead_id/client_id
(nullable FKs with ON DELETE CASCADE), title, content (NOT NULL),
call_date (date NOT NULL), created_at (timestamptz). Three indexes on
lead_id, client_id, call_date DESC. APPLY TO PROD via SSH before
pushing schema-dependent code (Plan 20-02/20-03).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- 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)
- 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>
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>
- 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>
- 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"
- 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
- 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)
- Hand-write 0008_offer_tier_schema.sql: additive ALTER TABLE ADD COLUMN IF NOT EXISTS for offer_macros/offer_micros, guarded DO-block CHECK constraint on tier_letter, CREATE TABLE IF NOT EXISTS offer_tier_services + index
- Append journal entry for 0008_offer_tier_schema following the 0006 entry shape
- Add scripts/push-12-offer-tier-schema.ts: idempotent push script with PRODUCTION DEPLOY NOTE — BLOCKING step for Plan 02 (SSH+docker exec) before Wave 3 query layer
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
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
- 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)
- 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
- 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
- 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>
- 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
- 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()
- TAG_COLORS 7-color pastel palette + getTagColorIndex() deterministic
name->color hash (D-07, no stored color column)
- renders tag badges with inline remove (X), calls removeTagFromService
- "+" dropdown to add a new tag via Enter, calls addTagToService
- closes dropdown on outside click and Escape; inline error display
- remove value-sync useEffect + render-time ref read that violated
react-hooks/set-state-in-effect and react-hooks/refs lint rules
- toggle display now reads value directly instead of tempValue,
which is only meaningful while isEditing is true
- 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
- 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
- Add polymorphic tags table (entity_type/entity_id/name) with unique
index on (entity_type, entity_id, name) and lookup index on
(entity_type, entity_id) — pattern follows comments table (D-05/D-06/D-07)
- Add Tag/NewTag types and tagsRelations (no direct FK, query-time join)
- Hand-write src/db/migrations/0006_add_tags_table.sql following the
established project convention (drizzle-kit generate is unusable here —
meta/_journal.json snapshots out of sync since 0001, pre-existing since
Phase 8) and add journal entry for 0006_add_tags_table
- Add scripts/push-11-tags-migration.ts (idempotent CREATE TABLE/INDEX IF
NOT EXISTS) with production deploy note for manual SSH+docker exec apply
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>
Stage A of staged Phase 10 redo: dependencies and shared UI only,
no schema or DB-querying code. Verifies Coolify rebuilds clean
before migration (Stage B) and CRM code (Stage C).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- layout.tsx: Simple centered public layout (no auth header) with quote branding
- page.tsx: Server component validating token, handling quote states (draft/accepted/rejected)
- actions.ts: Server actions for acceptQuote() and rejectQuote() with immutable accepted_at enforcement
- Validates token format (21-char nanoid), returns 404 for invalid tokens
- Shows 'already accepted' message if quote was previously accepted
- Integrates with QuoteMultistep component for active quotes
Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
- QuoteMultistep.tsx (110 lines): Parent component managing step state and navigation with visual indicator
- QuoteStep1Overview.tsx: Read-only overview with offer name, total price, and phase list
- QuoteStep2Selection.tsx: Phase/service listing (read-only MVP) with calculated total
- QuoteStep3Summary.tsx: Summary card with optional email and notes capture, Accept/Reject buttons
All components use shadcn/ui with Tailwind styling and follow accessibility patterns.
Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
- Enhanced proxy.ts with rate limit check for /quote/[token] routes
- Enforces 3 views per minute per IP address (MVP in-memory store)
- Returns 429 Too Many Requests when limit exceeded
- Rate limit utility supports distributed use (ready for Upstash Redis in Phase 10)
Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>