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>
- 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>
Tags table + legacy consolidation applied via SSH tunnel to Coolify Postgres.
Both validators: ALL CHECKS PASSED. Protected tables (clients/projects/payments/phases) unchanged.
Phase 11 verification: passed (OFFER-07/08/09/10/13 all Complete).
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()
- add 11-03-SUMMARY.md documenting EditableCell/TagMultiSelect delivery
and the react-hooks lint-rule fix (Rule 1 deviation)
- update STATE.md position (3 of 4 -> 4 of 4), progress 86% -> 89%,
performance metrics, and decisions log
- log gsd-sdk unavailability and OFFER-07/08 status timing as deferred
items for Phase 11
- 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
- SUMMARY.md for Plan 02 (ServiceWithTags + 4 new server actions)
- STATE.md: advance to Plan 3/4, record metrics and decision
- REQUIREMENTS.md: mark OFFER-07/08/09 backend foundation complete
- 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
- Advance STATE.md to plan 2/4, record metrics/decisions/blocker for 11-01
- Fix Status/Progress fields regressed by state advance-plan (status was
reset to "Ready to execute" / 0% despite phase being mid-execution)
- Note .planning/ROADMAP.md is an empty PLACEHOLDER (pre-existing v2.1
planning gap) — roadmap update-plan-progress is a no-op until populated
Documents Phase 11 Plan 1 outcome: tags table schema + migration scripts
complete and committed; DB-execution steps (push migration, run legacy
consolidation, run tag-assignment + validators) blocked by network/SSH
access gate to 178.104.27.55:54321 (firewalled, SSH-only per project
memory). Logs pre-existing drizzle-kit migration tooling drift and the
stale quote_items<->service_catalog JOIN as deferred non-blocking items.
- scripts/migrate-tags.ts: idempotently assigns the "Offerta" tag to every
services row where migrated_from = 'offer_services' (D-02)
- scripts/validate-tags-migration.ts: row-count + orphan checks for the
tags consolidation dimension of OFFER-13
NOTE: Not yet executed against the dev/staging DB (178.104.27.55:54321) —
that host is firewalled and only reachable via SSH+docker exec per project
memory, which is out of scope for this agent's authorization. Execution of
this script, scripts/migrate-services.ts, scripts/validate-services-migration.ts,
and scripts/push-11-tags-migration.ts is documented as a pending access gate
in 11-01-SUMMARY.md.
- 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>
- 3 tasks completed (rate limiting, components, page/actions)
- 9 files created, 1 file modified
- Public /quote/[token] route with multistep wizard (3 steps)
- Rate limiting: 3 views/min per IP via proxy middleware
- Immutable quote acceptance with optional email/notes capture
- quote_items never exposed; only accepted_total and phase summary visible
- All threat model mitigations in place
- npm run build: PASS (0 TypeScript errors)
Co-Authored-By: Claude Haiku 4.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>
- 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>