Files
simone 23cb057f0b docs(07): phase 7 planning complete — 2 plans (schema migration + admin crud)
Wave 1: 07-01-PLAN.md — Unified services table (audit trail, backfill, validation)
Wave 2: 07-02-PLAN.md — Admin catalog CRUD rewire (preserve historical references)

Ready for execution: /gsd-execute-phase 7

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
2026-06-11 06:17:29 +02:00

125 lines
6.6 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
phase: 05-offer-system
plan: 04
subsystem: ui
tags: [offer-system, client-dashboard, forecast, rsc, drizzle]
requires:
- phase: 05-01
provides: [project_offers, offer_micros, offer_micro_services, offer_services tables and TypeScript types]
- phase: 05-03
provides: [project offer assignment, getClientActiveOffers query pattern]
provides:
- activeOffers field in ProjectView and ClientView (public_name only — never internal_name)
- OffersSection client-facing component
- getRevenueForecast12Months() 12-bucket JS algorithm in forecast-queries.ts
- /admin/forecast RSC page with 12-month revenue table
affects: [client-dashboard, client-view, forecast-queries, admin-forecast]
tech-stack:
added: []
patterns: [explicit-column-selection for security (T-05-09/T-05-10), 12-bucket JS forecast algorithm mirroring getMonthlyCollected pattern, RSC data fetch + table render for admin forecast]
key-files:
created:
- src/lib/forecast-queries.ts
- src/components/client/OffersSection.tsx
- src/app/admin/forecast/page.tsx
modified:
- src/lib/client-view.ts
- src/components/client-dashboard.tsx
- src/app/client/[token]/page.tsx
key-decisions:
- "activeOffers query uses explicit .select({ public_name: offer_micros.public_name }) — internal_name never appears in the SELECT list (T-05-09 mitigation)"
- "activeOffers is undefined (not []) when no offers assigned — dashboard section conditionally renders only when truthy and non-empty (T-05-10 mitigation)"
- "Forecast algorithm runs entirely server-side in RSC (getRevenueForecast12Months called in page.tsx) — no client-side computation (T-05-12 mitigation)"
- "ForecastMonth label uses it-IT locale for Italian month abbreviations consistent with admin UI language"
- "cumulative_price computed via SQL SUM on offer_micro_services JOIN offer_services — not persisted, always fresh from DB"
patterns-established:
- "Pattern: Security-critical column selection in client-facing queries — explicit .select() with only public fields, comment naming excluded field"
- "Pattern: Conditional sidebar blocks using undefined (not []) — view.activeOffers && view.activeOffers.length > 0"
- "Pattern: 12-bucket JS array for time-series data initialized with Array.from then filled in a for loop (mirrors getMonthlyCollected)"
requirements-completed: [OFFER-05, OFFER-06]
duration: ~10 minutes
completed: 2026-05-30
---
# Phase 5 Plan 4: Client dashboard offer card + /admin/forecast revenue forecast page Summary
**Client dashboard now shows active offers per project (public_name + cumulative service price + accepted_total), and /admin/forecast displays a 12-month server-side revenue projection spread from project_offers.accepted_total across duration_months.**
## Performance
- **Duration:** ~10 minutes
- **Started:** 2026-05-30T00:00:00Z
- **Completed:** 2026-05-30T00:00:00Z
- **Tasks:** 2
- **Files modified:** 6
## Accomplishments
- Extended `ProjectView` and `ClientView` interfaces with `activeOffers` field (public_name, cumulative_price, accepted_total only — never internal_name)
- Created `forecast-queries.ts` with `getRevenueForecast12Months()` — builds 12 monthly buckets from current month and distributes each offer's accepted_total evenly across its duration_months window
- Built `OffersSection.tsx` client component showing per-offer card with public name and prices — zero internal data exposed
- Wired OffersSection into client-dashboard sidebar (conditionally rendered when offers exist), and mapped activeOffers through the projectViewToClientView adapter
- Delivered `/admin/forecast` RSC page — table with month, projected revenue, proportional bar; totals row; Italian locale labels; empty-state message
## Task Commits
Each task was committed atomically:
1. **Task 1: Extend client-view.ts + forecast-queries.ts** - `c398d6d` (feat)
2. **Task 2: OffersSection + client dashboard wiring + /admin/forecast page** - `745f8a7` (feat)
## Files Created/Modified
- `src/lib/client-view.ts` — Added offer table imports, sql import; activeOffers to ProjectView and ClientView interfaces; projectOfferRows + cumulativePriceRows queries in getProjectView(); activeOffers in return
- `src/lib/forecast-queries.ts` — New file: ForecastMonth type, getRevenueForecast12Months() with 12-bucket JS algorithm
- `src/components/client/OffersSection.tsx` — New RSC-compatible component: renders offer card per entry with public_name, cumulative_price, accepted_total
- `src/components/client-dashboard.tsx` — Added OffersSection import; Offerte Attive sidebar block between Pagamenti and Documenti
- `src/app/client/[token]/page.tsx` — Extended projectViewToClientView adapter to map activeOffers
- `src/app/admin/forecast/page.tsx` — New RSC page at /admin/forecast with table, bar column, totals row, empty state
## Decisions Made
- `activeOffers` returns `undefined` (not `[]`) when empty — allows simple truthy check in dashboard (`view.activeOffers && view.activeOffers.length > 0`)
- Security comment in `client-view.ts` offer query explicitly names `internal_name` as excluded — this is a documentation discipline from the existing payment amount comment pattern
- Forecast uses `offer_micros.duration_months` not a hardcoded value — ties forecast accuracy directly to micro-offer configuration
- `maxTotal` for bar width computed inside the `map` on each row iteration (idiomatic React, acceptable for 12 rows)
## Deviations from Plan
None — plan executed exactly as written.
## Issues Encountered
None.
## Known Stubs
None — all data paths wire to live DB queries. OffersSection receives real project_offers data. getRevenueForecast12Months reads from production DB.
## Threat Flags
No new trust boundaries beyond those in the plan's threat model. T-05-09 mitigated by explicit column selection (no internal_name in SELECT). T-05-10 mitigated by OffersSection receiving only public_name prop (confirmed by grep: 0 internal_name occurrences). T-05-12 mitigated by server-side RSC execution of getRevenueForecast12Months.
## Self-Check: PASSED
- `src/lib/forecast-queries.ts` exists: FOUND (c398d6d)
- `src/components/client/OffersSection.tsx` exists: FOUND (745f8a7)
- `src/app/admin/forecast/page.tsx` exists: FOUND (745f8a7)
- `src/lib/client-view.ts` activeOffers count: 4 matches (interface×2 + query + return)
- `grep "internal_name" src/components/client/OffersSection.tsx`: 0 matches
- `grep "internal_name" src/app/admin/forecast/page.tsx`: 0 matches
- `npx tsc --noEmit`: PASSED (no errors)
- Task 1 commit c398d6d: FOUND
- Task 2 commit 745f8a7: FOUND
---
*Phase: 05-offer-system*
*Completed: 2026-05-30*