- 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
Plan Phase 14 (CRM Attio-style & Fix) into 3 waves: data-layer
foundation (query helpers + server actions), LeadTable raw-table
rewrite with inline edit/tags, and isolated bug fixes for CRM-10/11/12.
UI-SPEC approved (5/6 PASS, 1 non-blocking flag on focal point).
Locks reuse of Phase 11 primitives (EditableCell, OptionSelect,
OptionMultiSelect) for the leads table, plus scope for the four
CRM bug fixes (i18n, type-safety, dead code).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
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>