--- plan_id: 05-04 phase: 5 wave: 3 title: "Client dashboard offer card + /admin/forecast revenue forecast page (12 months)" type: execute depends_on: [05-01, 05-03] files_modified: - src/lib/client-view.ts - src/components/client-dashboard.tsx - src/components/client/OffersSection.tsx - src/lib/forecast-queries.ts - src/app/admin/forecast/page.tsx requirements_addressed: [OFFER-05, OFFER-06] autonomous: true must_haves: truths: - "Client dashboard shows active offers for a project with public_name, cumulative_price, accepted_total — never internal_name or individual service prices" - "If a project has no active offers, the client dashboard does not show an offer section" - "Admin can visit /admin/forecast and see a 12-month revenue breakdown table based on active project_offers" - "Forecast uses project_offers.accepted_total (not projects.accepted_total) spread over duration_months starting from start_date" artifacts: - path: "src/lib/client-view.ts" provides: "Extended ProjectView interface with activeOffers field" contains: "activeOffers" - path: "src/components/client/OffersSection.tsx" provides: "Client-facing offer card showing public_name, cumulative_price, accepted_total" contains: "OffersSection" - path: "src/lib/forecast-queries.ts" provides: "getRevenueForecast12Months() server function" contains: "ForecastMonth, getRevenueForecast12Months" - path: "src/app/admin/forecast/page.tsx" provides: "Admin revenue forecast page at /admin/forecast" contains: "ForecastTable" key_links: - from: "src/lib/client-view.ts" to: "src/db/schema.ts" via: "project_offers + offer_micros + offer_micro_services + offer_services JOIN queries" pattern: "activeOffers" - from: "src/app/admin/forecast/page.tsx" to: "src/lib/forecast-queries.ts" via: "getRevenueForecast12Months() server import" pattern: "getRevenueForecast12Months" --- Extend the client dashboard to display active offers per project, and build the admin revenue forecast page at `/admin/forecast`. Purpose: These are the two read surfaces that give value to the offer data collected in Plans 01-03. The client sees what they are getting; the admin sees projected revenue. Output: `ProjectView` extended with `activeOffers`; `OffersSection` client component; `forecast-queries.ts` with computation function; `/admin/forecast` page. @/Users/simonecavalli/IAMCAVALLI/.claude/get-shit-done/workflows/execute-plan.md @/Users/simonecavalli/IAMCAVALLI/.claude/get-shit-done/templates/summary.md @/Users/simonecavalli/IAMCAVALLI/.planning/ROADMAP.md @/Users/simonecavalli/IAMCAVALLI/.planning/STATE.md @/Users/simonecavalli/IAMCAVALLI/.planning/phases/05-offer-system/05-RESEARCH.md @/Users/simonecavalli/IAMCAVALLI/.planning/phases/05-offer-system/05-01-SUMMARY.md @/Users/simonecavalli/IAMCAVALLI/.planning/phases/05-offer-system/05-03-SUMMARY.md ```typescript export interface ProjectView { project: { id: string; name: string; client_id: string; accepted_total: string; }; phases: Array<...>; payments: Array<{ id: string; label: string; status: string; }>; // ... other fields ... global_progress_pct: number; // ADD: // activeOffers?: Array<{ // id: string; // public_name: string; // offer_micros.public_name ONLY — NEVER internal_name // cumulative_price: string; // SUM of offer_services.price for this micro's services // accepted_total: string | null; // project_offers.accepted_total // }>; } ``` ```typescript export const project_offers = pgTable("project_offers", { id, project_id, micro_id, start_date, accepted_total, created_at }); export const offer_micros = pgTable("offer_micros", { id, macro_id, internal_name, public_name, transformation_promise, duration_months, sort_order }); export const offer_micro_services = pgTable("offer_micro_services", { micro_id, service_id // composite PK }); export const offer_services = pgTable("offer_services", { id, name, price, transformation_description, active }); ``` ```typescript // getMonthlyCollected() pattern: query → JS array fill → return const byMonth: number[] = Array(12).fill(0); for (const row of rows) { byMonth[(row.month as number) - 1] = parseFloat(row.total); } ``` ```tsx // In the sidebar