Initial kitchen scaffold — Phase 1 kitchen port (build-verified 2026-07-11)

FastAPI backend (Python 3.11, MSSQL ODBC for SambaPOS, Azure DI OCR),
kitchen_db on central PG. React/TS/Vite frontend with navy sidebar layout.

Backend: auth.py (APP_SLUG=kitchen, SimpleNamespace — archive routes use
.kitchen_id/.is_admin without modification), main.py (51 migrations, scheduler,
internal router for KDS bookings feed), api/internal.py, full archive API
(31 routers: invoices, recipes, menus, sambapos, resos, newbook, disputes,
purchase_orders, etc.), models, migrations, OCR pipeline.
kitchen_id pinned to 1 (B1 — single hotel).

Frontend: AuthGate (app=kitchen, token shim for archive compat — B5b pending),
Layout (navy sidebar, 6 sections, Lucide icons, teal --app-primary),
App.tsx (Outlet pattern, UploadApp outside Layout), index.css (full :root block).
strict: false — archive components have type issues; build clean.

Note: 45 archive components call fetch('/api/...') without /kitchen/ prefix
(B5b). Runtime 404s; deferred until after initial testing.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
jtricerolph 2026-07-12 12:15:39 +00:00
commit 8d688b459d
10003 changed files with 1928395 additions and 0 deletions

View file

@ -0,0 +1,390 @@
# Plan 1: Newbook Room-Level Occupancy Sync
## Current State
The ResidentsTableChart feature has:
- ✅ Database schema with room-level fields (room_number, booking_id, guest_name, is_dbb, is_package)
- ✅ API endpoint to serve room-level data
- ✅ Frontend chart to display room occupancy
- ❌ Sync service only fetches aggregated daily totals, not individual room details
**Current database state:**
```sql
room_number | booking_id | guest_name | is_dbb
------------|------------|------------|-------
NULL | NULL | NULL | false
```
## Problem Statement
The Newbook API sync (`backend/services/newbook.py`) currently only fetches:
- Daily aggregated occupancy (total_rooms, occupied_rooms, occupancy_percentage)
- Meal allocations (breakfast/dinner counts and revenue)
- Arrival tracking (booking IDs and details for arrivals)
It does NOT fetch:
- Individual room numbers
- Which rooms are occupied on which dates
- Guest names per room
- Booking IDs per occupied room
- Meal plan details per booking (DBB, package deals)
## Goal
Update the Newbook sync service to fetch and store room-level occupancy data so the ResidentsTableChart displays actual rooms instead of "Unknown Room".
## Investigation Required
Before implementation, we need to verify what data is available from the Newbook API:
### 1. Review Newbook API Documentation
- What endpoint provides room-level occupancy?
- Does it return individual room statuses?
- Is guest name available (privacy concerns)?
- How are meal plans (DBB, packages) represented?
### 2. Examine Existing Sync Code
**File:** `backend/services/newbook.py`
Current occupancy sync likely uses:
- `/api/occupancy` or similar endpoint
- Returns aggregated daily statistics
- May need different endpoint for room-level details
### 3. Check Existing Arrival Tracking
The code already populates `arrival_booking_details` (JSONB) which might contain:
- Booking reference numbers
- Room assignments
- Guest names
- Meal plan flags
**SQL to check existing arrival data:**
```sql
SELECT date, arrival_booking_details
FROM newbook_daily_occupancy
WHERE arrival_booking_details IS NOT NULL
LIMIT 5;
```
## Potential Approaches
### Option A: Expand Arrival Tracking to All Stays
If `arrival_booking_details` contains room information, extend this to track:
- Not just arrivals, but all bookings staying on each date
- Store as `active_bookings_details` JSONB field
- Parse and populate room_number, booking_id, guest_name from this JSON
**Pros:**
- Minimal API changes if data already available
- Leverages existing JSON structure
**Cons:**
- JSONB storage plus denormalized columns (redundancy)
- May not scale well with many rooms
### Option B: New Endpoint for Room Status
Use a Newbook API endpoint that returns room-by-room status:
**Expected API response:**
```json
{
"date": "2026-01-22",
"rooms": [
{
"room_number": "101",
"status": "occupied",
"booking_id": "NB-12345",
"guest_name": "John Smith",
"check_in": "2026-01-22",
"check_out": "2026-01-25",
"meal_plan": "DBB",
"is_package": false
},
{
"room_number": "102",
"status": "vacant"
}
]
}
```
**Pros:**
- Clean, structured data
- All room details in one call
- Easier to maintain
**Cons:**
- May require different API endpoint
- More data to fetch/store
### Option C: Hybrid Approach (RECOMMENDED)
1. Use existing occupancy endpoint for aggregated stats
2. Fetch booking details separately (may already be happening for arrivals)
3. Match bookings to dates they span (check_in to check_out)
4. Create separate row per room per night
**Storage strategy:**
```
Current: 1 row per date (aggregated)
New: 1 row per room per night (denormalized)
Example for date 2026-01-22:
OLD:
- date: 2026-01-22, occupied_rooms: 6
NEW:
- date: 2026-01-22, room: 101, booking_id: NB-12345, guest: Smith
- date: 2026-01-22, room: 102, booking_id: NB-12346, guest: Jones
- date: 2026-01-22, room: 103, booking_id: NB-12347, guest: Brown
...
```
**Trade-off:** More database rows, but enables room-level reporting.
## Schema Considerations
### Current Schema Issue
`newbook_daily_occupancy` has unique constraint:
```sql
CONSTRAINT uq_newbook_occupancy_per_day
UNIQUE(kitchen_id, date)
```
This **prevents** multiple rows per date!
### Solution 1: Change Unique Constraint
```sql
-- Drop old constraint
ALTER TABLE newbook_daily_occupancy
DROP CONSTRAINT uq_newbook_occupancy_per_day;
-- Add new constraint for room-level uniqueness
ALTER TABLE newbook_daily_occupancy
ADD CONSTRAINT uq_newbook_occupancy_per_room_per_day
UNIQUE(kitchen_id, date, room_number);
```
**Impact:**
- Breaking change to schema
- Need migration to handle existing aggregated rows
- Backward compatibility: API endpoint must handle both aggregated and room-level rows
### Solution 2: Separate Table (Alternative)
Create `newbook_room_occupancy` table:
```sql
CREATE TABLE newbook_room_occupancy (
id SERIAL PRIMARY KEY,
kitchen_id INTEGER NOT NULL REFERENCES kitchens(id),
date DATE NOT NULL,
room_number VARCHAR(50) NOT NULL,
booking_id VARCHAR(100),
guest_name VARCHAR(255),
check_in DATE,
check_out DATE,
is_dbb BOOLEAN DEFAULT FALSE,
is_package BOOLEAN DEFAULT FALSE,
fetched_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT uq_room_occupancy_per_day
UNIQUE(kitchen_id, date, room_number)
);
```
Keep `newbook_daily_occupancy` for aggregated stats.
**Pros:**
- No breaking changes
- Clean separation of concerns
- Can keep both aggregated and room-level data
**Cons:**
- Two tables to maintain
- API endpoint needs to join data or use new table
## Recommended Implementation Plan
### Phase 1: Investigate Newbook API
1. Review Newbook API documentation for room-level endpoints
2. Test API calls to see what data is available
3. Check if current `arrival_booking_details` contains room info
4. Determine if guest names are available (privacy/GDPR)
### Phase 2: Database Migration
**Option A (if room data is sparse/optional):**
- Keep current schema
- Allow room_number, booking_id to be NULL
- Populate when available
- Current unique constraint stays
**Option B (if room data is always available):**
- Change unique constraint to (kitchen_id, date, room_number)
- Migrate existing aggregated rows (need strategy)
- Update sync to create multiple rows per date
**Option C (cleanest):**
- Create new `newbook_room_occupancy` table
- Keep existing table for aggregated stats
- Update API endpoint to use new table
### Phase 3: Update Sync Service
**File:** `backend/services/newbook.py`
**Current sync logic:**
```python
async def sync_occupancy(kitchen_id, date_from, date_to):
# Fetch aggregated occupancy
data = await newbook_api.get_occupancy(date_from, date_to)
# Store one row per date
for day in data:
occupancy = NewbookDailyOccupancy(
kitchen_id=kitchen_id,
date=day['date'],
total_rooms=day['total_rooms'],
occupied_rooms=day['occupied_rooms'],
# ...
)
```
**New sync logic (Option B - room-level):**
```python
async def sync_occupancy(kitchen_id, date_from, date_to):
# Fetch room-level occupancy
data = await newbook_api.get_room_status(date_from, date_to)
# Delete existing rows for this date range (full refresh)
await db.execute(
delete(NewbookDailyOccupancy).where(
and_(
NewbookDailyOccupancy.kitchen_id == kitchen_id,
NewbookDailyOccupancy.date >= date_from,
NewbookDailyOccupancy.date <= date_to
)
)
)
# Store one row per room per date
for day in data:
for room in day['rooms']:
if room['status'] == 'occupied':
occupancy = NewbookDailyOccupancy(
kitchen_id=kitchen_id,
date=day['date'],
room_number=room['room_number'],
booking_id=room['booking_id'],
guest_name=room.get('guest_name'), # May be NULL for privacy
is_dbb=room.get('meal_plan') == 'DBB',
is_package=room.get('is_package', False),
# Aggregated stats on each row (redundant but simple)
total_rooms=day['total_rooms'],
occupied_rooms=day['occupied_rooms'],
# ...
)
db.add(occupancy)
```
### Phase 4: Update API Endpoint
**File:** `backend/api/residents_table_chart.py`
**Current logic:**
- Groups by (room_number, booking_id)
- Expects rows already grouped
**Updated logic (if using room-level rows):**
- No changes needed! Already groups correctly
- Just needs room_number and booking_id to be populated
**Alternative (if keeping aggregated + separate table):**
- Query `newbook_room_occupancy` instead
- Join with `newbook_daily_occupancy` for aggregated stats
### Phase 5: Testing
1. Run sync for a test date range
2. Verify database populated:
```sql
SELECT date, room_number, booking_id, guest_name
FROM newbook_daily_occupancy
WHERE date = '2026-01-22'
ORDER BY room_number;
```
3. Test ResidentsTableChart page
4. Verify rooms display with actual names
5. Check restaurant booking linkage (hotel_booking_number)
## Privacy & GDPR Considerations
**Guest Names:**
- May need to be masked/hashed for privacy
- Consider: "Guest in Room 101" instead of actual name
- Add setting: "Show guest names" (admin only)
- Log access to guest PII
**Data Retention:**
- How long to keep room-level data?
- May need purge policy for old bookings
- Aggregate historical data, delete room details
## Performance Considerations
**Database Size:**
- Current: ~365 rows per year (1 per day)
- New: ~365 × 25 rooms = 9,125 rows per year
- 10 years: ~91,000 rows (still manageable)
**Indexing:**
- Existing: (kitchen_id, date)
- New: (kitchen_id, date, room_number) - already created
- Consider: (kitchen_id, booking_id) for linking to Resos
**Query Performance:**
- ResidentsTableChart queries 7-day window
- Fetching 7 × 25 = 175 rows max
- Indexes should handle this easily
## Migration Strategy
**For Existing Aggregated Rows:**
**Option 1:** Delete and re-sync
```sql
DELETE FROM newbook_daily_occupancy;
-- Then run sync to repopulate with room-level data
```
**Option 2:** Keep aggregated rows, add room-level
- Set room_number = NULL for aggregated rows
- Add new room-level rows alongside
- API filters WHERE room_number IS NOT NULL
**Option 3:** Backfill from booking history (if available)
- Query Newbook API for historical bookings
- Reconstruct room occupancy for past dates
- May be slow/rate-limited
## Next Steps
1. **Immediate:** Check Newbook API docs for room-level endpoint
2. **Investigate:** Review current `arrival_booking_details` structure
3. **Decide:** Choose schema approach (modify existing vs new table)
4. **Prototype:** Test API calls to fetch room data
5. **Implement:** Update sync service based on findings
6. **Test:** Verify ResidentsTableChart shows real rooms
## Unknown/Questions
- [ ] Does Newbook API provide room-level occupancy?
- [ ] What endpoint? (e.g., `/api/room_status`, `/api/bookings`)
- [ ] Are guest names available?
- [ ] How are meal plans represented in API?
- [ ] Is booking ID always available?
- [ ] Rate limits for room-level sync?
- [ ] Historical data availability?
## Success Criteria
✅ ResidentsTableChart shows actual room numbers (e.g., "101", "102") instead of "Unknown Room"
✅ Guest names populated (if available and permitted)
✅ Booking IDs linked correctly
✅ DBB/package flags set correctly
✅ Restaurant bookings link via hotel_booking_number
✅ Data refreshes daily (or more frequently)
✅ Performance remains acceptable (<500ms API response)