v0.1.19 - Throughput overview & responsive header

- Throughput: new "Throughput Overview" header with Gauge icon and brand-green
  badge icons on each card (Today, This week, 4-week average, Horse Mix, Grain Mix)
- Throughput: inline rolling-range selector (7d / 4w / 6w / 12w, default 4 weeks)
  driving the customer-mix cards; stats window widened to 12 weeks so switching
  range is a pure client-side re-filter
- Throughput: cards collapse to a single even 5-across row on laptop and up,
  with container-query value text that scales to each card's width
- Throughput: date logic pinned to Australian Eastern time (fixes the day-early
  date); This week subtitle shows the Mon-Sun date range
- Throughput: subtler tinted add-form; removed the inline-entry kicker and the
  "Open full form" link
- Topbar: fix cramped laptop header - action toggles no longer wrap above the
  user button; search drops to its own row earlier

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-13 10:01:10 +12:00
co-authored by Claude Opus 4.8
parent 4ff372d307
commit 2de82776cb
64 changed files with 6034 additions and 1134 deletions
+37 -2
View File
@@ -2,7 +2,7 @@ from __future__ import annotations
from datetime import date
from fastapi import APIRouter, Depends, HTTPException, Query, status
from fastapi import APIRouter, Depends, File, HTTPException, Query, UploadFile, status
from sqlalchemy import select
from sqlalchemy.orm import Session
@@ -13,16 +13,21 @@ from app.schemas.throughput import (
ThroughputEntryCreate,
ThroughputEntryRead,
ThroughputEntryUpdate,
ThroughputImportResult,
ThroughputProductCreate,
ThroughputProductRead,
ThroughputProductUpdate,
)
from app.services.throughput_service import (
calculate_kg,
import_entries_from_file,
normalise_staff_name,
serialize_entry,
)
# Uploaded files larger than this are rejected before we read them into memory.
_MAX_IMPORT_BYTES = 10 * 1024 * 1024 # 10 MB
router = APIRouter(prefix="/api/throughput", tags=["operations-throughput"])
MODULE_KEY = "operations_throughput"
@@ -184,6 +189,33 @@ def create_entry(
return serialize_entry(entry)
@router.post("/import", response_model=ThroughputImportResult)
def import_entries(
file: UploadFile = File(...),
session: AuthSession = Depends(require_client_module_access(MODULE_KEY, "edit")),
db: Session = Depends(get_db),
):
content = file.file.read()
if not content:
raise HTTPException(status_code=400, detail="The uploaded file is empty.")
if len(content) > _MAX_IMPORT_BYTES:
raise HTTPException(status_code=413, detail="File is too large. Keep uploads under 10 MB.")
try:
result = import_entries_from_file(
db,
filename=file.filename or "upload.csv",
content=content,
tenant_id=session.tenant_id,
created_by=session.email,
)
except ValueError as exc:
db.rollback()
raise HTTPException(status_code=400, detail=str(exc)) from exc
return result
@router.get("/entries/{entry_id}", response_model=ThroughputEntryRead)
def get_entry(
entry_id: int,
@@ -233,7 +265,10 @@ def update_entry(
@router.delete("/entries/{entry_id}", status_code=status.HTTP_204_NO_CONTENT)
def delete_entry(
entry_id: int,
session: AuthSession = Depends(require_client_module_access(MODULE_KEY, "manage")),
# Correcting a mistaken run is part of day-to-day operating, so deleting an
# entry sits at the same "edit" level as adding/editing one. (No throughput
# role is granted "manage", so requiring it here would 403 everyone.)
session: AuthSession = Depends(require_client_module_access(MODULE_KEY, "edit")),
db: Session = Depends(get_db),
):
entry = db.scalar(