Commit Graph

113 Commits

Author SHA1 Message Date
simone 43238341c1 feat: service offer tags, catalog cleanup, offer editor filter
- 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>
2026-06-18 21:58:34 +02:00
simone 27e8706e39 fix(offer-editor): show all services regardless of offer category
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>
2026-06-18 18:35:37 +02:00
simone 4f7e0033c4 fix(offer-editor): fix category bug, add ripristina, always-saveable
- 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>
2026-06-18 18:25:50 +02:00
simone 791a6e64b1 chore: merge executor worktree (worktree-agent-a44f854f45ac76582) 2026-06-15 10:28:07 +02:00
simone 7df4b9c74a feat(12-04): build OfferListClient with category filter, archive toggle, card grid
- 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"
2026-06-15 10:25:15 +02:00
simone 68dc1b605b feat(12-04): rewrite /admin/offers page.tsx as new list page entry point
- Replace legacy macro/micro/offer_services CRUD page with thin server
  component fetching getOfferListCards() + getOfferFieldOptions()
- Delegate rendering to new OfferListClient (Task 2)
2026-06-15 10:25:03 +02:00
simone 8c5c918304 feat(12-05): build offer editor client component
- Add OfferEditorClient: full editor for /admin/offers/[id]/edit
- Categoria/Ticket single-select (OptionSelect) + Tipo/Obiettivo
  multi-select with on-the-fly creation (OptionMultiSelect)
- Services matrix (A/B/C checkboxes) pre-filtered by macro.category,
  with live "Totale Servizi" recalculation via useMemo (OFFER-11)
- Independent manual "Prezzo Pubblico" per tier (OFFER-16)
- 5-field "Promessa di Trasformazione" block via EditableCell (OFFER-17)
- Salva Offerta / Annulla / Archivia actions wired to saveOfferEditor,
  toggleOfferArchived and renameOfferOption (OFFER-15)
2026-06-15 10:25:02 +02:00
simone 8e0e4b9c59 feat(12-05): create offer editor route server component
- 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
2026-06-15 10:24:49 +02:00
simone a372c613e2 feat(12-03): add offer editor server actions
- saveOfferEditor(macroId, payload): persists macro scalars, per-tier
  upsert (tier_letter Zod-validated A/B/C), offer_tier_services
  delete-then-reinsert, and Tipo/Obiettivo tags delete-then-reinsert,
  all in one call
- toggleOfferArchived(macroId, archived) for OFFER-18
- addOfferTag/removeOfferTag/renameOfferOption for Tipo/Obiettivo/
  Categoria/Ticket dimensions, mirroring catalog option-pool pattern
- createOfferMacro(formData) — Plan 04 "+ Nuova Offerta" target
- scripts/verify-12-03-actions.ts documents the 9 spec'd test cases
  (typecheck-only, requireAdmin needs a request context)
2026-06-15 10:17:07 +02:00
simone 0d742f2328 feat(12-03): add offer editor query layer
- 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)
2026-06-15 10:15:19 +02:00
simone 89d15eeb54 feat(12-01): add migration 0008 for offer tier schema + idempotent push script
- 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
2026-06-14 21:19:10 +02:00
simone 11d6c1179f feat(12-01): extend offer schema with tier, pricing, and category fields
- Add additive offer_macros columns: description, category, ticket, is_archived, and structured transformation-promise fields (cliente_ideale/risultato/tempo/pain/metodo)
- Add additive offer_micros columns: tier_letter (A/B/C, CHECK constraint added in migration 0008) and public_price
- Add new offer_tier_services junction table (offer_micros <-> services), relations, and types
- Extend offerMicrosRelations with tierServices; legacy offer_micro_services/offer_services untouched
2026-06-14 21:17:52 +02:00
simone c8d53134ba fix(14): remove dead generate_new branch from assignQuoteToLead (CRM-12)
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.
2026-06-14 15:17:48 +02:00
simone ab7fa62059 feat(14-02): add LeadsSearch, rewire leads pages, surface tags in LeadDetail (CRM-08/09)
- 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
2026-06-14 13:21:55 +02:00
simone 4887a319f7 feat(14-02): rewrite LeadTable as raw-table database view (CRM-08/09)
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
2026-06-14 13:21:23 +02:00
simone 39bd1ce2eb chore: merge executor worktree (worktree-agent-aaf61d7161f7818a0)
# Conflicts:
#	.planning/phases/14-crm-attio-style-fix/deferred-items.md
2026-06-14 12:51:37 +02:00
simone a495d84511 fix(14-03): remove dead onSubmit branch in SendQuoteModal (CRM-12)
- 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)
2026-06-14 12:46:55 +02:00
simone ee509cd5fb fix(14-03): type LeadForm useForm with CreateLeadInput (CRM-11)
- 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
2026-06-14 12:43:00 +02:00
simone 7d98b27e75 feat(14-01): add updateLeadField and lead tag CRUD server actions
- 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
2026-06-14 12:41:36 +02:00
simone 272e363f4d fix(14-03): translate FollowUpWidget to Italian (CRM-10)
- Replace 6 hardcoded English strings with Italian equivalents
- Remove pluralization ternary (Italian "lead" is invariant)
2026-06-14 12:38:12 +02:00
simone 5b583bcc5e feat(14-01): add getLeadsWithTags and getLeadFieldOptions to admin-queries
- 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
2026-06-14 12:38:09 +02:00
simone e858a8f577 feat(catalog): Notion-style shared select fields (tag/pacchetto/categoria/fase)
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>
2026-06-13 21:40:34 +02:00
simone 42c16e1bab fix(11): address code-review findings in catalog inline-edit
- 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>
2026-06-13 21:02:50 +02:00
simone 912a892ba5 feat(11-04): add client-side search bar to catalog page, remove ServiceForm
- 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
2026-06-13 16:01:17 +02:00
simone c0bedf300e feat(11-04): rewrite ServiceTable as database-view (inline edit, tags, quick-add, active/inactive split)
- 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()
2026-06-13 15:58:36 +02:00
simone a567a90ce3 feat(11-03): add TagMultiSelect component with hashed tag colors
- 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
2026-06-13 15:51:49 +02:00
simone 55276c19fe fix(11-03): avoid setState/ref access during render in EditableCell
- 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
2026-06-13 15:51:41 +02:00
simone 3514a3710d feat(11-03): add EditableCell click-to-edit component
- text/number/textarea/toggle types with click-to-edit display mode
- Enter (non-textarea) saves, Escape cancels, blur saves
- required validation with inline error, disabled state styling
- ring-1 ring-primary focus per DESIGN-SYSTEM.md inline-edit pattern
2026-06-13 15:49:15 +02:00
simone 445de856e4 feat(11-02): add inline-edit, tag, and quick-add server actions
- 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
2026-06-13 15:39:55 +02:00
simone f7434102de feat(11-02): extend getAllServices with tags join (ServiceWithTags)
- 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
2026-06-13 15:38:57 +02:00
simone 4773487d0c feat(11-01): add tags table to schema with migration and push script
- 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
2026-06-13 15:26:14 +02:00
simone ea206854a9 fix(leads): await params Promise in lead detail page (Next.js 16)
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>
2026-06-12 22:13:26 +02:00
simone 008a43469d feat(10-redo-C): CRM leads module — schema, services, UI (Phase 10 redo)
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>
2026-06-11 20:58:14 +02:00
simone 5aa6614e41 feat(10-redo-A): add dialog/form UI primitives + deps (@radix-ui/react-dialog, date-fns)
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>
2026-06-11 18:33:04 +02:00
simone 6a35c97cfd feat(09-03): create public quote page with server actions
- 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>
2026-06-11 07:38:57 +02:00
simone 9facd3ff85 feat(09-03): create multistep quote form components (3 steps)
- 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>
2026-06-11 07:38:50 +02:00
simone f5d571e89d feat(09-03): add rate limiting for public quote routes
- 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>
2026-06-11 07:38:46 +02:00
simone 614cf0114f feat(09-02): implement admin quote builder UI with server actions
- 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>
2026-06-11 07:34:16 +02:00
simone abf37323fe feat(09-01): add quote validators and service layer for Phase 9 quote builder
- Updated quotes table schema: added accepted_total, state, client_email, client_notes fields
- Updated quote_items relations: changed service_id FK to unified services table
- Created quote-validators.ts: Zod schemas for quote creation, acceptance, and step validation
- Created quote-service.ts: public query layer (excludes line items) and admin queries
- Added Phase 9 migration: additive columns for quotes state machine and client capture
- Maintained backward compatibility: legacy quote_items tied to projects remain functional
- TypeScript builds successfully with no errors

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
2026-06-11 07:30:10 +02:00
simone cad582dd06 feat(08-03): add query layer for offer phases and quotes
- 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>
2026-06-11 07:16:02 +02:00
simone f727954dfb feat(08-02): create Phase 8 migration and validation script
- Create SQL migration 0003_offer_phases_quote_templates.sql
  - Creates offers_phases, offer_phase_services, quotes, leads tables
  - Adds new columns to quote_items, projects, phases (additive-only)
  - Includes proper FK constraints, indexes, and CHECK constraints
  - ZERO DATA LOSS: No drops, no truncates — fully reversible

- Create validation script scripts/validate-phase8-migration.ts
  - Checks all 4 new tables exist
  - Verifies 7 new columns added to existing tables
  - Validates FK references intact
  - Exit code 0 on success, 1 on failure

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
2026-06-11 07:15:27 +02:00
simone 37fe5eae48 feat(08-01): add offer_phases, offer_phase_services, and quotes table schema
- Create offer_phases table (hierarchical phases within offer micros)
- Create offer_phase_services junction (phases → unified services)
- Create quotes table (separate headers from line items)
- Create leads placeholder table (FK for quotes, Phase 10 CRM)
- Update quote_items: add quote_id, offer_micro_id, offer_phase_id columns
- Update projects: add offer_id, created_from_lead_id columns
- Update phases: add offer_phase_id column for audit trail
- Add all Drizzle relations and TypeScript types
- Build verified with npm run build

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
2026-06-11 07:14:56 +02:00
simone 4ed2f8b105 feat(07-unified-service-catalog): rewire /admin/catalog CRUD + query layer to services table
- 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>
2026-06-11 06:26:03 +02:00
simone e4ddb878ff feat(07-unified-service-catalog): add services table to schema with audit trail columns
- New `services` pgTable with: id, name, description, unit_price, category, active, migrated_from, migrated_id, created_at
- Audit trail columns (migrated_from, migrated_id) enable rollback safety during expand-contract migration
- Service and NewService TypeScript types exported
- Migration SQL file created (0001_add_services_table.sql) for schema push
- No FK relations added yet (Phase 8 will establish connections)
- service_catalog, offer_services tables unchanged — legacy compatibility preserved
- npm run build passes TypeScript checks

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
2026-06-11 06:21:35 +02:00
simone 19b540e116 fix(auth): allow internal routes when INTERNAL_SECRET unset — localhost-only by design 2026-05-31 20:39:10 +02:00
simone 40162e03ca feat(06-02): replace redirect with real dashboard RSC at /admin
- 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
2026-05-31 20:11:56 +02:00
simone a304328bd6 feat(06-02): create dashboard-queries.ts with getDashboardStats()
- 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
2026-05-31 20:11:31 +02:00
simone 4d52d87341 chore(06-01): delete NavBar.tsx — replaced by AdminSidebar
- NavBar is no longer imported anywhere (layout.tsx updated in prior task)
- tsc --noEmit and npm run build both pass clean
2026-05-31 20:09:32 +02:00
simone 191b548e78 feat(06-01): move client list to /admin/clients, /admin redirects to clients
- 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
2026-05-31 20:09:09 +02:00
simone 29e0e88267 feat(06-01): replace top NavBar with AppShell flex-row sidebar layout
- 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
2026-05-31 20:08:45 +02:00