Port occupancy sync + category ordering from the forecasting version

- Bookability showed no availability because newbook_occupancy_report_data
  was never populated: add reports_occupancy client method and
  sync_occupancy job, run before rates in both Sync Now and the daily
  schedule (single fast API call)
- Category order: default display_order to the Newbook category id on
  sync (was 0 → alphabetical), preserve manual order on re-sync, extend
  PATCH to accept display_order, add up/down reorder arrows in Settings

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
jtricerolph 2026-07-05 14:18:03 +00:00
parent c924d793e8
commit f51e1dcb76
5 changed files with 199 additions and 9 deletions

View file

@ -76,7 +76,8 @@ class RoomCategory(BaseModel):
class CategoryUpdate(BaseModel):
is_included: bool
is_included: Optional[bool] = None
display_order: Optional[int] = None
@router.get("/categories", response_model=List[RoomCategory])
@ -118,16 +119,23 @@ async def sync_categories(
raise HTTPException(status_code=502, detail="Newbook returned no room categories — check credentials in Settings")
for cat in categories:
# New categories default to Newbook's category id order; a manually
# set display_order is preserved on re-sync
try:
default_order = int(cat["category_id"])
except (TypeError, ValueError):
default_order = 0
await db.execute(
text("""
INSERT INTO newbook_room_categories (site_id, site_name, room_count, fetched_at)
VALUES (:site_id, :site_name, :room_count, NOW())
INSERT INTO newbook_room_categories (site_id, site_name, room_count, display_order, fetched_at)
VALUES (:site_id, :site_name, :room_count, :display_order, NOW())
ON CONFLICT (site_id) DO UPDATE SET
site_name = EXCLUDED.site_name,
room_count = EXCLUDED.room_count,
fetched_at = NOW()
"""),
{"site_id": cat["category_id"], "site_name": cat["category_name"], "room_count": cat["room_count"]}
{"site_id": cat["category_id"], "site_name": cat["category_name"],
"room_count": cat["room_count"], "display_order": default_order}
)
await db.commit()
return {"status": "success", "count": len(categories)}
@ -140,10 +148,19 @@ async def update_category(
db: AsyncSession = Depends(get_db),
current_user: dict = Depends(get_current_user)
):
"""Toggle whether a category is included in rate syncs."""
"""Update a category's include-in-sync flag and/or display order."""
sets, params = [], {"id": category_id}
if body.is_included is not None:
sets.append("is_included = :inc")
params["inc"] = body.is_included
if body.display_order is not None:
sets.append("display_order = :ord")
params["ord"] = body.display_order
if not sets:
raise HTTPException(status_code=400, detail="Nothing to update")
result = await db.execute(
text("UPDATE newbook_room_categories SET is_included = :inc WHERE id = :id RETURNING id"),
{"inc": body.is_included, "id": category_id}
text(f"UPDATE newbook_room_categories SET {', '.join(sets)} WHERE id = :id RETURNING id"),
params
)
if not result.fetchone():
raise HTTPException(status_code=404, detail="Category not found")
@ -564,6 +581,14 @@ async def refresh_rates(
Note: This can be slow as it respects Newbook API rate limits.
"""
from jobs.fetch_current_rates import run_fetch_current_rates
from jobs.sync_occupancy import run_sync_occupancy
# Occupancy first — a single fast API call that gives the matrix
# availability data even while the (slow) rates fetch is running
try:
await run_sync_occupancy()
except Exception as e:
logger.warning(f"Occupancy sync failed (continuing with rates): {e}")
# For now, just run the standard fetch
# TODO: Add support for custom date range and category filter