---
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*
