Files
simone 00783c4c15 docs(05-04): complete client dashboard offer card + admin forecast plan
- SUMMARY.md: 2 tasks, 6 files, commits c398d6d and 745f8a7
- activeOffers in ProjectView/ClientView (public_name only, T-05-09/10 mitigated)
- /admin/forecast 12-month RSC table complete
2026-05-30 16:44:25 +02:00

6.6 KiB
Raw Permalink Blame History

phase, plan, subsystem, tags, requires, provides, affects, tech-stack, key-files, key-decisions, patterns-established, requirements-completed, duration, completed
phase plan subsystem tags requires provides affects tech-stack key-files key-decisions patterns-established requirements-completed duration completed
05-offer-system 04 ui
offer-system
client-dashboard
forecast
rsc
drizzle
phase provides
05-01
project_offers
offer_micros
offer_micro_services
offer_services tables and TypeScript types
phase provides
05-03
project offer assignment
getClientActiveOffers query pattern
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
client-dashboard
client-view
forecast-queries
admin-forecast
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
created modified
src/lib/forecast-queries.ts
src/components/client/OffersSection.tsx
src/app/admin/forecast/page.tsx
src/lib/client-view.ts
src/components/client-dashboard.tsx
src/app/client/[token]/page.tsx
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
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)
OFFER-05
OFFER-06
~10 minutes 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