Forecasting app: hybrid port to HNF stack

Python FastAPI ML backend kept intact; auth replaced with central hnf_session cookie verification. Frontend rebuilt on React 18 + TS + Vite with stack design system, Plotly charts retained. Shared Postgres via DATABASE_URL; schema applied on startup.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
jtricerolph 2026-07-04 18:49:34 +00:00
commit 75d2c1fa9d
103 changed files with 70316 additions and 0 deletions

116
backend/utils/capacity.py Normal file
View file

@ -0,0 +1,116 @@
"""
Room capacity utilities
Functions for getting bookable room counts accounting for maintenance.
"""
from datetime import date
from sqlalchemy import text
def get_bookable_cap_sync(db, forecast_date: date = None, fallback_value: int = 25) -> int:
"""
Get the bookable rooms cap for a specific date (synchronous version).
Bookable = Total Rooms - Maintenance - Allotted
Args:
db: Synchronous database session
forecast_date: Specific date to get cap for (optional)
fallback_value: Default if no data found
Returns:
Bookable room count (cap for room forecasts)
"""
# First try to get specific date's bookable count from stats
if forecast_date:
result = db.execute(text("""
SELECT bookable_count
FROM newbook_bookings_stats
WHERE date = :target_date AND bookable_count IS NOT NULL
"""), {"target_date": forecast_date})
row = result.fetchone()
if row and row.bookable_count is not None:
return int(row.bookable_count)
# Try occupancy report data for future dates
result = db.execute(text("""
SELECT
SUM(COALESCE(o.available, 0) - COALESCE(o.maintenance, 0)) as bookable
FROM newbook_occupancy_report_data o
JOIN newbook_room_categories c ON o.category_id = c.site_id
WHERE o.date = :target_date AND c.is_included = true
"""), {"target_date": forecast_date})
row = result.fetchone()
if row and row.bookable is not None:
return int(row.bookable)
# Fall back to most recent bookable_count
result = db.execute(text("""
SELECT bookable_count
FROM newbook_bookings_stats
WHERE bookable_count IS NOT NULL
ORDER BY date DESC
LIMIT 1
"""))
row = result.fetchone()
if row and row.bookable_count:
return int(row.bookable_count)
return fallback_value
async def get_bookable_cap(db, forecast_date: date = None, fallback_value: int = 25) -> int:
"""
Get the bookable rooms cap for a specific date.
Bookable = Total Rooms - Maintenance - Allotted
For future dates without stats data, tries occupancy report data first,
then falls back to most recent bookable_count from stats.
Args:
db: Database session
forecast_date: Specific date to get cap for (optional)
fallback_value: Default if no data found
Returns:
Bookable room count (cap for room forecasts)
"""
# First try to get specific date's bookable count from stats
if forecast_date:
result = await db.execute(text("""
SELECT bookable_count
FROM newbook_bookings_stats
WHERE date = :target_date AND bookable_count IS NOT NULL
"""), {"target_date": forecast_date})
row = result.fetchone()
# Accept 0 as valid (all rooms in maintenance)
if row and row.bookable_count is not None:
return int(row.bookable_count)
# Try occupancy report data for future dates
result = await db.execute(text("""
SELECT
SUM(COALESCE(o.available, 0) - COALESCE(o.maintenance, 0)) as bookable
FROM newbook_occupancy_report_data o
JOIN newbook_room_categories c ON o.category_id = c.site_id
WHERE o.date = :target_date AND c.is_included = true
"""), {"target_date": forecast_date})
row = result.fetchone()
# Accept 0 as valid bookable count (all rooms in maintenance)
if row and row.bookable is not None:
return int(row.bookable)
# Fall back to most recent bookable_count
result = await db.execute(text("""
SELECT bookable_count
FROM newbook_bookings_stats
WHERE bookable_count IS NOT NULL
ORDER BY date DESC
LIMIT 1
"""))
row = result.fetchone()
if row and row.bookable_count:
return int(row.bookable_count)
return fallback_value