import { requireAuth } from '../auth.js' const FORECASTING_URL = process.env.FORECASTING_URL || 'http://10.10.10.113:3080' async function fcFetch(path) { const apiKey = process.env.FORECASTING_API_KEY if (!apiKey) throw new Error('FORECASTING_API_KEY not configured') const res = await fetch(`${FORECASTING_URL}/forecasting/api/public${path}`, { headers: { 'X-API-Key': apiKey }, }) if (!res.ok) throw new Error(`Forecasting API ${res.status} — ${path}`) return res.json() } function toDateStr(d) { return d.toISOString().split('T')[0] } function addDays(d, n) { const r = new Date(d) r.setDate(r.getDate() + n) return r } function buildDayMap(data) { const m = {} for (const d of (data?.data ?? [])) m[d.date] = d return m } const DOW_NAMES = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'] const MONTH_LABELS = ['Jan','Feb','Mar','Apr','May','Jun','Jul','Aug','Sep','Oct','Nov','Dec'] function weekSummary(revDayMap, weekSundayDate, weekEndingDateStr) { let net = 0, budget = 0, ly = 0 let net_rooms = 0, net_dry = 0, net_wet = 0 let budget_rooms = 0, budget_dry = 0, budget_wet = 0 let ly_rooms = 0, ly_dry = 0, ly_wet = 0 for (let i = 0; i < 7; i++) { const dateStr = toDateStr(addDays(weekSundayDate, i)) const rev = revDayMap[dateStr] || {} const r = parseFloat(rev.accom?.otb ?? 0) const d = parseFloat(rev.dry?.otb ?? 0) const w = parseFloat(rev.wet?.otb ?? 0) const br = parseFloat(rev.accom?.budget ?? 0) const bd = parseFloat(rev.dry?.budget ?? 0) const bw = parseFloat(rev.wet?.budget ?? 0) const lr = parseFloat(rev.accom?.prior_final ?? 0) const ld = parseFloat(rev.dry?.prior_final ?? 0) const lw = parseFloat(rev.wet?.prior_final ?? 0) net_rooms += r; net_dry += d; net_wet += w budget_rooms += br; budget_dry += bd; budget_wet += bw ly_rooms += lr; ly_dry += ld; ly_wet += lw net += r + d + w; budget += br + bd + bw; ly += lr + ld + lw } return { week_ending: weekEndingDateStr, gross: net * 1.20, net, pct_to_budget: budget > 0 ? ((net - budget) / budget) * 100 : null, budget_net: budget, pct_to_ly: ly > 0 ? ((net - ly) / ly) * 100 : null, ly_net: ly, dept: { rooms: { net: net_rooms, budget: budget_rooms, ly: ly_rooms }, dry: { net: net_dry, budget: budget_dry, ly: ly_dry }, wet: { net: net_wet, budget: budget_wet, ly: ly_wet }, }, } } export async function weeklyActualRoutes(fastify) { fastify.addHook('preHandler', requireAuth) // GET /api/weekly-actual/:date (date = week ending Saturday, YYYY-MM-DD) fastify.get('/api/weekly-actual/:date', async (request, reply) => { const weekEndDate = new Date(request.params.date + 'T00:00:00') if (isNaN(weekEndDate.getTime())) return reply.status(400).send({ error: 'Invalid date' }) // Week runs Sun → Sat const weekStartDate = addDays(weekEndDate, -6) const year = weekEndDate.getFullYear() const month = weekEndDate.getMonth() + 1 // 5-week window: start 4 weeks before this week's Sunday const fiveWeekStart = addDays(weekStartDate, -28) // Full calendar month (needed for daily chart — month progress filters by date) const monthStart = new Date(year, month - 1, 1) const daysInMonth = new Date(year, month, 0).getDate() // 36 months of raw totals needed to compute rolling 12-month averages for // 12 display points (indices 23–35) plus their LY equivalents (indices 0–23) const trendMonths = [] for (let i = 35; i >= 0; i--) { const d = new Date(year, month - 1 - i, 1) trendMonths.push({ year: d.getFullYear(), month: d.getMonth() + 1 }) } // All API calls in parallel const results = await Promise.all([ // 35 days of revenue (5 × 7 weeks) fcFetch(`/forecast/revenue?start_date=${toDateStr(fiveWeekStart)}&days=35&type=all`), // This week rooms (for occupancy table) fcFetch(`/forecast/rooms?start_date=${toDateStr(weekStartDate)}&days=7`), // Full month revenue (used for month progress totals + daily chart) fcFetch(`/forecast/revenue?start_date=${toDateStr(monthStart)}&days=${daysInMonth}&type=all`), // 12 monthly trend revenue calls ...trendMonths.map(({ year: y, month: m }) => { const firstDay = new Date(y, m - 1, 1) const daysInMon = new Date(y, m, 0).getDate() return fcFetch(`/forecast/revenue?start_date=${toDateStr(firstDay)}&days=${daysInMon}&type=all`) }), ]) const [fiveWeekRevRaw, thisWeekRoomsRaw, fullMonthRevRaw, ...trendRevRaw] = results const fiveWeekRevMap = buildDayMap(fiveWeekRevRaw) const thisWeekRoomsMap = buildDayMap(thisWeekRoomsRaw) const fullMonthRevMap = buildDayMap(fullMonthRevRaw) // ── Five week summaries ──────────────────────────────────────────────────── const fiveWeeks = [0, 1, 2, 3, 4].map(w => { const sundayDate = addDays(weekStartDate, -7 * w) const saturdayDate = addDays(sundayDate, 6) return weekSummary(fiveWeekRevMap, sundayDate, toDateStr(saturdayDate)) }) const thisWeek = fiveWeeks[0] const lastWeek = fiveWeeks[1] // ── Occupancy by DOW ────────────────────────────────────────────────────── let totalAvail = 0, totalSold = 0, totalAccomm = 0 const daily = [] for (let i = 0; i < 7; i++) { const d = addDays(weekStartDate, i) const dateStr = toDateStr(d) const rev = fiveWeekRevMap[dateStr] || {} const rm = thisWeekRoomsMap[dateStr] || {} const available = parseInt(rm.available_rooms ?? 0) const rooms_sold = parseInt(rm.otb_rooms ?? 0) const accomm = parseFloat(rev.accom?.otb ?? 0) totalAvail += available totalSold += rooms_sold totalAccomm += accomm daily.push({ date: dateStr, dow: DOW_NAMES[d.getDay()], available, rooms_sold, accomm, arr: rooms_sold > 0 ? accomm / rooms_sold : null, occ_pct: available > 0 ? (rooms_sold / available) * 100 : null, revpar: available > 0 ? accomm / available : null, }) } const occupancy = { daily, totals: { available: totalAvail, rooms_sold: totalSold, accomm: totalAccomm, arr: totalSold > 0 ? totalAccomm / totalSold : null, occ_pct: totalAvail > 0 ? (totalSold / totalAvail) * 100 : null, revpar: totalAvail > 0 ? totalAccomm / totalAvail : null, }, } // ── Month progress ───────────────────────────────────────────────────────── let mNet = { rooms: 0, dry: 0, wet: 0 } let mBud = { rooms: 0, dry: 0, wet: 0 } let mLy = { rooms: 0, dry: 0, wet: 0 } for (let i = 0; i < daysInMonth; i++) { const d = addDays(monthStart, i) if (d > weekEndDate) break const dateStr = toDateStr(d) const rev = fullMonthRevMap[dateStr] || {} mNet.rooms += parseFloat(rev.accom?.otb ?? 0) mNet.dry += parseFloat(rev.dry?.otb ?? 0) mNet.wet += parseFloat(rev.wet?.otb ?? 0) mBud.rooms += parseFloat(rev.accom?.budget ?? 0) mBud.dry += parseFloat(rev.dry?.budget ?? 0) mBud.wet += parseFloat(rev.wet?.budget ?? 0) mLy.rooms += parseFloat(rev.accom?.prior_final ?? 0) mLy.dry += parseFloat(rev.dry?.prior_final ?? 0) mLy.wet += parseFloat(rev.wet?.prior_final ?? 0) } const mNetTotal = mNet.rooms + mNet.dry + mNet.wet const mBudTotal = mBud.rooms + mBud.dry + mBud.wet const mLyTotal = mLy.rooms + mLy.dry + mLy.wet const month_progress = { year, month, date_to: toDateStr(weekEndDate), gross: mNetTotal * 1.20, net: mNetTotal, budget_net: mBudTotal, ly_net: mLyTotal, pct_to_budget: mBudTotal > 0 ? ((mNetTotal - mBudTotal) / mBudTotal) * 100 : null, pct_to_ly: mLyTotal > 0 ? ((mNetTotal - mLyTotal) / mLyTotal) * 100 : null, split: { rooms: { net: mNet.rooms, budget_net: mBud.rooms, ly_net: mLy.rooms, pct_to_budget: mBud.rooms > 0 ? ((mNet.rooms - mBud.rooms) / mBud.rooms) * 100 : null, pct_to_ly: mLy.rooms > 0 ? ((mNet.rooms - mLy.rooms) / mLy.rooms) * 100 : null }, dry: { net: mNet.dry, budget_net: mBud.dry, ly_net: mLy.dry, pct_to_budget: mBud.dry > 0 ? ((mNet.dry - mBud.dry) / mBud.dry) * 100 : null, pct_to_ly: mLy.dry > 0 ? ((mNet.dry - mLy.dry) / mLy.dry) * 100 : null }, wet: { net: mNet.wet, budget_net: mBud.wet, ly_net: mLy.wet, pct_to_budget: mBud.wet > 0 ? ((mNet.wet - mBud.wet) / mBud.wet) * 100 : null, pct_to_ly: mLy.wet > 0 ? ((mNet.wet - mLy.wet) / mLy.wet) * 100 : null }, }, } // ── Month daily (for cumulative progress line chart) ────────────────────── const weekEndStr = toDateStr(weekEndDate) const month_daily = [] for (let i = 0; i < daysInMonth; i++) { const d = addDays(monthStart, i) const dateStr = toDateStr(d) const rev = fullMonthRevMap[dateStr] || {} month_daily.push({ day: i + 1, date: dateStr, ty_rooms: parseFloat(rev.accom?.otb ?? 0), ty_dry: parseFloat(rev.dry?.otb ?? 0), ty_wet: parseFloat(rev.wet?.otb ?? 0), ly_rooms: parseFloat(rev.accom?.prior_final ?? 0), ly_dry: parseFloat(rev.dry?.prior_final ?? 0), ly_wet: parseFloat(rev.wet?.prior_final ?? 0), is_actual: dateStr <= weekEndStr, }) } // ── Monthly trend (rolling 12-month average) ────────────────────────────── // Build raw monthly totals for all 36 months const allMonthTotals = trendMonths.map((_, i) => { const data = trendRevRaw[i]?.data ?? [] let total = 0 for (const d of data) { total += parseFloat(d.accom?.otb ?? 0) + parseFloat(d.dry?.otb ?? 0) + parseFloat(d.wet?.otb ?? 0) } return total }) const rollingAvg = (arr, from, to) => arr.slice(from, to + 1).reduce((s, v) => s + v, 0) / (to - from + 1) // Display points: indices 23–35 (the last 12 months) // TY rolling avg at index i = average of allMonthTotals[i-11 .. i] // LY rolling avg at index i = average of allMonthTotals[i-23 .. i-12] const monthly_trend = [] for (let i = 23; i < 36; i++) { const tm = trendMonths[i] monthly_trend.push({ label: `${MONTH_LABELS[tm.month - 1]}-${String(tm.year).slice(2)}`, this_year_avg: rollingAvg(allMonthTotals, i - 11, i), last_year_avg: rollingAvg(allMonthTotals, i - 23, i - 12), }) } // Monthly dept split for last 24 months (indices 12–35) // — previous 12 months followed by last 12 months const monthly_split = [] for (let i = 12; i < 36; i++) { const tm = trendMonths[i] const data = trendRevRaw[i]?.data ?? [] let rooms = 0, dry = 0, wet = 0 for (const d of data) { rooms += parseFloat(d.accom?.otb ?? 0) dry += parseFloat(d.dry?.otb ?? 0) wet += parseFloat(d.wet?.otb ?? 0) } monthly_split.push({ label: `${MONTH_LABELS[tm.month - 1]}-${String(tm.year).slice(2)}`, rooms, dry, wet, }) } return { week_ending: toDateStr(weekEndDate), week_start: toDateStr(weekStartDate), this_week: thisWeek, last_week: lastWeek.dept, five_weeks: fiveWeeks, occupancy, month_progress, month_daily, monthly_trend, monthly_split, } }) }