00783c4c15
- SUMMARY.md: 2 tasks, 6 files, commitsc398d6dand745f8a7- activeOffers in ProjectView/ClientView (public_name only, T-05-09/10 mitigated) - /admin/forecast 12-month RSC table complete
125 lines
6.6 KiB
Markdown
125 lines
6.6 KiB
Markdown
---
|
||
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*
|