Backend overhaul: new models, routers, schemas for shifts, business day, flags, messages, settings
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
199
local_backend/routers/messages.py
Normal file
199
local_backend/routers/messages.py
Normal file
@@ -0,0 +1,199 @@
|
||||
import json
|
||||
from fastapi import APIRouter, Depends, HTTPException, status
|
||||
from sqlalchemy.orm import Session, joinedload
|
||||
from typing import List
|
||||
|
||||
from database import get_db
|
||||
from models.message import StaffMessage, StaffMessageAck, QuickMessageTemplate
|
||||
from models.user import User
|
||||
from schemas.message import (
|
||||
SendMessageRequest, StaffMessageOut,
|
||||
QuickTemplateCreate, QuickTemplateUpdate, QuickTemplateOut,
|
||||
)
|
||||
from routers.deps import get_current_user, require_manager
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
def _load_msg(db: Session, msg_id: int) -> StaffMessage:
|
||||
"""Reload a message with sender and acks eagerly loaded."""
|
||||
return db.query(StaffMessage).options(
|
||||
joinedload(StaffMessage.sender),
|
||||
joinedload(StaffMessage.acks),
|
||||
).filter(StaffMessage.id == msg_id).one()
|
||||
|
||||
|
||||
def _message_out(msg: StaffMessage) -> StaffMessageOut:
|
||||
sender_name = None
|
||||
try:
|
||||
sender_name = msg.sender.username if msg.sender else None
|
||||
except Exception:
|
||||
pass
|
||||
return StaffMessageOut(
|
||||
id=msg.id,
|
||||
sender_id=msg.sender_id,
|
||||
sender_name=sender_name,
|
||||
body=msg.body,
|
||||
target_waiter_ids=msg.target_waiter_ids,
|
||||
table_ids=msg.table_ids,
|
||||
created_at=msg.created_at,
|
||||
acked_by=[ack.waiter_id for ack in msg.acks],
|
||||
)
|
||||
|
||||
|
||||
# ─── Quick templates ──────────────────────────────────────────────────────────
|
||||
|
||||
@router.get("/templates", response_model=List[QuickTemplateOut])
|
||||
def list_templates(
|
||||
db: Session = Depends(get_db),
|
||||
user: User = Depends(get_current_user),
|
||||
):
|
||||
return db.query(QuickMessageTemplate).filter(
|
||||
QuickMessageTemplate.is_active == True
|
||||
).order_by(QuickMessageTemplate.sort_order, QuickMessageTemplate.id).all()
|
||||
|
||||
|
||||
@router.post("/templates", response_model=QuickTemplateOut, status_code=status.HTTP_201_CREATED)
|
||||
def create_template(
|
||||
body: QuickTemplateCreate,
|
||||
db: Session = Depends(get_db),
|
||||
user: User = Depends(require_manager),
|
||||
):
|
||||
t = QuickMessageTemplate(**body.model_dump())
|
||||
db.add(t)
|
||||
db.commit()
|
||||
db.refresh(t)
|
||||
return t
|
||||
|
||||
|
||||
@router.put("/templates/{template_id}", response_model=QuickTemplateOut)
|
||||
def update_template(
|
||||
template_id: int,
|
||||
body: QuickTemplateUpdate,
|
||||
db: Session = Depends(get_db),
|
||||
user: User = Depends(require_manager),
|
||||
):
|
||||
t = db.query(QuickMessageTemplate).filter(QuickMessageTemplate.id == template_id).first()
|
||||
if not t:
|
||||
raise HTTPException(status_code=404, detail="Template not found")
|
||||
for k, v in body.model_dump(exclude_unset=True).items():
|
||||
setattr(t, k, v)
|
||||
db.commit()
|
||||
db.refresh(t)
|
||||
return t
|
||||
|
||||
|
||||
@router.delete("/templates/{template_id}", status_code=status.HTTP_204_NO_CONTENT)
|
||||
def delete_template(
|
||||
template_id: int,
|
||||
db: Session = Depends(get_db),
|
||||
user: User = Depends(require_manager),
|
||||
):
|
||||
t = db.query(QuickMessageTemplate).filter(QuickMessageTemplate.id == template_id).first()
|
||||
if not t:
|
||||
raise HTTPException(status_code=404, detail="Template not found")
|
||||
t.is_active = False
|
||||
db.commit()
|
||||
|
||||
|
||||
# ─── Staff messages ───────────────────────────────────────────────────────────
|
||||
|
||||
@router.post("/send", response_model=StaffMessageOut, status_code=status.HTTP_201_CREATED)
|
||||
def send_message(
|
||||
body: SendMessageRequest,
|
||||
db: Session = Depends(get_db),
|
||||
user: User = Depends(require_manager),
|
||||
):
|
||||
msg = StaffMessage(
|
||||
sender_id=user.id,
|
||||
body=body.body,
|
||||
target_waiter_ids=json.dumps(body.target_waiter_ids),
|
||||
table_ids=json.dumps(body.table_ids or []),
|
||||
)
|
||||
db.add(msg)
|
||||
db.commit()
|
||||
msg = _load_msg(db, msg.id)
|
||||
return _message_out(msg)
|
||||
|
||||
|
||||
@router.get("/unread", response_model=List[StaffMessageOut])
|
||||
def get_unread_messages(
|
||||
db: Session = Depends(get_db),
|
||||
user: User = Depends(get_current_user),
|
||||
):
|
||||
"""
|
||||
Returns messages targeting this waiter that they haven't acked yet.
|
||||
A message targets a waiter if their ID is in target_waiter_ids,
|
||||
OR if target_waiter_ids is empty (broadcast to all).
|
||||
"""
|
||||
all_msgs = db.query(StaffMessage).options(
|
||||
joinedload(StaffMessage.sender),
|
||||
joinedload(StaffMessage.acks),
|
||||
).order_by(StaffMessage.created_at.desc()).limit(200).all()
|
||||
acked_ids = {
|
||||
ack.message_id
|
||||
for ack in db.query(StaffMessageAck).filter(StaffMessageAck.waiter_id == user.id).all()
|
||||
}
|
||||
|
||||
result = []
|
||||
for msg in all_msgs:
|
||||
if msg.id in acked_ids:
|
||||
continue
|
||||
targets = json.loads(msg.target_waiter_ids or "[]")
|
||||
# Empty list = broadcast to all
|
||||
if not targets or user.id in targets:
|
||||
result.append(_message_out(msg))
|
||||
|
||||
return result
|
||||
|
||||
|
||||
@router.get("/recent", response_model=List[StaffMessageOut])
|
||||
def get_recent_messages(
|
||||
limit: int = 10,
|
||||
db: Session = Depends(get_db),
|
||||
user: User = Depends(get_current_user),
|
||||
):
|
||||
"""Last N messages targeting this user (for notification history drawer)."""
|
||||
all_msgs = db.query(StaffMessage).options(
|
||||
joinedload(StaffMessage.sender),
|
||||
joinedload(StaffMessage.acks),
|
||||
).order_by(StaffMessage.created_at.desc()).limit(200).all()
|
||||
result = []
|
||||
for msg in all_msgs:
|
||||
targets = json.loads(msg.target_waiter_ids or "[]")
|
||||
if not targets or user.id in targets:
|
||||
result.append(_message_out(msg))
|
||||
if len(result) >= limit:
|
||||
break
|
||||
return result
|
||||
|
||||
|
||||
@router.post("/{message_id}/ack", status_code=status.HTTP_204_NO_CONTENT)
|
||||
def ack_message(
|
||||
message_id: int,
|
||||
db: Session = Depends(get_db),
|
||||
user: User = Depends(get_current_user),
|
||||
):
|
||||
msg = db.query(StaffMessage).filter(StaffMessage.id == message_id).first()
|
||||
if not msg:
|
||||
raise HTTPException(status_code=404, detail="Message not found")
|
||||
existing = db.query(StaffMessageAck).filter(
|
||||
StaffMessageAck.message_id == message_id,
|
||||
StaffMessageAck.waiter_id == user.id,
|
||||
).first()
|
||||
if not existing:
|
||||
db.add(StaffMessageAck(message_id=message_id, waiter_id=user.id))
|
||||
db.commit()
|
||||
|
||||
|
||||
@router.get("/all", response_model=List[StaffMessageOut])
|
||||
def list_all_messages(
|
||||
limit: int = 50,
|
||||
db: Session = Depends(get_db),
|
||||
user: User = Depends(require_manager),
|
||||
):
|
||||
msgs = db.query(StaffMessage).options(
|
||||
joinedload(StaffMessage.sender),
|
||||
joinedload(StaffMessage.acks),
|
||||
).order_by(StaffMessage.created_at.desc()).limit(limit).all()
|
||||
return [_message_out(m) for m in msgs]
|
||||
Reference in New Issue
Block a user