- 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>
- Check 6: Count of services with migrated_from IS NULL (net-new rows created via /admin/catalog after migration)
- Printed as informational line at the end of validation output
- Does not fail validation if count is zero (expected for fresh test runs)
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>
Two-plan, two-wave migration: schema + backfill (07-01) then admin
catalog CRUD rewire (07-02), per CAT-U-01/CAT-U-02 and the
expand-contract pattern from PITFALLS_V2 Pitfall 1.
- 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
- 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
- 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
- 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
- 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
- Create OffersTab.tsx client component with assign form and offer list
- Inline accepted_total editing via onBlur handler
- Add Offerte tab trigger and content to project workspace page
- Destructure projectOffers and availableMicros from getProjectFullDetail result
- 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
- RSC page at /admin/offers with macro/micro/service CRUD fully functional
- ServiceCheckboxList for many-to-many service assignment
- NavBar updated with Forecast and Offerte links
- Documents schema.ts changes (5 new tables, relations, TypeScript types)
- Documents Phase 4+5 DB migration applied via SSH due to drizzle-kit TTY limitation
- Confirms zero data loss; all 5 clients and related data preserved
- Created projects (5 rows from clients), settings tables
- Added project_id to phases (9), payments (10), documents (1), quote_items (3) via client_id pivot
- Added slug to clients
- Created offer_macros, offer_micros, offer_services, offer_micro_services, project_offers
- Migration applied via direct SQL on clienthub-db container (drizzle-kit TTY limitation bypassed)
- Zero rows deleted; all existing data preserved
- Add primaryKey to drizzle-orm/pg-core import
- Define offer_macros, offer_micros, offer_services, offer_micro_services, project_offers
- Add Drizzle relations for all five new tables (+ projectOffers in projectsRelations)
- Export TypeScript infer types for all new tables
- initProjectPayments: crea Acconto 50%/Saldo 50% se il progetto non ha pagamenti
- splitPayment: divide un pagamento in due rate con importi custom
- SplitPaymentForm: form client interattivo con anteprima live della rata 2
- PaymentsTab: stato vuoto con CTA + pulsante Dividi per ogni riga
- projectId prop opzionale per attivare le feature solo nel contesto progetto
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.