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:
commit
8d688b459d
10003 changed files with 1928395 additions and 0 deletions
236
backend/api/calendar_events.py
Normal file
236
backend/api/calendar_events.py
Normal 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
|
||||
]
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue