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>
236 lines
6.2 KiB
Python
236 lines
6.2 KiB
Python
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
|
|
]
|
|
}
|