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:
commit
75d2c1fa9d
103 changed files with 70316 additions and 0 deletions
116
backend/utils/capacity.py
Normal file
116
backend/utils/capacity.py
Normal 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
|
||||
Loading…
Add table
Add a link
Reference in a new issue