Commit Graph

90 Commits

Author SHA1 Message Date
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
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