Commit Graph

82 Commits

Author SHA1 Message Date
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
simone 2283740d20 feat(06-01): create AdminSidebar component with 7 nav links + logout
- Client component using usePathname for active route highlighting
- 7 nav links: Dashboard, Clienti, Progetti, Offerte, Forecast, Catalogo, Impostazioni
- Logout button at bottom calls signOut with /admin/login callback
- Green sidebar bg #1A463C matching brand
2026-05-31 20:08:33 +02:00
simone 745f8a78c0 feat(05-04): OffersSection component + client dashboard wiring + /admin/forecast page
- Create src/components/client/OffersSection.tsx — displays public_name, cumulative_price, accepted_total (no internal_name)
- Extend client-dashboard.tsx — import OffersSection, add Offerte Attive sidebar block conditional on view.activeOffers
- Extend client/[token]/page.tsx adapter — map activeOffers from ProjectView to ClientView
- Create src/app/admin/forecast/page.tsx — RSC with 12-month revenue table using getRevenueForecast12Months(), bar chart column, totals row, empty-state message
2026-05-30 16:43:14 +02:00
simone c398d6de34 feat(05-04): extend client-view with activeOffers + create forecast-queries.ts
- 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
2026-05-30 16:41:43 +02:00
simone 20f4fd8603 feat(05-03): add getClientActiveOffers query + active offers section on client detail page
- 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
2026-05-30 16:28:09 +02:00
simone 0c09d44b68 feat(05-03): add OffersTab component + Offerte tab in project workspace
- 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
2026-05-30 16:27:07 +02:00
simone b3f781b9b5 feat(05-03): extend getProjectFullDetail + add offer assignment server actions
- 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
2026-05-30 16:25:58 +02:00
simone ce8f95a163 feat(05-02): /admin/offers RSC page + ServiceCheckboxList + NavBar update
- Create src/app/admin/offers/page.tsx — RSC with macro/micro/service CRUD sections
- Create src/components/admin/offers/ServiceCheckboxList.tsx — client checkbox UI for service-to-micro assignment
- Update src/components/admin/NavBar.tsx — add Forecast and Offerte links (order: Statistiche | Forecast | Catalogo | Offerte | Impostazioni)
2026-05-30 16:22:19 +02:00
simone 56f084912a feat(05-02): add offer-queries.ts and server actions for offer catalog CRUD
- Create src/lib/offer-queries.ts with getCatalogWithMicros, getAllOfferServices, getMicroAssignedServiceIds
- Create src/app/admin/offers/actions.ts with createMacro, deleteMacro, createMicro, deleteMicro, createOfferService, toggleOfferServiceActive, updateMicroOfferServices
- All exported server actions guard with requireAdmin() + revalidatePath(/admin/offers)
2026-05-30 15:35:54 +02:00
simone f0034410ff feat(05-01): append five offer tables + Drizzle relations to schema.ts
- 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
2026-05-30 15:26:02 +02:00
simone 75097ccfa4 feat: init payments + split payment in project workspace
- 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
2026-05-23 10:18:06 +02:00
simone 5efb433c23 fix(04-07): remove debug code — public page restored
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.
2026-05-23 09:03:06 +02:00
simone 25ae19c9d2 debug: page-level file logging 2026-05-22 20:34:19 +02:00
simone d67be3b6b4 debug: file-based logging to /tmp/clienthub-debug.log 2026-05-22 20:18:31 +02:00
simone bc2fcc8670 debug: add generateMetadata log
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-22 18:54:58 +02:00
simone fca86fde73 debug: log token param in ClientPage
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-22 18:44:56 +02:00
simone 78a65f6ced debug: log tokenOrSlug in getClientWithProjectsByToken
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-22 18:40:18 +02:00
simone 7f9150b75a debug: add error logging to getClientWithProjectsByToken
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-22 18:38:41 +02:00
simone a478462aa4 security: full hardening pass — auth guards, rate limiting, headers, internal secret
- 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>
2026-05-22 14:36:16 +02:00
simone eab88c9f63 fix(04-06): five post-deploy fixes
- 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>
2026-05-22 13:42:38 +02:00
simone e6a9774cfe fix(04-05): three post-deploy bug fixes
- 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>
2026-05-22 12:13:31 +02:00
simone 5d785a1c1c feat(04-04): slug resolution + multi-project client dashboard + slug edit
- validate-slug API route: resolves clients.slug for Edge middleware
- proxy.ts: slug-first resolution (D-06) — tries slug then falls back to token
- client-view.ts: complete rewrite — getClientWithProjectsByToken + getProjectView
  - No quote_items, no payment amounts in client API (CLAUDE.md security invariants)
- client/[token]/page.tsx: multi-project dashboard — 1 project = direct view,
  2+ projects = shadcn Tabs with project names (D-09/D-10)
- edit/page.tsx: slug field with link preview + unique constraint error handling
- actions.ts: updateClient now persists slug, redirects on success/slug_taken

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-22 11:31:32 +02:00
simone 1d9fa10961 feat(04-03): project workspace + timer analytics + settings page
- /admin/projects/[id]: full workspace with 7 tabs (Fasi, Pagamenti,
  Documenti, Note, Commenti, Preventivo, Timer)
- TimerTab: TimerCell + ProfitabilityCard with €/h real, ideal cost, delta
- ProfitabilityCard: hours worked, €/h real vs target, profit/loss delta
- /admin/impostazioni: target_hourly_rate setting (default 50€/h)
- actions.ts: resolveEntity() helper — actions now work transparently
  with both clientId and projectId (same tab components reused)
- quote-actions.ts: same resolveEntity pattern for addQuoteItem,
  removeQuoteItem, updateAcceptedTotal
- timer-actions.ts: revalidate /admin/projects and /admin/projects/[id]

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-22 11:05:31 +02:00
simone ef05d647fe feat(04-02): admin projects list + client detail project cards
- NavBar: add Progetti (/admin/projects) and Impostazioni links
- ProjectRow: project table row with €/h calc (accepted_total / tracked hours)
- /admin/projects: table listing all projects with value, payments, timer, €/h
- /admin/projects/new: create form with client select + ?client_id pre-selection
- project-actions: createProject, archiveProject, unarchiveProject, updateProjectAcceptedTotal
- /admin/clients/[id]: rewritten to show project cards grid instead of tabbed workspace
- getAllClientsWithPayments: add projectBrands[] and ltv (sum of project accepted_totals)
- ClientRow: show project brand names under client name, LTV column replaces Totale

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-22 08:45:10 +02:00
simone b92b1c447b fix(db): skip DATABASE_URL check during Next.js build phase
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>
2026-05-21 22:11:36 +02:00
simone 63c9f750df feat(04-01): multi-project schema migration — projects, settings, FK pivot
- schema.ts: add projects table, settings kv table, slug on clients;
  migrate 6 FK from client_id to project_id (phases, payments, documents, notes,
  time_entries, quote_items); update all relations and TypeScript types
- admin-queries.ts: fix getAllClientsWithPayments + getClientFullDetail to aggregate
  through projects; add getAllProjectsWithPayments, getProjectFullDetail,
  getClientWithProjects, ClientWithProjects type
- settings.ts: new file — getSetting, updateSetting, getTargetHourlyRate, SETTINGS_KEYS
- Fix all downstream files: actions.ts, quote-actions.ts, new/actions.ts,
  timer-actions.ts, approve/route.ts, comment/route.ts, TimerCell.tsx,
  analytics-queries.ts, client-view.ts, seed.ts
- DB migration applied to Coolify Postgres (all test data cleared, schema rebuilt)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-21 21:58:15 +02:00
simone 5bf5dfce71 infra(04-00): route /c/ → /client/, Dockerfile, Gitea deploy
- Rename src/app/c/[token] → src/app/client/[token]
- Update proxy.ts, ClientRow, admin client detail with /client/ path
- Add output: "standalone" to next.config.ts for Docker build
- Add Dockerfile (multi-stage, node:20-alpine) and .dockerignore
- Push schema to Coolify Postgres via SSH tunnel (drizzle-kit push ✓)
- Update CLAUDE.md constraint 4 to reflect /client/ route
- Add Phase 4 planning artifacts (04-00, 04-RESEARCH, 04-PATTERNS)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-21 16:12:05 +02:00
Simone Cavalli 2aade028b8 fix(css): exclude .planning/ from Tailwind v4 source scan — prevents [-:|] regex comment from generating invalid CSS 2026-05-19 23:04:34 +02:00
Simone Cavalli 511c7d1d43 fix: resolve merge conflict in admin-queries.ts — keep both quote_items and service_catalog imports 2026-05-17 11:50:38 +02:00
Simone Cavalli 48f81e7110 feat(03-03): QuoteTab component + Preventivo tab in client detail page
- Create QuoteTab.tsx: catalog dropdown + freeform toggle + items table + accepted total editor
- Wire QuoteTab as 5th tab (Preventivo) in /admin/clients/[id]/page.tsx
- Destructure quoteItems + activeServices from getClientFullDetail result
- TypeScript clean, build passes
2026-05-17 11:44:57 +02:00
Simone Cavalli 4aae2e0d0f feat(03-02): catalog page + ServiceTable + ServiceForm + NavBar link
- Create src/app/admin/catalog/page.tsx: server component, fetches all services, renders ServiceForm + ServiceTable
- Create src/components/admin/catalog/ServiceForm.tsx: add-new-service form with open/collapse toggle
- Create src/components/admin/catalog/ServiceTable.tsx: per-row inline edit (DocumentRow pattern), Attivo/Disattivato badges, opacity-50 for inactive
- Modify src/components/admin/NavBar.tsx: add Catalogo link after Statistiche
- TypeScript clean, npm run build compiled successfully
2026-05-17 11:43:49 +02:00
Simone Cavalli db81829b85 feat(03-03): Server Actions quote CRUD + extend getClientFullDetail
- Create quote-actions.ts: addQuoteItem, removeQuoteItem, updateAcceptedTotal
- All three actions guarded by requireAdmin() + Zod validation
- Extend admin-queries.ts: QuoteItemWithLabel type, quoteItems + activeServices queries
- quote_items NEVER exposed via client-facing routes (security constraint enforced)
2026-05-17 11:42:44 +02:00