- 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>
- 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>
- 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>
- 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>
- 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
- 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.
- Add requireAdmin() to all unprotected admin server actions
(clients/new, clients/[id], timer-actions — 17 functions total)
- Protect /api/internal/* endpoints with X-Internal-Secret header
(proxy.ts sends it; routes reject requests without it)
- Randomize auto-generated client slugs with 4-char suffix
to prevent enumeration via predictable name-based slugs
- Add in-memory rate limiting to /api/client/approve (20/min)
and /api/client/comment (10/min) per IP
- Add security headers: X-Frame-Options, X-Content-Type-Options,
Referrer-Policy, Permissions-Policy, X-DNS-Prefetch-Control
- Reduce JWT session from 30 days to 7 days with daily rotation
- Remove hardcoded NEXTAUTH_URL from Dockerfile (pass via Coolify env)
- Genericize client API error messages to not leak data structure
- Update .env.example with all required variables including INTERNAL_SECRET
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Middleware (proxy.ts): switch internal API calls to localhost instead of
request.url — avoids Docker hairpin NAT issues where the container can't
reach its own external hostname via Traefik
- ProjectRow timer: pass projectId={project.id} to TimerCell so it calls
startTimer(projectId) directly instead of startTimerForClient(project.id)
- Admin client list: add slug field to ClientWithPayments + show slug link
when available (falls back to token) — so admins see the human-readable URL
- Analytics contracted: sum projects.accepted_total (authoritative) instead
of clients.accepted_total (always 0 in multi-project architecture)
- createClient: auto-generate slug from client name at creation time
(e.g. "Mario Rossi" → "mario-rossi"), with -2/-3 suffix on conflict
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Admin client list: remove Timer column, rename Acconto→Importo incassato
and Saldo→Importo da saldare (calculated from payment amounts, not badges)
- Project workspace timer: pass projectId prop to TimerCell so it calls
startTimer(projectId) directly instead of startTimerForClient(projectId)
which was failing with "nessun progetto trovato"
- Public client page: pass client.token (real DB token) not the URL path
segment (which may be a slug) to ClientDashboard → fixes approve/chat APIs
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
NEXT_PHASE=phase-production-build is set by Next.js during static analysis.
Route handlers are analyzed but not executed, so the DB client is never
actually used — the placeholder URL never connects to anything.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Create src/app/admin/catalog/actions.ts with createService, updateService, toggleServiceActive
- Each action includes requireAdmin() guard via getServerSession
- Zod validation: name (min 1), unit_price (coerce.number min 0.01)
- Add getAllServices() to src/lib/admin-queries.ts ordered by name
- Import service_catalog and ServiceCatalog types in admin-queries.ts
- Remove .notNull() from service_id to allow free-form items without catalog ref
- Add custom_label: text("custom_label") for free-form item label storage
- TypeScript compiles with zero errors (QuoteItem.service_id now string | null)