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,236 @@
from datetime import date, timedelta
from typing import Optional
from fastapi import APIRouter, Depends, HTTPException
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy import select, and_
from pydantic import BaseModel
from database import get_db
from models.user import User
from models.calendar_events import CalendarEvent
from auth import get_current_user, require_cap
router = APIRouter()
# Pydantic schemas
class CalendarEventCreate(BaseModel):
event_date: date
event_type: str # reminder, event, note
title: str
description: str | None = None
class CalendarEventUpdate(BaseModel):
event_date: date | None = None
event_type: str | None = None
title: str | None = None
description: str | None = None
class CalendarEventResponse(BaseModel):
id: int
event_date: date
event_type: str
title: str
description: str | None
created_at: str
class Config:
from_attributes = True
# Endpoints
@router.get("/")
async def list_events(
from_date: Optional[date] = None,
to_date: Optional[date] = None,
current_user: User = Depends(get_current_user),
db: AsyncSession = Depends(get_db)
) -> list[CalendarEventResponse]:
"""List events for date range"""
query = select(CalendarEvent).where(
CalendarEvent.kitchen_id == current_user.kitchen_id
)
if from_date:
query = query.where(CalendarEvent.event_date >= from_date)
if to_date:
query = query.where(CalendarEvent.event_date <= to_date)
query = query.order_by(CalendarEvent.event_date, CalendarEvent.created_at)
result = await db.execute(query)
events = result.scalars().all()
return [
CalendarEventResponse(
id=e.id,
event_date=e.event_date,
event_type=e.event_type,
title=e.title,
description=e.description,
created_at=e.created_at.isoformat()
)
for e in events
]
@router.get("/{date}")
async def get_events_for_date(
date: date,
current_user: User = Depends(get_current_user),
db: AsyncSession = Depends(get_db)
) -> list[CalendarEventResponse]:
"""Get all events for a specific date"""
result = await db.execute(
select(CalendarEvent).where(
and_(
CalendarEvent.kitchen_id == current_user.kitchen_id,
CalendarEvent.event_date == date
)
).order_by(CalendarEvent.created_at)
)
events = result.scalars().all()
return [
CalendarEventResponse(
id=e.id,
event_date=e.event_date,
event_type=e.event_type,
title=e.title,
description=e.description,
created_at=e.created_at.isoformat()
)
for e in events
]
@router.post("/")
async def create_event(
event: CalendarEventCreate,
current_user: User = Depends(get_current_user),
db: AsyncSession = Depends(get_db)
) -> CalendarEventResponse:
"""Create new event"""
new_event = CalendarEvent(
kitchen_id=current_user.kitchen_id,
event_date=event.event_date,
event_type=event.event_type,
title=event.title,
description=event.description,
created_by=current_user.id
)
db.add(new_event)
await db.commit()
await db.refresh(new_event)
return CalendarEventResponse(
id=new_event.id,
event_date=new_event.event_date,
event_type=new_event.event_type,
title=new_event.title,
description=new_event.description,
created_at=new_event.created_at.isoformat()
)
@router.put("/{id}")
async def update_event(
id: int,
update: CalendarEventUpdate,
current_user: User = Depends(get_current_user),
db: AsyncSession = Depends(get_db)
):
"""Update existing event"""
result = await db.execute(
select(CalendarEvent).where(
and_(
CalendarEvent.id == id,
CalendarEvent.kitchen_id == current_user.kitchen_id
)
)
)
event = result.scalar_one_or_none()
if not event:
raise HTTPException(status_code=404, detail="Event not found")
if update.event_date is not None:
event.event_date = update.event_date
if update.event_type is not None:
event.event_type = update.event_type
if update.title is not None:
event.title = update.title
if update.description is not None:
event.description = update.description
await db.commit()
await db.refresh(event)
return CalendarEventResponse(
id=event.id,
event_date=event.event_date,
event_type=event.event_type,
title=event.title,
description=event.description,
created_at=event.created_at.isoformat()
)
@router.delete("/{id}")
async def delete_event(
id: int,
current_user: User = Depends(get_current_user),
db: AsyncSession = Depends(get_db)
):
"""Delete event"""
result = await db.execute(
select(CalendarEvent).where(
and_(
CalendarEvent.id == id,
CalendarEvent.kitchen_id == current_user.kitchen_id
)
)
)
event = result.scalar_one_or_none()
if not event:
raise HTTPException(status_code=404, detail="Event not found")
await db.delete(event)
await db.commit()
return {"message": "Event deleted"}
@router.get("/dashboard/upcoming")
async def get_upcoming_events(
limit: int = 3,
current_user: User = Depends(get_current_user),
db: AsyncSession = Depends(get_db)
):
"""Get next N upcoming events for dashboard widget"""
today = date.today()
result = await db.execute(
select(CalendarEvent).where(
and_(
CalendarEvent.kitchen_id == current_user.kitchen_id,
CalendarEvent.event_date >= today
)
).order_by(CalendarEvent.event_date, CalendarEvent.created_at).limit(limit)
)
events = result.scalars().all()
return {
"total_count": len(events),
"upcoming_events": [
{
"id": e.id,
"event_date": e.event_date.isoformat(),
"event_type": e.event_type,
"title": e.title
}
for e in events
]
}