From 2f335c99c5911428fbb624bb0472e7c0062b8ed6 Mon Sep 17 00:00:00 2001 From: jtricerolph Date: Tue, 21 Jul 2026 19:07:29 +0000 Subject: [PATCH] Save computed forecast dry/wet on worksheet save; report reads saved values Worksheet save now persists forecast_dry/forecast_wet per day to day_overrides so the report page always shows the same totals the user saw on the worksheet, rather than making a second independent forecasting API call that can return slightly different values. Co-Authored-By: Claude Sonnet 4.6 --- backend/src/db.js | 9 +++++++++ backend/src/routes/directors-forecast.js | 21 ++++++++++++++------- frontend/src/pages/DirectorsForecast.tsx | 20 +++++++++++++------- 3 files changed, 36 insertions(+), 14 deletions(-) diff --git a/backend/src/db.js b/backend/src/db.js index 0d4fb1a5..9ea51551 100644 --- a/backend/src/db.js +++ b/backend/src/db.js @@ -60,6 +60,15 @@ export async function initDb() { occ_pct DECIMAL(5,2) ); CREATE INDEX IF NOT EXISTS idx_forecast_snapshots_session ON forecast_snapshots(session_id); + `) + + // Add saved forecast columns — idempotent for existing deployments + await pool.query(` + ALTER TABLE day_overrides ADD COLUMN IF NOT EXISTS forecast_dry DECIMAL(12,2); + ALTER TABLE day_overrides ADD COLUMN IF NOT EXISTS forecast_wet DECIMAL(12,2); + `) + + await pool.query(` CREATE TABLE IF NOT EXISTS app_settings ( key TEXT PRIMARY KEY, diff --git a/backend/src/routes/directors-forecast.js b/backend/src/routes/directors-forecast.js index 819d3cce..36090cc5 100644 --- a/backend/src/routes/directors-forecast.js +++ b/backend/src/routes/directors-forecast.js @@ -31,7 +31,7 @@ async function getSessionAndOverrides(year, month) { const session = sessionRes.rows[0] const overridesRes = await pool.query( - `SELECT date::text, pickup_rooms, pickup_avg_rate, dry_override, wet_override, actual_accomm + `SELECT date::text, pickup_rooms, pickup_avg_rate, dry_override, wet_override, actual_accomm, forecast_dry, forecast_wet FROM day_overrides WHERE session_id = $1`, [session.id] ) @@ -165,17 +165,20 @@ export async function directorsForecastRoutes(fastify) { for (const o of overrides) { if (!o.date) continue await pool.query( - `INSERT INTO day_overrides (session_id, date, pickup_rooms, pickup_avg_rate, dry_override, wet_override, actual_accomm) - VALUES ($1, $2, $3, $4, $5, $6, $7) + `INSERT INTO day_overrides (session_id, date, pickup_rooms, pickup_avg_rate, dry_override, wet_override, actual_accomm, forecast_dry, forecast_wet) + VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9) ON CONFLICT (session_id, date) DO UPDATE SET pickup_rooms = EXCLUDED.pickup_rooms, pickup_avg_rate = EXCLUDED.pickup_avg_rate, dry_override = EXCLUDED.dry_override, wet_override = EXCLUDED.wet_override, - actual_accomm = EXCLUDED.actual_accomm`, + actual_accomm = EXCLUDED.actual_accomm, + forecast_dry = EXCLUDED.forecast_dry, + forecast_wet = EXCLUDED.forecast_wet`, [session.id, o.date, o.pickup_rooms ?? null, o.pickup_avg_rate ?? null, - o.dry_override ?? null, o.wet_override ?? null, o.actual_accomm ?? null] + o.dry_override ?? null, o.wet_override ?? null, o.actual_accomm ?? null, + o.forecast_dry ?? null, o.forecast_wet ?? null] ) } } @@ -229,8 +232,12 @@ export async function directorsForecastRoutes(fastify) { const actualWet = isPast ? (rev.wet?.otb ?? null) : null const fAccomm = (isPast && actualAccomm != null) ? actualAccomm : otbNetRev + pickupRooms * dayRate - const fDry = ovr.dry_override != null ? parseFloat(ovr.dry_override) : (rev.dry?.forecast ?? 0) - const fWet = ovr.wet_override != null ? parseFloat(ovr.wet_override) : (rev.wet?.forecast ?? 0) + const fDry = ovr.forecast_dry != null ? parseFloat(ovr.forecast_dry) + : ovr.dry_override != null ? parseFloat(ovr.dry_override) + : (rev.dry?.forecast ?? 0) + const fWet = ovr.forecast_wet != null ? parseFloat(ovr.forecast_wet) + : ovr.wet_override != null ? parseFloat(ovr.wet_override) + : (rev.wet?.forecast ?? 0) totalAvailable += available totalOtbRooms += otbRooms diff --git a/frontend/src/pages/DirectorsForecast.tsx b/frontend/src/pages/DirectorsForecast.tsx index 46fe8f70..973d53f2 100644 --- a/frontend/src/pages/DirectorsForecast.tsx +++ b/frontend/src/pages/DirectorsForecast.tsx @@ -108,13 +108,19 @@ function WorksheetTab({ year, month, dowAlign }: { year: number; month: number; if (!canEdit) return setSaving(true) try { - const ovrList = Object.entries(overrides).map(([date, o]) => ({ - date, - pickup_rooms: o.pickup_rooms !== '' ? parseInt(o.pickup_rooms) : null, - pickup_avg_rate: o.pickup_avg_rate !== '' ? parseFloat(o.pickup_avg_rate) : null, - dry_override: o.dry_override !== '' ? parseFloat(o.dry_override) : null, - wet_override: o.wet_override !== '' ? parseFloat(o.wet_override) : null, - })) + const dayMap = new Map(data!.days.map(d => [d.date, d])) + const ovrList = Object.entries(overrides).map(([date, o]) => { + const computed = computedDay(dayMap.get(date)!) + return { + date, + pickup_rooms: o.pickup_rooms !== '' ? parseInt(o.pickup_rooms) : null, + pickup_avg_rate: o.pickup_avg_rate !== '' ? parseFloat(o.pickup_avg_rate) : null, + dry_override: o.dry_override !== '' ? parseFloat(o.dry_override) : null, + wet_override: o.wet_override !== '' ? parseFloat(o.wet_override) : null, + forecast_dry: computed.dry, + forecast_wet: computed.wet, + } + }) await dfSaveWorksheet(year, month, { pickup_avg_rate: parseFloat(sessionRate) || 135, overrides: ovrList }) setDirty(false) await load()