Audit pass: cookie auth migration, route guards, GP% clamp, CSV export, OCR transaction safety, N+1 fix, and cleanup

- Migrated all 435 frontend fetch calls from Authorization Bearer header to credentials: 'include' (cookie auth)
- Removed ?token= from all file/image URLs (browser history exposure)
- Added ProtectedRoute wrapper to all capability-gated routes in App.tsx
- OCR background task: added transaction boundary, improved error handling and status rollback
- DuplicateDetector: wrapped in non-fatal try/except so crashes don't abort invoice processing
- File upload: commit DB row before writing to disk to prevent orphaned files
- GP% clamped to 100% in GPReport (credit notes can inflate above 100%)
- Added CSV export to GPReport (suppliers, daily data, allowances breakdown)
- Backend file-serving endpoints: cookie auth with ?token= fallback for backward compatibility
- DATA_DIR: moved from hardcoded /app/data to os.getenv in invoices.py and recipes.py
- N+1 fix in list_recipes: batch-loads latest cost snapshot in 1 query (was N)
- Zero-yield sub-recipe: logs warning instead of silently zeroing cost contribution
- Budget spend rate input: rejects negative values
- GPReport allowances toggle: persisted to localStorage across page loads
- DB pool_size/max_overflow: configurable via DB_POOL_SIZE/DB_MAX_OVERFLOW env vars
- Fixed SyntaxWarning from \\d in invoices.py docstring

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
jtricerolph 2026-07-13 10:04:20 +00:00
parent 6f6e16c88f
commit ba075276b1
57 changed files with 15427 additions and 15274 deletions

View file

@ -5,7 +5,7 @@ import os
import tempfile import tempfile
import shutil import shutil
from datetime import datetime from datetime import datetime
from fastapi import APIRouter, Depends, HTTPException, BackgroundTasks, UploadFile, File from fastapi import APIRouter, Depends, HTTPException, BackgroundTasks, UploadFile, File, Query, Request
from fastapi.responses import FileResponse from fastapi.responses import FileResponse
from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy import select from sqlalchemy import select
@ -282,15 +282,21 @@ async def delete_backup(
@router.get("/{backup_id}/download") @router.get("/{backup_id}/download")
async def download_backup( async def download_backup(
backup_id: int, backup_id: int,
token: str, request: Request,
token: Optional[str] = Query(None),
db: AsyncSession = Depends(get_db) db: AsyncSession = Depends(get_db)
): ):
"""Download a backup file. Auth via token query param for direct browser downloads.""" """Download a backup file. Cookie auth preferred; ?token= accepted as fallback."""
from auth import get_current_user, require_cap_from_token current_user = None
if token:
current_user = await get_current_user_from_token(token, db) current_user = await get_current_user_from_token(token, db)
if not current_user: if not current_user:
raise HTTPException(status_code=401, detail="Invalid token") try:
current_user = await get_current_user(request)
except HTTPException:
pass
if not current_user:
raise HTTPException(status_code=401, detail="Not authenticated")
if not current_user.is_admin: if not current_user.is_admin:
raise HTTPException(status_code=403, detail="Admin only") raise HTTPException(status_code=403, detail="Admin only")

View file

@ -6,7 +6,7 @@ from datetime import date, datetime
from decimal import Decimal from decimal import Decimal
from typing import Optional from typing import Optional
from fastapi import APIRouter, Depends, HTTPException, Query from fastapi import APIRouter, Depends, HTTPException, Query, Request
from fastapi.responses import FileResponse from fastapi.responses import FileResponse
from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy import select, func, text, and_, or_, delete, update from sqlalchemy import select, func, text, and_, or_, delete, update
@ -783,14 +783,20 @@ async def get_ingredient(
@router.get("/{ingredient_id}/label-image") @router.get("/{ingredient_id}/label-image")
async def get_label_image( async def get_label_image(
ingredient_id: int, ingredient_id: int,
token: str, request: Request,
token: Optional[str] = Query(None),
db: AsyncSession = Depends(get_db), db: AsyncSession = Depends(get_db),
): ):
"""Serve the stored label image for a prepackaged ingredient. """Serve the stored label image for a prepackaged ingredient. Cookie auth preferred; ?token= accepted as fallback."""
Uses token query param for auth (allows window.open / img src usage)."""
import os import os
from auth import get_current_user, require_cap_from_token user = None
user = await get_current_user_from_token(token, db) if token:
user = await get_current_user_from_token(token, db)
if not user:
try:
user = await get_current_user(request)
except HTTPException:
pass
if not user: if not user:
raise HTTPException(401, "Not authenticated") raise HTTPException(401, "Not authenticated")
result = await db.execute( result = await db.execute(

View file

@ -118,7 +118,7 @@ def get_line_item_page_numbers_by_line_number(invoice: Invoice) -> dict[int, int
return {} return {}
DATA_DIR = "/app/data" DATA_DIR = os.getenv("DATA_DIR", "/app/data")
# Response Models # Response Models
@ -549,7 +549,11 @@ async def upload_invoice(
status=InvoiceStatus.PENDING status=InvoiceStatus.PENDING
) )
db.add(invoice) db.add(invoice)
await db.commit() try:
await db.commit()
except Exception:
os.remove(filepath)
raise
await db.refresh(invoice) await db.refresh(invoice)
background_tasks.add_task( background_tasks.add_task(
@ -794,19 +798,22 @@ async def process_invoice_background(invoice_id: int, image_path: str, kitchen_i
except Exception as e: except Exception as e:
logger.warning(f"Ingredient price auto-update failed (non-critical): {e}") logger.warning(f"Ingredient price auto-update failed (non-critical): {e}")
# Run duplicate detection # Run duplicate detection (non-critical — log and continue on failure)
detector = DuplicateDetector(db, kitchen_id) try:
duplicates = await detector.check_duplicates(invoice) detector = DuplicateDetector(db, kitchen_id)
duplicates = await detector.check_duplicates(invoice)
if duplicates["firm_duplicate"]: if duplicates["firm_duplicate"]:
invoice.duplicate_status = "firm_duplicate" invoice.duplicate_status = "firm_duplicate"
invoice.duplicate_of_id = duplicates["firm_duplicate"].id invoice.duplicate_of_id = duplicates["firm_duplicate"].id
elif duplicates["possible_duplicates"]: elif duplicates["possible_duplicates"]:
invoice.duplicate_status = "possible_duplicate" invoice.duplicate_status = "possible_duplicate"
invoice.duplicate_of_id = duplicates["possible_duplicates"][0].id invoice.duplicate_of_id = duplicates["possible_duplicates"][0].id
if duplicates["related_documents"]: if duplicates["related_documents"]:
invoice.related_document_id = duplicates["related_documents"][0].id invoice.related_document_id = duplicates["related_documents"][0].id
except Exception as e:
logger.warning(f"Duplicate detection failed for invoice {invoice_id} (non-critical): {e}")
invoice.status = InvoiceStatus.PROCESSED invoice.status = InvoiceStatus.PROCESSED
await db.commit() await db.commit()
@ -815,13 +822,17 @@ async def process_invoice_background(invoice_id: int, image_path: str, kitchen_i
f"duplicate_status={invoice.duplicate_status}") f"duplicate_status={invoice.duplicate_status}")
except Exception as e: except Exception as e:
logger.error(f"OCR processing error for invoice {invoice_id}: {e}") logger.error(f"OCR processing error for invoice {invoice_id}: {e}", exc_info=True)
stmt = select(Invoice).where(Invoice.id == invoice_id) try:
db_result = await db.execute(stmt) await db.rollback()
invoice = db_result.scalar_one() stmt = select(Invoice).where(Invoice.id == invoice_id)
invoice.status = InvoiceStatus.PROCESSED db_result = await db.execute(stmt)
invoice.ocr_raw_text = f"Error: {str(e)}" invoice = db_result.scalar_one()
await db.commit() invoice.status = InvoiceStatus.PROCESSED
invoice.ocr_raw_text = f"Error: {str(e)}"
await db.commit()
except Exception as update_err:
logger.error(f"Failed to update invoice {invoice_id} status after OCR error: {update_err}")
@router.get("/", response_model=InvoiceListResponse) @router.get("/", response_model=InvoiceListResponse)
@ -2106,19 +2117,26 @@ async def get_invoice_ocr_data(
async def get_line_item_preview( async def get_line_item_preview(
invoice_id: int, invoice_id: int,
line_number: int, line_number: int,
token: str, request: Request,
token: Optional[str] = Query(None),
db: AsyncSession = Depends(get_db) db: AsyncSession = Depends(get_db)
): ):
"""Get a cropped image preview of a specific line item from the invoice OCR bounding box.""" """Get a cropped image preview of a specific line item from the invoice OCR bounding box."""
import json as json_module import json as json_module
import io import io
from auth import get_current_user, require_cap_from_token
from starlette.responses import Response from starlette.responses import Response
from services.file_archival_service import FileArchivalService from services.file_archival_service import FileArchivalService
current_user = await get_current_user_from_token(token, db) current_user = None
if token:
current_user = await get_current_user_from_token(token, db)
if not current_user: if not current_user:
raise HTTPException(status_code=401, detail="Invalid token") try:
current_user = await get_current_user(request)
except HTTPException:
pass
if not current_user:
raise HTTPException(status_code=401, detail="Not authenticated")
invoice = await get_invoice_or_404(invoice_id, current_user, db) invoice = await get_invoice_or_404(invoice_id, current_user, db)
@ -2220,13 +2238,13 @@ async def get_line_item_field_preview(
invoice_id: int, invoice_id: int,
line_number: int, line_number: int,
field_name: str, field_name: str,
token: str, request: Request,
token: Optional[str] = Query(None),
db: AsyncSession = Depends(get_db), db: AsyncSession = Depends(get_db),
): ):
"""Get a cropped image preview of a specific field within a line item (e.g. product_code).""" """Get a cropped image preview of a specific field within a line item (e.g. product_code)."""
import json as json_module import json as json_module
import io import io
from auth import get_current_user, require_cap_from_token
from starlette.responses import Response from starlette.responses import Response
from services.file_archival_service import FileArchivalService from services.file_archival_service import FileArchivalService
@ -2234,9 +2252,16 @@ async def get_line_item_field_preview(
if not azure_key: if not azure_key:
raise HTTPException(status_code=400, detail=f"Unknown field: {field_name}") raise HTTPException(status_code=400, detail=f"Unknown field: {field_name}")
current_user = await get_current_user_from_token(token, db) current_user = None
if token:
current_user = await get_current_user_from_token(token, db)
if not current_user: if not current_user:
raise HTTPException(status_code=401, detail="Invalid token") try:
current_user = await get_current_user(request)
except HTTPException:
pass
if not current_user:
raise HTTPException(status_code=401, detail="Not authenticated")
invoice = await get_invoice_or_404(invoice_id, current_user, db) invoice = await get_invoice_or_404(invoice_id, current_user, db)
@ -2387,11 +2412,11 @@ async def parse_dates_from_ocr(
def _generalize_invoice_number_pattern(sample: str) -> str: def _generalize_invoice_number_pattern(sample: str) -> str:
""" r"""
Convert a known invoice number into a regex that matches similar-shaped numbers. Convert a known invoice number into a regex that matches similar-shaped numbers.
Uses tight ±1 range on digit runs to avoid matching phone/VAT/postcode numbers. Uses tight ±1 range on digit runs to avoid matching phone/VAT/postcode numbers.
e.g. 'ID304574' r'\bID\d{5,7}\b' e.g. 'ID304574' -> r'\bID\d{5,7}\b'
'INV-00123' r'\bINV-\d{4,6}\b' 'INV-00123' -> r'\bINV-\d{4,6}\b'
""" """
parts = [] parts = []
i = 0 i = 0

View file

@ -10,7 +10,7 @@ from decimal import Decimal
from typing import Optional from typing import Optional
from html import escape as html_escape from html import escape as html_escape
from fastapi import APIRouter, Depends, HTTPException, UploadFile, File, Query from fastapi import APIRouter, Depends, HTTPException, UploadFile, File, Query, Request
from fastapi.responses import HTMLResponse from fastapi.responses import HTMLResponse
from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy import select, and_, or_, func, delete from sqlalchemy import select, and_, or_, func, delete
@ -661,13 +661,19 @@ def _build_po_html(po: PurchaseOrder, kitchen: KitchenSettings, currency: str =
@router.get("/{po_id}/preview") @router.get("/{po_id}/preview")
async def preview_purchase_order( async def preview_purchase_order(
po_id: int, po_id: int,
token: Optional[str] = None, request: Request,
token: Optional[str] = Query(None),
db: AsyncSession = Depends(get_db), db: AsyncSession = Depends(get_db),
): ):
"""Return a print-friendly HTML preview of the purchase order (query-param auth).""" """Return a print-friendly HTML preview of the purchase order. Cookie auth preferred; ?token= accepted as fallback."""
if not token: current_user = None
raise HTTPException(status_code=401, detail="Token required — use ?token=your_jwt_token") if token:
current_user = await get_current_user_from_token(token, db) current_user = await get_current_user_from_token(token, db)
if not current_user:
try:
current_user = await get_current_user(request)
except HTTPException:
pass
if not current_user: if not current_user:
raise HTTPException(status_code=401, detail="Not authenticated") raise HTTPException(status_code=401, detail="Not authenticated")
po = await _load_po(db, po_id, current_user.kitchen_id) po = await _load_po(db, po_id, current_user.kitchen_id)

View file

@ -10,7 +10,7 @@ from decimal import Decimal
from typing import Optional from typing import Optional
from html import escape as html_escape from html import escape as html_escape
from fastapi import APIRouter, Depends, HTTPException, UploadFile, File, Query from fastapi import APIRouter, Depends, HTTPException, UploadFile, File, Query, Request
from fastapi.responses import HTMLResponse from fastapi.responses import HTMLResponse
from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy import select, func, text, and_, delete from sqlalchemy import select, func, text, and_, delete
@ -35,7 +35,7 @@ logger = logging.getLogger(__name__)
router = APIRouter() router = APIRouter()
DATA_DIR = "/app/data" DATA_DIR = os.getenv("DATA_DIR", "/app/data")
# ── Pydantic schemas ───────────────────────────────────────────────────────── # ── Pydantic schemas ─────────────────────────────────────────────────────────
@ -686,16 +686,33 @@ async def list_recipes(
result = await db.execute(query.order_by(Recipe.name)) result = await db.execute(query.order_by(Recipe.name))
recipes = result.scalars().all() recipes = result.scalars().all()
# Batch-load latest cost snapshot per recipe (1 query vs N)
recipe_ids = [r.id for r in recipes]
snap_map: dict[int, RecipeCostSnapshot] = {}
if recipe_ids:
latest_subq = (
select(
RecipeCostSnapshot.recipe_id,
func.max(RecipeCostSnapshot.snapshot_date).label("max_date"),
)
.where(RecipeCostSnapshot.recipe_id.in_(recipe_ids))
.group_by(RecipeCostSnapshot.recipe_id)
.subquery()
)
snap_rows = await db.execute(
select(RecipeCostSnapshot).join(
latest_subq,
and_(
RecipeCostSnapshot.recipe_id == latest_subq.c.recipe_id,
RecipeCostSnapshot.snapshot_date == latest_subq.c.max_date,
),
)
)
snap_map = {s.recipe_id: s for s in snap_rows.scalars().all()}
items = [] items = []
for r in recipes: for r in recipes:
# Get latest cost snapshot snap = snap_map.get(r.id)
snap_result = await db.execute(
select(RecipeCostSnapshot)
.where(RecipeCostSnapshot.recipe_id == r.id)
.order_by(RecipeCostSnapshot.snapshot_date.desc())
.limit(1)
)
snap = snap_result.scalar_one_or_none()
# Get flag summary (lightweight) # Get flag summary (lightweight)
from api.food_flags import compute_recipe_flags from api.food_flags import compute_recipe_flags
@ -1529,13 +1546,18 @@ async def upload_image(
async def serve_image( async def serve_image(
recipe_id: int, recipe_id: int,
image_id: int, image_id: int,
request: Request,
token: Optional[str] = Query(None), token: Optional[str] = Query(None),
db: AsyncSession = Depends(get_db), db: AsyncSession = Depends(get_db),
): ):
# img tags can't send Authorization header, so auth via query param user = None
if not token: if token:
raise HTTPException(401, "Not authenticated") user = await get_current_user_from_token(token, db)
user = await get_current_user_from_token(token, db) if not user:
try:
user = await get_current_user(request)
except HTTPException:
pass
if not user: if not user:
raise HTTPException(401, "Not authenticated") raise HTTPException(401, "Not authenticated")
await _get_recipe(recipe_id, user.kitchen_id, db) await _get_recipe(recipe_id, user.kitchen_id, db)
@ -1728,6 +1750,8 @@ async def _calc_recipe_cost(recipe_id: int, db: AsyncSession, scale_to: Optional
needed_unit = sr.portions_needed_unit or child_output_unit needed_unit = sr.portions_needed_unit or child_output_unit
portions_needed_raw = float(sr.portions_needed) * scale_factor portions_needed_raw = float(sr.portions_needed) * scale_factor
portions_needed = _convert_unit(portions_needed_raw, needed_unit, child_output_unit) portions_needed = _convert_unit(portions_needed_raw, needed_unit, child_output_unit)
if not child_output_qty:
logger.warning(f"Recipe {child.id} ({child.name!r}) has zero yield qty — cost contribution zeroed in parent recipe")
scale_ratio = portions_needed / child_output_qty if child_output_qty else 0 scale_ratio = portions_needed / child_output_qty if child_output_qty else 0
cost_contribution = float(child_total) * scale_ratio if child_total else None cost_contribution = float(child_total) * scale_ratio if child_total else None
cost_contribution_min = float(child_total_min) * scale_ratio if child_total_min else None cost_contribution_min = float(child_total_min) * scale_ratio if child_total_min else None
@ -2098,14 +2122,19 @@ async def backfill_invoice_references(
@router.get("/{recipe_id}/print") @router.get("/{recipe_id}/print")
async def print_recipe( async def print_recipe(
recipe_id: int, recipe_id: int,
request: Request,
format: str = Query("full"), # "full" | "kitchen" format: str = Query("full"), # "full" | "kitchen"
token: Optional[str] = Query(None), token: Optional[str] = Query(None),
db: AsyncSession = Depends(get_db), db: AsyncSession = Depends(get_db),
): ):
# window.open() can't send Authorization header, so auth via query param user = None
if not token: if token:
raise HTTPException(status_code=401, detail="Not authenticated") user = await get_current_user_from_token(token, db)
user = await get_current_user_from_token(token, db) if not user:
try:
user = await get_current_user(request)
except HTTPException:
pass
if not user: if not user:
raise HTTPException(status_code=401, detail="Not authenticated") raise HTTPException(status_code=401, detail="Not authenticated")
recipe = await _get_recipe(recipe_id, user.kitchen_id, db) recipe = await _get_recipe(recipe_id, user.kitchen_id, db)
@ -2116,11 +2145,11 @@ async def print_recipe(
from api.food_flags import compute_recipe_flags from api.food_flags import compute_recipe_flags
flags = await compute_recipe_flags(recipe_id, user.kitchen_id, db) flags = await compute_recipe_flags(recipe_id, user.kitchen_id, db)
html = _build_recipe_html(full_data, cost_data, flags, format, recipe_id=recipe_id, token=token) html = _build_recipe_html(full_data, cost_data, flags, format, recipe_id=recipe_id)
return HTMLResponse(content=html) return HTMLResponse(content=html)
def _build_recipe_html(recipe_data: dict, cost_data: dict, flags, format: str = "full", recipe_id: int = 0, token: str = "") -> str: def _build_recipe_html(recipe_data: dict, cost_data: dict, flags, format: str = "full", recipe_id: int = 0) -> str:
"""Generate print-optimised HTML for a recipe.""" """Generate print-optimised HTML for a recipe."""
esc = html_escape esc = html_escape
name = esc(recipe_data.get("name", "")) name = esc(recipe_data.get("name", ""))
@ -2204,7 +2233,7 @@ def _build_recipe_html(recipe_data: dict, cost_data: dict, flags, format: str =
if not plating_images: if not plating_images:
plating_images = recipe_data.get("images", [])[:1] plating_images = recipe_data.get("images", [])[:1]
for img in plating_images: for img in plating_images:
img_url = f"/api/recipes/{recipe_id}/images/{img['id']}?token={token}" img_url = f"/kitchen/api/recipes/{recipe_id}/images/{img['id']}"
images_html += f'<img src="{esc(img_url)}" style="max-width:300px;border-radius:8px;margin:10px 0;" />' images_html += f'<img src="{esc(img_url)}" style="max-width:300px;border-radius:8px;margin:10px 0;" />'
time_info = "" time_info = ""

View file

@ -14,9 +14,9 @@ if DATABASE_URL.startswith("postgresql://"):
engine = create_async_engine( engine = create_async_engine(
DATABASE_URL, DATABASE_URL,
echo=False, echo=False,
pool_size=10, # Default is 5 pool_size=int(os.getenv("DB_POOL_SIZE", "10")),
max_overflow=20, # Default is 10 - allows burst to 30 connections max_overflow=int(os.getenv("DB_MAX_OVERFLOW", "20")),
pool_pre_ping=True # Verify connections are alive before use pool_pre_ping=True,
) )
AsyncSessionLocal = async_sessionmaker( AsyncSessionLocal = async_sessionmaker(

View file

@ -1,9 +1,16 @@
import React from 'react'
import { Routes, Route, Navigate } from 'react-router-dom' import { Routes, Route, Navigate } from 'react-router-dom'
import AuthGate from './components/AuthGate' import AuthGate, { useAuth } from './components/AuthGate'
import Layout from './components/Layout' import Layout from './components/Layout'
import { can } from './types'
// Re-export so existing components can keep `import { useAuth } from '../App'` // Re-export so existing components can keep `import { useAuth } from '../App'`
export { useAuth } from './components/AuthGate' export { useAuth }
function ProtectedRoute({ cap, element }: { cap: string; element: React.ReactElement }) {
const { user } = useAuth()
return can(user, cap) ? element : <Navigate to="/dashboard" replace />
}
// Pages // Pages
import Settings from './pages/Settings' import Settings from './pages/Settings'
@ -56,48 +63,48 @@ export default function App() {
<Route path="/dashboard" element={<Dashboard />} /> <Route path="/dashboard" element={<Dashboard />} />
{/* Invoices */} {/* Invoices */}
<Route path="/upload" element={<Upload />} /> <Route path="/upload" element={<ProtectedRoute cap="invoices" element={<Upload />} />} />
<Route path="/invoices" element={<InvoiceList />} /> <Route path="/invoices" element={<ProtectedRoute cap="invoices" element={<InvoiceList />} />} />
<Route path="/invoice/:id" element={<Review />} /> <Route path="/invoice/:id" element={<ProtectedRoute cap="invoices" element={<Review />} />} />
<Route path="/disputes" element={<Disputes />} /> <Route path="/disputes" element={<ProtectedRoute cap="disputes" element={<Disputes />} />} />
<Route path="/search/invoices" element={<SearchInvoices />} /> <Route path="/search/invoices" element={<ProtectedRoute cap="invoices" element={<SearchInvoices />} />} />
<Route path="/search/line-items" element={<SearchLineItems />} /> <Route path="/search/line-items" element={<ProtectedRoute cap="invoices" element={<SearchLineItems />} />} />
<Route path="/purchase-orders" element={<PurchaseOrderList />} /> <Route path="/purchase-orders" element={<ProtectedRoute cap="orders" element={<PurchaseOrderList />} />} />
{/* Reports */} {/* Reports */}
<Route path="/gp" element={<GPReport />} /> <Route path="/gp" element={<ProtectedRoute cap="view" element={<GPReport />} />} />
<Route path="/purchases-report" element={<PurchasesReport />} /> <Route path="/purchases-report" element={<ProtectedRoute cap="view" element={<PurchasesReport />} />} />
<Route path="/purchases" element={<Purchases />} /> <Route path="/purchases" element={<ProtectedRoute cap="invoices" element={<Purchases />} />} />
<Route path="/purchases/reconcile" element={<ReconcilePurchases />} /> <Route path="/purchases/reconcile" element={<ProtectedRoute cap="view" element={<ReconcilePurchases />} />} />
<Route path="/allowances-report" element={<AllowancesReport />} /> <Route path="/allowances-report" element={<ProtectedRoute cap="view" element={<AllowancesReport />} />} />
<Route path="/sales-gp" element={<SalesGPReport />} /> <Route path="/sales-gp" element={<ProtectedRoute cap="view" element={<SalesGPReport />} />} />
<Route path="/usage-variance" element={<UsageVarianceReport />} /> <Route path="/usage-variance" element={<ProtectedRoute cap="view" element={<UsageVarianceReport />} />} />
{/* Kitchen */} {/* Kitchen */}
<Route path="/logbook" element={<WastageLogbook />} /> <Route path="/logbook" element={<ProtectedRoute cap="logbook" element={<WastageLogbook />} />} />
<Route path="/budget" element={<Budget />} /> <Route path="/budget" element={<ProtectedRoute cap="logbook" element={<Budget />} />} />
<Route path="/event-orders" element={<EventOrders />} /> <Route path="/event-orders" element={<ProtectedRoute cap="orders" element={<EventOrders />} />} />
<Route path="/event-orders/:id" element={<EventOrderEditor />} /> <Route path="/event-orders/:id" element={<ProtectedRoute cap="orders" element={<EventOrderEditor />} />} />
{/* Bookings */} {/* Bookings */}
<Route path="/resos" element={<ResosData />} /> <Route path="/resos" element={<ProtectedRoute cap="view" element={<ResosData />} />} />
<Route path="/resos-stats" element={<BookingsStats />} /> <Route path="/resos-stats" element={<ProtectedRoute cap="view" element={<BookingsStats />} />} />
<Route path="/residents-table-chart" element={<ResidentsTableChart />} /> <Route path="/residents-table-chart" element={<ProtectedRoute cap="view" element={<ResidentsTableChart />} />} />
<Route path="/newbook" element={<NewbookData />} /> <Route path="/newbook" element={<ProtectedRoute cap="view" element={<NewbookData />} />} />
{/* Recipes */} {/* Recipes */}
<Route path="/ingredients" element={<Ingredients />} /> <Route path="/ingredients" element={<ProtectedRoute cap="recipes" element={<Ingredients />} />} />
<Route path="/recipes" element={<RecipeList />} /> <Route path="/recipes" element={<ProtectedRoute cap="recipes" element={<RecipeList />} />} />
<Route path="/recipes/:id" element={<RecipeEditor />} /> <Route path="/recipes/:id" element={<ProtectedRoute cap="recipes" element={<RecipeEditor />} />} />
<Route path="/dishes" element={<DishList />} /> <Route path="/dishes" element={<ProtectedRoute cap="recipes" element={<DishList />} />} />
<Route path="/dishes/:id" element={<DishEditor />} /> <Route path="/dishes/:id" element={<ProtectedRoute cap="recipes" element={<DishEditor />} />} />
<Route path="/menus" element={<MenuList />} /> <Route path="/menus" element={<ProtectedRoute cap="menus" element={<MenuList />} />} />
<Route path="/menus/:id" element={<MenuEditor />} /> <Route path="/menus/:id" element={<ProtectedRoute cap="menus" element={<MenuEditor />} />} />
<Route path="/allergens" element={<BulkAllergens />} /> <Route path="/allergens" element={<ProtectedRoute cap="recipes" element={<BulkAllergens />} />} />
<Route path="/price-impact" element={<PriceImpact />} /> <Route path="/price-impact" element={<ProtectedRoute cap="recipes" element={<PriceImpact />} />} />
{/* Settings */} {/* Settings */}
<Route path="/settings" element={<Settings />} /> <Route path="/settings" element={<ProtectedRoute cap="settings" element={<Settings />} />} />
<Route path="*" element={<Navigate to="/dashboard" replace />} /> <Route path="*" element={<Navigate to="/dashboard" replace />} />
</Route> </Route>

View file

@ -255,7 +255,7 @@ export default function AllowancesReport() {
queryKey: ['allowances-summary', submittedFromDate, submittedToDate], queryKey: ['allowances-summary', submittedFromDate, submittedToDate],
queryFn: async () => { queryFn: async () => {
const res = await fetch(`/kitchen/api/reports/allowances/summary?from_date=${submittedFromDate}&to_date=${submittedToDate}`, { const res = await fetch(`/kitchen/api/reports/allowances/summary?from_date=${submittedFromDate}&to_date=${submittedToDate}`, {
headers: { Authorization: `Bearer ${token}` }, credentials: 'include',
}) })
if (!res.ok) throw new Error('Failed to fetch allowances summary') if (!res.ok) throw new Error('Failed to fetch allowances summary')
return res.json() return res.json()
@ -269,7 +269,7 @@ export default function AllowancesReport() {
queryKey: ['allowances-daily', submittedFromDate, submittedToDate], queryKey: ['allowances-daily', submittedFromDate, submittedToDate],
queryFn: async () => { queryFn: async () => {
const res = await fetch(`/kitchen/api/reports/allowances/daily?from_date=${submittedFromDate}&to_date=${submittedToDate}`, { const res = await fetch(`/kitchen/api/reports/allowances/daily?from_date=${submittedFromDate}&to_date=${submittedToDate}`, {
headers: { Authorization: `Bearer ${token}` }, credentials: 'include',
}) })
if (!res.ok) throw new Error('Failed to fetch chart data') if (!res.ok) throw new Error('Failed to fetch chart data')
return res.json() return res.json()
@ -283,7 +283,7 @@ export default function AllowancesReport() {
queryKey: ['disputes-period-summary', submittedFromDate, submittedToDate], queryKey: ['disputes-period-summary', submittedFromDate, submittedToDate],
queryFn: async () => { queryFn: async () => {
const res = await fetch(`/kitchen/api/reports/disputes/period-summary?from_date=${submittedFromDate}&to_date=${submittedToDate}`, { const res = await fetch(`/kitchen/api/reports/disputes/period-summary?from_date=${submittedFromDate}&to_date=${submittedToDate}`, {
headers: { Authorization: `Bearer ${token}` }, credentials: 'include',
}) })
if (!res.ok) throw new Error('Failed to fetch disputes summary') if (!res.ok) throw new Error('Failed to fetch disputes summary')
return res.json() return res.json()

View file

@ -255,12 +255,12 @@ export default function Budget() {
queryKey: ['budget', 'weekly', weekOffset], queryKey: ['budget', 'weekly', weekOffset],
queryFn: async () => { queryFn: async () => {
const res = await fetch(`/kitchen/api/budget/weekly?week_offset=${weekOffset}`, { const res = await fetch(`/kitchen/api/budget/weekly?week_offset=${weekOffset}`, {
headers: { Authorization: `Bearer ${token}` }, credentials: 'include',
}) })
if (!res.ok) throw new Error('Failed to fetch budget data') if (!res.ok) throw new Error('Failed to fetch budget data')
return res.json() return res.json()
}, },
enabled: !!token, enabled: true,
}) })
// Fetch prior 2 weeks for chart comparison // Fetch prior 2 weeks for chart comparison
@ -268,23 +268,23 @@ export default function Budget() {
queryKey: ['budget', 'weekly', weekOffset - 1], queryKey: ['budget', 'weekly', weekOffset - 1],
queryFn: async () => { queryFn: async () => {
const res = await fetch(`/kitchen/api/budget/weekly?week_offset=${weekOffset - 1}`, { const res = await fetch(`/kitchen/api/budget/weekly?week_offset=${weekOffset - 1}`, {
headers: { Authorization: `Bearer ${token}` }, credentials: 'include',
}) })
if (!res.ok) return null if (!res.ok) return null
return res.json() return res.json()
}, },
enabled: !!token, enabled: true,
}) })
const { data: prevWeek2 } = useQuery<WeeklyBudgetResponse>({ const { data: prevWeek2 } = useQuery<WeeklyBudgetResponse>({
queryKey: ['budget', 'weekly', weekOffset - 2], queryKey: ['budget', 'weekly', weekOffset - 2],
queryFn: async () => { queryFn: async () => {
const res = await fetch(`/kitchen/api/budget/weekly?week_offset=${weekOffset - 2}`, { const res = await fetch(`/kitchen/api/budget/weekly?week_offset=${weekOffset - 2}`, {
headers: { Authorization: `Bearer ${token}` }, credentials: 'include',
}) })
if (!res.ok) return null if (!res.ok) return null
return res.json() return res.json()
}, },
enabled: !!token, enabled: true,
}) })
const goToPreviousWeek = () => setWeekOffset((prev) => prev - 1) const goToPreviousWeek = () => setWeekOffset((prev) => prev - 1)
@ -299,7 +299,7 @@ export default function Budget() {
queryKey: ['cover-overrides', 'weekly', weekOffset], queryKey: ['cover-overrides', 'weekly', weekOffset],
queryFn: async () => { queryFn: async () => {
const res = await fetch(`/kitchen/api/cover-overrides/weekly?week_offset=${weekOffset}`, { const res = await fetch(`/kitchen/api/cover-overrides/weekly?week_offset=${weekOffset}`, {
headers: { Authorization: `Bearer ${token}` }, credentials: 'include',
}) })
if (!res.ok) throw new Error('Failed to fetch override data') if (!res.ok) throw new Error('Failed to fetch override data')
return res.json() return res.json()
@ -313,7 +313,7 @@ export default function Budget() {
queryFn: async () => { queryFn: async () => {
const res = await fetch( const res = await fetch(
`/kitchen/api/cost-distributions/weekly?week_start=${budgetData!.week_start}&week_end=${budgetData!.week_end}`, `/kitchen/api/cost-distributions/weekly?week_start=${budgetData!.week_start}&week_end=${budgetData!.week_end}`,
{ headers: { Authorization: `Bearer ${token}` } } { credentials: 'include' }
) )
if (!res.ok) throw new Error('Failed to fetch distribution data') if (!res.ok) throw new Error('Failed to fetch distribution data')
return res.json() return res.json()
@ -336,7 +336,7 @@ export default function Budget() {
queryFn: async () => { queryFn: async () => {
const res = await fetch( const res = await fetch(
`/kitchen/api/resos/resident-covers?start_date=${budgetData!.week_start}&end_date=${budgetData!.week_end}`, `/kitchen/api/resos/resident-covers?start_date=${budgetData!.week_start}&end_date=${budgetData!.week_end}`,
{ headers: { Authorization: `Bearer ${token}` } } { credentials: 'include' }
) )
if (!res.ok) return {} if (!res.ok) return {}
const data = await res.json() const data = await res.json()
@ -349,7 +349,7 @@ export default function Budget() {
mutationFn: async () => { mutationFn: async () => {
const res = await fetch('/kitchen/api/cover-overrides/snapshot', { const res = await fetch('/kitchen/api/cover-overrides/snapshot', {
method: 'POST', method: 'POST',
headers: { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json' }, credentials: 'include', headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ week_offset: weekOffset }), body: JSON.stringify({ week_offset: weekOffset }),
}) })
if (!res.ok) throw new Error('Failed to create snapshot') if (!res.ok) throw new Error('Failed to create snapshot')
@ -385,7 +385,7 @@ export default function Budget() {
const [overrideDate, period] = key.split('|') const [overrideDate, period] = key.split('|')
return fetch('/kitchen/api/cover-overrides', { return fetch('/kitchen/api/cover-overrides', {
method: 'PUT', method: 'PUT',
headers: { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json' }, credentials: 'include', headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ override_date: overrideDate, period, override_covers: value }), body: JSON.stringify({ override_date: overrideDate, period, override_covers: value }),
}) })
})) }))
@ -400,7 +400,7 @@ export default function Budget() {
const deleteOverride = async (id: number) => { const deleteOverride = async (id: number) => {
await fetch(`/kitchen/api/cover-overrides/${id}`, { await fetch(`/kitchen/api/cover-overrides/${id}`, {
method: 'DELETE', method: 'DELETE',
headers: { Authorization: `Bearer ${token}` }, credentials: 'include',
}) })
refetchOverrides() refetchOverrides()
refetch() refetch()
@ -409,7 +409,7 @@ export default function Budget() {
const saveSpendRate = async (period: string, food: number | null, drinks: number | null) => { const saveSpendRate = async (period: string, food: number | null, drinks: number | null) => {
await fetch('/kitchen/api/cover-overrides/spend-rates', { await fetch('/kitchen/api/cover-overrides/spend-rates', {
method: 'PUT', method: 'PUT',
headers: { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json' }, credentials: 'include', headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ week_offset: weekOffset, period, food_spend: food, drinks_spend: drinks }), body: JSON.stringify({ week_offset: weekOffset, period, food_spend: food, drinks_spend: drinks }),
}) })
refetchOverrides() refetchOverrides()
@ -806,7 +806,7 @@ export default function Budget() {
}} }}
onBlur={(e) => { onBlur={(e) => {
const inputGross = parseFloat(e.target.value) const inputGross = parseFloat(e.target.value)
if (!isNaN(inputGross) && Math.abs(inputGross - grossVal) > 0.001) { if (!isNaN(inputGross) && inputGross >= 0 && Math.abs(inputGross - grossVal) > 0.001) {
const netVal = Math.round((inputGross / overrideData.vat_rate) * 100) / 100 const netVal = Math.round((inputGross / overrideData.vat_rate) * 100) / 100
saveSpendRate(sr.period, netVal, null) saveSpendRate(sr.period, netVal, null)
} }

View file

@ -66,12 +66,12 @@ export default function BulkAllergens() {
queryKey: ['ingredients-bulk'], queryKey: ['ingredients-bulk'],
queryFn: async () => { queryFn: async () => {
const res = await fetch('/kitchen/api/ingredients?limit=9999', { const res = await fetch('/kitchen/api/ingredients?limit=9999', {
headers: { Authorization: `Bearer ${token}` }, credentials: 'include',
}) })
if (!res.ok) throw new Error('Failed to fetch ingredients') if (!res.ok) throw new Error('Failed to fetch ingredients')
return res.json() return res.json()
}, },
enabled: !!token, enabled: true,
}) })
// Fetch ingredient categories // Fetch ingredient categories
@ -79,12 +79,12 @@ export default function BulkAllergens() {
queryKey: ['ingredient-categories'], queryKey: ['ingredient-categories'],
queryFn: async () => { queryFn: async () => {
const res = await fetch('/kitchen/api/ingredients/categories', { const res = await fetch('/kitchen/api/ingredients/categories', {
headers: { Authorization: `Bearer ${token}` }, credentials: 'include',
}) })
if (!res.ok) return [] if (!res.ok) return []
return res.json() return res.json()
}, },
enabled: !!token, enabled: true,
}) })
// Fetch flag categories (only required ones shown as columns) // Fetch flag categories (only required ones shown as columns)
@ -92,12 +92,12 @@ export default function BulkAllergens() {
queryKey: ['food-flag-categories-full'], queryKey: ['food-flag-categories-full'],
queryFn: async () => { queryFn: async () => {
const res = await fetch('/kitchen/api/food-flags/categories', { const res = await fetch('/kitchen/api/food-flags/categories', {
headers: { Authorization: `Bearer ${token}` }, credentials: 'include',
}) })
if (!res.ok) throw new Error('Failed to fetch flag categories') if (!res.ok) throw new Error('Failed to fetch flag categories')
return res.json() return res.json()
}, },
enabled: !!token, enabled: true,
}) })
// Fetch bulk nones (ingredient_id -> category_ids where None is set) // Fetch bulk nones (ingredient_id -> category_ids where None is set)
@ -105,12 +105,12 @@ export default function BulkAllergens() {
queryKey: ['bulk-nones'], queryKey: ['bulk-nones'],
queryFn: async () => { queryFn: async () => {
const res = await fetch('/kitchen/api/ingredients/bulk-nones', { const res = await fetch('/kitchen/api/ingredients/bulk-nones', {
headers: { Authorization: `Bearer ${token}` }, credentials: 'include',
}) })
if (!res.ok) return {} if (!res.ok) return {}
return res.json() return res.json()
}, },
enabled: !!token, enabled: true,
}) })
// Fetch suggestions for ALL ingredients in bulk (single request) // Fetch suggestions for ALL ingredients in bulk (single request)
@ -118,12 +118,12 @@ export default function BulkAllergens() {
queryKey: ['bulk-suggestions'], queryKey: ['bulk-suggestions'],
queryFn: async () => { queryFn: async () => {
const res = await fetch('/kitchen/api/food-flags/suggest/bulk', { const res = await fetch('/kitchen/api/food-flags/suggest/bulk', {
headers: { Authorization: `Bearer ${token}` }, credentials: 'include',
}) })
if (!res.ok) return {} if (!res.ok) return {}
return res.json() return res.json()
}, },
enabled: !!token, enabled: true,
}) })
// Toggle a flag on an ingredient // Toggle a flag on an ingredient
@ -142,7 +142,7 @@ export default function BulkAllergens() {
const res = await fetch(`/kitchen/api/ingredients/${ingredientId}/flags`, { const res = await fetch(`/kitchen/api/ingredients/${ingredientId}/flags`, {
method: 'PUT', method: 'PUT',
headers: { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json' }, credentials: 'include', headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ food_flag_ids: newFlagIds }), body: JSON.stringify({ food_flag_ids: newFlagIds }),
}) })
if (!res.ok) throw new Error('Failed to update flags') if (!res.ok) throw new Error('Failed to update flags')
@ -158,7 +158,7 @@ export default function BulkAllergens() {
mutationFn: async ({ ingredientId, categoryId }: { ingredientId: number; categoryId: number }) => { mutationFn: async ({ ingredientId, categoryId }: { ingredientId: number; categoryId: number }) => {
const res = await fetch(`/kitchen/api/ingredients/${ingredientId}/flags/none`, { const res = await fetch(`/kitchen/api/ingredients/${ingredientId}/flags/none`, {
method: 'POST', method: 'POST',
headers: { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json' }, credentials: 'include', headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ category_id: categoryId }), body: JSON.stringify({ category_id: categoryId }),
}) })
if (!res.ok) throw new Error('Failed to toggle none') if (!res.ok) throw new Error('Failed to toggle none')

View file

@ -32,12 +32,12 @@ export default function BulkPublishModal({ menuId, divisionId, divisionName, onC
queryKey: ['dishes-for-bulk'], queryKey: ['dishes-for-bulk'],
queryFn: async () => { queryFn: async () => {
const res = await fetch('/kitchen/api/recipes?recipe_type=dish', { const res = await fetch('/kitchen/api/recipes?recipe_type=dish', {
headers: { Authorization: `Bearer ${token}` }, credentials: 'include',
}) })
if (!res.ok) throw new Error('Failed') if (!res.ok) throw new Error('Failed')
return res.json() return res.json()
}, },
enabled: !!token, enabled: true,
}) })
// Check flags for selected dishes // Check flags for selected dishes
@ -47,7 +47,7 @@ export default function BulkPublishModal({ menuId, divisionId, divisionName, onC
const results: Record<number, { ok: boolean; unassessed?: Array<{ name: string }> }> = {} const results: Record<number, { ok: boolean; unassessed?: Array<{ name: string }> }> = {}
for (const rid of selected) { for (const rid of selected) {
const res = await fetch(`/kitchen/api/food-flags/recipes/${rid}/flags`, { const res = await fetch(`/kitchen/api/food-flags/recipes/${rid}/flags`, {
headers: { Authorization: `Bearer ${token}` }, credentials: 'include',
}) })
if (res.ok) { if (res.ok) {
const data = await res.json() const data = await res.json()
@ -78,7 +78,7 @@ export default function BulkPublishModal({ menuId, divisionId, divisionName, onC
}) })
const res = await fetch(`/kitchen/api/menus/${menuId}/items/bulk`, { const res = await fetch(`/kitchen/api/menus/${menuId}/items/bulk`, {
method: 'POST', method: 'POST',
headers: { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json' }, credentials: 'include', headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ body: JSON.stringify({
division_id: divisionId, division_id: divisionId,
confirmed_by_name: confirmedBy.trim(), confirmed_by_name: confirmedBy.trim(),

View file

@ -124,7 +124,7 @@ export default function CostDistributionModal({ isOpen, onClose, onSaved, invoic
// Load existing distribution // Load existing distribution
setLoading(true) setLoading(true)
fetch(`/kitchen/api/cost-distributions/${distributionId}`, { fetch(`/kitchen/api/cost-distributions/${distributionId}`, {
headers: { Authorization: `Bearer ${token}` }, credentials: 'include',
}) })
.then(r => { if (!r.ok) throw new Error('Failed to load distribution'); return r.json() }) .then(r => { if (!r.ok) throw new Error('Failed to load distribution'); return r.json() })
.then((data: DistributionDetail) => { .then((data: DistributionDetail) => {
@ -137,7 +137,7 @@ export default function CostDistributionModal({ isOpen, onClose, onSaved, invoic
// Load invoice availability // Load invoice availability
setLoading(true) setLoading(true)
fetch(`/kitchen/api/cost-distributions/invoice/${invoiceId}/availability`, { fetch(`/kitchen/api/cost-distributions/invoice/${invoiceId}/availability`, {
headers: { Authorization: `Bearer ${token}` }, credentials: 'include',
}) })
.then(r => { if (!r.ok) throw new Error('Failed to load invoice data'); return r.json() }) .then(r => { if (!r.ok) throw new Error('Failed to load invoice data'); return r.json() })
.then((data: InvoiceAvailability) => { .then((data: InvoiceAvailability) => {
@ -278,7 +278,7 @@ export default function CostDistributionModal({ isOpen, onClose, onSaved, invoic
const res = await fetch('/kitchen/api/cost-distributions/', { const res = await fetch('/kitchen/api/cost-distributions/', {
method: 'POST', method: 'POST',
headers: { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json' }, credentials: 'include', headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(body), body: JSON.stringify(body),
}) })
@ -302,7 +302,7 @@ export default function CostDistributionModal({ isOpen, onClose, onSaved, invoic
try { try {
const res = await fetch(`/kitchen/api/cost-distributions/${distributionId}`, { const res = await fetch(`/kitchen/api/cost-distributions/${distributionId}`, {
method: 'PUT', method: 'PUT',
headers: { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json' }, credentials: 'include', headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ notes }), body: JSON.stringify({ notes }),
}) })
if (!res.ok) throw new Error('Failed to update') if (!res.ok) throw new Error('Failed to update')
@ -322,7 +322,7 @@ export default function CostDistributionModal({ isOpen, onClose, onSaved, invoic
try { try {
const res = await fetch(`/kitchen/api/cost-distributions/${distributionId}`, { const res = await fetch(`/kitchen/api/cost-distributions/${distributionId}`, {
method: 'DELETE', method: 'DELETE',
headers: { Authorization: `Bearer ${token}` }, credentials: 'include',
}) })
if (!res.ok) { if (!res.ok) {
const data = await res.json() const data = await res.json()
@ -345,7 +345,7 @@ export default function CostDistributionModal({ isOpen, onClose, onSaved, invoic
if (!settleAll && settleAmount) body.amount = parseFloat(settleAmount) if (!settleAll && settleAmount) body.amount = parseFloat(settleAmount)
const res = await fetch(`/kitchen/api/cost-distributions/${distributionId}/settle-early`, { const res = await fetch(`/kitchen/api/cost-distributions/${distributionId}/settle-early`, {
method: 'POST', method: 'POST',
headers: { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json' }, credentials: 'include', headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(body), body: JSON.stringify(body),
}) })
if (!res.ok) { if (!res.ok) {

View file

@ -71,9 +71,9 @@ export default function CreateDisputeModal({
mutationFn: async (data: CreateDisputeRequest) => { mutationFn: async (data: CreateDisputeRequest) => {
const res = await fetch('/kitchen/api/disputes', { const res = await fetch('/kitchen/api/disputes', {
method: 'POST', method: 'POST',
credentials: 'include',
headers: { headers: {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
Authorization: `Bearer ${token}`,
}, },
body: JSON.stringify(data), body: JSON.stringify(data),
}) })

View file

@ -137,19 +137,19 @@ export default function Dashboard() {
queryKey: ['resos-settings'], queryKey: ['resos-settings'],
queryFn: async () => { queryFn: async () => {
const res = await fetch('/kitchen/api/resos/settings', { const res = await fetch('/kitchen/api/resos/settings', {
headers: { Authorization: `Bearer ${token}` }, credentials: 'include',
}) })
if (!res.ok) throw new Error('Failed to fetch Resos settings') if (!res.ok) throw new Error('Failed to fetch Resos settings')
return res.json() return res.json()
}, },
enabled: !!token, enabled: true,
}) })
const { data, isLoading, error } = useQuery<DashboardData>({ const { data, isLoading, error } = useQuery<DashboardData>({
queryKey: ['dashboard'], queryKey: ['dashboard'],
queryFn: async () => { queryFn: async () => {
const res = await fetch('/kitchen/api/reports/dashboard', { const res = await fetch('/kitchen/api/reports/dashboard', {
headers: { Authorization: `Bearer ${token}` }, credentials: 'include',
}) })
if (!res.ok) throw new Error('Failed to fetch dashboard') if (!res.ok) throw new Error('Failed to fetch dashboard')
return res.json() return res.json()
@ -160,7 +160,7 @@ export default function Dashboard() {
queryKey: ['resos-dashboard-covers'], queryKey: ['resos-dashboard-covers'],
queryFn: async () => { queryFn: async () => {
const res = await fetch('/kitchen/api/resos/dashboard/today-tomorrow', { const res = await fetch('/kitchen/api/resos/dashboard/today-tomorrow', {
headers: { Authorization: `Bearer ${token}` }, credentials: 'include',
}) })
if (!res.ok) throw new Error('Failed to fetch Resos covers') if (!res.ok) throw new Error('Failed to fetch Resos covers')
const data = await res.json() const data = await res.json()
@ -173,12 +173,12 @@ export default function Dashboard() {
queryKey: ['newbook-arrival-stats'], queryKey: ['newbook-arrival-stats'],
queryFn: async () => { queryFn: async () => {
const res = await fetch('/kitchen/api/newbook/dashboard/arrivals?days=3', { const res = await fetch('/kitchen/api/newbook/dashboard/arrivals?days=3', {
headers: { Authorization: `Bearer ${token}` }, credentials: 'include',
}) })
if (!res.ok) throw new Error('Failed to fetch arrival stats') if (!res.ok) throw new Error('Failed to fetch arrival stats')
return res.json() return res.json()
}, },
enabled: !!token, enabled: true,
staleTime: 5 * 60 * 1000, // Cache for 5 minutes staleTime: 5 * 60 * 1000, // Cache for 5 minutes
}) })
@ -194,12 +194,12 @@ export default function Dashboard() {
queryKey: ['upcoming-events'], queryKey: ['upcoming-events'],
queryFn: async () => { queryFn: async () => {
const res = await fetch('/kitchen/api/calendar-events/dashboard/upcoming', { const res = await fetch('/kitchen/api/calendar-events/dashboard/upcoming', {
headers: { Authorization: `Bearer ${token}` } credentials: 'include'
}) })
if (!res.ok) throw new Error('Failed to fetch upcoming events') if (!res.ok) throw new Error('Failed to fetch upcoming events')
return res.json() return res.json()
}, },
enabled: !!token, enabled: true,
staleTime: 5 * 60 * 1000 // Cache for 5 minutes staleTime: 5 * 60 * 1000 // Cache for 5 minutes
}) })
@ -218,12 +218,12 @@ export default function Dashboard() {
queryKey: ['recipe-dashboard-stats'], queryKey: ['recipe-dashboard-stats'],
queryFn: async () => { queryFn: async () => {
const res = await fetch('/kitchen/api/recipes/dashboard-stats', { const res = await fetch('/kitchen/api/recipes/dashboard-stats', {
headers: { Authorization: `Bearer ${token}` }, credentials: 'include',
}) })
if (!res.ok) throw new Error('Failed to fetch recipe stats') if (!res.ok) throw new Error('Failed to fetch recipe stats')
return res.json() return res.json()
}, },
enabled: !!token, enabled: true,
staleTime: 5 * 60 * 1000, staleTime: 5 * 60 * 1000,
}) })
@ -231,12 +231,12 @@ export default function Dashboard() {
queryKey: ['dispute-stats'], queryKey: ['dispute-stats'],
queryFn: async () => { queryFn: async () => {
const res = await fetch('/kitchen/api/disputes/stats/summary', { const res = await fetch('/kitchen/api/disputes/stats/summary', {
headers: { Authorization: `Bearer ${token}` } credentials: 'include'
}) })
if (!res.ok) throw new Error('Failed to fetch dispute stats') if (!res.ok) throw new Error('Failed to fetch dispute stats')
return res.json() return res.json()
}, },
enabled: !!token, enabled: true,
staleTime: 5 * 60 * 1000 // Cache for 5 minutes staleTime: 5 * 60 * 1000 // Cache for 5 minutes
}) })

View file

@ -289,7 +289,7 @@ export default function DishEditor() {
queryKey: ['recipe', recipeId], queryKey: ['recipe', recipeId],
queryFn: async () => { queryFn: async () => {
const res = await fetch(`/kitchen/api/recipes/${recipeId}`, { const res = await fetch(`/kitchen/api/recipes/${recipeId}`, {
headers: { Authorization: `Bearer ${token}` }, credentials: 'include',
}) })
if (!res.ok) throw new Error('Not found') if (!res.ok) throw new Error('Not found')
return res.json() return res.json()
@ -302,11 +302,11 @@ export default function DishEditor() {
queryKey: ['dish-courses'], queryKey: ['dish-courses'],
queryFn: async () => { queryFn: async () => {
const res = await fetch('/kitchen/api/recipes/menu-sections?section_type=dish', { const res = await fetch('/kitchen/api/recipes/menu-sections?section_type=dish', {
headers: { Authorization: `Bearer ${token}` }, credentials: 'include',
}) })
return res.json() return res.json()
}, },
enabled: !!token, enabled: true,
}) })
// Fetch costing (base, unscaled) // Fetch costing (base, unscaled)
@ -314,7 +314,7 @@ export default function DishEditor() {
queryKey: ['recipe-cost', recipeId], queryKey: ['recipe-cost', recipeId],
queryFn: async () => { queryFn: async () => {
const res = await fetch(`/kitchen/api/recipes/${recipeId}/costing`, { const res = await fetch(`/kitchen/api/recipes/${recipeId}/costing`, {
headers: { Authorization: `Bearer ${token}` }, credentials: 'include',
}) })
return res.json() return res.json()
}, },
@ -326,7 +326,7 @@ export default function DishEditor() {
queryKey: ['recipe-cost-scaled', recipeId, scalePortions], queryKey: ['recipe-cost-scaled', recipeId, scalePortions],
queryFn: async () => { queryFn: async () => {
const res = await fetch(`/kitchen/api/recipes/${recipeId}/costing?scale_to=${scalePortions}`, { const res = await fetch(`/kitchen/api/recipes/${recipeId}/costing?scale_to=${scalePortions}`, {
headers: { Authorization: `Bearer ${token}` }, credentials: 'include',
}) })
return res.json() return res.json()
}, },
@ -338,7 +338,7 @@ export default function DishEditor() {
queryKey: ['recipe-flags', recipeId], queryKey: ['recipe-flags', recipeId],
queryFn: async () => { queryFn: async () => {
const res = await fetch(`/kitchen/api/food-flags/recipes/${recipeId}/flags`, { const res = await fetch(`/kitchen/api/food-flags/recipes/${recipeId}/flags`, {
headers: { Authorization: `Bearer ${token}` }, credentials: 'include',
}) })
return res.json() return res.json()
}, },
@ -350,11 +350,11 @@ export default function DishEditor() {
queryKey: ['food-flag-categories'], queryKey: ['food-flag-categories'],
queryFn: async () => { queryFn: async () => {
const res = await fetch('/kitchen/api/food-flags/categories', { const res = await fetch('/kitchen/api/food-flags/categories', {
headers: { Authorization: `Bearer ${token}` }, credentials: 'include',
}) })
return res.json() return res.json()
}, },
enabled: !!token, enabled: true,
}) })
// Fetch change log // Fetch change log
@ -362,7 +362,7 @@ export default function DishEditor() {
queryKey: ['recipe-changelog', recipeId], queryKey: ['recipe-changelog', recipeId],
queryFn: async () => { queryFn: async () => {
const res = await fetch(`/kitchen/api/recipes/${recipeId}/change-log`, { const res = await fetch(`/kitchen/api/recipes/${recipeId}/change-log`, {
headers: { Authorization: `Bearer ${token}` }, credentials: 'include',
}) })
return res.json() return res.json()
}, },
@ -374,7 +374,7 @@ export default function DishEditor() {
queryKey: ['recipe-cost-trend', recipeId], queryKey: ['recipe-cost-trend', recipeId],
queryFn: async () => { queryFn: async () => {
const res = await fetch(`/kitchen/api/recipes/${recipeId}/cost-trend`, { const res = await fetch(`/kitchen/api/recipes/${recipeId}/cost-trend`, {
headers: { Authorization: `Bearer ${token}` }, credentials: 'include',
}) })
if (!res.ok) throw new Error('Failed to fetch cost trend') if (!res.ok) throw new Error('Failed to fetch cost trend')
return res.json() return res.json()
@ -387,7 +387,7 @@ export default function DishEditor() {
queryKey: ['dish-menus', recipeId], queryKey: ['dish-menus', recipeId],
queryFn: async () => { queryFn: async () => {
const res = await fetch(`/kitchen/api/menus/dish/${recipeId}/menus`, { const res = await fetch(`/kitchen/api/menus/dish/${recipeId}/menus`, {
headers: { Authorization: `Bearer ${token}` }, credentials: 'include',
}) })
if (!res.ok) return [] if (!res.ok) return []
return res.json() return res.json()
@ -400,7 +400,7 @@ export default function DishEditor() {
queryKey: ['ingredient-edit', editIngId], queryKey: ['ingredient-edit', editIngId],
queryFn: async () => { queryFn: async () => {
const res = await fetch(`/kitchen/api/ingredients/${editIngId}`, { const res = await fetch(`/kitchen/api/ingredients/${editIngId}`, {
headers: { Authorization: `Bearer ${token}` }, credentials: 'include',
}) })
if (!res.ok) throw new Error('Not found') if (!res.ok) throw new Error('Not found')
const data = await res.json() const data = await res.json()
@ -426,7 +426,7 @@ export default function DishEditor() {
queryKey: ['recipes-list-for-sub'], queryKey: ['recipes-list-for-sub'],
queryFn: async () => { queryFn: async () => {
const res = await fetch('/kitchen/api/recipes?recipe_type=component', { const res = await fetch('/kitchen/api/recipes?recipe_type=component', {
headers: { Authorization: `Bearer ${token}` }, credentials: 'include',
}) })
return res.json() return res.json()
}, },
@ -439,7 +439,7 @@ export default function DishEditor() {
queryKey: ['sambapos-menu-items-portions'], queryKey: ['sambapos-menu-items-portions'],
queryFn: async () => { queryFn: async () => {
const res = await fetch('/kitchen/api/sambapos/menu-items-with-portions', { const res = await fetch('/kitchen/api/sambapos/menu-items-with-portions', {
headers: { Authorization: `Bearer ${token}` }, credentials: 'include',
}) })
if (!res.ok) return [] if (!res.ok) return []
return res.json() return res.json()
@ -468,7 +468,7 @@ export default function DishEditor() {
const timer = setTimeout(async () => { const timer = setTimeout(async () => {
try { try {
const res = await fetch(`/kitchen/api/ingredients/suggest?description=${encodeURIComponent(ingSearch)}`, { const res = await fetch(`/kitchen/api/ingredients/suggest?description=${encodeURIComponent(ingSearch)}`, {
headers: { Authorization: `Bearer ${token}` }, credentials: 'include',
}) })
if (res.ok) { if (res.ok) {
const data = await res.json() const data = await res.json()
@ -486,7 +486,7 @@ export default function DishEditor() {
mutationFn: async (data: Record<string, unknown>) => { mutationFn: async (data: Record<string, unknown>) => {
const res = await fetch(`/kitchen/api/recipes/${recipeId}`, { const res = await fetch(`/kitchen/api/recipes/${recipeId}`, {
method: 'PATCH', method: 'PATCH',
headers: { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json' }, credentials: 'include', headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data), body: JSON.stringify(data),
}) })
if (!res.ok) throw new Error('Failed to update') if (!res.ok) throw new Error('Failed to update')
@ -504,7 +504,7 @@ export default function DishEditor() {
mutationFn: async (data: { ingredient_id: number; quantity: number; unit?: string; notes?: string }) => { mutationFn: async (data: { ingredient_id: number; quantity: number; unit?: string; notes?: string }) => {
const res = await fetch(`/kitchen/api/recipes/${recipeId}/ingredients`, { const res = await fetch(`/kitchen/api/recipes/${recipeId}/ingredients`, {
method: 'POST', method: 'POST',
headers: { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json' }, credentials: 'include', headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data), body: JSON.stringify(data),
}) })
if (!res.ok) throw new Error('Failed to add') if (!res.ok) throw new Error('Failed to add')
@ -529,7 +529,7 @@ export default function DishEditor() {
mutationFn: async ({ riId, quantity, unit, yield_percent, notes }: { riId: number; quantity?: number; unit?: string; yield_percent?: number; notes?: string }) => { mutationFn: async ({ riId, quantity, unit, yield_percent, notes }: { riId: number; quantity?: number; unit?: string; yield_percent?: number; notes?: string }) => {
const res = await fetch(`/kitchen/api/recipes/recipe-ingredients/${riId}`, { const res = await fetch(`/kitchen/api/recipes/recipe-ingredients/${riId}`, {
method: 'PATCH', method: 'PATCH',
headers: { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json' }, credentials: 'include', headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ quantity, unit, yield_percent, notes }), body: JSON.stringify({ quantity, unit, yield_percent, notes }),
}) })
if (!res.ok) throw new Error('Failed to update') if (!res.ok) throw new Error('Failed to update')
@ -547,7 +547,7 @@ export default function DishEditor() {
mutationFn: async (riId: number) => { mutationFn: async (riId: number) => {
const res = await fetch(`/kitchen/api/recipes/recipe-ingredients/${riId}`, { const res = await fetch(`/kitchen/api/recipes/recipe-ingredients/${riId}`, {
method: 'DELETE', method: 'DELETE',
headers: { Authorization: `Bearer ${token}` }, credentials: 'include',
}) })
if (!res.ok) throw new Error('Failed to remove') if (!res.ok) throw new Error('Failed to remove')
}, },
@ -564,7 +564,7 @@ export default function DishEditor() {
mutationFn: async (data: { child_recipe_id: number; portions_needed: number; portions_needed_unit?: string }) => { mutationFn: async (data: { child_recipe_id: number; portions_needed: number; portions_needed_unit?: string }) => {
const res = await fetch(`/kitchen/api/recipes/${recipeId}/sub-recipes`, { const res = await fetch(`/kitchen/api/recipes/${recipeId}/sub-recipes`, {
method: 'POST', method: 'POST',
headers: { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json' }, credentials: 'include', headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data), body: JSON.stringify(data),
}) })
if (!res.ok) { if (!res.ok) {
@ -585,7 +585,7 @@ export default function DishEditor() {
mutationFn: async (srId: number) => { mutationFn: async (srId: number) => {
const res = await fetch(`/kitchen/api/recipes/recipe-sub-recipes/${srId}`, { const res = await fetch(`/kitchen/api/recipes/recipe-sub-recipes/${srId}`, {
method: 'DELETE', method: 'DELETE',
headers: { Authorization: `Bearer ${token}` }, credentials: 'include',
}) })
if (!res.ok) throw new Error('Failed') if (!res.ok) throw new Error('Failed')
}, },
@ -601,7 +601,7 @@ export default function DishEditor() {
mutationFn: async (data: { title?: string; instruction: string; step_number: number; duration_minutes?: number }) => { mutationFn: async (data: { title?: string; instruction: string; step_number: number; duration_minutes?: number }) => {
const res = await fetch(`/kitchen/api/recipes/${recipeId}/steps`, { const res = await fetch(`/kitchen/api/recipes/${recipeId}/steps`, {
method: 'POST', method: 'POST',
headers: { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json' }, credentials: 'include', headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data), body: JSON.stringify(data),
}) })
if (!res.ok) throw new Error('Failed') if (!res.ok) throw new Error('Failed')
@ -619,7 +619,7 @@ export default function DishEditor() {
mutationFn: async ({ stepId, data }: { stepId: number; data: { title?: string; instruction?: string; duration_minutes?: number } }) => { mutationFn: async ({ stepId, data }: { stepId: number; data: { title?: string; instruction?: string; duration_minutes?: number } }) => {
const res = await fetch(`/kitchen/api/recipes/recipe-steps/${stepId}`, { const res = await fetch(`/kitchen/api/recipes/recipe-steps/${stepId}`, {
method: 'PATCH', method: 'PATCH',
headers: { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json' }, credentials: 'include', headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data), body: JSON.stringify(data),
}) })
if (!res.ok) throw new Error('Failed') if (!res.ok) throw new Error('Failed')
@ -638,7 +638,7 @@ export default function DishEditor() {
mutationFn: async (stepId: number) => { mutationFn: async (stepId: number) => {
const res = await fetch(`/kitchen/api/recipes/recipe-steps/${stepId}`, { const res = await fetch(`/kitchen/api/recipes/recipe-steps/${stepId}`, {
method: 'DELETE', method: 'DELETE',
headers: { Authorization: `Bearer ${token}` }, credentials: 'include',
}) })
if (!res.ok) throw new Error('Failed') if (!res.ok) throw new Error('Failed')
}, },
@ -654,7 +654,7 @@ export default function DishEditor() {
formData.append('image_type', image_type) formData.append('image_type', image_type)
const res = await fetch(`/kitchen/api/recipes/${recipeId}/images`, { const res = await fetch(`/kitchen/api/recipes/${recipeId}/images`, {
method: 'POST', method: 'POST',
headers: { Authorization: `Bearer ${token}` }, credentials: 'include',
body: formData, body: formData,
}) })
if (!res.ok) throw new Error('Failed to upload image') if (!res.ok) throw new Error('Failed to upload image')
@ -672,7 +672,7 @@ export default function DishEditor() {
mutationFn: async (imageId: number) => { mutationFn: async (imageId: number) => {
const res = await fetch(`/kitchen/api/recipes/${recipeId}/images/${imageId}`, { const res = await fetch(`/kitchen/api/recipes/${recipeId}/images/${imageId}`, {
method: 'DELETE', method: 'DELETE',
headers: { Authorization: `Bearer ${token}` }, credentials: 'include',
}) })
if (!res.ok) throw new Error('Failed to delete image') if (!res.ok) throw new Error('Failed to delete image')
}, },
@ -686,7 +686,7 @@ export default function DishEditor() {
mutationFn: async (ingredientIds: number[]) => { mutationFn: async (ingredientIds: number[]) => {
const res = await fetch(`/kitchen/api/recipes/${recipeId}/ingredients/reorder`, { const res = await fetch(`/kitchen/api/recipes/${recipeId}/ingredients/reorder`, {
method: 'PATCH', method: 'PATCH',
headers: { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json' }, credentials: 'include', headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ ingredient_ids: ingredientIds }), body: JSON.stringify({ ingredient_ids: ingredientIds }),
}) })
if (!res.ok) throw new Error('Failed to reorder') if (!res.ok) throw new Error('Failed to reorder')
@ -701,7 +701,7 @@ export default function DishEditor() {
mutationFn: async (subRecipeIds: number[]) => { mutationFn: async (subRecipeIds: number[]) => {
const res = await fetch(`/kitchen/api/recipes/${recipeId}/sub-recipes/reorder`, { const res = await fetch(`/kitchen/api/recipes/${recipeId}/sub-recipes/reorder`, {
method: 'PATCH', method: 'PATCH',
headers: { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json' }, credentials: 'include', headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ sub_recipe_ids: subRecipeIds }), body: JSON.stringify({ sub_recipe_ids: subRecipeIds }),
}) })
if (!res.ok) throw new Error('Failed to reorder') if (!res.ok) throw new Error('Failed to reorder')
@ -716,7 +716,7 @@ export default function DishEditor() {
mutationFn: async (stepIds: number[]) => { mutationFn: async (stepIds: number[]) => {
const res = await fetch(`/kitchen/api/recipes/${recipeId}/steps/reorder`, { const res = await fetch(`/kitchen/api/recipes/${recipeId}/steps/reorder`, {
method: 'PATCH', method: 'PATCH',
headers: { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json' }, credentials: 'include', headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ step_ids: stepIds }), body: JSON.stringify({ step_ids: stepIds }),
}) })
if (!res.ok) throw new Error('Failed to reorder steps') if (!res.ok) throw new Error('Failed to reorder steps')
@ -931,7 +931,7 @@ export default function DishEditor() {
try { try {
const res = await fetch(`/kitchen/api/food-flags/recipes/${recipeId}/text-dismissals`, { const res = await fetch(`/kitchen/api/food-flags/recipes/${recipeId}/text-dismissals`, {
method: 'POST', method: 'POST',
headers: { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json' }, credentials: 'include', headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ body: JSON.stringify({
food_flag_id: s.flag_id, food_flag_id: s.flag_id,
dismissed_by_name: user?.name || user?.email || 'Unknown', dismissed_by_name: user?.name || user?.email || 'Unknown',
@ -1345,10 +1345,10 @@ export default function DishEditor() {
{recipe.images.map(img => ( {recipe.images.map(img => (
<div key={img.id} style={styles.imageCard}> <div key={img.id} style={styles.imageCard}>
<img <img
src={`/kitchen/api/recipes/${recipeId}/images/${img.id}?token=${token}`} src={`/kitchen/api/recipes/${recipeId}/images/${img.id}`}
alt={img.caption || 'Dish image'} alt={img.caption || 'Dish image'}
style={{ ...styles.imageThumb, cursor: 'pointer' }} style={{ ...styles.imageThumb, cursor: 'pointer' }}
onClick={() => setLightboxImg(`/kitchen/api/recipes/${recipeId}/images/${img.id}?token=${token}`)} onClick={() => setLightboxImg(`/kitchen/api/recipes/${recipeId}/images/${img.id}`)}
/> />
<div style={{ padding: '0.4rem' }}> <div style={{ padding: '0.4rem' }}>
{img.caption && <div style={{ fontSize: '0.8rem', color: '#333', marginTop: '2px' }}>{img.caption}</div>} {img.caption && <div style={{ fontSize: '0.8rem', color: '#333', marginTop: '2px' }}>{img.caption}</div>}

View file

@ -121,12 +121,12 @@ export default function DishList() {
queryKey: ['dish-courses'], queryKey: ['dish-courses'],
queryFn: async () => { queryFn: async () => {
const res = await fetch('/kitchen/api/recipes/menu-sections?section_type=dish', { const res = await fetch('/kitchen/api/recipes/menu-sections?section_type=dish', {
headers: { Authorization: `Bearer ${token}` }, credentials: 'include',
}) })
if (!res.ok) throw new Error('Failed to fetch sections') if (!res.ok) throw new Error('Failed to fetch sections')
return res.json() return res.json()
}, },
enabled: !!token, enabled: true,
}) })
const { data: recipes, isLoading } = useQuery<RecipeItem[]>({ const { data: recipes, isLoading } = useQuery<RecipeItem[]>({
@ -138,12 +138,12 @@ export default function DishList() {
if (sectionFilter) params.set('menu_section_id', sectionFilter) if (sectionFilter) params.set('menu_section_id', sectionFilter)
if (showArchived) params.set('archived', 'true') if (showArchived) params.set('archived', 'true')
const res = await fetch(`/kitchen/api/recipes?${params}`, { const res = await fetch(`/kitchen/api/recipes?${params}`, {
headers: { Authorization: `Bearer ${token}` }, credentials: 'include',
}) })
if (!res.ok) throw new Error('Failed to fetch dishes') if (!res.ok) throw new Error('Failed to fetch dishes')
return res.json() return res.json()
}, },
enabled: !!token, enabled: true,
}) })
// Price impact data for badge overlay // Price impact data for badge overlay
@ -151,12 +151,12 @@ export default function DishList() {
queryKey: ['price-impact-dishes', costChangeDays], queryKey: ['price-impact-dishes', costChangeDays],
queryFn: async () => { queryFn: async () => {
const res = await fetch(`/kitchen/api/recipes/price-impact?days=${costChangeDays}`, { const res = await fetch(`/kitchen/api/recipes/price-impact?days=${costChangeDays}`, {
headers: { Authorization: `Bearer ${token}` }, credentials: 'include',
}) })
if (!res.ok) throw new Error('Failed to fetch price impact') if (!res.ok) throw new Error('Failed to fetch price impact')
return res.json() return res.json()
}, },
enabled: !!token, enabled: true,
}) })
// Build lookup: recipe_id → impact item (dishes only) // Build lookup: recipe_id → impact item (dishes only)
@ -168,7 +168,7 @@ export default function DishList() {
queryKey: ['cost-trend', expandedCostId], queryKey: ['cost-trend', expandedCostId],
queryFn: async () => { queryFn: async () => {
const res = await fetch(`/kitchen/api/recipes/${expandedCostId}/cost-trend`, { const res = await fetch(`/kitchen/api/recipes/${expandedCostId}/cost-trend`, {
headers: { Authorization: `Bearer ${token}` }, credentials: 'include',
}) })
if (!res.ok) throw new Error('Failed to fetch cost trend') if (!res.ok) throw new Error('Failed to fetch cost trend')
return res.json() return res.json()
@ -180,7 +180,7 @@ export default function DishList() {
mutationFn: async (data: Record<string, unknown>) => { mutationFn: async (data: Record<string, unknown>) => {
const res = await fetch('/kitchen/api/recipes', { const res = await fetch('/kitchen/api/recipes', {
method: 'POST', method: 'POST',
headers: { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json' }, credentials: 'include', headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data), body: JSON.stringify(data),
}) })
if (!res.ok) throw new Error('Failed to create dish') if (!res.ok) throw new Error('Failed to create dish')
@ -197,7 +197,7 @@ export default function DishList() {
mutationFn: async (id: number) => { mutationFn: async (id: number) => {
const res = await fetch(`/kitchen/api/recipes/${id}/duplicate`, { const res = await fetch(`/kitchen/api/recipes/${id}/duplicate`, {
method: 'POST', method: 'POST',
headers: { Authorization: `Bearer ${token}` }, credentials: 'include',
}) })
if (!res.ok) throw new Error('Failed to duplicate') if (!res.ok) throw new Error('Failed to duplicate')
return res.json() return res.json()
@ -212,7 +212,7 @@ export default function DishList() {
mutationFn: async (id: number) => { mutationFn: async (id: number) => {
const res = await fetch(`/kitchen/api/recipes/${id}`, { const res = await fetch(`/kitchen/api/recipes/${id}`, {
method: 'DELETE', method: 'DELETE',
headers: { Authorization: `Bearer ${token}` }, credentials: 'include',
}) })
if (!res.ok) throw new Error('Failed to archive') if (!res.ok) throw new Error('Failed to archive')
}, },
@ -223,7 +223,7 @@ export default function DishList() {
mutationFn: async (id: number) => { mutationFn: async (id: number) => {
const res = await fetch(`/kitchen/api/recipes/${id}`, { const res = await fetch(`/kitchen/api/recipes/${id}`, {
method: 'PATCH', method: 'PATCH',
headers: { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json' }, credentials: 'include', headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ is_archived: false }), body: JSON.stringify({ is_archived: false }),
}) })
if (!res.ok) throw new Error('Failed to unarchive') if (!res.ok) throw new Error('Failed to unarchive')
@ -235,7 +235,7 @@ export default function DishList() {
mutationFn: async (name: string) => { mutationFn: async (name: string) => {
const res = await fetch('/kitchen/api/recipes/menu-sections', { const res = await fetch('/kitchen/api/recipes/menu-sections', {
method: 'POST', method: 'POST',
headers: { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json' }, credentials: 'include', headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ name, section_type: 'dish' }), body: JSON.stringify({ name, section_type: 'dish' }),
}) })
if (!res.ok) throw new Error('Failed to create course') if (!res.ok) throw new Error('Failed to create course')
@ -252,7 +252,7 @@ export default function DishList() {
mutationFn: async ({ id, name }: { id: number; name: string }) => { mutationFn: async ({ id, name }: { id: number; name: string }) => {
const res = await fetch(`/kitchen/api/recipes/menu-sections/${id}`, { const res = await fetch(`/kitchen/api/recipes/menu-sections/${id}`, {
method: 'PATCH', method: 'PATCH',
headers: { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json' }, credentials: 'include', headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ name }), body: JSON.stringify({ name }),
}) })
if (!res.ok) throw new Error('Failed to update course') if (!res.ok) throw new Error('Failed to update course')
@ -270,7 +270,7 @@ export default function DishList() {
mutationFn: async (id: number) => { mutationFn: async (id: number) => {
const res = await fetch(`/kitchen/api/recipes/menu-sections/${id}`, { const res = await fetch(`/kitchen/api/recipes/menu-sections/${id}`, {
method: 'DELETE', method: 'DELETE',
headers: { Authorization: `Bearer ${token}` }, credentials: 'include',
}) })
if (!res.ok) throw new Error('Failed to delete course') if (!res.ok) throw new Error('Failed to delete course')
return res.json() return res.json()
@ -285,7 +285,7 @@ export default function DishList() {
queryKey: ['food-flag-categories'], queryKey: ['food-flag-categories'],
queryFn: async () => { queryFn: async () => {
const res = await fetch('/kitchen/api/food-flags/categories', { const res = await fetch('/kitchen/api/food-flags/categories', {
headers: { Authorization: `Bearer ${token}` }, credentials: 'include',
}) })
if (!res.ok) throw new Error('Failed to fetch flag categories') if (!res.ok) throw new Error('Failed to fetch flag categories')
return res.json() return res.json()

View file

@ -125,11 +125,11 @@ export default function DisputeDetailModal({ disputeId, onClose, onUpdate }: Dis
const { data: settings } = useQuery<{ llm_enabled?: boolean; anthropic_api_key_set?: boolean }>({ const { data: settings } = useQuery<{ llm_enabled?: boolean; anthropic_api_key_set?: boolean }>({
queryKey: ['settings'], queryKey: ['settings'],
queryFn: async () => { queryFn: async () => {
const res = await fetch('/kitchen/api/settings', { headers: { Authorization: `Bearer ${token}` } }) const res = await fetch('/kitchen/api/settings', { credentials: 'include' })
if (!res.ok) return {} if (!res.ok) return {}
return res.json() return res.json()
}, },
enabled: !!token, enabled: true,
staleTime: 60000, staleTime: 60000,
}) })
@ -137,21 +137,21 @@ export default function DisputeDetailModal({ disputeId, onClose, onUpdate }: Dis
queryKey: ['dispute', disputeId], queryKey: ['dispute', disputeId],
queryFn: async () => { queryFn: async () => {
const res = await fetch(`/kitchen/api/disputes/${disputeId}`, { const res = await fetch(`/kitchen/api/disputes/${disputeId}`, {
headers: { Authorization: `Bearer ${token}` }, credentials: 'include',
}) })
if (!res.ok) throw new Error('Failed to fetch dispute') if (!res.ok) throw new Error('Failed to fetch dispute')
return res.json() return res.json()
}, },
enabled: !!token, enabled: true,
}) })
const updateMutation = useMutation({ const updateMutation = useMutation({
mutationFn: async (data: { status?: string; priority?: string; supplier_response?: string; supplier_contact_name?: string; title?: string; description?: string }) => { mutationFn: async (data: { status?: string; priority?: string; supplier_response?: string; supplier_contact_name?: string; title?: string; description?: string }) => {
const res = await fetch(`/kitchen/api/disputes/${disputeId}`, { const res = await fetch(`/kitchen/api/disputes/${disputeId}`, {
method: 'PATCH', method: 'PATCH',
credentials: 'include',
headers: { headers: {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
Authorization: `Bearer ${token}`,
}, },
body: JSON.stringify(data), body: JSON.stringify(data),
}) })
@ -174,8 +174,8 @@ export default function DisputeDetailModal({ disputeId, onClose, onUpdate }: Dis
mutationFn: async () => { mutationFn: async () => {
const res = await fetch(`/kitchen/api/disputes/${disputeId}`, { const res = await fetch(`/kitchen/api/disputes/${disputeId}`, {
method: 'DELETE', method: 'DELETE',
credentials: 'include',
headers: { headers: {
Authorization: `Bearer ${token}`,
}, },
}) })
if (!res.ok) { if (!res.ok) {
@ -253,7 +253,7 @@ export default function DisputeDetailModal({ disputeId, onClose, onUpdate }: Dis
try { try {
const res = await fetch(`/kitchen/api/disputes/${disputeId}/draft-email`, { const res = await fetch(`/kitchen/api/disputes/${disputeId}/draft-email`, {
method: 'POST', method: 'POST',
headers: { Authorization: `Bearer ${token}` }, credentials: 'include',
}) })
if (!res.ok) { if (!res.ok) {
const err = await res.json() const err = await res.json()
@ -292,8 +292,8 @@ export default function DisputeDetailModal({ disputeId, onClose, onUpdate }: Dis
const res = await fetch(`/kitchen/api/disputes/${disputeId}/attachments?${params}`, { const res = await fetch(`/kitchen/api/disputes/${disputeId}/attachments?${params}`, {
method: 'POST', method: 'POST',
credentials: 'include',
headers: { headers: {
Authorization: `Bearer ${token}`,
}, },
body: formData, body: formData,
}) })

View file

@ -60,12 +60,12 @@ export default function Disputes() {
queryKey: ['suppliers'], queryKey: ['suppliers'],
queryFn: async () => { queryFn: async () => {
const res = await fetch('/kitchen/api/suppliers/', { const res = await fetch('/kitchen/api/suppliers/', {
headers: { Authorization: `Bearer ${token}` }, credentials: 'include',
}) })
if (!res.ok) throw new Error('Failed to fetch suppliers') if (!res.ok) throw new Error('Failed to fetch suppliers')
return res.json() return res.json()
}, },
enabled: !!token, enabled: true,
}) })
// Build query params // Build query params
@ -85,12 +85,12 @@ export default function Disputes() {
queryFn: async () => { queryFn: async () => {
const url = queryString ? `/kitchen/api/disputes?${queryString}` : '/kitchen/api/disputes' const url = queryString ? `/kitchen/api/disputes?${queryString}` : '/kitchen/api/disputes'
const res = await fetch(url, { const res = await fetch(url, {
headers: { Authorization: `Bearer ${token}` }, credentials: 'include',
}) })
if (!res.ok) throw new Error('Failed to fetch disputes') if (!res.ok) throw new Error('Failed to fetch disputes')
return res.json() return res.json()
}, },
enabled: !!token, enabled: true,
}) })
// Client-side filter for "not_resolved" - show all except resolved // Client-side filter for "not_resolved" - show all except resolved

View file

@ -129,7 +129,7 @@ export default function EventOrderEditor() {
queryKey: ['event-order', orderId], queryKey: ['event-order', orderId],
queryFn: async () => { queryFn: async () => {
const res = await fetch(`/kitchen/api/event-orders/${orderId}`, { const res = await fetch(`/kitchen/api/event-orders/${orderId}`, {
headers: { Authorization: `Bearer ${token}` }, credentials: 'include',
}) })
if (!res.ok) throw new Error('Not found') if (!res.ok) throw new Error('Not found')
return res.json() return res.json()
@ -143,7 +143,7 @@ export default function EventOrderEditor() {
queryKey: ['recipes-for-event', recipeType], queryKey: ['recipes-for-event', recipeType],
queryFn: async () => { queryFn: async () => {
const res = await fetch(`/kitchen/api/recipes?recipe_type=${recipeType}`, { const res = await fetch(`/kitchen/api/recipes?recipe_type=${recipeType}`, {
headers: { Authorization: `Bearer ${token}` }, credentials: 'include',
}) })
if (!res.ok) throw new Error('Failed') if (!res.ok) throw new Error('Failed')
return res.json() return res.json()
@ -156,7 +156,7 @@ export default function EventOrderEditor() {
queryKey: ['menus-for-event'], queryKey: ['menus-for-event'],
queryFn: async () => { queryFn: async () => {
const res = await fetch('/kitchen/api/menus', { const res = await fetch('/kitchen/api/menus', {
headers: { Authorization: `Bearer ${token}` }, credentials: 'include',
}) })
if (!res.ok) throw new Error('Failed') if (!res.ok) throw new Error('Failed')
return res.json() return res.json()
@ -169,7 +169,7 @@ export default function EventOrderEditor() {
queryKey: ['menu-detail-for-event', selectedMenuId], queryKey: ['menu-detail-for-event', selectedMenuId],
queryFn: async () => { queryFn: async () => {
const res = await fetch(`/kitchen/api/menus/${selectedMenuId}`, { const res = await fetch(`/kitchen/api/menus/${selectedMenuId}`, {
headers: { Authorization: `Bearer ${token}` }, credentials: 'include',
}) })
if (!res.ok) throw new Error('Failed') if (!res.ok) throw new Error('Failed')
return res.json() return res.json()
@ -182,7 +182,7 @@ export default function EventOrderEditor() {
queryFn: async () => { queryFn: async () => {
const params = groupBySupplier ? '?group_by_supplier=true' : '' const params = groupBySupplier ? '?group_by_supplier=true' : ''
const res = await fetch(`/kitchen/api/event-orders/${orderId}/shopping-list${params}`, { const res = await fetch(`/kitchen/api/event-orders/${orderId}/shopping-list${params}`, {
headers: { Authorization: `Bearer ${token}` }, credentials: 'include',
}) })
return res.json() return res.json()
}, },
@ -193,7 +193,7 @@ export default function EventOrderEditor() {
mutationFn: async (data: { recipe_id: number; quantity: number }) => { mutationFn: async (data: { recipe_id: number; quantity: number }) => {
const res = await fetch(`/kitchen/api/event-orders/${orderId}/items`, { const res = await fetch(`/kitchen/api/event-orders/${orderId}/items`, {
method: 'POST', method: 'POST',
headers: { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json' }, credentials: 'include', headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data), body: JSON.stringify(data),
}) })
if (!res.ok) throw new Error('Failed') if (!res.ok) throw new Error('Failed')
@ -209,7 +209,7 @@ export default function EventOrderEditor() {
mutationFn: async (items: Array<{ recipe_id: number; quantity: number; notes?: string }>) => { mutationFn: async (items: Array<{ recipe_id: number; quantity: number; notes?: string }>) => {
const res = await fetch(`/kitchen/api/event-orders/${orderId}/items/bulk`, { const res = await fetch(`/kitchen/api/event-orders/${orderId}/items/bulk`, {
method: 'POST', method: 'POST',
headers: { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json' }, credentials: 'include', headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ items }), body: JSON.stringify({ items }),
}) })
if (!res.ok) throw new Error('Failed') if (!res.ok) throw new Error('Failed')
@ -226,7 +226,7 @@ export default function EventOrderEditor() {
mutationFn: async (itemId: number) => { mutationFn: async (itemId: number) => {
const res = await fetch(`/kitchen/api/event-orders/items/${itemId}`, { const res = await fetch(`/kitchen/api/event-orders/items/${itemId}`, {
method: 'DELETE', method: 'DELETE',
headers: { Authorization: `Bearer ${token}` }, credentials: 'include',
}) })
if (!res.ok) throw new Error('Failed') if (!res.ok) throw new Error('Failed')
}, },
@ -240,7 +240,7 @@ export default function EventOrderEditor() {
mutationFn: async (status: string) => { mutationFn: async (status: string) => {
const res = await fetch(`/kitchen/api/event-orders/${orderId}`, { const res = await fetch(`/kitchen/api/event-orders/${orderId}`, {
method: 'PATCH', method: 'PATCH',
headers: { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json' }, credentials: 'include', headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ status }), body: JSON.stringify({ status }),
}) })
if (!res.ok) throw new Error('Failed') if (!res.ok) throw new Error('Failed')

View file

@ -29,19 +29,19 @@ export default function EventOrders() {
queryKey: ['event-orders'], queryKey: ['event-orders'],
queryFn: async () => { queryFn: async () => {
const res = await fetch('/kitchen/api/event-orders', { const res = await fetch('/kitchen/api/event-orders', {
headers: { Authorization: `Bearer ${token}` }, credentials: 'include',
}) })
if (!res.ok) throw new Error('Failed') if (!res.ok) throw new Error('Failed')
return res.json() return res.json()
}, },
enabled: !!token, enabled: true,
}) })
const createMutation = useMutation({ const createMutation = useMutation({
mutationFn: async (data: { name: string; event_date?: string; notes?: string }) => { mutationFn: async (data: { name: string; event_date?: string; notes?: string }) => {
const res = await fetch('/kitchen/api/event-orders', { const res = await fetch('/kitchen/api/event-orders', {
method: 'POST', method: 'POST',
headers: { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json' }, credentials: 'include', headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data), body: JSON.stringify(data),
}) })
if (!res.ok) throw new Error('Failed to create') if (!res.ok) throw new Error('Failed to create')
@ -58,7 +58,7 @@ export default function EventOrders() {
mutationFn: async (id: number) => { mutationFn: async (id: number) => {
const res = await fetch(`/kitchen/api/event-orders/${id}`, { const res = await fetch(`/kitchen/api/event-orders/${id}`, {
method: 'DELETE', method: 'DELETE',
headers: { Authorization: `Bearer ${token}` }, credentials: 'include',
}) })
if (!res.ok) throw new Error('Failed') if (!res.ok) throw new Error('Failed')
}, },

View file

@ -158,14 +158,22 @@ export default function GPReport() {
const monthOptions = getMonthOptions() const monthOptions = getMonthOptions()
// Allowances checkbox state - default: all checked EXCEPT wastage // Allowances checkbox state - default: all checked EXCEPT wastage
const [allowancesSelection, setAllowancesSelection] = useState({ const _defaultAllowances = {
wastage: false, // Wastage: unchecked by default wastage: false,
transfer: true, // Transfer: checked by default transfer: true,
staffFood: true, // Staff Food: checked by default staffFood: true,
manualAdjustment: true, // Manual Adjustment: checked by default manualAdjustment: true,
disputes: true, // Open Disputes: checked by default disputes: true,
cdDeductions: true, // Distributed Deductions: checked by default cdDeductions: true,
cdReallocations: true // Distributed Reallocations: checked by default cdReallocations: true,
}
const [allowancesSelection, setAllowancesSelection] = useState(() => {
try {
const stored = localStorage.getItem('gpreport_allowances_v1')
return stored ? { ..._defaultAllowances, ...JSON.parse(stored) } : _defaultAllowances
} catch {
return _defaultAllowances
}
}) })
// Track if dates have changed since last generation // Track if dates have changed since last generation
@ -318,7 +326,7 @@ export default function GPReport() {
queryKey: ['gp-range', submittedFromDate, submittedToDate], queryKey: ['gp-range', submittedFromDate, submittedToDate],
queryFn: async () => { queryFn: async () => {
const res = await fetch(`/kitchen/api/reports/gp/range?from_date=${submittedFromDate}&to_date=${submittedToDate}`, { const res = await fetch(`/kitchen/api/reports/gp/range?from_date=${submittedFromDate}&to_date=${submittedToDate}`, {
headers: { Authorization: `Bearer ${token}` }, credentials: 'include',
}) })
if (!res.ok) throw new Error('Failed to fetch GP data') if (!res.ok) throw new Error('Failed to fetch GP data')
return res.json() return res.json()
@ -332,7 +340,7 @@ export default function GPReport() {
queryKey: ['gp-daily', submittedFromDate, submittedToDate], queryKey: ['gp-daily', submittedFromDate, submittedToDate],
queryFn: async () => { queryFn: async () => {
const res = await fetch(`/kitchen/api/reports/gp/daily?from_date=${submittedFromDate}&to_date=${submittedToDate}`, { const res = await fetch(`/kitchen/api/reports/gp/daily?from_date=${submittedFromDate}&to_date=${submittedToDate}`, {
headers: { Authorization: `Bearer ${token}` }, credentials: 'include',
}) })
if (!res.ok) throw new Error('Failed to fetch chart data') if (!res.ok) throw new Error('Failed to fetch chart data')
return res.json() return res.json()
@ -346,7 +354,7 @@ export default function GPReport() {
queryKey: ['gp-top-sellers', submittedFromDate, submittedToDate], queryKey: ['gp-top-sellers', submittedFromDate, submittedToDate],
queryFn: async () => { queryFn: async () => {
const res = await fetch(`/kitchen/api/reports/gp/top-sellers?from_date=${submittedFromDate}&to_date=${submittedToDate}`, { const res = await fetch(`/kitchen/api/reports/gp/top-sellers?from_date=${submittedFromDate}&to_date=${submittedToDate}`, {
headers: { Authorization: `Bearer ${token}` }, credentials: 'include',
}) })
if (!res.ok) { if (!res.ok) {
// Don't throw for top sellers - just return empty data // Don't throw for top sellers - just return empty data
@ -554,12 +562,62 @@ export default function GPReport() {
// Calculate GP with selected allowances + CD adjustments // Calculate GP with selected allowances + CD adjustments
const gpWithSelectedAllowances = salesNum > 0 const gpWithSelectedAllowances = salesNum > 0
? ((salesNum - adjustedPurchases + selectedAllowancesTotal) / salesNum * 100) ? Math.min((salesNum - adjustedPurchases + selectedAllowancesTotal) / salesNum * 100, 100)
: 0 : 0
const downloadCSV = () => {
if (!data) return
const rows: string[][] = [
['Kitchen Flash GP Report'],
['Period', period_label],
[],
['Metric', 'Value'],
['Net Food Sales', net_food_sales.toString()],
['Net Food Purchases', net_food_purchases.toString()],
['Gross Profit', gross_profit.toString()],
['Gross Profit %', Number(gross_profit_percent).toFixed(2)],
]
if (hasAnyAllowancesData) {
rows.push([])
rows.push(['Adjustments', ''])
if (hasWastage) rows.push(['Wastage', (wastage_total ?? 0).toString()])
if (hasTransfer) rows.push(['Transfers', (transfer_total ?? 0).toString()])
if (hasStaffFood) rows.push(['Staff Food', (staff_food_total ?? 0).toString()])
if (hasManualAdjustment) rows.push(['Manual Adjustments', (manual_adjustment_total ?? 0).toString()])
if (hasDisputes) rows.push(['Disputes', (disputes_total ?? 0).toString()])
rows.push(['GP with Adjustments %', gpWithSelectedAllowances.toFixed(2)])
}
if (data.supplier_breakdown?.length) {
rows.push([])
rows.push(['Supplier', 'Net Purchases', '% of Total'])
data.supplier_breakdown.forEach(s => {
rows.push([s.supplier_name, s.net_purchases.toString(), s.percentage.toFixed(1)])
})
}
if (chartData?.data?.length) {
rows.push([])
rows.push(['Date', 'Net Sales', 'Net Purchases', 'Covers'])
chartData.data.forEach(d => {
rows.push([d.date, d.net_sales.toString(), d.net_purchases.toString(), (d.total_covers ?? '').toString()])
})
}
const csv = rows.map(r => r.map(c => `"${String(c).replace(/"/g, '""')}"`).join(',')).join('\n')
const blob = new Blob([csv], { type: 'text/csv' })
const url = URL.createObjectURL(blob)
const a = document.createElement('a')
a.href = url
a.download = `kitchen-flash-${submittedFromDate}-to-${submittedToDate}.csv`
a.click()
URL.revokeObjectURL(url)
}
// Toggle checkbox handler // Toggle checkbox handler
const toggleAllowance = (key: keyof typeof allowancesSelection) => { const toggleAllowance = (key: keyof typeof allowancesSelection) => {
setAllowancesSelection(prev => ({ ...prev, [key]: !prev[key] })) setAllowancesSelection(prev => {
const next = { ...prev, [key]: !prev[key] }
try { localStorage.setItem('gpreport_allowances_v1', JSON.stringify(next)) } catch {}
return next
})
} }
return ( return (
@ -626,7 +684,14 @@ export default function GPReport() {
</div> </div>
{/* Period Label */} {/* Period Label */}
<div style={styles.periodLabel}>{getPeriodPrefix()}{period_label}</div> <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', marginBottom: '1rem' }}>
<div style={styles.periodLabel}>{getPeriodPrefix()}{period_label}</div>
{data && (
<button onClick={downloadCSV} style={styles.csvBtn}>
Download CSV
</button>
)}
</div>
{/* Main Content - GP Estimate and Chart side by side */} {/* Main Content - GP Estimate and Chart side by side */}
<div style={styles.mainContent}> <div style={styles.mainContent}>
@ -1252,9 +1317,18 @@ const styles: Record<string, React.CSSProperties> = {
fontSize: '1.1rem', fontSize: '1.1rem',
fontWeight: 'bold', fontWeight: 'bold',
color: '#1a1a2e', color: '#1a1a2e',
marginBottom: '1rem',
textAlign: 'center', textAlign: 'center',
}, },
csvBtn: {
padding: '0.4rem 0.9rem',
fontSize: '0.8rem',
background: 'transparent',
border: '1px solid #1a1a2e',
color: '#1a1a2e',
borderRadius: '6px',
cursor: 'pointer',
whiteSpace: 'nowrap' as const,
},
mainContent: { mainContent: {
display: 'flex', display: 'flex',
gap: '1.5rem', gap: '1.5rem',

View file

@ -84,12 +84,12 @@ export default function IngredientFlagEditor({ ingredientId, token, onChange, on
queryKey: ['food-flag-categories'], queryKey: ['food-flag-categories'],
queryFn: async () => { queryFn: async () => {
const res = await fetch('/kitchen/api/food-flags/categories', { const res = await fetch('/kitchen/api/food-flags/categories', {
headers: { Authorization: `Bearer ${token}` }, credentials: 'include',
}) })
if (!res.ok) return [] if (!res.ok) return []
return res.json() return res.json()
}, },
enabled: !!token, enabled: true,
}) })
// Fetch current ingredient flags (edit mode only) // Fetch current ingredient flags (edit mode only)
@ -97,7 +97,7 @@ export default function IngredientFlagEditor({ ingredientId, token, onChange, on
queryKey: ['ingredient-flags', ingredientId], queryKey: ['ingredient-flags', ingredientId],
queryFn: async () => { queryFn: async () => {
const res = await fetch(`/kitchen/api/ingredients/${ingredientId}/flags`, { const res = await fetch(`/kitchen/api/ingredients/${ingredientId}/flags`, {
headers: { Authorization: `Bearer ${token}` }, credentials: 'include',
}) })
if (!res.ok) return [] if (!res.ok) return []
return res.json() return res.json()
@ -110,7 +110,7 @@ export default function IngredientFlagEditor({ ingredientId, token, onChange, on
queryKey: ['ingredient-flag-nones', ingredientId], queryKey: ['ingredient-flag-nones', ingredientId],
queryFn: async () => { queryFn: async () => {
const res = await fetch(`/kitchen/api/ingredients/${ingredientId}/flags/nones`, { const res = await fetch(`/kitchen/api/ingredients/${ingredientId}/flags/nones`, {
headers: { Authorization: `Bearer ${token}` }, credentials: 'include',
}) })
if (!res.ok) return { none_category_ids: [] } if (!res.ok) return { none_category_ids: [] }
return res.json() return res.json()
@ -123,7 +123,7 @@ export default function IngredientFlagEditor({ ingredientId, token, onChange, on
queryKey: ['ingredient-flag-dismissals', ingredientId], queryKey: ['ingredient-flag-dismissals', ingredientId],
queryFn: async () => { queryFn: async () => {
const res = await fetch(`/kitchen/api/ingredients/${ingredientId}/flags/dismissals`, { const res = await fetch(`/kitchen/api/ingredients/${ingredientId}/flags/dismissals`, {
headers: { Authorization: `Bearer ${token}` }, credentials: 'include',
}) })
if (!res.ok) return [] if (!res.ok) return []
return res.json() return res.json()
@ -140,7 +140,7 @@ export default function IngredientFlagEditor({ ingredientId, token, onChange, on
if (debouncedLineItem) params.set('line_item', debouncedLineItem) if (debouncedLineItem) params.set('line_item', debouncedLineItem)
if (debouncedText) params.set('text', debouncedText) if (debouncedText) params.set('text', debouncedText)
const res = await fetch(`/kitchen/api/food-flags/suggest?${params}`, { const res = await fetch(`/kitchen/api/food-flags/suggest?${params}`, {
headers: { Authorization: `Bearer ${token}` }, credentials: 'include',
}) })
if (!res.ok) return [] if (!res.ok) return []
return res.json() return res.json()
@ -218,7 +218,7 @@ export default function IngredientFlagEditor({ ingredientId, token, onChange, on
if (ingredientId) { if (ingredientId) {
fetch(`/kitchen/api/ingredients/${ingredientId}/flags`, { fetch(`/kitchen/api/ingredients/${ingredientId}/flags`, {
method: 'PUT', method: 'PUT',
headers: { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json' }, credentials: 'include', headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ food_flag_ids: [...newFlags] }), body: JSON.stringify({ food_flag_ids: [...newFlags] }),
}).then(() => { }).then(() => {
queryClient.invalidateQueries({ queryKey: ['ingredient-flags', ingredientId] }) queryClient.invalidateQueries({ queryKey: ['ingredient-flags', ingredientId] })
@ -255,7 +255,7 @@ export default function IngredientFlagEditor({ ingredientId, token, onChange, on
if (!hasActiveFlags) { if (!hasActiveFlags) {
fetch(`/kitchen/api/ingredients/${ingredientId}/flags/none`, { fetch(`/kitchen/api/ingredients/${ingredientId}/flags/none`, {
method: 'POST', method: 'POST',
headers: { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json' }, credentials: 'include', headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ category_id: catId }), body: JSON.stringify({ category_id: catId }),
}).catch(() => {}) }).catch(() => {})
} }
@ -327,7 +327,7 @@ export default function IngredientFlagEditor({ ingredientId, token, onChange, on
try { try {
await fetch(`/kitchen/api/ingredients/${ingredientId}/flags`, { await fetch(`/kitchen/api/ingredients/${ingredientId}/flags`, {
method: 'PUT', method: 'PUT',
headers: { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json' }, credentials: 'include', headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ food_flag_ids: [...newFlags] }), body: JSON.stringify({ food_flag_ids: [...newFlags] }),
}) })
queryClient.invalidateQueries({ queryKey: ['ingredient-flags', ingredientId] }) queryClient.invalidateQueries({ queryKey: ['ingredient-flags', ingredientId] })
@ -358,7 +358,7 @@ export default function IngredientFlagEditor({ ingredientId, token, onChange, on
try { try {
await fetch(`/kitchen/api/ingredients/${ingredientId}/flags/none`, { await fetch(`/kitchen/api/ingredients/${ingredientId}/flags/none`, {
method: 'POST', method: 'POST',
headers: { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json' }, credentials: 'include', headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ category_id: categoryId }), body: JSON.stringify({ category_id: categoryId }),
}) })
queryClient.invalidateQueries({ queryKey: ['ingredient-flags', ingredientId] }) queryClient.invalidateQueries({ queryKey: ['ingredient-flags', ingredientId] })
@ -395,7 +395,7 @@ export default function IngredientFlagEditor({ ingredientId, token, onChange, on
try { try {
const res = await fetch(`/kitchen/api/ingredients/${ingredientId}/flags/dismissals`, { const res = await fetch(`/kitchen/api/ingredients/${ingredientId}/flags/dismissals`, {
method: 'POST', method: 'POST',
headers: { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json' }, credentials: 'include', headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(dismissal), body: JSON.stringify(dismissal),
}) })
if (res.ok) { if (res.ok) {
@ -423,7 +423,7 @@ export default function IngredientFlagEditor({ ingredientId, token, onChange, on
try { try {
await fetch(`/kitchen/api/ingredients/${ingredientId}/flags/dismissals/${dismissal.id}`, { await fetch(`/kitchen/api/ingredients/${ingredientId}/flags/dismissals/${dismissal.id}`, {
method: 'DELETE', method: 'DELETE',
headers: { Authorization: `Bearer ${token}` }, credentials: 'include',
}) })
} catch { /* ignore */ } } catch { /* ignore */ }
} }
@ -454,7 +454,7 @@ export default function IngredientFlagEditor({ ingredientId, token, onChange, on
try { try {
await fetch(`/kitchen/api/ingredients/${ingredientId}/flags`, { await fetch(`/kitchen/api/ingredients/${ingredientId}/flags`, {
method: 'PUT', method: 'PUT',
headers: { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json' }, credentials: 'include', headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ food_flag_ids: [...newFlags] }), body: JSON.stringify({ food_flag_ids: [...newFlags] }),
}) })
queryClient.invalidateQueries({ queryKey: ['ingredient-flags', ingredientId] }) queryClient.invalidateQueries({ queryKey: ['ingredient-flags', ingredientId] })

View file

@ -166,7 +166,7 @@ export default function IngredientModal({
queryKey: ['ingredient-categories'], queryKey: ['ingredient-categories'],
queryFn: async () => { queryFn: async () => {
const res = await fetch('/kitchen/api/ingredients/categories', { const res = await fetch('/kitchen/api/ingredients/categories', {
headers: { Authorization: `Bearer ${token}` }, credentials: 'include',
}) })
if (!res.ok) throw new Error('Failed to fetch categories') if (!res.ok) throw new Error('Failed to fetch categories')
return res.json() return res.json()
@ -177,7 +177,7 @@ export default function IngredientModal({
const { data: liSuppliers } = useQuery<Array<{ id: number; name: string }>>({ const { data: liSuppliers } = useQuery<Array<{ id: number; name: string }>>({
queryKey: ['suppliers'], queryKey: ['suppliers'],
queryFn: async () => { queryFn: async () => {
const res = await fetch('/kitchen/api/suppliers/', { headers: { Authorization: `Bearer ${token}` } }) const res = await fetch('/kitchen/api/suppliers/', { credentials: 'include' })
if (!res.ok) return [] if (!res.ok) return []
const data = await res.json() const data = await res.json()
return data.suppliers || data || [] return data.suppliers || data || []
@ -200,7 +200,7 @@ export default function IngredientModal({
queryKey: ['ingredient-sources', editingIngredient?.id], queryKey: ['ingredient-sources', editingIngredient?.id],
queryFn: async () => { queryFn: async () => {
const res = await fetch(`/kitchen/api/ingredients/${editingIngredient!.id}/sources`, { const res = await fetch(`/kitchen/api/ingredients/${editingIngredient!.id}/sources`, {
headers: { Authorization: `Bearer ${token}` }, credentials: 'include',
}) })
if (!res.ok) return [] if (!res.ok) return []
return res.json() return res.json()
@ -217,7 +217,7 @@ export default function IngredientModal({
if (liSupplierId) params.set('supplier_id', liSupplierId) if (liSupplierId) params.set('supplier_id', liSupplierId)
params.set('limit', '100') params.set('limit', '100')
const res = await fetch(`/kitchen/api/search/line-items?${params}`, { const res = await fetch(`/kitchen/api/search/line-items?${params}`, {
headers: { Authorization: `Bearer ${token}` }, credentials: 'include',
}) })
if (!res.ok) return { items: [], total_count: 0 } if (!res.ok) return { items: [], total_count: 0 }
return res.json() return res.json()
@ -231,7 +231,7 @@ export default function IngredientModal({
queryKey: ['settings-llm-check'], queryKey: ['settings-llm-check'],
queryFn: async () => { queryFn: async () => {
const res = await fetch('/kitchen/api/settings/', { const res = await fetch('/kitchen/api/settings/', {
headers: { Authorization: `Bearer ${token}` }, credentials: 'include',
}) })
if (!res.ok) return { llm_enabled: false } if (!res.ok) return { llm_enabled: false }
return res.json() return res.json()
@ -254,8 +254,8 @@ export default function IngredientModal({
try { try {
const res = await fetch('/kitchen/api/food-flags/analyse-label', { const res = await fetch('/kitchen/api/food-flags/analyse-label', {
method: 'POST', method: 'POST',
credentials: 'include',
headers: { headers: {
Authorization: `Bearer ${token}`,
'Content-Type': 'application/json', 'Content-Type': 'application/json',
}, },
body: JSON.stringify({ ingredients_text: debouncedProductIngredients }), body: JSON.stringify({ ingredients_text: debouncedProductIngredients }),
@ -295,7 +295,7 @@ export default function IngredientModal({
setYieldHintLoading(true) setYieldHintLoading(true)
try { try {
const res = await fetch(`/kitchen/api/ingredients/ai-estimate-yield?name=${encodeURIComponent(debouncedFormName.trim())}`, { const res = await fetch(`/kitchen/api/ingredients/ai-estimate-yield?name=${encodeURIComponent(debouncedFormName.trim())}`, {
headers: { Authorization: `Bearer ${token}` }, credentials: 'include',
}) })
if (res.ok && !cancelled) { if (res.ok && !cancelled) {
const data = await res.json() const data = await res.json()
@ -390,7 +390,7 @@ export default function IngredientModal({
const params = new URLSearchParams({ [lookup.paramName]: productCode }) const params = new URLSearchParams({ [lookup.paramName]: productCode })
if (force) params.set('force', 'true') if (force) params.set('force', 'true')
const res = await fetch(`${lookup.endpoint}?${params}`, { const res = await fetch(`${lookup.endpoint}?${params}`, {
headers: { Authorization: `Bearer ${token}` }, credentials: 'include',
}) })
if (res.ok) { if (res.ok) {
const data = await res.json() const data = await res.json()
@ -438,7 +438,7 @@ export default function IngredientModal({
: '/kitchen/api/food-flags/scan-label' : '/kitchen/api/food-flags/scan-label'
const res = await fetch(url, { const res = await fetch(url, {
method: 'POST', method: 'POST',
headers: { Authorization: `Bearer ${token}` }, credentials: 'include',
body: formData, body: formData,
}) })
if (res.ok) { if (res.ok) {
@ -469,7 +469,7 @@ export default function IngredientModal({
if (selectedLi.most_recent_invoice_id) sourceData.invoice_id = selectedLi.most_recent_invoice_id if (selectedLi.most_recent_invoice_id) sourceData.invoice_id = selectedLi.most_recent_invoice_id
const srcRes = await fetch(`/kitchen/api/ingredients/${ingredientId}/sources`, { const srcRes = await fetch(`/kitchen/api/ingredients/${ingredientId}/sources`, {
method: 'POST', method: 'POST',
headers: { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json' }, credentials: 'include', headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(sourceData), body: JSON.stringify(sourceData),
}) })
if (!srcRes.ok) { if (!srcRes.ok) {
@ -486,7 +486,7 @@ export default function IngredientModal({
try { try {
await fetch(`/kitchen/api/ingredients/${ingredientId}/flags`, { await fetch(`/kitchen/api/ingredients/${ingredientId}/flags`, {
method: 'PUT', method: 'PUT',
headers: { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json' }, credentials: 'include', headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ food_flag_ids: pendingFlagIds }), body: JSON.stringify({ food_flag_ids: pendingFlagIds }),
}) })
} catch { /* ignore */ } } catch { /* ignore */ }
@ -495,7 +495,7 @@ export default function IngredientModal({
try { try {
await fetch(`/kitchen/api/ingredients/${ingredientId}/flags/none`, { await fetch(`/kitchen/api/ingredients/${ingredientId}/flags/none`, {
method: 'POST', method: 'POST',
headers: { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json' }, credentials: 'include', headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ category_id: catId }), body: JSON.stringify({ category_id: catId }),
}) })
} catch { /* ignore */ } } catch { /* ignore */ }
@ -505,7 +505,7 @@ export default function IngredientModal({
try { try {
await fetch(`/kitchen/api/ingredients/${ingredientId}/flags/dismissals/batch`, { await fetch(`/kitchen/api/ingredients/${ingredientId}/flags/dismissals/batch`, {
method: 'POST', method: 'POST',
headers: { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json' }, credentials: 'include', headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ dismissals: pendingDismissals }), body: JSON.stringify({ dismissals: pendingDismissals }),
}) })
} catch { /* ignore */ } } catch { /* ignore */ }
@ -529,7 +529,7 @@ export default function IngredientModal({
setFormFree(editingIngredient.is_free || false) setFormFree(editingIngredient.is_free || false)
setFormPrepackaged(editingIngredient.is_prepackaged || false) setFormPrepackaged(editingIngredient.is_prepackaged || false)
setFormProductIngredients(editingIngredient.product_ingredients || '') setFormProductIngredients(editingIngredient.product_ingredients || '')
setLabelPreview(editingIngredient.has_label_image ? `/kitchen/api/ingredients/${editingIngredient.id}/label-image?token=${encodeURIComponent(token || '')}` : null) setLabelPreview(editingIngredient.has_label_image ? `/kitchen/api/ingredients/${editingIngredient.id}/label-image` : null)
setLiSearch(editingIngredient.name) setLiSearch(editingIngredient.name)
} else { } else {
const name = prePopulateName || '' const name = prePopulateName || ''
@ -551,7 +551,7 @@ export default function IngredientModal({
// LLM FEATURE — AI pack size deduction when regex can't parse // LLM FEATURE — AI pack size deduction when regex can't parse
setAiPackLoading(true) setAiPackLoading(true)
fetch(`/kitchen/api/ingredients/ai-pack-size?description=${encodeURIComponent(preSelectLineItem.description)}`, { fetch(`/kitchen/api/ingredients/ai-pack-size?description=${encodeURIComponent(preSelectLineItem.description)}`, {
headers: { Authorization: `Bearer ${token}` }, credentials: 'include',
}).then(res => res.ok ? res.json() : null).then(data => { }).then(res => res.ok ? res.json() : null).then(data => {
if (data?.pack_quantity && data?.unit_size) { if (data?.pack_quantity && data?.unit_size) {
setLiPackQty(data.pack_quantity) setLiPackQty(data.pack_quantity)
@ -590,7 +590,7 @@ export default function IngredientModal({
const timer = setTimeout(async () => { const timer = setTimeout(async () => {
try { try {
const res = await fetch(`/kitchen/api/ingredients/check-duplicate?name=${encodeURIComponent(formName)}`, { const res = await fetch(`/kitchen/api/ingredients/check-duplicate?name=${encodeURIComponent(formName)}`, {
headers: { Authorization: `Bearer ${token}` }, credentials: 'include',
}) })
if (res.ok) { if (res.ok) {
const data = await res.json() const data = await res.json()
@ -607,7 +607,7 @@ export default function IngredientModal({
mutationFn: async (data: Record<string, unknown>) => { mutationFn: async (data: Record<string, unknown>) => {
const res = await fetch('/kitchen/api/ingredients', { const res = await fetch('/kitchen/api/ingredients', {
method: 'POST', method: 'POST',
headers: { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json' }, credentials: 'include', headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data), body: JSON.stringify(data),
}) })
if (!res.ok) { if (!res.ok) {
@ -630,7 +630,7 @@ export default function IngredientModal({
mutationFn: async ({ id, data }: { id: number; data: Record<string, unknown> }) => { mutationFn: async ({ id, data }: { id: number; data: Record<string, unknown> }) => {
const res = await fetch(`/kitchen/api/ingredients/${id}`, { const res = await fetch(`/kitchen/api/ingredients/${id}`, {
method: 'PATCH', method: 'PATCH',
headers: { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json' }, credentials: 'include', headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data), body: JSON.stringify(data),
}) })
if (!res.ok) throw new Error('Failed to update') if (!res.ok) throw new Error('Failed to update')
@ -988,7 +988,7 @@ export default function IngredientModal({
title="Click to enlarge" title="Click to enlarge"
> >
<img <img
src={`/kitchen/api/invoices/${selectedLi.most_recent_invoice_id}/line-items/${selectedLi.most_recent_line_number}/preview?token=${encodeURIComponent(token || '')}`} src={`/kitchen/api/invoices/${selectedLi.most_recent_invoice_id}/line-items/${selectedLi.most_recent_line_number}/preview`}
alt="Invoice line item" alt="Invoice line item"
style={{ width: '100%', height: 'auto', display: 'block' }} style={{ width: '100%', height: 'auto', display: 'block' }}
onError={(e) => { (e.target as HTMLImageElement).parentElement!.style.display = 'none' }} onError={(e) => { (e.target as HTMLImageElement).parentElement!.style.display = 'none' }}
@ -1058,7 +1058,7 @@ export default function IngredientModal({
{selectedLi.most_recent_line_number != null && ( {selectedLi.most_recent_line_number != null && (
<div style={{ flex: '0 0 auto', maxWidth: '120px', borderRadius: '4px', overflow: 'hidden', border: '1px solid #e0e0e0' }}> <div style={{ flex: '0 0 auto', maxWidth: '120px', borderRadius: '4px', overflow: 'hidden', border: '1px solid #e0e0e0' }}>
<img <img
src={`/kitchen/api/invoices/${selectedLi.most_recent_invoice_id}/line-items/${selectedLi.most_recent_line_number}/preview/field/product_code?token=${encodeURIComponent(token || '')}`} src={`/kitchen/api/invoices/${selectedLi.most_recent_invoice_id}/line-items/${selectedLi.most_recent_line_number}/preview/field/product_code`}
alt="Product code from invoice" alt="Product code from invoice"
style={{ width: '100%', height: 'auto', display: 'block' }} style={{ width: '100%', height: 'auto', display: 'block' }}
onError={(e) => { (e.target as HTMLImageElement).parentElement!.style.display = 'none' }} onError={(e) => { (e.target as HTMLImageElement).parentElement!.style.display = 'none' }}
@ -1246,7 +1246,7 @@ export default function IngredientModal({
{'\u2715'} {'\u2715'}
</button> </button>
<img <img
src={`/kitchen/api/invoices/${selectedLi.most_recent_invoice_id}/line-items/${selectedLi.most_recent_line_number}/preview?token=${encodeURIComponent(token || '')}`} src={`/kitchen/api/invoices/${selectedLi.most_recent_invoice_id}/line-items/${selectedLi.most_recent_line_number}/preview`}
alt="Invoice line item" alt="Invoice line item"
style={{ maxWidth: '95vw', maxHeight: '90vh', borderRadius: '8px', boxShadow: '0 4px 20px rgba(0,0,0,0.3)' }} style={{ maxWidth: '95vw', maxHeight: '90vh', borderRadius: '8px', boxShadow: '0 4px 20px rgba(0,0,0,0.3)' }}
/> />

View file

@ -67,12 +67,12 @@ export default function Ingredients() {
queryKey: ['ingredient-categories'], queryKey: ['ingredient-categories'],
queryFn: async () => { queryFn: async () => {
const res = await fetch('/kitchen/api/ingredients/categories', { const res = await fetch('/kitchen/api/ingredients/categories', {
headers: { Authorization: `Bearer ${token}` }, credentials: 'include',
}) })
if (!res.ok) throw new Error('Failed to fetch categories') if (!res.ok) throw new Error('Failed to fetch categories')
return res.json() return res.json()
}, },
enabled: !!token, enabled: true,
}) })
// Ingredients list // Ingredients list
@ -85,12 +85,12 @@ export default function Ingredients() {
if (showUnmapped) params.set('unmapped', 'true') if (showUnmapped) params.set('unmapped', 'true')
if (showArchived) params.set('archived', 'true') if (showArchived) params.set('archived', 'true')
const res = await fetch(`/kitchen/api/ingredients?${params}`, { const res = await fetch(`/kitchen/api/ingredients?${params}`, {
headers: { Authorization: `Bearer ${token}` }, credentials: 'include',
}) })
if (!res.ok) throw new Error('Failed to fetch ingredients') if (!res.ok) throw new Error('Failed to fetch ingredients')
return res.json() return res.json()
}, },
enabled: !!token, enabled: true,
}) })
// Sources for expanded ingredient // Sources for expanded ingredient
@ -98,7 +98,7 @@ export default function Ingredients() {
queryKey: ['ingredient-sources', expandedId], queryKey: ['ingredient-sources', expandedId],
queryFn: async () => { queryFn: async () => {
const res = await fetch(`/kitchen/api/ingredients/${expandedId}/sources`, { const res = await fetch(`/kitchen/api/ingredients/${expandedId}/sources`, {
headers: { Authorization: `Bearer ${token}` }, credentials: 'include',
}) })
if (!res.ok) throw new Error('Failed to fetch sources') if (!res.ok) throw new Error('Failed to fetch sources')
return res.json() return res.json()
@ -110,7 +110,7 @@ export default function Ingredients() {
mutationFn: async (id: number) => { mutationFn: async (id: number) => {
const res = await fetch(`/kitchen/api/ingredients/${id}`, { const res = await fetch(`/kitchen/api/ingredients/${id}`, {
method: 'DELETE', method: 'DELETE',
headers: { Authorization: `Bearer ${token}` }, credentials: 'include',
}) })
if (!res.ok) throw new Error('Failed to archive') if (!res.ok) throw new Error('Failed to archive')
}, },
@ -121,7 +121,7 @@ export default function Ingredients() {
mutationFn: async (id: number) => { mutationFn: async (id: number) => {
const res = await fetch(`/kitchen/api/ingredients/${id}`, { const res = await fetch(`/kitchen/api/ingredients/${id}`, {
method: 'PATCH', method: 'PATCH',
headers: { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json' }, credentials: 'include', headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ is_archived: false }), body: JSON.stringify({ is_archived: false }),
}) })
if (!res.ok) throw new Error('Failed to unarchive') if (!res.ok) throw new Error('Failed to unarchive')

View file

@ -101,7 +101,7 @@ export default function InvoiceList() {
queryKey: ['suppliers'], queryKey: ['suppliers'],
queryFn: async () => { queryFn: async () => {
const res = await fetch('/kitchen/api/suppliers/', { const res = await fetch('/kitchen/api/suppliers/', {
headers: { Authorization: `Bearer ${token}` }, credentials: 'include',
}) })
if (!res.ok) return [] if (!res.ok) return []
return res.json() return res.json()
@ -121,7 +121,7 @@ export default function InvoiceList() {
queryFn: async () => { queryFn: async () => {
const url = queryString ? `/kitchen/api/invoices/?${queryString}` : '/kitchen/api/invoices/' const url = queryString ? `/kitchen/api/invoices/?${queryString}` : '/kitchen/api/invoices/'
const res = await fetch(url, { const res = await fetch(url, {
headers: { Authorization: `Bearer ${token}` }, credentials: 'include',
}) })
if (!res.ok) throw new Error('Failed to fetch invoices') if (!res.ok) throw new Error('Failed to fetch invoices')
return res.json() return res.json()
@ -138,7 +138,7 @@ export default function InvoiceList() {
params.set('limit', '20') params.set('limit', '20')
params.set('sort', 'recent') params.set('sort', 'recent')
const res = await fetch(`/kitchen/api/invoices/?${params}`, { const res = await fetch(`/kitchen/api/invoices/?${params}`, {
headers: { Authorization: `Bearer ${token}` }, credentials: 'include',
}) })
if (!res.ok) throw new Error('Failed to fetch completed invoices') if (!res.ok) throw new Error('Failed to fetch completed invoices')
return res.json() return res.json()

View file

@ -88,7 +88,7 @@ export default function LineItemHistoryModal({
if (dateTo) params.set('date_to', dateTo) if (dateTo) params.set('date_to', dateTo)
const res = await fetch(`/kitchen/api/search/line-items/history?${params}`, { const res = await fetch(`/kitchen/api/search/line-items/history?${params}`, {
headers: { Authorization: `Bearer ${token}` }, credentials: 'include',
}) })
if (!res.ok) throw new Error('Failed to fetch history') if (!res.ok) throw new Error('Failed to fetch history')
return res.json() return res.json()
@ -101,8 +101,8 @@ export default function LineItemHistoryModal({
mutationFn: async () => { mutationFn: async () => {
const res = await fetch('/kitchen/api/search/line-items/acknowledge-price', { const res = await fetch('/kitchen/api/search/line-items/acknowledge-price', {
method: 'POST', method: 'POST',
credentials: 'include',
headers: { headers: {
Authorization: `Bearer ${token}`,
'Content-Type': 'application/json', 'Content-Type': 'application/json',
}, },
body: JSON.stringify({ body: JSON.stringify({

View file

@ -65,7 +65,7 @@ export default function LinkDisputeModal({
queryKey: ['open-disputes', supplierId], queryKey: ['open-disputes', supplierId],
queryFn: async () => { queryFn: async () => {
const res = await fetch(`/kitchen/api/disputes/supplier/${supplierId}/open`, { const res = await fetch(`/kitchen/api/disputes/supplier/${supplierId}/open`, {
headers: { Authorization: `Bearer ${token}` }, credentials: 'include',
}) })
if (!res.ok) throw new Error('Failed to fetch disputes') if (!res.ok) throw new Error('Failed to fetch disputes')
return res.json() return res.json()
@ -80,9 +80,9 @@ export default function LinkDisputeModal({
const res = await fetch(`/kitchen/api/disputes/${selectedDisputeId}/link-credit-note`, { const res = await fetch(`/kitchen/api/disputes/${selectedDisputeId}/link-credit-note`, {
method: 'POST', method: 'POST',
credentials: 'include',
headers: { headers: {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
Authorization: `Bearer ${token}`,
}, },
body: JSON.stringify({ body: JSON.stringify({
credit_note_invoice_id: creditNoteInvoiceId, credit_note_invoice_id: creditNoteInvoiceId,

View file

@ -95,7 +95,7 @@ export default function MapLineItemsModal({ ingredient, onClose, onSaved }: MapL
const { data: suppliers } = useQuery<Supplier[]>({ const { data: suppliers } = useQuery<Supplier[]>({
queryKey: ['suppliers'], queryKey: ['suppliers'],
queryFn: async () => { queryFn: async () => {
const res = await fetch('/kitchen/api/suppliers/', { headers: { Authorization: `Bearer ${token}` } }) const res = await fetch('/kitchen/api/suppliers/', { credentials: 'include' })
if (!res.ok) return [] if (!res.ok) return []
const data = await res.json() const data = await res.json()
return data.suppliers || data || [] return data.suppliers || data || []
@ -113,7 +113,7 @@ export default function MapLineItemsModal({ ingredient, onClose, onSaved }: MapL
params.set('date_to', dateTo) params.set('date_to', dateTo)
params.set('limit', '50') params.set('limit', '50')
const res = await fetch(`/kitchen/api/search/line-items?${params}`, { const res = await fetch(`/kitchen/api/search/line-items?${params}`, {
headers: { Authorization: `Bearer ${token}` }, credentials: 'include',
}) })
if (!res.ok) return { items: [], total_count: 0 } if (!res.ok) return { items: [], total_count: 0 }
return res.json() return res.json()
@ -208,7 +208,7 @@ export default function MapLineItemsModal({ ingredient, onClose, onSaved }: MapL
const res = await fetch(`/kitchen/api/ingredients/${ingredient.id}/sources`, { const res = await fetch(`/kitchen/api/ingredients/${ingredient.id}/sources`, {
method: 'POST', method: 'POST',
headers: { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json' }, credentials: 'include', headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(sourceData), body: JSON.stringify(sourceData),
}) })
@ -233,7 +233,7 @@ export default function MapLineItemsModal({ ingredient, onClose, onSaved }: MapL
// Preview URL for the selected line item // Preview URL for the selected line item
const previewUrl = selectedItem?.most_recent_invoice_id && selectedItem?.most_recent_line_number != null const previewUrl = selectedItem?.most_recent_invoice_id && selectedItem?.most_recent_line_number != null
? `/kitchen/api/invoices/${selectedItem.most_recent_invoice_id}/line-items/${selectedItem.most_recent_line_number}/preview?token=${token}` ? `/kitchen/api/invoices/${selectedItem.most_recent_invoice_id}/line-items/${selectedItem.most_recent_line_number}/preview`
: null : null
return ( return (

View file

@ -85,7 +85,7 @@ export default function MenuEditor() {
queryKey: ['menu', menuId], queryKey: ['menu', menuId],
queryFn: async () => { queryFn: async () => {
const res = await fetch(`/kitchen/api/menus/${menuId}`, { const res = await fetch(`/kitchen/api/menus/${menuId}`, {
headers: { Authorization: `Bearer ${token}` }, credentials: 'include',
}) })
if (!res.ok) throw new Error('Failed to fetch menu') if (!res.ok) throw new Error('Failed to fetch menu')
return res.json() return res.json()
@ -99,7 +99,7 @@ export default function MenuEditor() {
mutationFn: async (data: Record<string, unknown>) => { mutationFn: async (data: Record<string, unknown>) => {
const res = await fetch(`/kitchen/api/menus/${menuId}`, { const res = await fetch(`/kitchen/api/menus/${menuId}`, {
method: 'PUT', method: 'PUT',
headers: { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json' }, credentials: 'include', headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data), body: JSON.stringify(data),
}) })
if (!res.ok) throw new Error('Failed to update') if (!res.ok) throw new Error('Failed to update')
@ -111,7 +111,7 @@ export default function MenuEditor() {
mutationFn: async (name: string) => { mutationFn: async (name: string) => {
const res = await fetch(`/kitchen/api/menus/${menuId}/divisions`, { const res = await fetch(`/kitchen/api/menus/${menuId}/divisions`, {
method: 'POST', method: 'POST',
headers: { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json' }, credentials: 'include', headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ name }), body: JSON.stringify({ name }),
}) })
if (!res.ok) throw new Error('Failed to add division') if (!res.ok) throw new Error('Failed to add division')
@ -127,7 +127,7 @@ export default function MenuEditor() {
mutationFn: async ({ divId, name }: { divId: number; name: string }) => { mutationFn: async ({ divId, name }: { divId: number; name: string }) => {
const res = await fetch(`/kitchen/api/menus/${menuId}/divisions/${divId}`, { const res = await fetch(`/kitchen/api/menus/${menuId}/divisions/${divId}`, {
method: 'PUT', method: 'PUT',
headers: { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json' }, credentials: 'include', headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ name }), body: JSON.stringify({ name }),
}) })
if (!res.ok) throw new Error('Failed to rename') if (!res.ok) throw new Error('Failed to rename')
@ -142,7 +142,7 @@ export default function MenuEditor() {
mutationFn: async (divId: number) => { mutationFn: async (divId: number) => {
const res = await fetch(`/kitchen/api/menus/${menuId}/divisions/${divId}`, { const res = await fetch(`/kitchen/api/menus/${menuId}/divisions/${divId}`, {
method: 'DELETE', method: 'DELETE',
headers: { Authorization: `Bearer ${token}` }, credentials: 'include',
}) })
if (!res.ok) throw new Error('Failed to delete') if (!res.ok) throw new Error('Failed to delete')
}, },
@ -153,7 +153,7 @@ export default function MenuEditor() {
mutationFn: async (ids: number[]) => { mutationFn: async (ids: number[]) => {
const res = await fetch(`/kitchen/api/menus/${menuId}/divisions/reorder`, { const res = await fetch(`/kitchen/api/menus/${menuId}/divisions/reorder`, {
method: 'PATCH', method: 'PATCH',
headers: { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json' }, credentials: 'include', headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ ids }), body: JSON.stringify({ ids }),
}) })
if (!res.ok) throw new Error('Failed to reorder') if (!res.ok) throw new Error('Failed to reorder')
@ -165,7 +165,7 @@ export default function MenuEditor() {
mutationFn: async ({ itemId, data }: { itemId: number; data: Record<string, unknown> }) => { mutationFn: async ({ itemId, data }: { itemId: number; data: Record<string, unknown> }) => {
const res = await fetch(`/kitchen/api/menus/${menuId}/items/${itemId}`, { const res = await fetch(`/kitchen/api/menus/${menuId}/items/${itemId}`, {
method: 'PUT', method: 'PUT',
headers: { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json' }, credentials: 'include', headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data), body: JSON.stringify(data),
}) })
if (!res.ok) throw new Error('Failed to update') if (!res.ok) throw new Error('Failed to update')
@ -180,7 +180,7 @@ export default function MenuEditor() {
mutationFn: async (itemId: number) => { mutationFn: async (itemId: number) => {
const res = await fetch(`/kitchen/api/menus/${menuId}/items/${itemId}`, { const res = await fetch(`/kitchen/api/menus/${menuId}/items/${itemId}`, {
method: 'DELETE', method: 'DELETE',
headers: { Authorization: `Bearer ${token}` }, credentials: 'include',
}) })
if (!res.ok) throw new Error('Failed to remove') if (!res.ok) throw new Error('Failed to remove')
}, },
@ -191,7 +191,7 @@ export default function MenuEditor() {
mutationFn: async ({ itemId, confirmed_by_name }: { itemId: number; confirmed_by_name: string }) => { mutationFn: async ({ itemId, confirmed_by_name }: { itemId: number; confirmed_by_name: string }) => {
const res = await fetch(`/kitchen/api/menus/${menuId}/items/${itemId}/republish`, { const res = await fetch(`/kitchen/api/menus/${menuId}/items/${itemId}/republish`, {
method: 'POST', method: 'POST',
headers: { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json' }, credentials: 'include', headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ confirmed_by_name }), body: JSON.stringify({ confirmed_by_name }),
}) })
if (!res.ok) throw new Error('Failed to republish') if (!res.ok) throw new Error('Failed to republish')
@ -203,7 +203,7 @@ export default function MenuEditor() {
mutationFn: async (ids: number[]) => { mutationFn: async (ids: number[]) => {
const res = await fetch(`/kitchen/api/menus/${menuId}/items/reorder`, { const res = await fetch(`/kitchen/api/menus/${menuId}/items/reorder`, {
method: 'PATCH', method: 'PATCH',
headers: { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json' }, credentials: 'include', headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ ids }), body: JSON.stringify({ ids }),
}) })
if (!res.ok) throw new Error('Failed to reorder') if (!res.ok) throw new Error('Failed to reorder')
@ -215,7 +215,7 @@ export default function MenuEditor() {
mutationFn: async (body: { confirmed_by_name: string; items: Array<{ id: number; confirmed: boolean }> }) => { mutationFn: async (body: { confirmed_by_name: string; items: Array<{ id: number; confirmed: boolean }> }) => {
const res = await fetch(`/kitchen/api/menus/${menuId}/republish-stale`, { const res = await fetch(`/kitchen/api/menus/${menuId}/republish-stale`, {
method: 'POST', method: 'POST',
headers: { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json' }, credentials: 'include', headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(body), body: JSON.stringify(body),
}) })
if (!res.ok) throw new Error('Failed to batch republish') if (!res.ok) throw new Error('Failed to batch republish')
@ -233,7 +233,7 @@ export default function MenuEditor() {
formData.append('file', file) formData.append('file', file)
const res = await fetch(`/kitchen/api/menus/${menuId}/items/${itemId}/image`, { const res = await fetch(`/kitchen/api/menus/${menuId}/items/${itemId}/image`, {
method: 'POST', method: 'POST',
headers: { Authorization: `Bearer ${token}` }, credentials: 'include',
body: formData, body: formData,
}) })
if (!res.ok) throw new Error('Failed to upload image') if (!res.ok) throw new Error('Failed to upload image')
@ -245,7 +245,7 @@ export default function MenuEditor() {
mutationFn: async (itemId: number) => { mutationFn: async (itemId: number) => {
const res = await fetch(`/kitchen/api/menus/${menuId}/items/${itemId}/image`, { const res = await fetch(`/kitchen/api/menus/${menuId}/items/${itemId}/image`, {
method: 'DELETE', method: 'DELETE',
headers: { Authorization: `Bearer ${token}` }, credentials: 'include',
}) })
if (!res.ok) throw new Error('Failed to delete image') if (!res.ok) throw new Error('Failed to delete image')
}, },
@ -471,7 +471,7 @@ export default function MenuEditor() {
{/* Image thumbnail */} {/* Image thumbnail */}
{item.has_image ? ( {item.has_image ? (
<img <img
src={`/kitchen/api/menus/${menuId}/items/${item.id}/image?token=${token}`} src={`/kitchen/api/menus/${menuId}/items/${item.id}/image`}
alt="" alt=""
style={styles.thumbnail} style={styles.thumbnail}
/> />

View file

@ -40,12 +40,12 @@ export default function MenuFlagMatrix({ menuId, menuName, onClose }: Props) {
queryKey: ['menu-flag-matrix', menuId], queryKey: ['menu-flag-matrix', menuId],
queryFn: async () => { queryFn: async () => {
const res = await fetch(`/kitchen/api/menus/${menuId}/flags`, { const res = await fetch(`/kitchen/api/menus/${menuId}/flags`, {
headers: { Authorization: `Bearer ${token}` }, credentials: 'include',
}) })
if (!res.ok) throw new Error('Failed') if (!res.ok) throw new Error('Failed')
return res.json() return res.json()
}, },
enabled: !!token, enabled: true,
}) })
if (isLoading) return ( if (isLoading) return (

View file

@ -38,12 +38,12 @@ export default function MenuList() {
queryKey: ['menus'], queryKey: ['menus'],
queryFn: async () => { queryFn: async () => {
const res = await fetch('/kitchen/api/menus', { const res = await fetch('/kitchen/api/menus', {
headers: { Authorization: `Bearer ${token}` }, credentials: 'include',
}) })
if (!res.ok) throw new Error('Failed to fetch menus') if (!res.ok) throw new Error('Failed to fetch menus')
return res.json() return res.json()
}, },
enabled: !!token, enabled: true,
staleTime: 0, staleTime: 0,
}) })
@ -51,7 +51,7 @@ export default function MenuList() {
mutationFn: async (data: { name: string; description: string | null; notes: string | null; preset_divisions: boolean }) => { mutationFn: async (data: { name: string; description: string | null; notes: string | null; preset_divisions: boolean }) => {
const res = await fetch('/kitchen/api/menus', { const res = await fetch('/kitchen/api/menus', {
method: 'POST', method: 'POST',
headers: { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json' }, credentials: 'include', headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data), body: JSON.stringify(data),
}) })
if (!res.ok) { if (!res.ok) {
@ -74,7 +74,7 @@ export default function MenuList() {
mutationFn: async ({ id, is_active }: { id: number; is_active: boolean }) => { mutationFn: async ({ id, is_active }: { id: number; is_active: boolean }) => {
const res = await fetch(`/kitchen/api/menus/${id}`, { const res = await fetch(`/kitchen/api/menus/${id}`, {
method: 'PUT', method: 'PUT',
headers: { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json' }, credentials: 'include', headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ is_active }), body: JSON.stringify({ is_active }),
}) })
if (!res.ok) throw new Error('Failed to update') if (!res.ok) throw new Error('Failed to update')
@ -86,7 +86,7 @@ export default function MenuList() {
mutationFn: async (id: number) => { mutationFn: async (id: number) => {
const res = await fetch(`/kitchen/api/menus/${id}`, { const res = await fetch(`/kitchen/api/menus/${id}`, {
method: 'DELETE', method: 'DELETE',
headers: { Authorization: `Bearer ${token}` }, credentials: 'include',
}) })
if (!res.ok) throw new Error('Failed to delete') if (!res.ok) throw new Error('Failed to delete')
}, },
@ -97,7 +97,7 @@ export default function MenuList() {
mutationFn: async ({ id, name }: { id: number; name: string }) => { mutationFn: async ({ id, name }: { id: number; name: string }) => {
const res = await fetch(`/kitchen/api/menus/${id}/duplicate`, { const res = await fetch(`/kitchen/api/menus/${id}/duplicate`, {
method: 'POST', method: 'POST',
headers: { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json' }, credentials: 'include', headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ name }), body: JSON.stringify({ name }),
}) })
if (!res.ok) { if (!res.ok) {
@ -118,7 +118,7 @@ export default function MenuList() {
mutationFn: async (ids: number[]) => { mutationFn: async (ids: number[]) => {
const res = await fetch('/kitchen/api/menus/reorder', { const res = await fetch('/kitchen/api/menus/reorder', {
method: 'PATCH', method: 'PATCH',
headers: { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json' }, credentials: 'include', headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ ids }), body: JSON.stringify({ ids }),
}) })
if (!res.ok) throw new Error('Failed to reorder') if (!res.ok) throw new Error('Failed to reorder')

View file

@ -49,12 +49,12 @@ export default function PriceImpact() {
queryKey: ['price-impact', days], queryKey: ['price-impact', days],
queryFn: async () => { queryFn: async () => {
const res = await fetch(`/kitchen/api/recipes/price-impact?days=${days}`, { const res = await fetch(`/kitchen/api/recipes/price-impact?days=${days}`, {
headers: { Authorization: `Bearer ${token}` }, credentials: 'include',
}) })
if (!res.ok) throw new Error('Failed to fetch price impact') if (!res.ok) throw new Error('Failed to fetch price impact')
return res.json() return res.json()
}, },
enabled: !!token, enabled: true,
}) })
const filtered = data?.recipes.filter(r => typeFilter === 'all' || r.recipe_type === typeFilter) || [] const filtered = data?.recipes.filter(r => typeFilter === 'all' || r.recipe_type === typeFilter) || []

View file

@ -58,7 +58,7 @@ export default function PublishToMenuModal({
const { data: llmSettings } = useQuery<{ llm_enabled: boolean; anthropic_api_key_set: boolean }>({ const { data: llmSettings } = useQuery<{ llm_enabled: boolean; anthropic_api_key_set: boolean }>({
queryKey: ['settings'], queryKey: ['settings'],
queryFn: async () => { queryFn: async () => {
const res = await fetch('/kitchen/api/settings/', { headers: { Authorization: `Bearer ${token}` } }) const res = await fetch('/kitchen/api/settings/', { credentials: 'include' })
if (!res.ok) return { llm_enabled: false, anthropic_api_key_set: false } if (!res.ok) return { llm_enabled: false, anthropic_api_key_set: false }
return res.json() return res.json()
}, },
@ -72,7 +72,7 @@ export default function PublishToMenuModal({
try { try {
const res = await fetch('/kitchen/api/menus/generate-description', { const res = await fetch('/kitchen/api/menus/generate-description', {
method: 'POST', method: 'POST',
headers: { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json' }, credentials: 'include', headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ recipe_id: recipeIdToUse, recipe_name: displayName || recipeName || '', ingredients: [], allergen_flags: [] }), body: JSON.stringify({ recipe_id: recipeIdToUse, recipe_name: displayName || recipeName || '', ingredients: [], allergen_flags: [] }),
}) })
if (res.ok) { if (res.ok) {
@ -88,7 +88,7 @@ export default function PublishToMenuModal({
queryKey: ['dishes-for-menu'], queryKey: ['dishes-for-menu'],
queryFn: async () => { queryFn: async () => {
const res = await fetch('/kitchen/api/recipes?recipe_type=dish', { const res = await fetch('/kitchen/api/recipes?recipe_type=dish', {
headers: { Authorization: `Bearer ${token}` }, credentials: 'include',
}) })
if (!res.ok) throw new Error('Failed to fetch dishes') if (!res.ok) throw new Error('Failed to fetch dishes')
return res.json() return res.json()
@ -101,7 +101,7 @@ export default function PublishToMenuModal({
queryKey: ['menus-for-publish'], queryKey: ['menus-for-publish'],
queryFn: async () => { queryFn: async () => {
const res = await fetch('/kitchen/api/menus', { const res = await fetch('/kitchen/api/menus', {
headers: { Authorization: `Bearer ${token}` }, credentials: 'include',
}) })
if (!res.ok) throw new Error('Failed to fetch menus') if (!res.ok) throw new Error('Failed to fetch menus')
return res.json() return res.json()
@ -114,7 +114,7 @@ export default function PublishToMenuModal({
queryKey: ['menu-divisions', selectedMenuId], queryKey: ['menu-divisions', selectedMenuId],
queryFn: async () => { queryFn: async () => {
const res = await fetch(`/kitchen/api/menus/${selectedMenuId}`, { const res = await fetch(`/kitchen/api/menus/${selectedMenuId}`, {
headers: { Authorization: `Bearer ${token}` }, credentials: 'include',
}) })
if (!res.ok) throw new Error('Failed') if (!res.ok) throw new Error('Failed')
return res.json() return res.json()
@ -130,7 +130,7 @@ export default function PublishToMenuModal({
queryKey: ['recipe-flags-for-publish', selectedRecipeId], queryKey: ['recipe-flags-for-publish', selectedRecipeId],
queryFn: async () => { queryFn: async () => {
const res = await fetch(`/kitchen/api/food-flags/recipes/${selectedRecipeId}/flags`, { const res = await fetch(`/kitchen/api/food-flags/recipes/${selectedRecipeId}/flags`, {
headers: { Authorization: `Bearer ${token}` }, credentials: 'include',
}) })
if (!res.ok) throw new Error('Failed') if (!res.ok) throw new Error('Failed')
return res.json() return res.json()
@ -160,7 +160,7 @@ export default function PublishToMenuModal({
mutationFn: async () => { mutationFn: async () => {
const res = await fetch(`/kitchen/api/menus/${selectedMenuId}/items`, { const res = await fetch(`/kitchen/api/menus/${selectedMenuId}/items`, {
method: 'POST', method: 'POST',
headers: { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json' }, credentials: 'include', headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ body: JSON.stringify({
recipe_id: selectedRecipeId, recipe_id: selectedRecipeId,
division_id: selectedDivId, division_id: selectedDivId,

View file

@ -36,12 +36,12 @@ export default function PurchaseOrderList() {
queryKey: ['suppliers'], queryKey: ['suppliers'],
queryFn: async () => { queryFn: async () => {
const res = await fetch('/kitchen/api/suppliers/', { const res = await fetch('/kitchen/api/suppliers/', {
headers: { Authorization: `Bearer ${token}` }, credentials: 'include',
}) })
if (!res.ok) throw new Error('Failed to fetch suppliers') if (!res.ok) throw new Error('Failed to fetch suppliers')
return res.json() return res.json()
}, },
enabled: !!token, enabled: true,
}) })
// Build query string // Build query string
@ -53,12 +53,12 @@ export default function PurchaseOrderList() {
queryKey: ['purchase-orders', statusFilter, supplierFilter], queryKey: ['purchase-orders', statusFilter, supplierFilter],
queryFn: async () => { queryFn: async () => {
const res = await fetch(`/kitchen/api/purchase-orders/?${params}`, { const res = await fetch(`/kitchen/api/purchase-orders/?${params}`, {
headers: { Authorization: `Bearer ${token}` }, credentials: 'include',
}) })
if (!res.ok) throw new Error('Failed to fetch purchase orders') if (!res.ok) throw new Error('Failed to fetch purchase orders')
return res.json() return res.json()
}, },
enabled: !!token, enabled: true,
}) })
const formatDate = (d: string) => { const formatDate = (d: string) => {

View file

@ -64,7 +64,7 @@ export default function PurchaseOrderModal({ isOpen, onClose, onSaved, poId, def
// Load suppliers (including order_email for email button visibility) // Load suppliers (including order_email for email button visibility)
useEffect(() => { useEffect(() => {
if (!token) return if (!token) return
fetch('/kitchen/api/suppliers/', { headers: { Authorization: `Bearer ${token}` } }) fetch('/kitchen/api/suppliers/', { credentials: 'include' })
.then(r => r.json()) .then(r => r.json())
.then(data => setSuppliers(data.suppliers || data || [])) .then(data => setSuppliers(data.suppliers || data || []))
.catch(() => {}) .catch(() => {})
@ -73,7 +73,7 @@ export default function PurchaseOrderModal({ isOpen, onClose, onSaved, poId, def
// Check if SMTP is configured (for email button visibility) // Check if SMTP is configured (for email button visibility)
useEffect(() => { useEffect(() => {
if (!token) return if (!token) return
fetch('/kitchen/api/settings/', { headers: { Authorization: `Bearer ${token}` } }) fetch('/kitchen/api/settings/', { credentials: 'include' })
.then(r => r.json()) .then(r => r.json())
.then(data => setSmtpConfigured(!!(data.smtp_host && data.smtp_from_email))) .then(data => setSmtpConfigured(!!(data.smtp_host && data.smtp_from_email)))
.catch(() => setSmtpConfigured(false)) .catch(() => setSmtpConfigured(false))
@ -83,7 +83,7 @@ export default function PurchaseOrderModal({ isOpen, onClose, onSaved, poId, def
useEffect(() => { useEffect(() => {
if (!poId || !token) return if (!poId || !token) return
setLoading(true) setLoading(true)
fetch(`/kitchen/api/purchase-orders/${poId}`, { headers: { Authorization: `Bearer ${token}` } }) fetch(`/kitchen/api/purchase-orders/${poId}`, { credentials: 'include' })
.then(r => { .then(r => {
if (!r.ok) throw new Error('Failed to load') if (!r.ok) throw new Error('Failed to load')
return r.json() return r.json()
@ -145,7 +145,7 @@ export default function PurchaseOrderModal({ isOpen, onClose, onSaved, poId, def
const params = new URLSearchParams({ query: searchQuery }) const params = new URLSearchParams({ query: searchQuery })
if (supplierId) params.append('supplier_id', String(supplierId)) if (supplierId) params.append('supplier_id', String(supplierId))
fetch(`/kitchen/api/purchase-orders/products/search?${params}`, { fetch(`/kitchen/api/purchase-orders/products/search?${params}`, {
headers: { Authorization: `Bearer ${token}` }, credentials: 'include',
}) })
.then(r => r.json()) .then(r => r.json())
.then(setSearchResults) .then(setSearchResults)
@ -247,7 +247,7 @@ export default function PurchaseOrderModal({ isOpen, onClose, onSaved, poId, def
const method = poId ? 'PUT' : 'POST' const method = poId ? 'PUT' : 'POST'
const res = await fetch(url, { const res = await fetch(url, {
method, method,
headers: { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json' }, credentials: 'include', headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(body), body: JSON.stringify(body),
}) })
if (!res.ok) { if (!res.ok) {
@ -271,7 +271,7 @@ export default function PurchaseOrderModal({ isOpen, onClose, onSaved, poId, def
try { try {
const res = await fetch(`/kitchen/api/purchase-orders/${poId}/attachment`, { const res = await fetch(`/kitchen/api/purchase-orders/${poId}/attachment`, {
method: 'POST', method: 'POST',
headers: { Authorization: `Bearer ${token}` }, credentials: 'include',
body: form, body: form,
}) })
if (!res.ok) throw new Error('Upload failed') if (!res.ok) throw new Error('Upload failed')
@ -287,7 +287,7 @@ export default function PurchaseOrderModal({ isOpen, onClose, onSaved, poId, def
try { try {
await fetch(`/kitchen/api/purchase-orders/${poId}/attachment`, { await fetch(`/kitchen/api/purchase-orders/${poId}/attachment`, {
method: 'DELETE', method: 'DELETE',
headers: { Authorization: `Bearer ${token}` }, credentials: 'include',
}) })
setAttachmentName(null) setAttachmentName(null)
} catch { } catch {
@ -301,7 +301,7 @@ export default function PurchaseOrderModal({ isOpen, onClose, onSaved, poId, def
try { try {
const res = await fetch(`/kitchen/api/purchase-orders/${poId}`, { const res = await fetch(`/kitchen/api/purchase-orders/${poId}`, {
method: 'DELETE', method: 'DELETE',
headers: { Authorization: `Bearer ${token}` }, credentials: 'include',
}) })
if (!res.ok) { if (!res.ok) {
const err = await res.json().catch(() => ({})) const err = await res.json().catch(() => ({}))
@ -348,12 +348,12 @@ export default function PurchaseOrderModal({ isOpen, onClose, onSaved, poId, def
try { try {
const res = await fetch('/kitchen/api/purchase-orders/', { const res = await fetch('/kitchen/api/purchase-orders/', {
method: 'POST', method: 'POST',
headers: { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json' }, credentials: 'include', headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(body), body: JSON.stringify(body),
}) })
if (!res.ok) throw new Error('Failed to save') if (!res.ok) throw new Error('Failed to save')
const data = await res.json() const data = await res.json()
window.open(`/kitchen/api/purchase-orders/${data.id}/preview?token=${encodeURIComponent(token || '')}`, '_blank') window.open(`/kitchen/api/purchase-orders/${data.id}/preview`, '_blank')
onSaved() onSaved()
onClose() onClose()
} catch (e: any) { } catch (e: any) {
@ -364,7 +364,7 @@ export default function PurchaseOrderModal({ isOpen, onClose, onSaved, poId, def
} else { } else {
// Existing PO — save current state then preview // Existing PO — save current state then preview
await handleSave() await handleSave()
window.open(`/kitchen/api/purchase-orders/${poId}/preview?token=${encodeURIComponent(token || '')}`, '_blank') window.open(`/kitchen/api/purchase-orders/${poId}/preview`, '_blank')
} }
} }
@ -404,7 +404,7 @@ export default function PurchaseOrderModal({ isOpen, onClose, onSaved, poId, def
const method = poId ? 'PUT' : 'POST' const method = poId ? 'PUT' : 'POST'
const saveRes = await fetch(url, { const saveRes = await fetch(url, {
method, method,
headers: { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json' }, credentials: 'include', headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(body), body: JSON.stringify(body),
}) })
if (!saveRes.ok) throw new Error('Failed to save PO') if (!saveRes.ok) throw new Error('Failed to save PO')
@ -413,7 +413,7 @@ export default function PurchaseOrderModal({ isOpen, onClose, onSaved, poId, def
// Now send the email // Now send the email
const emailRes = await fetch(`/kitchen/api/purchase-orders/${savedPo.id}/send-email`, { const emailRes = await fetch(`/kitchen/api/purchase-orders/${savedPo.id}/send-email`, {
method: 'POST', method: 'POST',
headers: { Authorization: `Bearer ${token}` }, credentials: 'include',
}) })
if (!emailRes.ok) { if (!emailRes.ok) {
const err = await emailRes.json().catch(() => ({})) const err = await emailRes.json().catch(() => ({}))
@ -712,7 +712,7 @@ export default function PurchaseOrderModal({ isOpen, onClose, onSaved, poId, def
</> </>
)} )}
{!isEditable && poId && ( {!isEditable && poId && (
<button style={styles.previewBtn} onClick={() => window.open(`/kitchen/api/purchase-orders/${poId}/preview?token=${encodeURIComponent(token || '')}`, '_blank')}> <button style={styles.previewBtn} onClick={() => window.open(`/kitchen/api/purchase-orders/${poId}/preview`, '_blank')}>
Preview Preview
</button> </button>
)} )}

View file

@ -282,7 +282,7 @@ export default function Purchases() {
queryKey: ['purchases-range', submittedFromDate, submittedToDate], queryKey: ['purchases-range', submittedFromDate, submittedToDate],
queryFn: async () => { queryFn: async () => {
const res = await fetch(`/kitchen/api/reports/purchases/range?from_date=${submittedFromDate}&to_date=${submittedToDate}`, { const res = await fetch(`/kitchen/api/reports/purchases/range?from_date=${submittedFromDate}&to_date=${submittedToDate}`, {
headers: { Authorization: `Bearer ${token}` }, credentials: 'include',
}) })
if (!res.ok) throw new Error('Failed to fetch purchases') if (!res.ok) throw new Error('Failed to fetch purchases')
return res.json() return res.json()
@ -294,7 +294,7 @@ export default function Purchases() {
queryKey: ['daily-dispute-stats', submittedFromDate, submittedToDate], queryKey: ['daily-dispute-stats', submittedFromDate, submittedToDate],
queryFn: async () => { queryFn: async () => {
const res = await fetch(`/kitchen/api/disputes/stats/daily?from_date=${submittedFromDate}&to_date=${submittedToDate}`, { const res = await fetch(`/kitchen/api/disputes/stats/daily?from_date=${submittedFromDate}&to_date=${submittedToDate}`, {
headers: { Authorization: `Bearer ${token}` }, credentials: 'include',
}) })
if (!res.ok) throw new Error('Failed to fetch dispute stats') if (!res.ok) throw new Error('Failed to fetch dispute stats')
return res.json() return res.json()
@ -306,7 +306,7 @@ export default function Purchases() {
queryKey: ['daily-allowance-stats', submittedFromDate, submittedToDate], queryKey: ['daily-allowance-stats', submittedFromDate, submittedToDate],
queryFn: async () => { queryFn: async () => {
const res = await fetch(`/kitchen/api/logbook/daily-stats?date_from=${submittedFromDate}&date_to=${submittedToDate}`, { const res = await fetch(`/kitchen/api/logbook/daily-stats?date_from=${submittedFromDate}&date_to=${submittedToDate}`, {
headers: { Authorization: `Bearer ${token}` }, credentials: 'include',
}) })
if (!res.ok) throw new Error('Failed to fetch allowance stats') if (!res.ok) throw new Error('Failed to fetch allowance stats')
return res.json() return res.json()
@ -380,7 +380,7 @@ export default function Purchases() {
queryKey: ['weekly-chart-data', weeklyChartDateRange.from, weeklyChartDateRange.to], queryKey: ['weekly-chart-data', weeklyChartDateRange.from, weeklyChartDateRange.to],
queryFn: async () => { queryFn: async () => {
const res = await fetch(`/kitchen/api/reports/gp/daily?from_date=${weeklyChartDateRange.from}&to_date=${weeklyChartDateRange.to}`, { const res = await fetch(`/kitchen/api/reports/gp/daily?from_date=${weeklyChartDateRange.from}&to_date=${weeklyChartDateRange.to}`, {
headers: { Authorization: `Bearer ${token}` }, credentials: 'include',
}) })
if (!res.ok) throw new Error('Failed to fetch weekly chart data') if (!res.ok) throw new Error('Failed to fetch weekly chart data')
return res.json() return res.json()

View file

@ -271,7 +271,7 @@ export default function PurchasesReport() {
queryKey: ['purchases-summary', submittedFromDate, submittedToDate], queryKey: ['purchases-summary', submittedFromDate, submittedToDate],
queryFn: async () => { queryFn: async () => {
const res = await fetch(`/kitchen/api/reports/purchases/summary?from_date=${submittedFromDate}&to_date=${submittedToDate}`, { const res = await fetch(`/kitchen/api/reports/purchases/summary?from_date=${submittedFromDate}&to_date=${submittedToDate}`, {
headers: { Authorization: `Bearer ${token}` }, credentials: 'include',
}) })
if (!res.ok) throw new Error('Failed to fetch purchases summary') if (!res.ok) throw new Error('Failed to fetch purchases summary')
return res.json() return res.json()
@ -285,7 +285,7 @@ export default function PurchasesReport() {
queryKey: ['purchases-daily-supplier', submittedFromDate, submittedToDate], queryKey: ['purchases-daily-supplier', submittedFromDate, submittedToDate],
queryFn: async () => { queryFn: async () => {
const res = await fetch(`/kitchen/api/reports/purchases/daily-by-supplier?from_date=${submittedFromDate}&to_date=${submittedToDate}`, { const res = await fetch(`/kitchen/api/reports/purchases/daily-by-supplier?from_date=${submittedFromDate}&to_date=${submittedToDate}`, {
headers: { Authorization: `Bearer ${token}` }, credentials: 'include',
}) })
if (!res.ok) throw new Error('Failed to fetch chart data') if (!res.ok) throw new Error('Failed to fetch chart data')
return res.json() return res.json()
@ -303,7 +303,7 @@ export default function PurchasesReport() {
url += `&supplier_id=${topItemsSupplierFilter}` url += `&supplier_id=${topItemsSupplierFilter}`
} }
const res = await fetch(url, { const res = await fetch(url, {
headers: { Authorization: `Bearer ${token}` }, credentials: 'include',
}) })
if (!res.ok) throw new Error('Failed to fetch top items') if (!res.ok) throw new Error('Failed to fetch top items')
return res.json() return res.json()

View file

@ -285,7 +285,7 @@ export default function RecipeEditor() {
queryKey: ['recipe', recipeId], queryKey: ['recipe', recipeId],
queryFn: async () => { queryFn: async () => {
const res = await fetch(`/kitchen/api/recipes/${recipeId}`, { const res = await fetch(`/kitchen/api/recipes/${recipeId}`, {
headers: { Authorization: `Bearer ${token}` }, credentials: 'include',
}) })
if (!res.ok) throw new Error('Not found') if (!res.ok) throw new Error('Not found')
return res.json() return res.json()
@ -298,11 +298,11 @@ export default function RecipeEditor() {
queryKey: ['recipe-sections'], queryKey: ['recipe-sections'],
queryFn: async () => { queryFn: async () => {
const res = await fetch('/kitchen/api/recipes/menu-sections?section_type=recipe', { const res = await fetch('/kitchen/api/recipes/menu-sections?section_type=recipe', {
headers: { Authorization: `Bearer ${token}` }, credentials: 'include',
}) })
return res.json() return res.json()
}, },
enabled: !!token, enabled: true,
}) })
// Fetch costing (base, unscaled) // Fetch costing (base, unscaled)
@ -310,7 +310,7 @@ export default function RecipeEditor() {
queryKey: ['recipe-cost', recipeId], queryKey: ['recipe-cost', recipeId],
queryFn: async () => { queryFn: async () => {
const res = await fetch(`/kitchen/api/recipes/${recipeId}/costing`, { const res = await fetch(`/kitchen/api/recipes/${recipeId}/costing`, {
headers: { Authorization: `Bearer ${token}` }, credentials: 'include',
}) })
return res.json() return res.json()
}, },
@ -322,7 +322,7 @@ export default function RecipeEditor() {
queryKey: ['recipe-cost-scaled', recipeId, scalePortions], queryKey: ['recipe-cost-scaled', recipeId, scalePortions],
queryFn: async () => { queryFn: async () => {
const res = await fetch(`/kitchen/api/recipes/${recipeId}/costing?scale_to=${scalePortions}`, { const res = await fetch(`/kitchen/api/recipes/${recipeId}/costing?scale_to=${scalePortions}`, {
headers: { Authorization: `Bearer ${token}` }, credentials: 'include',
}) })
return res.json() return res.json()
}, },
@ -334,7 +334,7 @@ export default function RecipeEditor() {
queryKey: ['recipe-flags', recipeId], queryKey: ['recipe-flags', recipeId],
queryFn: async () => { queryFn: async () => {
const res = await fetch(`/kitchen/api/food-flags/recipes/${recipeId}/flags`, { const res = await fetch(`/kitchen/api/food-flags/recipes/${recipeId}/flags`, {
headers: { Authorization: `Bearer ${token}` }, credentials: 'include',
}) })
return res.json() return res.json()
}, },
@ -346,11 +346,11 @@ export default function RecipeEditor() {
queryKey: ['food-flag-categories'], queryKey: ['food-flag-categories'],
queryFn: async () => { queryFn: async () => {
const res = await fetch('/kitchen/api/food-flags/categories', { const res = await fetch('/kitchen/api/food-flags/categories', {
headers: { Authorization: `Bearer ${token}` }, credentials: 'include',
}) })
return res.json() return res.json()
}, },
enabled: !!token, enabled: true,
}) })
// Fetch change log // Fetch change log
@ -358,7 +358,7 @@ export default function RecipeEditor() {
queryKey: ['recipe-changelog', recipeId], queryKey: ['recipe-changelog', recipeId],
queryFn: async () => { queryFn: async () => {
const res = await fetch(`/kitchen/api/recipes/${recipeId}/change-log`, { const res = await fetch(`/kitchen/api/recipes/${recipeId}/change-log`, {
headers: { Authorization: `Bearer ${token}` }, credentials: 'include',
}) })
return res.json() return res.json()
}, },
@ -370,7 +370,7 @@ export default function RecipeEditor() {
queryKey: ['recipe-cost-trend', recipeId], queryKey: ['recipe-cost-trend', recipeId],
queryFn: async () => { queryFn: async () => {
const res = await fetch(`/kitchen/api/recipes/${recipeId}/cost-trend`, { const res = await fetch(`/kitchen/api/recipes/${recipeId}/cost-trend`, {
headers: { Authorization: `Bearer ${token}` }, credentials: 'include',
}) })
if (!res.ok) throw new Error('Failed to fetch cost trend') if (!res.ok) throw new Error('Failed to fetch cost trend')
return res.json() return res.json()
@ -383,7 +383,7 @@ export default function RecipeEditor() {
queryKey: ['ingredient-edit', editIngId], queryKey: ['ingredient-edit', editIngId],
queryFn: async () => { queryFn: async () => {
const res = await fetch(`/kitchen/api/ingredients/${editIngId}`, { const res = await fetch(`/kitchen/api/ingredients/${editIngId}`, {
headers: { Authorization: `Bearer ${token}` }, credentials: 'include',
}) })
if (!res.ok) throw new Error('Not found') if (!res.ok) throw new Error('Not found')
const data = await res.json() const data = await res.json()
@ -409,7 +409,7 @@ export default function RecipeEditor() {
queryKey: ['recipes-list-for-sub'], queryKey: ['recipes-list-for-sub'],
queryFn: async () => { queryFn: async () => {
const res = await fetch('/kitchen/api/recipes?recipe_type=component', { const res = await fetch('/kitchen/api/recipes?recipe_type=component', {
headers: { Authorization: `Bearer ${token}` }, credentials: 'include',
}) })
return res.json() return res.json()
}, },
@ -439,7 +439,7 @@ export default function RecipeEditor() {
const timer = setTimeout(async () => { const timer = setTimeout(async () => {
try { try {
const res = await fetch(`/kitchen/api/ingredients/suggest?description=${encodeURIComponent(ingSearch)}`, { const res = await fetch(`/kitchen/api/ingredients/suggest?description=${encodeURIComponent(ingSearch)}`, {
headers: { Authorization: `Bearer ${token}` }, credentials: 'include',
}) })
if (res.ok) { if (res.ok) {
const data = await res.json() const data = await res.json()
@ -457,7 +457,7 @@ export default function RecipeEditor() {
mutationFn: async (data: Record<string, unknown>) => { mutationFn: async (data: Record<string, unknown>) => {
const res = await fetch(`/kitchen/api/recipes/${recipeId}`, { const res = await fetch(`/kitchen/api/recipes/${recipeId}`, {
method: 'PATCH', method: 'PATCH',
headers: { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json' }, credentials: 'include', headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data), body: JSON.stringify(data),
}) })
if (!res.ok) throw new Error('Failed to update') if (!res.ok) throw new Error('Failed to update')
@ -475,7 +475,7 @@ export default function RecipeEditor() {
mutationFn: async (data: { ingredient_id: number; quantity: number; unit?: string; notes?: string }) => { mutationFn: async (data: { ingredient_id: number; quantity: number; unit?: string; notes?: string }) => {
const res = await fetch(`/kitchen/api/recipes/${recipeId}/ingredients`, { const res = await fetch(`/kitchen/api/recipes/${recipeId}/ingredients`, {
method: 'POST', method: 'POST',
headers: { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json' }, credentials: 'include', headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data), body: JSON.stringify(data),
}) })
if (!res.ok) throw new Error('Failed to add') if (!res.ok) throw new Error('Failed to add')
@ -500,7 +500,7 @@ export default function RecipeEditor() {
mutationFn: async ({ riId, quantity, unit, yield_percent, notes }: { riId: number; quantity?: number; unit?: string; yield_percent?: number; notes?: string }) => { mutationFn: async ({ riId, quantity, unit, yield_percent, notes }: { riId: number; quantity?: number; unit?: string; yield_percent?: number; notes?: string }) => {
const res = await fetch(`/kitchen/api/recipes/recipe-ingredients/${riId}`, { const res = await fetch(`/kitchen/api/recipes/recipe-ingredients/${riId}`, {
method: 'PATCH', method: 'PATCH',
headers: { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json' }, credentials: 'include', headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ quantity, unit, yield_percent, notes }), body: JSON.stringify({ quantity, unit, yield_percent, notes }),
}) })
if (!res.ok) throw new Error('Failed to update') if (!res.ok) throw new Error('Failed to update')
@ -518,7 +518,7 @@ export default function RecipeEditor() {
mutationFn: async (riId: number) => { mutationFn: async (riId: number) => {
const res = await fetch(`/kitchen/api/recipes/recipe-ingredients/${riId}`, { const res = await fetch(`/kitchen/api/recipes/recipe-ingredients/${riId}`, {
method: 'DELETE', method: 'DELETE',
headers: { Authorization: `Bearer ${token}` }, credentials: 'include',
}) })
if (!res.ok) throw new Error('Failed to remove') if (!res.ok) throw new Error('Failed to remove')
}, },
@ -535,7 +535,7 @@ export default function RecipeEditor() {
mutationFn: async (data: { child_recipe_id: number; portions_needed: number; portions_needed_unit?: string }) => { mutationFn: async (data: { child_recipe_id: number; portions_needed: number; portions_needed_unit?: string }) => {
const res = await fetch(`/kitchen/api/recipes/${recipeId}/sub-recipes`, { const res = await fetch(`/kitchen/api/recipes/${recipeId}/sub-recipes`, {
method: 'POST', method: 'POST',
headers: { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json' }, credentials: 'include', headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data), body: JSON.stringify(data),
}) })
if (!res.ok) { if (!res.ok) {
@ -556,7 +556,7 @@ export default function RecipeEditor() {
mutationFn: async (srId: number) => { mutationFn: async (srId: number) => {
const res = await fetch(`/kitchen/api/recipes/recipe-sub-recipes/${srId}`, { const res = await fetch(`/kitchen/api/recipes/recipe-sub-recipes/${srId}`, {
method: 'DELETE', method: 'DELETE',
headers: { Authorization: `Bearer ${token}` }, credentials: 'include',
}) })
if (!res.ok) throw new Error('Failed') if (!res.ok) throw new Error('Failed')
}, },
@ -572,7 +572,7 @@ export default function RecipeEditor() {
mutationFn: async (data: { title?: string; instruction: string; step_number: number; duration_minutes?: number }) => { mutationFn: async (data: { title?: string; instruction: string; step_number: number; duration_minutes?: number }) => {
const res = await fetch(`/kitchen/api/recipes/${recipeId}/steps`, { const res = await fetch(`/kitchen/api/recipes/${recipeId}/steps`, {
method: 'POST', method: 'POST',
headers: { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json' }, credentials: 'include', headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data), body: JSON.stringify(data),
}) })
if (!res.ok) throw new Error('Failed') if (!res.ok) throw new Error('Failed')
@ -590,7 +590,7 @@ export default function RecipeEditor() {
mutationFn: async ({ stepId, data }: { stepId: number; data: { title?: string; instruction?: string; duration_minutes?: number } }) => { mutationFn: async ({ stepId, data }: { stepId: number; data: { title?: string; instruction?: string; duration_minutes?: number } }) => {
const res = await fetch(`/kitchen/api/recipes/recipe-steps/${stepId}`, { const res = await fetch(`/kitchen/api/recipes/recipe-steps/${stepId}`, {
method: 'PATCH', method: 'PATCH',
headers: { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json' }, credentials: 'include', headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data), body: JSON.stringify(data),
}) })
if (!res.ok) throw new Error('Failed') if (!res.ok) throw new Error('Failed')
@ -609,7 +609,7 @@ export default function RecipeEditor() {
mutationFn: async (stepId: number) => { mutationFn: async (stepId: number) => {
const res = await fetch(`/kitchen/api/recipes/recipe-steps/${stepId}`, { const res = await fetch(`/kitchen/api/recipes/recipe-steps/${stepId}`, {
method: 'DELETE', method: 'DELETE',
headers: { Authorization: `Bearer ${token}` }, credentials: 'include',
}) })
if (!res.ok) throw new Error('Failed') if (!res.ok) throw new Error('Failed')
}, },
@ -625,7 +625,7 @@ export default function RecipeEditor() {
formData.append('image_type', image_type) formData.append('image_type', image_type)
const res = await fetch(`/kitchen/api/recipes/${recipeId}/images`, { const res = await fetch(`/kitchen/api/recipes/${recipeId}/images`, {
method: 'POST', method: 'POST',
headers: { Authorization: `Bearer ${token}` }, credentials: 'include',
body: formData, body: formData,
}) })
if (!res.ok) throw new Error('Failed to upload image') if (!res.ok) throw new Error('Failed to upload image')
@ -643,7 +643,7 @@ export default function RecipeEditor() {
mutationFn: async (imageId: number) => { mutationFn: async (imageId: number) => {
const res = await fetch(`/kitchen/api/recipes/${recipeId}/images/${imageId}`, { const res = await fetch(`/kitchen/api/recipes/${recipeId}/images/${imageId}`, {
method: 'DELETE', method: 'DELETE',
headers: { Authorization: `Bearer ${token}` }, credentials: 'include',
}) })
if (!res.ok) throw new Error('Failed to delete image') if (!res.ok) throw new Error('Failed to delete image')
}, },
@ -657,7 +657,7 @@ export default function RecipeEditor() {
mutationFn: async (ingredientIds: number[]) => { mutationFn: async (ingredientIds: number[]) => {
const res = await fetch(`/kitchen/api/recipes/${recipeId}/ingredients/reorder`, { const res = await fetch(`/kitchen/api/recipes/${recipeId}/ingredients/reorder`, {
method: 'PATCH', method: 'PATCH',
headers: { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json' }, credentials: 'include', headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ ingredient_ids: ingredientIds }), body: JSON.stringify({ ingredient_ids: ingredientIds }),
}) })
if (!res.ok) throw new Error('Failed to reorder') if (!res.ok) throw new Error('Failed to reorder')
@ -672,7 +672,7 @@ export default function RecipeEditor() {
mutationFn: async (subRecipeIds: number[]) => { mutationFn: async (subRecipeIds: number[]) => {
const res = await fetch(`/kitchen/api/recipes/${recipeId}/sub-recipes/reorder`, { const res = await fetch(`/kitchen/api/recipes/${recipeId}/sub-recipes/reorder`, {
method: 'PATCH', method: 'PATCH',
headers: { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json' }, credentials: 'include', headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ sub_recipe_ids: subRecipeIds }), body: JSON.stringify({ sub_recipe_ids: subRecipeIds }),
}) })
if (!res.ok) throw new Error('Failed to reorder') if (!res.ok) throw new Error('Failed to reorder')
@ -687,7 +687,7 @@ export default function RecipeEditor() {
mutationFn: async (stepIds: number[]) => { mutationFn: async (stepIds: number[]) => {
const res = await fetch(`/kitchen/api/recipes/${recipeId}/steps/reorder`, { const res = await fetch(`/kitchen/api/recipes/${recipeId}/steps/reorder`, {
method: 'PATCH', method: 'PATCH',
headers: { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json' }, credentials: 'include', headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ step_ids: stepIds }), body: JSON.stringify({ step_ids: stepIds }),
}) })
if (!res.ok) throw new Error('Failed to reorder steps') if (!res.ok) throw new Error('Failed to reorder steps')
@ -895,7 +895,7 @@ export default function RecipeEditor() {
try { try {
const res = await fetch(`/kitchen/api/food-flags/recipes/${recipeId}/text-dismissals`, { const res = await fetch(`/kitchen/api/food-flags/recipes/${recipeId}/text-dismissals`, {
method: 'POST', method: 'POST',
headers: { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json' }, credentials: 'include', headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ body: JSON.stringify({
food_flag_id: s.flag_id, food_flag_id: s.flag_id,
dismissed_by_name: user?.name || user?.email || 'Unknown', dismissed_by_name: user?.name || user?.email || 'Unknown',
@ -1309,10 +1309,10 @@ export default function RecipeEditor() {
{recipe.images.map(img => ( {recipe.images.map(img => (
<div key={img.id} style={styles.imageCard}> <div key={img.id} style={styles.imageCard}>
<img <img
src={`/kitchen/api/recipes/${recipeId}/images/${img.id}?token=${token}`} src={`/kitchen/api/recipes/${recipeId}/images/${img.id}`}
alt={img.caption || 'Recipe image'} alt={img.caption || 'Recipe image'}
style={{ ...styles.imageThumb, cursor: 'pointer' }} style={{ ...styles.imageThumb, cursor: 'pointer' }}
onClick={() => setLightboxImg(`/kitchen/api/recipes/${recipeId}/images/${img.id}?token=${token}`)} onClick={() => setLightboxImg(`/kitchen/api/recipes/${recipeId}/images/${img.id}`)}
/> />
<div style={{ padding: '0.4rem' }}> <div style={{ padding: '0.4rem' }}>
{img.caption && <div style={{ fontSize: '0.8rem', color: '#333', marginTop: '2px' }}>{img.caption}</div>} {img.caption && <div style={{ fontSize: '0.8rem', color: '#333', marginTop: '2px' }}>{img.caption}</div>}

View file

@ -46,7 +46,7 @@ export default function RecipeFlagMatrix({ recipeId, categoryId }: Props) {
queryKey: ['recipe-flag-matrix', recipeId], queryKey: ['recipe-flag-matrix', recipeId],
queryFn: async () => { queryFn: async () => {
const res = await fetch(`/kitchen/api/food-flags/recipes/${recipeId}/flags/matrix`, { const res = await fetch(`/kitchen/api/food-flags/recipes/${recipeId}/flags/matrix`, {
headers: { Authorization: `Bearer ${token}` }, credentials: 'include',
}) })
if (!res.ok) throw new Error('Failed to fetch matrix') if (!res.ok) throw new Error('Failed to fetch matrix')
return res.json() return res.json()
@ -64,7 +64,7 @@ export default function RecipeFlagMatrix({ recipeId, categoryId }: Props) {
mutationFn: async ({ ingredientId, flagId, hasFlag }: { ingredientId: number; flagId: number; hasFlag: boolean }) => { mutationFn: async ({ ingredientId, flagId, hasFlag }: { ingredientId: number; flagId: number; hasFlag: boolean }) => {
const res = await fetch(`/kitchen/api/food-flags/recipes/${recipeId}/flags/matrix`, { const res = await fetch(`/kitchen/api/food-flags/recipes/${recipeId}/flags/matrix`, {
method: 'PUT', method: 'PUT',
headers: { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json' }, credentials: 'include', headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ updates: [{ ingredient_id: ingredientId, food_flag_id: flagId, has_flag: hasFlag }] }), body: JSON.stringify({ updates: [{ ingredient_id: ingredientId, food_flag_id: flagId, has_flag: hasFlag }] }),
}) })
if (!res.ok) throw new Error('Failed to update flag') if (!res.ok) throw new Error('Failed to update flag')
@ -88,7 +88,7 @@ export default function RecipeFlagMatrix({ recipeId, categoryId }: Props) {
mutationFn: async ({ ingredientId, catId }: { ingredientId: number; catId: number }) => { mutationFn: async ({ ingredientId, catId }: { ingredientId: number; catId: number }) => {
const res = await fetch(`/kitchen/api/food-flags/recipes/${recipeId}/flags/matrix/none`, { const res = await fetch(`/kitchen/api/food-flags/recipes/${recipeId}/flags/matrix/none`, {
method: 'POST', method: 'POST',
headers: { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json' }, credentials: 'include', headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ ingredient_id: ingredientId, category_id: catId }), body: JSON.stringify({ ingredient_id: ingredientId, category_id: catId }),
}) })
if (!res.ok) throw new Error('Failed to toggle none') if (!res.ok) throw new Error('Failed to toggle none')

View file

@ -82,12 +82,12 @@ export default function RecipeList() {
queryKey: ['recipe-sections'], queryKey: ['recipe-sections'],
queryFn: async () => { queryFn: async () => {
const res = await fetch('/kitchen/api/recipes/menu-sections?section_type=recipe', { const res = await fetch('/kitchen/api/recipes/menu-sections?section_type=recipe', {
headers: { Authorization: `Bearer ${token}` }, credentials: 'include',
}) })
if (!res.ok) throw new Error('Failed to fetch sections') if (!res.ok) throw new Error('Failed to fetch sections')
return res.json() return res.json()
}, },
enabled: !!token, enabled: true,
}) })
const { data: recipes, isLoading } = useQuery<RecipeItem[]>({ const { data: recipes, isLoading } = useQuery<RecipeItem[]>({
@ -99,19 +99,19 @@ export default function RecipeList() {
if (sectionFilter) params.set('menu_section_id', sectionFilter) if (sectionFilter) params.set('menu_section_id', sectionFilter)
if (showArchived) params.set('archived', 'true') if (showArchived) params.set('archived', 'true')
const res = await fetch(`/kitchen/api/recipes?${params}`, { const res = await fetch(`/kitchen/api/recipes?${params}`, {
headers: { Authorization: `Bearer ${token}` }, credentials: 'include',
}) })
if (!res.ok) throw new Error('Failed to fetch recipes') if (!res.ok) throw new Error('Failed to fetch recipes')
return res.json() return res.json()
}, },
enabled: !!token, enabled: true,
}) })
const createMutation = useMutation({ const createMutation = useMutation({
mutationFn: async (data: Record<string, unknown>) => { mutationFn: async (data: Record<string, unknown>) => {
const res = await fetch('/kitchen/api/recipes', { const res = await fetch('/kitchen/api/recipes', {
method: 'POST', method: 'POST',
headers: { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json' }, credentials: 'include', headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data), body: JSON.stringify(data),
}) })
if (!res.ok) throw new Error('Failed to create recipe') if (!res.ok) throw new Error('Failed to create recipe')
@ -128,7 +128,7 @@ export default function RecipeList() {
mutationFn: async (id: number) => { mutationFn: async (id: number) => {
const res = await fetch(`/kitchen/api/recipes/${id}/duplicate`, { const res = await fetch(`/kitchen/api/recipes/${id}/duplicate`, {
method: 'POST', method: 'POST',
headers: { Authorization: `Bearer ${token}` }, credentials: 'include',
}) })
if (!res.ok) throw new Error('Failed to duplicate') if (!res.ok) throw new Error('Failed to duplicate')
return res.json() return res.json()
@ -143,7 +143,7 @@ export default function RecipeList() {
mutationFn: async (id: number) => { mutationFn: async (id: number) => {
const res = await fetch(`/kitchen/api/recipes/${id}`, { const res = await fetch(`/kitchen/api/recipes/${id}`, {
method: 'DELETE', method: 'DELETE',
headers: { Authorization: `Bearer ${token}` }, credentials: 'include',
}) })
if (!res.ok) throw new Error('Failed to archive') if (!res.ok) throw new Error('Failed to archive')
}, },
@ -154,7 +154,7 @@ export default function RecipeList() {
mutationFn: async (id: number) => { mutationFn: async (id: number) => {
const res = await fetch(`/kitchen/api/recipes/${id}`, { const res = await fetch(`/kitchen/api/recipes/${id}`, {
method: 'PATCH', method: 'PATCH',
headers: { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json' }, credentials: 'include', headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ is_archived: false }), body: JSON.stringify({ is_archived: false }),
}) })
if (!res.ok) throw new Error('Failed to unarchive') if (!res.ok) throw new Error('Failed to unarchive')
@ -166,7 +166,7 @@ export default function RecipeList() {
mutationFn: async (name: string) => { mutationFn: async (name: string) => {
const res = await fetch('/kitchen/api/recipes/menu-sections', { const res = await fetch('/kitchen/api/recipes/menu-sections', {
method: 'POST', method: 'POST',
headers: { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json' }, credentials: 'include', headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ name, section_type: 'recipe' }), body: JSON.stringify({ name, section_type: 'recipe' }),
}) })
if (!res.ok) throw new Error('Failed to create section') if (!res.ok) throw new Error('Failed to create section')
@ -183,7 +183,7 @@ export default function RecipeList() {
mutationFn: async ({ id, name }: { id: number; name: string }) => { mutationFn: async ({ id, name }: { id: number; name: string }) => {
const res = await fetch(`/kitchen/api/recipes/menu-sections/${id}`, { const res = await fetch(`/kitchen/api/recipes/menu-sections/${id}`, {
method: 'PATCH', method: 'PATCH',
headers: { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json' }, credentials: 'include', headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ name }), body: JSON.stringify({ name }),
}) })
if (!res.ok) throw new Error('Failed to update section') if (!res.ok) throw new Error('Failed to update section')
@ -201,7 +201,7 @@ export default function RecipeList() {
mutationFn: async (id: number) => { mutationFn: async (id: number) => {
const res = await fetch(`/kitchen/api/recipes/menu-sections/${id}`, { const res = await fetch(`/kitchen/api/recipes/menu-sections/${id}`, {
method: 'DELETE', method: 'DELETE',
headers: { Authorization: `Bearer ${token}` }, credentials: 'include',
}) })
if (!res.ok) throw new Error('Failed to delete section') if (!res.ok) throw new Error('Failed to delete section')
return res.json() return res.json()
@ -216,7 +216,7 @@ export default function RecipeList() {
queryKey: ['food-flag-categories'], queryKey: ['food-flag-categories'],
queryFn: async () => { queryFn: async () => {
const res = await fetch('/kitchen/api/food-flags/categories', { const res = await fetch('/kitchen/api/food-flags/categories', {
headers: { Authorization: `Bearer ${token}` }, credentials: 'include',
}) })
if (!res.ok) throw new Error('Failed to fetch flag categories') if (!res.ok) throw new Error('Failed to fetch flag categories')
return res.json() return res.json()

View file

@ -85,7 +85,7 @@ export default function ReconcilePurchases() {
formData.append('file', file) formData.append('file', file)
const res = await fetch('/kitchen/api/reports/purchases/reconcile', { const res = await fetch('/kitchen/api/reports/purchases/reconcile', {
method: 'POST', method: 'POST',
headers: { Authorization: `Bearer ${token}` }, credentials: 'include',
body: formData, body: formData,
}) })
if (!res.ok) { if (!res.ok) {

View file

@ -542,7 +542,7 @@ export default function Review() {
queryKey: ['invoice', id], queryKey: ['invoice', id],
queryFn: async () => { queryFn: async () => {
const res = await fetch(`/kitchen/api/invoices/${id}`, { const res = await fetch(`/kitchen/api/invoices/${id}`, {
headers: { Authorization: `Bearer ${token}` }, credentials: 'include',
}) })
if (!res.ok) throw new Error('Failed to fetch invoice') if (!res.ok) throw new Error('Failed to fetch invoice')
return res.json() return res.json()
@ -553,14 +553,14 @@ export default function Review() {
// Direct URL with token - simpler approach // Direct URL with token - simpler approach
const imageUrl = invoice const imageUrl = invoice
? `/kitchen/api/invoices/${id}/file?token=${encodeURIComponent(token || '')}#toolbar=0&navpanes=0&view=FitH` ? `/kitchen/api/invoices/${id}/file#toolbar=0&navpanes=0&view=FitH`
: null : null
const { data: lineItems, refetch: refetchLineItems } = useQuery<LineItem[]>({ const { data: lineItems, refetch: refetchLineItems } = useQuery<LineItem[]>({
queryKey: ['invoice-line-items', id], queryKey: ['invoice-line-items', id],
queryFn: async () => { queryFn: async () => {
const res = await fetch(`/kitchen/api/invoices/${id}/line-items`, { const res = await fetch(`/kitchen/api/invoices/${id}/line-items`, {
headers: { Authorization: `Bearer ${token}` }, credentials: 'include',
}) })
if (!res.ok) throw new Error('Failed to fetch line items') if (!res.ok) throw new Error('Failed to fetch line items')
return res.json() return res.json()
@ -577,7 +577,7 @@ export default function Review() {
queryKey: ['invoice-stock-history', id], queryKey: ['invoice-stock-history', id],
queryFn: async () => { queryFn: async () => {
const res = await fetch(`/kitchen/api/invoices/${id}/stock-history`, { const res = await fetch(`/kitchen/api/invoices/${id}/stock-history`, {
headers: { Authorization: `Bearer ${token}` }, credentials: 'include',
}) })
if (!res.ok) throw new Error('Failed to fetch stock history') if (!res.ok) throw new Error('Failed to fetch stock history')
return res.json() return res.json()
@ -589,7 +589,7 @@ export default function Review() {
queryKey: ['invoice-duplicates', id], queryKey: ['invoice-duplicates', id],
queryFn: async () => { queryFn: async () => {
const res = await fetch(`/kitchen/api/invoices/${id}/duplicates`, { const res = await fetch(`/kitchen/api/invoices/${id}/duplicates`, {
headers: { Authorization: `Bearer ${token}` }, credentials: 'include',
}) })
if (!res.ok) throw new Error('Failed to fetch duplicates') if (!res.ok) throw new Error('Failed to fetch duplicates')
return res.json() return res.json()
@ -601,7 +601,7 @@ export default function Review() {
queryKey: ['invoice-ocr-data', id], queryKey: ['invoice-ocr-data', id],
queryFn: async () => { queryFn: async () => {
const res = await fetch(`/kitchen/api/invoices/${id}/ocr-data`, { const res = await fetch(`/kitchen/api/invoices/${id}/ocr-data`, {
headers: { Authorization: `Bearer ${token}` }, credentials: 'include',
}) })
if (!res.ok) throw new Error('Failed to fetch OCR data') if (!res.ok) throw new Error('Failed to fetch OCR data')
return res.json() return res.json()
@ -613,7 +613,7 @@ export default function Review() {
queryKey: ['settings'], queryKey: ['settings'],
queryFn: async () => { queryFn: async () => {
const res = await fetch('/kitchen/api/settings/', { const res = await fetch('/kitchen/api/settings/', {
headers: { Authorization: `Bearer ${token}` }, credentials: 'include',
}) })
if (!res.ok) throw new Error('Failed to fetch settings') if (!res.ok) throw new Error('Failed to fetch settings')
return res.json() return res.json()
@ -639,7 +639,7 @@ export default function Review() {
fetch('/kitchen/api/ingredients/sources/alias-suggestions', { fetch('/kitchen/api/ingredients/sources/alias-suggestions', {
method: 'POST', method: 'POST',
headers: { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json' }, credentials: 'include', headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ supplier_id: parseInt(supplierId), items: uniqueItems }), body: JSON.stringify({ supplier_id: parseInt(supplierId), items: uniqueItems }),
}) })
.then(res => res.ok ? res.json() : []) .then(res => res.ok ? res.json() : [])
@ -886,7 +886,7 @@ export default function Review() {
queryKey: ['suppliers'], queryKey: ['suppliers'],
queryFn: async () => { queryFn: async () => {
const res = await fetch('/kitchen/api/suppliers/', { const res = await fetch('/kitchen/api/suppliers/', {
headers: { Authorization: `Bearer ${token}` }, credentials: 'include',
}) })
if (!res.ok) return [] if (!res.ok) return []
return res.json() return res.json()
@ -916,7 +916,7 @@ export default function Review() {
queryKey: ['po-match', id], queryKey: ['po-match', id],
queryFn: async () => { queryFn: async () => {
const res = await fetch(`/kitchen/api/purchase-orders/matching/for-invoice?invoice_id=${id}`, { const res = await fetch(`/kitchen/api/purchase-orders/matching/for-invoice?invoice_id=${id}`, {
headers: { Authorization: `Bearer ${token}` }, credentials: 'include',
}) })
if (!res.ok) return { matches: [], linked_po: null } if (!res.ok) return { matches: [], linked_po: null }
return res.json() return res.json()
@ -945,7 +945,7 @@ export default function Review() {
const pollInterval = setInterval(async () => { const pollInterval = setInterval(async () => {
try { try {
const checkRes = await fetch(`/kitchen/api/invoices/${id}`, { const checkRes = await fetch(`/kitchen/api/invoices/${id}`, {
headers: { Authorization: `Bearer ${token}` }, credentials: 'include',
}) })
if (checkRes.ok) { if (checkRes.ok) {
const inv = await checkRes.json() const inv = await checkRes.json()
@ -971,8 +971,8 @@ export default function Review() {
try { try {
// Fetch the PDF // Fetch the PDF
const pdfUrl = `/kitchen/api/invoices/${id}/file?token=${encodeURIComponent(token)}` const pdfUrl = `/kitchen/api/invoices/${id}/file`
const response = await fetch(pdfUrl) const response = await fetch(pdfUrl, { credentials: 'include' })
const arrayBuffer = await response.arrayBuffer() const arrayBuffer = await response.arrayBuffer()
// Load the PDF document // Load the PDF document
@ -1043,8 +1043,8 @@ export default function Review() {
mutationFn: async (data: Partial<Invoice>) => { mutationFn: async (data: Partial<Invoice>) => {
const res = await fetch(`/kitchen/api/invoices/${id}`, { const res = await fetch(`/kitchen/api/invoices/${id}`, {
method: 'PATCH', method: 'PATCH',
credentials: 'include',
headers: { headers: {
Authorization: `Bearer ${token}`,
'Content-Type': 'application/json', 'Content-Type': 'application/json',
}, },
body: JSON.stringify(data), body: JSON.stringify(data),
@ -1069,7 +1069,7 @@ export default function Review() {
mutationFn: async () => { mutationFn: async () => {
const res = await fetch(`/kitchen/api/invoices/${id}`, { const res = await fetch(`/kitchen/api/invoices/${id}`, {
method: 'DELETE', method: 'DELETE',
headers: { Authorization: `Bearer ${token}` }, credentials: 'include',
}) })
if (!res.ok) throw new Error('Failed to delete') if (!res.ok) throw new Error('Failed to delete')
return res.json() return res.json()
@ -1084,8 +1084,8 @@ export default function Review() {
mutationFn: async ({ itemId, data }: { itemId: number; data: Partial<LineItem> }) => { mutationFn: async ({ itemId, data }: { itemId: number; data: Partial<LineItem> }) => {
const res = await fetch(`/kitchen/api/invoices/${id}/line-items/${itemId}`, { const res = await fetch(`/kitchen/api/invoices/${id}/line-items/${itemId}`, {
method: 'PATCH', method: 'PATCH',
credentials: 'include',
headers: { headers: {
Authorization: `Bearer ${token}`,
'Content-Type': 'application/json', 'Content-Type': 'application/json',
}, },
body: JSON.stringify(data), body: JSON.stringify(data),
@ -1106,8 +1106,8 @@ export default function Review() {
mutationFn: async (data: Partial<LineItem>) => { mutationFn: async (data: Partial<LineItem>) => {
const res = await fetch(`/kitchen/api/invoices/${id}/line-items`, { const res = await fetch(`/kitchen/api/invoices/${id}/line-items`, {
method: 'POST', method: 'POST',
credentials: 'include',
headers: { headers: {
Authorization: `Bearer ${token}`,
'Content-Type': 'application/json', 'Content-Type': 'application/json',
}, },
body: JSON.stringify(data), body: JSON.stringify(data),
@ -1127,8 +1127,8 @@ export default function Review() {
mutationFn: async (itemId: number) => { mutationFn: async (itemId: number) => {
const res = await fetch(`/kitchen/api/invoices/${id}/line-items/${itemId}`, { const res = await fetch(`/kitchen/api/invoices/${id}/line-items/${itemId}`, {
method: 'DELETE', method: 'DELETE',
credentials: 'include',
headers: { headers: {
Authorization: `Bearer ${token}`,
}, },
}) })
if (!res.ok) throw new Error('Failed to delete line item') if (!res.ok) throw new Error('Failed to delete line item')
@ -1145,8 +1145,8 @@ export default function Review() {
mutationFn: async ({ itemId, portionDesc }: { itemId: number; portionDesc?: string }) => { mutationFn: async ({ itemId, portionDesc }: { itemId: number; portionDesc?: string }) => {
const res = await fetch(`/kitchen/api/invoices/${id}/line-items/${itemId}/save-definition`, { const res = await fetch(`/kitchen/api/invoices/${id}/line-items/${itemId}/save-definition`, {
method: 'POST', method: 'POST',
credentials: 'include',
headers: { headers: {
Authorization: `Bearer ${token}`,
'Content-Type': 'application/json', 'Content-Type': 'application/json',
}, },
body: JSON.stringify({ portion_description: portionDesc || null }), body: JSON.stringify({ portion_description: portionDesc || null }),
@ -1163,8 +1163,8 @@ export default function Review() {
mutationFn: async (name: string) => { mutationFn: async (name: string) => {
const res = await fetch('/kitchen/api/suppliers/', { const res = await fetch('/kitchen/api/suppliers/', {
method: 'POST', method: 'POST',
credentials: 'include',
headers: { headers: {
Authorization: `Bearer ${token}`,
'Content-Type': 'application/json', 'Content-Type': 'application/json',
}, },
body: JSON.stringify({ name }), body: JSON.stringify({ name }),
@ -1184,8 +1184,8 @@ export default function Review() {
mutationFn: async ({ supplierId, alias, invoiceId }: { supplierId: number; alias: string; invoiceId?: number }) => { mutationFn: async ({ supplierId, alias, invoiceId }: { supplierId: number; alias: string; invoiceId?: number }) => {
const res = await fetch(`/kitchen/api/suppliers/${supplierId}/aliases`, { const res = await fetch(`/kitchen/api/suppliers/${supplierId}/aliases`, {
method: 'POST', method: 'POST',
credentials: 'include',
headers: { headers: {
Authorization: `Bearer ${token}`,
'Content-Type': 'application/json', 'Content-Type': 'application/json',
}, },
body: JSON.stringify({ alias, invoice_id: invoiceId }), body: JSON.stringify({ alias, invoice_id: invoiceId }),
@ -1203,8 +1203,8 @@ export default function Review() {
mutationFn: async ({ sourceId, alias }: { sourceId: number; alias: string }) => { mutationFn: async ({ sourceId, alias }: { sourceId: number; alias: string }) => {
const res = await fetch(`/kitchen/api/ingredients/sources/${sourceId}/aliases`, { const res = await fetch(`/kitchen/api/ingredients/sources/${sourceId}/aliases`, {
method: 'POST', method: 'POST',
credentials: 'include',
headers: { headers: {
Authorization: `Bearer ${token}`,
'Content-Type': 'application/json', 'Content-Type': 'application/json',
}, },
body: JSON.stringify({ alias }), body: JSON.stringify({ alias }),
@ -1230,7 +1230,7 @@ export default function Review() {
try { try {
const res = await fetch(`/kitchen/api/purchase-orders/${poId}/link`, { const res = await fetch(`/kitchen/api/purchase-orders/${poId}/link`, {
method: 'POST', method: 'POST',
headers: { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json' }, credentials: 'include', headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ invoice_id: parseInt(id!) }), body: JSON.stringify({ invoice_id: parseInt(id!) }),
}) })
if (!res.ok) throw new Error('Failed to link PO') if (!res.ok) throw new Error('Failed to link PO')
@ -1244,7 +1244,7 @@ export default function Review() {
try { try {
const res = await fetch(`/kitchen/api/purchase-orders/${poId}/unlink`, { const res = await fetch(`/kitchen/api/purchase-orders/${poId}/unlink`, {
method: 'POST', method: 'POST',
headers: { Authorization: `Bearer ${token}` }, credentials: 'include',
}) })
if (!res.ok) throw new Error('Failed to unlink PO') if (!res.ok) throw new Error('Failed to unlink PO')
refetchPoMatch() refetchPoMatch()
@ -1388,7 +1388,7 @@ export default function Review() {
try { try {
const res = await fetch(`/kitchen/api/invoices/${id}/mark-dext-sent`, { const res = await fetch(`/kitchen/api/invoices/${id}/mark-dext-sent`, {
method: 'POST', method: 'POST',
headers: { Authorization: `Bearer ${token}` }, credentials: 'include',
}) })
if (!res.ok) { if (!res.ok) {
@ -1425,7 +1425,7 @@ export default function Review() {
try { try {
const res = await fetch(`/kitchen/api/invoices/${id}/reprocess`, { const res = await fetch(`/kitchen/api/invoices/${id}/reprocess`, {
method: 'POST', method: 'POST',
headers: { Authorization: `Bearer ${token}` }, credentials: 'include',
}) })
if (!res.ok) { if (!res.ok) {
@ -1458,7 +1458,7 @@ export default function Review() {
try { try {
const res = await fetch(`/kitchen/api/invoices/${id}/parse-dates`, { const res = await fetch(`/kitchen/api/invoices/${id}/parse-dates`, {
headers: { Authorization: `Bearer ${token}` }, credentials: 'include',
}) })
if (!res.ok) { if (!res.ok) {
@ -1486,7 +1486,7 @@ export default function Review() {
setShowInvoiceNumberModal(true) setShowInvoiceNumberModal(true)
try { try {
const res = await fetch(`/kitchen/api/invoices/${id}/parse-invoice-number`, { const res = await fetch(`/kitchen/api/invoices/${id}/parse-invoice-number`, {
headers: { Authorization: `Bearer ${token}` }, credentials: 'include',
}) })
if (!res.ok) throw new Error('Failed to search for invoice number') if (!res.ok) throw new Error('Failed to search for invoice number')
const data = await res.json() const data = await res.json()
@ -1515,7 +1515,7 @@ export default function Review() {
try { try {
const res = await fetch(`/kitchen/api/invoices/${id}/resend-to-azure`, { const res = await fetch(`/kitchen/api/invoices/${id}/resend-to-azure`, {
method: 'POST', method: 'POST',
headers: { Authorization: `Bearer ${token}` }, credentials: 'include',
}) })
if (!res.ok) { if (!res.ok) {
@ -1533,7 +1533,7 @@ export default function Review() {
const pollInterval = setInterval(async () => { const pollInterval = setInterval(async () => {
try { try {
const checkRes = await fetch(`/kitchen/api/invoices/${id}`, { const checkRes = await fetch(`/kitchen/api/invoices/${id}`, {
headers: { Authorization: `Bearer ${token}` }, credentials: 'include',
}) })
if (checkRes.ok) { if (checkRes.ok) {
const inv = await checkRes.json() const inv = await checkRes.json()
@ -1567,7 +1567,7 @@ export default function Review() {
try { try {
const res = await fetch(`/kitchen/api/invoices/${id}/regenerate-highlights`, { const res = await fetch(`/kitchen/api/invoices/${id}/regenerate-highlights`, {
method: 'POST', method: 'POST',
headers: { Authorization: `Bearer ${token}` }, credentials: 'include',
}) })
if (!res.ok) { if (!res.ok) {
@ -1758,9 +1758,9 @@ export default function Review() {
try { try {
const res = await fetch('/kitchen/api/invoices/line-items/search', { const res = await fetch('/kitchen/api/invoices/line-items/search', {
method: 'POST', method: 'POST',
credentials: 'include',
headers: { headers: {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
Authorization: `Bearer ${token}`,
}, },
body: JSON.stringify({ body: JSON.stringify({
query, query,
@ -1783,8 +1783,8 @@ export default function Review() {
const promises = lineItems.map(item => const promises = lineItems.map(item =>
fetch(`/kitchen/api/invoices/${id}/line-items/${item.id}`, { fetch(`/kitchen/api/invoices/${id}/line-items/${item.id}`, {
method: 'PATCH', method: 'PATCH',
credentials: 'include',
headers: { headers: {
Authorization: `Bearer ${token}`,
'Content-Type': 'application/json', 'Content-Type': 'application/json',
}, },
body: JSON.stringify({ is_non_stock: !markAsStock }), body: JSON.stringify({ is_non_stock: !markAsStock }),
@ -1806,7 +1806,7 @@ export default function Review() {
setAiMatchResults([]) setAiMatchResults([])
try { try {
const res = await fetch(`/kitchen/api/ingredients/ai-match?description=${encodeURIComponent(description)}`, { const res = await fetch(`/kitchen/api/ingredients/ai-match?description=${encodeURIComponent(description)}`, {
headers: { Authorization: `Bearer ${token}` }, credentials: 'include',
}) })
if (res.ok) { if (res.ok) {
const data = await res.json() const data = await res.json()
@ -1829,7 +1829,7 @@ export default function Review() {
try { try {
const res = await fetch(`/kitchen/api/invoices/${id}/ai-assist`, { const res = await fetch(`/kitchen/api/invoices/${id}/ai-assist`, {
method: 'POST', method: 'POST',
headers: { Authorization: `Bearer ${token}` }, credentials: 'include',
}) })
if (!res.ok) { if (!res.ok) {
let detail = 'AI analysis failed' let detail = 'AI analysis failed'
@ -1907,7 +1907,7 @@ export default function Review() {
setDefinitionLoading(true) setDefinitionLoading(true)
try { try {
const res = await fetch(`/kitchen/api/invoices/${id}/line-items/${item.id}/definition`, { const res = await fetch(`/kitchen/api/invoices/${id}/line-items/${item.id}/definition`, {
headers: { Authorization: `Bearer ${token}` }, credentials: 'include',
}) })
if (res.ok) { if (res.ok) {
const def = await res.json() const def = await res.json()
@ -1928,7 +1928,7 @@ export default function Review() {
setAiPackSizeLoading(true) setAiPackSizeLoading(true)
try { try {
const packRes = await fetch(`/kitchen/api/invoices/${id}/line-items/${item.id}/ai-pack-size`, { const packRes = await fetch(`/kitchen/api/invoices/${id}/line-items/${item.id}/ai-pack-size`, {
headers: { Authorization: `Bearer ${token}` }, credentials: 'include',
}) })
if (packRes.ok) { if (packRes.ok) {
const packData = await packRes.json() const packData = await packRes.json()
@ -1962,7 +1962,7 @@ export default function Review() {
setIngredientSearchLoading(true) setIngredientSearchLoading(true)
try { try {
const res = await fetch(`/kitchen/api/ingredients/suggest?description=${encodeURIComponent(query)}`, { const res = await fetch(`/kitchen/api/ingredients/suggest?description=${encodeURIComponent(query)}`, {
headers: { Authorization: `Bearer ${token}` }, credentials: 'include',
}) })
if (res.ok) { if (res.ok) {
const data = await res.json() const data = await res.json()
@ -2055,7 +2055,7 @@ export default function Review() {
// Set ingredient_id on line item // Set ingredient_id on line item
await fetch(`/kitchen/api/invoices/${id}/line-items/${itemId}`, { await fetch(`/kitchen/api/invoices/${id}/line-items/${itemId}`, {
method: 'PATCH', method: 'PATCH',
headers: { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json' }, credentials: 'include', headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ ingredient_id: selectedIngredientId }), body: JSON.stringify({ ingredient_id: selectedIngredientId }),
}) })
@ -2083,7 +2083,7 @@ export default function Review() {
} }
const srcRes = await fetch(`/kitchen/api/ingredients/${selectedIngredientId}/sources`, { const srcRes = await fetch(`/kitchen/api/ingredients/${selectedIngredientId}/sources`, {
method: 'POST', method: 'POST',
headers: { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json' }, credentials: 'include', headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(sourceData), body: JSON.stringify(sourceData),
}) })
if (!srcRes.ok) { if (!srcRes.ok) {
@ -2610,7 +2610,7 @@ export default function Review() {
)} )}
{isPDF && imageUrl && ( {isPDF && imageUrl && (
<a <a
href={`/kitchen/api/invoices/${id}/file?token=${encodeURIComponent(token || '')}`} href={`/kitchen/api/invoices/${id}/file`}
target="_blank" target="_blank"
rel="noopener noreferrer" rel="noopener noreferrer"
style={styles.openPdfLink} style={styles.openPdfLink}
@ -3094,9 +3094,9 @@ export default function Review() {
try { try {
await fetch(`/kitchen/api/invoices/${id}`, { await fetch(`/kitchen/api/invoices/${id}`, {
method: 'PATCH', method: 'PATCH',
credentials: 'include',
headers: { headers: {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
Authorization: `Bearer ${token}`
}, },
body: JSON.stringify({ notes: invoiceNotes }) body: JSON.stringify({ notes: invoiceNotes })
}) })
@ -4560,7 +4560,7 @@ export default function Review() {
width: '100%', width: '100%',
}}> }}>
<img <img
src={`${imageUrl.split('?')[0]}?token=${encodeURIComponent(token || '')}`} src={`${imageUrl.split('?')[0]}`}
alt="Line item location" alt="Line item location"
style={{ style={{
width: '100%', width: '100%',
@ -4675,7 +4675,7 @@ export default function Review() {
try { try {
const res = await fetch(`/kitchen/api/invoices/${id}/send-to-dext`, { const res = await fetch(`/kitchen/api/invoices/${id}/send-to-dext`, {
method: 'POST', method: 'POST',
headers: { Authorization: `Bearer ${token}` } credentials: 'include'
}) })
if (res.ok) { if (res.ok) {
const result = await res.json() const result = await res.json()

View file

@ -76,7 +76,7 @@ export default function SalesGPReport() {
queryKey: ['sales-gp', submittedFrom, submittedTo], queryKey: ['sales-gp', submittedFrom, submittedTo],
queryFn: async () => { queryFn: async () => {
const res = await fetch(`/kitchen/api/reports/sales-gp?from_date=${submittedFrom}&to_date=${submittedTo}`, { const res = await fetch(`/kitchen/api/reports/sales-gp?from_date=${submittedFrom}&to_date=${submittedTo}`, {
headers: { Authorization: `Bearer ${token}` }, credentials: 'include',
}) })
if (!res.ok) { if (!res.ok) {
const err = await res.json().catch(() => ({ detail: 'Request failed' })) const err = await res.json().catch(() => ({ detail: 'Request failed' }))
@ -92,7 +92,7 @@ export default function SalesGPReport() {
queryKey: ['recipes-for-mapping', recipeSearch], queryKey: ['recipes-for-mapping', recipeSearch],
queryFn: async () => { queryFn: async () => {
const url = `/kitchen/api/recipes?recipe_type=dish${recipeSearch ? `&search=${encodeURIComponent(recipeSearch)}` : ''}` const url = `/kitchen/api/recipes?recipe_type=dish${recipeSearch ? `&search=${encodeURIComponent(recipeSearch)}` : ''}`
const res = await fetch(url, { headers: { Authorization: `Bearer ${token}` } }) const res = await fetch(url, { credentials: 'include' })
return res.json() return res.json()
}, },
enabled: !!token && !!mappingItem, enabled: !!token && !!mappingItem,
@ -103,7 +103,7 @@ export default function SalesGPReport() {
mutationFn: async ({ recipeId, menuItemName, portionName }: { recipeId: number; menuItemName: string; portionName: string }) => { mutationFn: async ({ recipeId, menuItemName, portionName }: { recipeId: number; menuItemName: string; portionName: string }) => {
const res = await fetch(`/kitchen/api/recipes/${recipeId}`, { const res = await fetch(`/kitchen/api/recipes/${recipeId}`, {
method: 'PATCH', method: 'PATCH',
headers: { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json' }, credentials: 'include', headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ body: JSON.stringify({
kds_menu_item_name: menuItemName, kds_menu_item_name: menuItemName,
sambapos_portion_name: portionName === 'Normal' ? null : portionName, sambapos_portion_name: portionName === 'Normal' ? null : portionName,

View file

@ -105,7 +105,7 @@ export default function SearchDefinitions() {
queryKey: ['suppliers'], queryKey: ['suppliers'],
queryFn: async () => { queryFn: async () => {
const res = await fetch('/kitchen/api/suppliers/', { const res = await fetch('/kitchen/api/suppliers/', {
headers: { Authorization: `Bearer ${token}` }, credentials: 'include',
}) })
if (!res.ok) throw new Error('Failed to fetch suppliers') if (!res.ok) throw new Error('Failed to fetch suppliers')
return res.json() return res.json()
@ -124,12 +124,12 @@ export default function SearchDefinitions() {
params.set('limit', '200') params.set('limit', '200')
const res = await fetch(`/kitchen/api/search/definitions?${params}`, { const res = await fetch(`/kitchen/api/search/definitions?${params}`, {
headers: { Authorization: `Bearer ${token}` }, credentials: 'include',
}) })
if (!res.ok) throw new Error('Search failed') if (!res.ok) throw new Error('Search failed')
return res.json() return res.json()
}, },
enabled: !!token, enabled: true,
}) })
// Fetch invoice line items when editing (to find matching line item for bounding box) // Fetch invoice line items when editing (to find matching line item for bounding box)
@ -137,7 +137,7 @@ export default function SearchDefinitions() {
queryKey: ['invoice-line-items', editingDef?.source_invoice_id], queryKey: ['invoice-line-items', editingDef?.source_invoice_id],
queryFn: async () => { queryFn: async () => {
const res = await fetch(`/kitchen/api/invoices/${editingDef!.source_invoice_id}/line-items`, { const res = await fetch(`/kitchen/api/invoices/${editingDef!.source_invoice_id}/line-items`, {
headers: { Authorization: `Bearer ${token}` }, credentials: 'include',
}) })
if (!res.ok) throw new Error('Failed to fetch line items') if (!res.ok) throw new Error('Failed to fetch line items')
return res.json() return res.json()
@ -150,7 +150,7 @@ export default function SearchDefinitions() {
queryKey: ['invoice-ocr-data', editingDef?.source_invoice_id], queryKey: ['invoice-ocr-data', editingDef?.source_invoice_id],
queryFn: async () => { queryFn: async () => {
const res = await fetch(`/kitchen/api/invoices/${editingDef!.source_invoice_id}/ocr-data`, { const res = await fetch(`/kitchen/api/invoices/${editingDef!.source_invoice_id}/ocr-data`, {
headers: { Authorization: `Bearer ${token}` }, credentials: 'include',
}) })
if (!res.ok) throw new Error('Failed to fetch OCR data') if (!res.ok) throw new Error('Failed to fetch OCR data')
return res.json() return res.json()
@ -163,8 +163,8 @@ export default function SearchDefinitions() {
mutationFn: async (data: { id: number; updates: typeof editFormData }) => { mutationFn: async (data: { id: number; updates: typeof editFormData }) => {
const res = await fetch(`/kitchen/api/search/definitions/${data.id}`, { const res = await fetch(`/kitchen/api/search/definitions/${data.id}`, {
method: 'PATCH', method: 'PATCH',
credentials: 'include',
headers: { headers: {
Authorization: `Bearer ${token}`,
'Content-Type': 'application/json', 'Content-Type': 'application/json',
}, },
body: JSON.stringify(data.updates), body: JSON.stringify(data.updates),
@ -195,7 +195,7 @@ export default function SearchDefinitions() {
setInvoiceImageUrl(url) setInvoiceImageUrl(url)
// Check if PDF by fetching headers // Check if PDF by fetching headers
fetch(url, { headers: { Authorization: `Bearer ${token}` } }) fetch(url, { credentials: 'include' })
.then((res) => { .then((res) => {
const contentType = res.headers.get('content-type') || '' const contentType = res.headers.get('content-type') || ''
setIsPDF(contentType.includes('pdf')) setIsPDF(contentType.includes('pdf'))

View file

@ -73,7 +73,7 @@ export default function SearchInvoices() {
queryKey: ['suppliers'], queryKey: ['suppliers'],
queryFn: async () => { queryFn: async () => {
const res = await fetch('/kitchen/api/suppliers/', { const res = await fetch('/kitchen/api/suppliers/', {
headers: { Authorization: `Bearer ${token}` }, credentials: 'include',
}) })
if (!res.ok) throw new Error('Failed to fetch suppliers') if (!res.ok) throw new Error('Failed to fetch suppliers')
return res.json() return res.json()
@ -104,12 +104,12 @@ export default function SearchInvoices() {
params.set('limit', '200') params.set('limit', '200')
const res = await fetch(`/kitchen/api/search/invoices?${params}`, { const res = await fetch(`/kitchen/api/search/invoices?${params}`, {
headers: { Authorization: `Bearer ${token}` }, credentials: 'include',
}) })
if (!res.ok) throw new Error('Search failed') if (!res.ok) throw new Error('Search failed')
return res.json() return res.json()
}, },
enabled: !!token, enabled: true,
}) })
const toggleGroup = (name: string) => { const toggleGroup = (name: string) => {

View file

@ -145,7 +145,7 @@ export default function SearchLineItems() {
queryKey: ['suppliers'], queryKey: ['suppliers'],
queryFn: async () => { queryFn: async () => {
const res = await fetch('/kitchen/api/suppliers/', { const res = await fetch('/kitchen/api/suppliers/', {
headers: { Authorization: `Bearer ${token}` }, credentials: 'include',
}) })
if (!res.ok) throw new Error('Failed to fetch suppliers') if (!res.ok) throw new Error('Failed to fetch suppliers')
return res.json() return res.json()
@ -157,7 +157,7 @@ export default function SearchLineItems() {
queryKey: ['search-settings'], queryKey: ['search-settings'],
queryFn: async () => { queryFn: async () => {
const res = await fetch('/kitchen/api/search/settings', { const res = await fetch('/kitchen/api/search/settings', {
headers: { Authorization: `Bearer ${token}` }, credentials: 'include',
}) })
if (!res.ok) throw new Error('Failed to fetch search settings') if (!res.ok) throw new Error('Failed to fetch search settings')
return res.json() return res.json()
@ -179,12 +179,12 @@ export default function SearchLineItems() {
params.set('limit', '200') params.set('limit', '200')
const res = await fetch(`/kitchen/api/search/line-items?${params}`, { const res = await fetch(`/kitchen/api/search/line-items?${params}`, {
headers: { Authorization: `Bearer ${token}` }, credentials: 'include',
}) })
if (!res.ok) throw new Error('Search failed') if (!res.ok) throw new Error('Search failed')
return res.json() return res.json()
}, },
enabled: !!token, enabled: true,
}) })
// Fetch alias suggestions for unmapped items without product codes // Fetch alias suggestions for unmapped items without product codes
@ -207,7 +207,7 @@ export default function SearchLineItems() {
const promises = Array.from(bySupplier.entries()).map(([sid, items]) => const promises = Array.from(bySupplier.entries()).map(([sid, items]) =>
fetch('/kitchen/api/ingredients/sources/alias-suggestions', { fetch('/kitchen/api/ingredients/sources/alias-suggestions', {
method: 'POST', method: 'POST',
headers: { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json' }, credentials: 'include', headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ supplier_id: sid, items }), body: JSON.stringify({ supplier_id: sid, items }),
}) })
.then(res => res.ok ? res.json() : []) .then(res => res.ok ? res.json() : [])
@ -225,8 +225,8 @@ export default function SearchLineItems() {
mutationFn: async ({ sourceId, alias }: { sourceId: number; alias: string }) => { mutationFn: async ({ sourceId, alias }: { sourceId: number; alias: string }) => {
const res = await fetch(`/kitchen/api/ingredients/sources/${sourceId}/aliases`, { const res = await fetch(`/kitchen/api/ingredients/sources/${sourceId}/aliases`, {
method: 'POST', method: 'POST',
credentials: 'include',
headers: { headers: {
Authorization: `Bearer ${token}`,
'Content-Type': 'application/json', 'Content-Type': 'application/json',
}, },
body: JSON.stringify({ alias }), body: JSON.stringify({ alias }),
@ -301,7 +301,7 @@ export default function SearchLineItems() {
setIngredientSearchLoading(true) setIngredientSearchLoading(true)
try { try {
const res = await fetch(`/kitchen/api/ingredients/suggest?description=${encodeURIComponent(query)}`, { const res = await fetch(`/kitchen/api/ingredients/suggest?description=${encodeURIComponent(query)}`, {
headers: { Authorization: `Bearer ${token}` }, credentials: 'include',
}) })
if (res.ok) setIngredientSuggestions(await res.json()) if (res.ok) setIngredientSuggestions(await res.json())
} catch { /* ignore */ } } catch { /* ignore */ }
@ -373,7 +373,7 @@ export default function SearchLineItems() {
if (modalItem.most_recent_line_item_id && modalItem.most_recent_invoice_id) { if (modalItem.most_recent_line_item_id && modalItem.most_recent_invoice_id) {
await fetch(`/kitchen/api/invoices/${modalItem.most_recent_invoice_id}/line-items/${modalItem.most_recent_line_item_id}`, { await fetch(`/kitchen/api/invoices/${modalItem.most_recent_invoice_id}/line-items/${modalItem.most_recent_line_item_id}`, {
method: 'PATCH', method: 'PATCH',
headers: { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json' }, credentials: 'include', headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ body: JSON.stringify({
pack_quantity: costEdits.pack_quantity || null, pack_quantity: costEdits.pack_quantity || null,
unit_size: costEdits.unit_size || null, unit_size: costEdits.unit_size || null,
@ -401,7 +401,7 @@ export default function SearchLineItems() {
await fetch(`/kitchen/api/ingredients/${selectedIngredientId}/sources`, { await fetch(`/kitchen/api/ingredients/${selectedIngredientId}/sources`, {
method: 'POST', method: 'POST',
headers: { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json' }, credentials: 'include', headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(sourceData), body: JSON.stringify(sourceData),
}) })
} }

View file

@ -32,7 +32,7 @@ export default function Suppliers() {
queryKey: ['suppliers'], queryKey: ['suppliers'],
queryFn: async () => { queryFn: async () => {
const res = await fetch('/kitchen/api/suppliers/', { const res = await fetch('/kitchen/api/suppliers/', {
headers: { Authorization: `Bearer ${token}` }, credentials: 'include',
}) })
if (!res.ok) throw new Error('Failed to fetch suppliers') if (!res.ok) throw new Error('Failed to fetch suppliers')
return res.json() return res.json()
@ -43,8 +43,8 @@ export default function Suppliers() {
mutationFn: async ({ name, aliases, skip_dext, order_email, account_number }: { name: string; aliases: string[]; skip_dext: boolean; order_email: string; account_number: string }) => { mutationFn: async ({ name, aliases, skip_dext, order_email, account_number }: { name: string; aliases: string[]; skip_dext: boolean; order_email: string; account_number: string }) => {
const res = await fetch('/kitchen/api/suppliers/', { const res = await fetch('/kitchen/api/suppliers/', {
method: 'POST', method: 'POST',
credentials: 'include',
headers: { headers: {
Authorization: `Bearer ${token}`,
'Content-Type': 'application/json', 'Content-Type': 'application/json',
}, },
body: JSON.stringify({ name, aliases, skip_dext, order_email: order_email || null, account_number: account_number || null }), body: JSON.stringify({ name, aliases, skip_dext, order_email: order_email || null, account_number: account_number || null }),
@ -68,8 +68,8 @@ export default function Suppliers() {
mutationFn: async ({ id, name, aliases, skip_dext, order_email, account_number }: { id: number; name: string; aliases: string[]; skip_dext: boolean; order_email: string; account_number: string }) => { mutationFn: async ({ id, name, aliases, skip_dext, order_email, account_number }: { id: number; name: string; aliases: string[]; skip_dext: boolean; order_email: string; account_number: string }) => {
const res = await fetch(`/kitchen/api/suppliers/${id}`, { const res = await fetch(`/kitchen/api/suppliers/${id}`, {
method: 'PATCH', method: 'PATCH',
credentials: 'include',
headers: { headers: {
Authorization: `Bearer ${token}`,
'Content-Type': 'application/json', 'Content-Type': 'application/json',
}, },
body: JSON.stringify({ name, aliases, skip_dext, order_email: order_email || null, account_number: account_number || null }), body: JSON.stringify({ name, aliases, skip_dext, order_email: order_email || null, account_number: account_number || null }),
@ -93,7 +93,7 @@ export default function Suppliers() {
mutationFn: async (id: number) => { mutationFn: async (id: number) => {
const res = await fetch(`/kitchen/api/suppliers/${id}`, { const res = await fetch(`/kitchen/api/suppliers/${id}`, {
method: 'DELETE', method: 'DELETE',
headers: { Authorization: `Bearer ${token}` }, credentials: 'include',
}) })
if (!res.ok) throw new Error('Failed to delete supplier') if (!res.ok) throw new Error('Failed to delete supplier')
return res.json() return res.json()

View file

@ -19,12 +19,12 @@ export default function SupportButton() {
queryKey: ['support-enabled'], queryKey: ['support-enabled'],
queryFn: async () => { queryFn: async () => {
const res = await fetch('/kitchen/api/support/enabled', { const res = await fetch('/kitchen/api/support/enabled', {
headers: { Authorization: `Bearer ${token}` }, credentials: 'include',
}) })
if (!res.ok) return { enabled: false } if (!res.ok) return { enabled: false }
return res.json() return res.json()
}, },
enabled: !!token, enabled: true,
staleTime: 5 * 60 * 1000, // Cache for 5 minutes staleTime: 5 * 60 * 1000, // Cache for 5 minutes
}) })
@ -33,9 +33,9 @@ export default function SupportButton() {
mutationFn: async (data: { description: string; screenshot: string }) => { mutationFn: async (data: { description: string; screenshot: string }) => {
const res = await fetch('/kitchen/api/support/request', { const res = await fetch('/kitchen/api/support/request', {
method: 'POST', method: 'POST',
credentials: 'include',
headers: { headers: {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
Authorization: `Bearer ${token}`,
}, },
body: JSON.stringify({ body: JSON.stringify({
description: data.description, description: data.description,

View file

@ -161,7 +161,7 @@ export default function Upload() {
const res = await fetch('/kitchen/api/invoices/upload', { const res = await fetch('/kitchen/api/invoices/upload', {
method: 'POST', method: 'POST',
headers: { Authorization: `Bearer ${token}` }, credentials: 'include',
body: formData, body: formData,
}) })

View file

@ -108,7 +108,7 @@ export default function UsageVarianceReport() {
try { try {
const res = await fetch( const res = await fetch(
`/kitchen/api/reports/usage-variance?from_date=${fromDate}&to_date=${toDate}`, `/kitchen/api/reports/usage-variance?from_date=${fromDate}&to_date=${toDate}`,
{ headers: { Authorization: `Bearer ${token}` } } { credentials: 'include' }
) )
if (!res.ok) { if (!res.ok) {
const err = await res.json().catch(() => ({ detail: 'Request failed' })) const err = await res.json().catch(() => ({ detail: 'Request failed' }))

View file

@ -152,7 +152,7 @@ export default function BookingsStats() {
queryKey: ['resos-stats', submittedFromDate, submittedToDate], queryKey: ['resos-stats', submittedFromDate, submittedToDate],
queryFn: async () => { queryFn: async () => {
const res = await fetch(`/kitchen/api/resos/stats?from_date=${submittedFromDate}&to_date=${submittedToDate}`, { const res = await fetch(`/kitchen/api/resos/stats?from_date=${submittedFromDate}&to_date=${submittedToDate}`, {
headers: { Authorization: `Bearer ${token}` } credentials: 'include'
}) })
if (!res.ok) throw new Error('Failed to fetch stats') if (!res.ok) throw new Error('Failed to fetch stats')
return res.json() return res.json()
@ -183,7 +183,7 @@ export default function BookingsStats() {
queryFn: async () => { queryFn: async () => {
const prevDates = getPreviousPeriodDates() const prevDates = getPreviousPeriodDates()
const res = await fetch(`/kitchen/api/resos/stats?from_date=${prevDates.from}&to_date=${prevDates.to}`, { const res = await fetch(`/kitchen/api/resos/stats?from_date=${prevDates.from}&to_date=${prevDates.to}`, {
headers: { Authorization: `Bearer ${token}` } credentials: 'include'
}) })
if (!res.ok) throw new Error('Failed to fetch previous stats') if (!res.ok) throw new Error('Failed to fetch previous stats')
return res.json() return res.json()
@ -196,7 +196,7 @@ export default function BookingsStats() {
queryKey: ['resos-bookings', selectedDate], queryKey: ['resos-bookings', selectedDate],
queryFn: async () => { queryFn: async () => {
const res = await fetch(`/kitchen/api/resos/bookings/${selectedDate}`, { const res = await fetch(`/kitchen/api/resos/bookings/${selectedDate}`, {
headers: { Authorization: `Bearer ${token}` } credentials: 'include'
}) })
if (!res.ok) throw new Error('Failed to fetch bookings') if (!res.ok) throw new Error('Failed to fetch bookings')
return res.json() return res.json()

View file

@ -52,12 +52,12 @@ export default function NewbookData() {
queryKey: ['settings'], queryKey: ['settings'],
queryFn: async () => { queryFn: async () => {
const res = await fetch('/kitchen/api/settings/', { const res = await fetch('/kitchen/api/settings/', {
headers: { Authorization: `Bearer ${token}` }, credentials: 'include',
}) })
if (!res.ok) throw new Error('Failed to fetch settings') if (!res.ok) throw new Error('Failed to fetch settings')
return res.json() return res.json()
}, },
enabled: !!token, enabled: true,
}) })
const currencySymbol = settings?.currency_symbol || '£' const currencySymbol = settings?.currency_symbol || '£'
@ -66,12 +66,12 @@ export default function NewbookData() {
queryKey: ['newbook-calendar', year, month], queryKey: ['newbook-calendar', year, month],
queryFn: async () => { queryFn: async () => {
const res = await fetch(`/kitchen/api/newbook/calendar/${year}/${month}`, { const res = await fetch(`/kitchen/api/newbook/calendar/${year}/${month}`, {
headers: { Authorization: `Bearer ${token}` }, credentials: 'include',
}) })
if (!res.ok) throw new Error('Failed to fetch calendar data') if (!res.ok) throw new Error('Failed to fetch calendar data')
return res.json() return res.json()
}, },
enabled: !!token, enabled: true,
}) })
const goToPrevMonth = () => { const goToPrevMonth = () => {

View file

@ -67,7 +67,7 @@ export default function ResidentsTableChart() {
queryKey: ['residents-table-chart', 'v2', startDate], // v2 to invalidate old cache queryKey: ['residents-table-chart', 'v2', startDate], // v2 to invalidate old cache
queryFn: async () => { queryFn: async () => {
const res = await fetch(`/kitchen/api/residents-table-chart?start_date=${startDate}`, { const res = await fetch(`/kitchen/api/residents-table-chart?start_date=${startDate}`, {
headers: { Authorization: `Bearer ${token}` } credentials: 'include'
}) })
if (!res.ok) throw new Error('Failed to fetch chart data') if (!res.ok) throw new Error('Failed to fetch chart data')
return res.json() return res.json()

View file

@ -116,12 +116,12 @@ export default function ResosData() {
queryKey: ['resos-settings'], queryKey: ['resos-settings'],
queryFn: async () => { queryFn: async () => {
const res = await fetch('/kitchen/api/resos/settings', { const res = await fetch('/kitchen/api/resos/settings', {
headers: { Authorization: `Bearer ${token}` }, credentials: 'include',
}) })
if (!res.ok) throw new Error('Failed to fetch settings') if (!res.ok) throw new Error('Failed to fetch settings')
return res.json() return res.json()
}, },
enabled: !!token, enabled: true,
}) })
// Fetch daily stats for the month // Fetch daily stats for the month
@ -133,12 +133,12 @@ export default function ResosData() {
const toDate = `${year}-${String(month).padStart(2, '0')}-${String(lastDay.getDate()).padStart(2, '0')}` const toDate = `${year}-${String(month).padStart(2, '0')}-${String(lastDay.getDate()).padStart(2, '0')}`
const res = await fetch(`/kitchen/api/resos/daily-stats?from_date=${firstDay}&to_date=${toDate}`, { const res = await fetch(`/kitchen/api/resos/daily-stats?from_date=${firstDay}&to_date=${toDate}`, {
headers: { Authorization: `Bearer ${token}` }, credentials: 'include',
}) })
if (!res.ok) throw new Error('Failed to fetch daily stats') if (!res.ok) throw new Error('Failed to fetch daily stats')
return res.json() return res.json()
}, },
enabled: !!token, enabled: true,
}) })
// Fetch previous month's stats for comparison // Fetch previous month's stats for comparison
@ -152,12 +152,12 @@ export default function ResosData() {
const toDate = `${prevYear}-${String(prevMonth).padStart(2, '0')}-${String(lastDay.getDate()).padStart(2, '0')}` const toDate = `${prevYear}-${String(prevMonth).padStart(2, '0')}-${String(lastDay.getDate()).padStart(2, '0')}`
const res = await fetch(`/kitchen/api/resos/daily-stats?from_date=${firstDay}&to_date=${toDate}`, { const res = await fetch(`/kitchen/api/resos/daily-stats?from_date=${firstDay}&to_date=${toDate}`, {
headers: { Authorization: `Bearer ${token}` }, credentials: 'include',
}) })
if (!res.ok) throw new Error('Failed to fetch previous month stats') if (!res.ok) throw new Error('Failed to fetch previous month stats')
return res.json() return res.json()
}, },
enabled: !!token, enabled: true,
}) })
// Fetch calendar events for the month // Fetch calendar events for the month
@ -168,7 +168,7 @@ export default function ResosData() {
const lastDay = new Date(year, month, 0) const lastDay = new Date(year, month, 0)
const toDate = `${year}-${String(month).padStart(2, '0')}-${String(lastDay.getDate()).padStart(2, '0')}` const toDate = `${year}-${String(month).padStart(2, '0')}-${String(lastDay.getDate()).padStart(2, '0')}`
const res = await fetch(`/kitchen/api/calendar-events/?from_date=${firstDay}&to_date=${toDate}`, { const res = await fetch(`/kitchen/api/calendar-events/?from_date=${firstDay}&to_date=${toDate}`, {
headers: { Authorization: `Bearer ${token}` } credentials: 'include'
}) })
if (!res.ok) throw new Error('Failed to fetch calendar events') if (!res.ok) throw new Error('Failed to fetch calendar events')
return res.json() return res.json()
@ -182,7 +182,7 @@ export default function ResosData() {
queryFn: async () => { queryFn: async () => {
if (!selectedDate) return [] if (!selectedDate) return []
const res = await fetch(`/kitchen/api/resos/bookings/${selectedDate}`, { const res = await fetch(`/kitchen/api/resos/bookings/${selectedDate}`, {
headers: { Authorization: `Bearer ${token}` }, credentials: 'include',
}) })
if (!res.ok) throw new Error('Failed to fetch bookings') if (!res.ok) throw new Error('Failed to fetch bookings')
return res.json() return res.json()
@ -195,7 +195,7 @@ export default function ResosData() {
queryKey: ['resos-all-opening-hours'], queryKey: ['resos-all-opening-hours'],
queryFn: async () => { queryFn: async () => {
const res = await fetch(`/kitchen/api/resos/opening-hours`, { const res = await fetch(`/kitchen/api/resos/opening-hours`, {
headers: { Authorization: `Bearer ${token}` } credentials: 'include'
}) })
if (!res.ok) throw new Error('Failed to fetch opening hours') if (!res.ok) throw new Error('Failed to fetch opening hours')
return res.json() return res.json()
@ -231,7 +231,7 @@ export default function ResosData() {
queryFn: async () => { queryFn: async () => {
if (!selectedDate) return [] if (!selectedDate) return []
const res = await fetch(`/kitchen/api/resos/opening-hours/${selectedDate}`, { const res = await fetch(`/kitchen/api/resos/opening-hours/${selectedDate}`, {
headers: { Authorization: `Bearer ${token}` } credentials: 'include'
}) })
if (!res.ok) throw new Error('Failed to fetch opening hours') if (!res.ok) throw new Error('Failed to fetch opening hours')
return res.json() return res.json()
@ -1324,7 +1324,7 @@ export default function ResosData() {
if (confirm('Delete this event?')) { if (confirm('Delete this event?')) {
await fetch(`/kitchen/api/calendar-events/${editingEvent.id}`, { await fetch(`/kitchen/api/calendar-events/${editingEvent.id}`, {
method: 'DELETE', method: 'DELETE',
headers: { Authorization: `Bearer ${token}` } credentials: 'include'
}) })
setShowEventModal(false) setShowEventModal(false)
refetchEvents() refetchEvents()
@ -1365,8 +1365,8 @@ export default function ResosData() {
await fetch(url, { await fetch(url, {
method, method,
credentials: 'include',
headers: { headers: {
Authorization: `Bearer ${token}`,
'Content-Type': 'application/json' 'Content-Type': 'application/json'
}, },
body: JSON.stringify(eventForm) body: JSON.stringify(eventForm)

View file

@ -536,7 +536,7 @@ export default function Settings() {
queryKey: ['settings'], queryKey: ['settings'],
queryFn: async () => { queryFn: async () => {
const res = await fetch('/kitchen/api/settings/', { const res = await fetch('/kitchen/api/settings/', {
headers: { Authorization: `Bearer ${token}` }, credentials: 'include',
}) })
if (!res.ok) throw new Error('Failed to fetch settings') if (!res.ok) throw new Error('Failed to fetch settings')
return res.json() return res.json()
@ -548,7 +548,7 @@ export default function Settings() {
queryKey: ['newbook-settings'], queryKey: ['newbook-settings'],
queryFn: async () => { queryFn: async () => {
const res = await fetch('/kitchen/api/newbook/settings', { const res = await fetch('/kitchen/api/newbook/settings', {
headers: { Authorization: `Bearer ${token}` }, credentials: 'include',
}) })
if (!res.ok) { if (!res.ok) {
const errorText = await res.text() const errorText = await res.text()
@ -567,7 +567,7 @@ export default function Settings() {
queryKey: ['resos-settings'], queryKey: ['resos-settings'],
queryFn: async () => { queryFn: async () => {
const res = await fetch('/kitchen/api/resos/settings', { const res = await fetch('/kitchen/api/resos/settings', {
headers: { Authorization: `Bearer ${token}` }, credentials: 'include',
}) })
if (!res.ok) { if (!res.ok) {
const errorText = await res.text() const errorText = await res.text()
@ -585,7 +585,7 @@ export default function Settings() {
queryKey: ['gl-accounts'], queryKey: ['gl-accounts'],
queryFn: async () => { queryFn: async () => {
const res = await fetch('/kitchen/api/newbook/gl-accounts', { const res = await fetch('/kitchen/api/newbook/gl-accounts', {
headers: { Authorization: `Bearer ${token}` }, credentials: 'include',
}) })
if (!res.ok) return [] if (!res.ok) return []
return res.json() return res.json()
@ -597,7 +597,7 @@ export default function Settings() {
queryKey: ['room-categories'], queryKey: ['room-categories'],
queryFn: async () => { queryFn: async () => {
const res = await fetch('/kitchen/api/newbook/room-categories', { const res = await fetch('/kitchen/api/newbook/room-categories', {
headers: { Authorization: `Bearer ${token}` }, credentials: 'include',
}) })
if (!res.ok) return [] if (!res.ok) return []
return res.json() return res.json()
@ -610,12 +610,12 @@ export default function Settings() {
queryKey: ['sambapos-settings'], queryKey: ['sambapos-settings'],
queryFn: async () => { queryFn: async () => {
const res = await fetch('/kitchen/api/sambapos/settings', { const res = await fetch('/kitchen/api/sambapos/settings', {
headers: { Authorization: `Bearer ${token}` }, credentials: 'include',
}) })
if (!res.ok) throw new Error('Failed to fetch SambaPOS settings') if (!res.ok) throw new Error('Failed to fetch SambaPOS settings')
return res.json() return res.json()
}, },
enabled: !!token, enabled: true,
}) })
// Fetch SambaPOS categories (only when connection is configured) // Fetch SambaPOS categories (only when connection is configured)
@ -623,7 +623,7 @@ export default function Settings() {
queryKey: ['sambapos-categories'], queryKey: ['sambapos-categories'],
queryFn: async () => { queryFn: async () => {
const res = await fetch('/kitchen/api/sambapos/categories', { const res = await fetch('/kitchen/api/sambapos/categories', {
headers: { Authorization: `Bearer ${token}` }, credentials: 'include',
}) })
if (!res.ok) { if (!res.ok) {
const data = await res.json() const data = await res.json()
@ -640,7 +640,7 @@ export default function Settings() {
queryKey: ['sambapos-group-codes'], queryKey: ['sambapos-group-codes'],
queryFn: async () => { queryFn: async () => {
const res = await fetch('/kitchen/api/sambapos/group-codes', { const res = await fetch('/kitchen/api/sambapos/group-codes', {
headers: { Authorization: `Bearer ${token}` }, credentials: 'include',
}) })
if (!res.ok) { if (!res.ok) {
const data = await res.json() const data = await res.json()
@ -657,7 +657,7 @@ export default function Settings() {
queryKey: ['sambapos-gl-codes'], queryKey: ['sambapos-gl-codes'],
queryFn: async () => { queryFn: async () => {
const res = await fetch('/kitchen/api/sambapos/gl-codes', { const res = await fetch('/kitchen/api/sambapos/gl-codes', {
headers: { Authorization: `Bearer ${token}` }, credentials: 'include',
}) })
if (!res.ok) { if (!res.ok) {
const data = await res.json() const data = await res.json()
@ -674,12 +674,12 @@ export default function Settings() {
queryKey: ['sambapos-selected-gl-codes'], queryKey: ['sambapos-selected-gl-codes'],
queryFn: async () => { queryFn: async () => {
const res = await fetch('/kitchen/api/sambapos/gl-codes/selected', { const res = await fetch('/kitchen/api/sambapos/gl-codes/selected', {
headers: { Authorization: `Bearer ${token}` }, credentials: 'include',
}) })
if (!res.ok) throw new Error('Failed to fetch selected GL codes') if (!res.ok) throw new Error('Failed to fetch selected GL codes')
return res.json() return res.json()
}, },
enabled: !!token, enabled: true,
}) })
// Fetch KDS settings // Fetch KDS settings
@ -687,12 +687,12 @@ export default function Settings() {
queryKey: ['kds-settings'], queryKey: ['kds-settings'],
queryFn: async () => { queryFn: async () => {
const res = await fetch('/kitchen/api/kds/settings', { const res = await fetch('/kitchen/api/kds/settings', {
headers: { Authorization: `Bearer ${token}` }, credentials: 'include',
}) })
if (!res.ok) throw new Error('Failed to fetch KDS settings') if (!res.ok) throw new Error('Failed to fetch KDS settings')
return res.json() return res.json()
}, },
enabled: !!token, enabled: true,
}) })
// Fetch Kitchen Details // Fetch Kitchen Details
@ -710,12 +710,12 @@ export default function Settings() {
queryKey: ['kitchen-details'], queryKey: ['kitchen-details'],
queryFn: async () => { queryFn: async () => {
const res = await fetch('/kitchen/api/settings/kitchen-details', { const res = await fetch('/kitchen/api/settings/kitchen-details', {
headers: { Authorization: `Bearer ${token}` }, credentials: 'include',
}) })
if (!res.ok) throw new Error('Failed to fetch kitchen details') if (!res.ok) throw new Error('Failed to fetch kitchen details')
return res.json() return res.json()
}, },
enabled: !!token, enabled: true,
}) })
// Fetch Budget settings // Fetch Budget settings
@ -730,12 +730,12 @@ export default function Settings() {
queryKey: ['budget-settings'], queryKey: ['budget-settings'],
queryFn: async () => { queryFn: async () => {
const res = await fetch('/kitchen/api/budget/settings', { const res = await fetch('/kitchen/api/budget/settings', {
headers: { Authorization: `Bearer ${token}` }, credentials: 'include',
}) })
if (!res.ok) throw new Error('Failed to fetch Budget settings') if (!res.ok) throw new Error('Failed to fetch Budget settings')
return res.json() return res.json()
}, },
enabled: !!token, enabled: true,
}) })
// Fetch Nextcloud settings // Fetch Nextcloud settings
@ -743,12 +743,12 @@ export default function Settings() {
queryKey: ['nextcloud-settings'], queryKey: ['nextcloud-settings'],
queryFn: async () => { queryFn: async () => {
const res = await fetch('/kitchen/api/settings/nextcloud', { const res = await fetch('/kitchen/api/settings/nextcloud', {
headers: { Authorization: `Bearer ${token}` }, credentials: 'include',
}) })
if (!res.ok) throw new Error('Failed to fetch Nextcloud settings') if (!res.ok) throw new Error('Failed to fetch Nextcloud settings')
return res.json() return res.json()
}, },
enabled: !!token, enabled: true,
}) })
// Fetch which integrations are configured in the central stack settings service // Fetch which integrations are configured in the central stack settings service
@ -756,12 +756,12 @@ export default function Settings() {
queryKey: ['global-integration-status'], queryKey: ['global-integration-status'],
queryFn: async () => { queryFn: async () => {
const res = await fetch('/kitchen/api/settings/global-status', { const res = await fetch('/kitchen/api/settings/global-status', {
headers: { Authorization: `Bearer ${token}` }, credentials: 'include',
}) })
if (!res.ok) return {} if (!res.ok) return {}
return res.json() return res.json()
}, },
enabled: !!token, enabled: true,
staleTime: 60000, staleTime: 60000,
}) })
@ -770,12 +770,12 @@ export default function Settings() {
queryKey: ['nextcloud-stats'], queryKey: ['nextcloud-stats'],
queryFn: async () => { queryFn: async () => {
const res = await fetch('/kitchen/api/settings/nextcloud/stats', { const res = await fetch('/kitchen/api/settings/nextcloud/stats', {
headers: { Authorization: `Bearer ${token}` }, credentials: 'include',
}) })
if (!res.ok) throw new Error('Failed to fetch Nextcloud stats') if (!res.ok) throw new Error('Failed to fetch Nextcloud stats')
return res.json() return res.json()
}, },
enabled: !!token, enabled: true,
}) })
// Fetch Backup settings // Fetch Backup settings
@ -783,12 +783,12 @@ export default function Settings() {
queryKey: ['backup-settings'], queryKey: ['backup-settings'],
queryFn: async () => { queryFn: async () => {
const res = await fetch('/kitchen/api/backup/settings', { const res = await fetch('/kitchen/api/backup/settings', {
headers: { Authorization: `Bearer ${token}` }, credentials: 'include',
}) })
if (!res.ok) throw new Error('Failed to fetch backup settings') if (!res.ok) throw new Error('Failed to fetch backup settings')
return res.json() return res.json()
}, },
enabled: !!token, enabled: true,
}) })
// Fetch Backup history // Fetch Backup history
@ -796,12 +796,12 @@ export default function Settings() {
queryKey: ['backup-history'], queryKey: ['backup-history'],
queryFn: async () => { queryFn: async () => {
const res = await fetch('/kitchen/api/backup/history', { const res = await fetch('/kitchen/api/backup/history', {
headers: { Authorization: `Bearer ${token}` }, credentials: 'include',
}) })
if (!res.ok) throw new Error('Failed to fetch backup history') if (!res.ok) throw new Error('Failed to fetch backup history')
return res.json() return res.json()
}, },
enabled: !!token, enabled: true,
}) })
// Fetch Search settings // Fetch Search settings
@ -809,12 +809,12 @@ export default function Settings() {
queryKey: ['search-settings'], queryKey: ['search-settings'],
queryFn: async () => { queryFn: async () => {
const res = await fetch('/kitchen/api/search/settings', { const res = await fetch('/kitchen/api/search/settings', {
headers: { Authorization: `Bearer ${token}` }, credentials: 'include',
}) })
if (!res.ok) throw new Error('Failed to fetch search settings') if (!res.ok) throw new Error('Failed to fetch search settings')
return res.json() return res.json()
}, },
enabled: !!token, enabled: true,
}) })
// Fetch IMAP settings // Fetch IMAP settings
@ -822,7 +822,7 @@ export default function Settings() {
queryKey: ['imap-settings'], queryKey: ['imap-settings'],
queryFn: async () => { queryFn: async () => {
const res = await fetch('/kitchen/api/imap/settings', { const res = await fetch('/kitchen/api/imap/settings', {
headers: { Authorization: `Bearer ${token}` }, credentials: 'include',
}) })
if (!res.ok) throw new Error('Failed to fetch IMAP settings') if (!res.ok) throw new Error('Failed to fetch IMAP settings')
return res.json() return res.json()
@ -835,7 +835,7 @@ export default function Settings() {
queryKey: ['imap-logs'], queryKey: ['imap-logs'],
queryFn: async () => { queryFn: async () => {
const res = await fetch('/kitchen/api/imap/logs?limit=20', { const res = await fetch('/kitchen/api/imap/logs?limit=20', {
headers: { Authorization: `Bearer ${token}` }, credentials: 'include',
}) })
if (!res.ok) throw new Error('Failed to fetch IMAP logs') if (!res.ok) throw new Error('Failed to fetch IMAP logs')
return res.json() return res.json()
@ -848,7 +848,7 @@ export default function Settings() {
queryKey: ['imap-stats'], queryKey: ['imap-stats'],
queryFn: async () => { queryFn: async () => {
const res = await fetch('/kitchen/api/imap/logs/stats', { const res = await fetch('/kitchen/api/imap/logs/stats', {
headers: { Authorization: `Bearer ${token}` }, credentials: 'include',
}) })
if (!res.ok) throw new Error('Failed to fetch IMAP stats') if (!res.ok) throw new Error('Failed to fetch IMAP stats')
return res.json() return res.json()
@ -1053,7 +1053,7 @@ export default function Settings() {
// Auto-fetch custom fields if mapping exists but fields list is empty // Auto-fetch custom fields if mapping exists but fields list is empty
if (resosSettings.resos_custom_field_mapping && Object.keys(resosSettings.resos_custom_field_mapping).length > 0 && customFields.length === 0) { if (resosSettings.resos_custom_field_mapping && Object.keys(resosSettings.resos_custom_field_mapping).length > 0 && customFields.length === 0) {
fetch('/kitchen/api/resos/custom-fields', { fetch('/kitchen/api/resos/custom-fields', {
headers: { Authorization: `Bearer ${token}` } credentials: 'include'
}) })
.then(res => res.json()) .then(res => res.json())
.then(data => setCustomFields(data.custom_fields || [])) .then(data => setCustomFields(data.custom_fields || []))
@ -1063,7 +1063,7 @@ export default function Settings() {
// Auto-fetch opening hours if mapping exists but hours list is empty // Auto-fetch opening hours if mapping exists but hours list is empty
if (resosSettings.resos_opening_hours_mapping && resosSettings.resos_opening_hours_mapping.length > 0 && openingHours.length === 0) { if (resosSettings.resos_opening_hours_mapping && resosSettings.resos_opening_hours_mapping.length > 0 && openingHours.length === 0) {
fetch('/kitchen/api/resos/opening-hours', { fetch('/kitchen/api/resos/opening-hours', {
headers: { Authorization: `Bearer ${token}` } credentials: 'include'
}) })
.then(res => res.json()) .then(res => res.json())
.then(data => setOpeningHours(data.opening_hours || [])) .then(data => setOpeningHours(data.opening_hours || []))
@ -1077,7 +1077,7 @@ export default function Settings() {
queryKey: ['food-flag-categories'], queryKey: ['food-flag-categories'],
queryFn: async () => { queryFn: async () => {
const res = await fetch('/kitchen/api/food-flags/categories', { const res = await fetch('/kitchen/api/food-flags/categories', {
headers: { Authorization: `Bearer ${token}` }, credentials: 'include',
}) })
if (!res.ok) return [] if (!res.ok) return []
return res.json() return res.json()
@ -1089,7 +1089,7 @@ export default function Settings() {
queryKey: ['api-access-settings'], queryKey: ['api-access-settings'],
queryFn: async () => { queryFn: async () => {
const res = await fetch('/kitchen/api/settings/api-access', { const res = await fetch('/kitchen/api/settings/api-access', {
headers: { Authorization: `Bearer ${token}` }, credentials: 'include',
}) })
if (!res.ok) return { api_key: null, api_key_enabled: false } if (!res.ok) return { api_key: null, api_key_enabled: false }
return res.json() return res.json()
@ -1109,7 +1109,7 @@ export default function Settings() {
mutationFn: async (data: { name: string; propagation_type: string }) => { mutationFn: async (data: { name: string; propagation_type: string }) => {
const res = await fetch('/kitchen/api/food-flags/categories', { const res = await fetch('/kitchen/api/food-flags/categories', {
method: 'POST', method: 'POST',
headers: { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json' }, credentials: 'include', headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data), body: JSON.stringify(data),
}) })
if (!res.ok) throw new Error('Failed to create category') if (!res.ok) throw new Error('Failed to create category')
@ -1130,7 +1130,7 @@ export default function Settings() {
mutationFn: async ({ id, data }: { id: number; data: { name?: string; propagation_type?: string; required?: boolean } }) => { mutationFn: async ({ id, data }: { id: number; data: { name?: string; propagation_type?: string; required?: boolean } }) => {
const res = await fetch(`/kitchen/api/food-flags/categories/${id}`, { const res = await fetch(`/kitchen/api/food-flags/categories/${id}`, {
method: 'PATCH', method: 'PATCH',
headers: { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json' }, credentials: 'include', headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data), body: JSON.stringify(data),
}) })
if (!res.ok) throw new Error('Failed to update category') if (!res.ok) throw new Error('Failed to update category')
@ -1149,7 +1149,7 @@ export default function Settings() {
mutationFn: async (id: number) => { mutationFn: async (id: number) => {
const res = await fetch(`/kitchen/api/food-flags/categories/${id}`, { const res = await fetch(`/kitchen/api/food-flags/categories/${id}`, {
method: 'DELETE', method: 'DELETE',
headers: { Authorization: `Bearer ${token}` }, credentials: 'include',
}) })
if (!res.ok) throw new Error('Failed to delete category') if (!res.ok) throw new Error('Failed to delete category')
return res.json() return res.json()
@ -1166,7 +1166,7 @@ export default function Settings() {
mutationFn: async (data: { category_id: number; name: string; code?: string; icon?: string }) => { mutationFn: async (data: { category_id: number; name: string; code?: string; icon?: string }) => {
const res = await fetch('/kitchen/api/food-flags/flags', { const res = await fetch('/kitchen/api/food-flags/flags', {
method: 'POST', method: 'POST',
headers: { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json' }, credentials: 'include', headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data), body: JSON.stringify(data),
}) })
if (!res.ok) throw new Error('Failed to create flag') if (!res.ok) throw new Error('Failed to create flag')
@ -1188,7 +1188,7 @@ export default function Settings() {
mutationFn: async ({ id, data }: { id: number; data: { name?: string; code?: string; icon?: string } }) => { mutationFn: async ({ id, data }: { id: number; data: { name?: string; code?: string; icon?: string } }) => {
const res = await fetch(`/kitchen/api/food-flags/flags/${id}`, { const res = await fetch(`/kitchen/api/food-flags/flags/${id}`, {
method: 'PATCH', method: 'PATCH',
headers: { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json' }, credentials: 'include', headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data), body: JSON.stringify(data),
}) })
if (!res.ok) throw new Error('Failed to update flag') if (!res.ok) throw new Error('Failed to update flag')
@ -1207,7 +1207,7 @@ export default function Settings() {
mutationFn: async (id: number) => { mutationFn: async (id: number) => {
const res = await fetch(`/kitchen/api/food-flags/flags/${id}`, { const res = await fetch(`/kitchen/api/food-flags/flags/${id}`, {
method: 'DELETE', method: 'DELETE',
headers: { Authorization: `Bearer ${token}` }, credentials: 'include',
}) })
if (!res.ok) throw new Error('Failed to delete flag') if (!res.ok) throw new Error('Failed to delete flag')
return res.json() return res.json()
@ -1238,7 +1238,7 @@ export default function Settings() {
queryKey: ['allergen-keywords'], queryKey: ['allergen-keywords'],
queryFn: async () => { queryFn: async () => {
const res = await fetch('/kitchen/api/food-flags/keywords', { const res = await fetch('/kitchen/api/food-flags/keywords', {
headers: { Authorization: `Bearer ${token}` }, credentials: 'include',
}) })
if (!res.ok) return [] if (!res.ok) return []
return res.json() return res.json()
@ -1250,7 +1250,7 @@ export default function Settings() {
mutationFn: async ({ food_flag_id, keyword }: { food_flag_id: number; keyword: string }) => { mutationFn: async ({ food_flag_id, keyword }: { food_flag_id: number; keyword: string }) => {
const res = await fetch('/kitchen/api/food-flags/keywords', { const res = await fetch('/kitchen/api/food-flags/keywords', {
method: 'POST', method: 'POST',
headers: { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json' }, credentials: 'include', headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ food_flag_id, keyword }), body: JSON.stringify({ food_flag_id, keyword }),
}) })
if (!res.ok) { if (!res.ok) {
@ -1275,7 +1275,7 @@ export default function Settings() {
mutationFn: async (keywordId: number) => { mutationFn: async (keywordId: number) => {
const res = await fetch(`/kitchen/api/food-flags/keywords/${keywordId}`, { const res = await fetch(`/kitchen/api/food-flags/keywords/${keywordId}`, {
method: 'DELETE', method: 'DELETE',
headers: { Authorization: `Bearer ${token}` }, credentials: 'include',
}) })
if (!res.ok) throw new Error('Failed to delete keyword') if (!res.ok) throw new Error('Failed to delete keyword')
}, },
@ -1288,7 +1288,7 @@ export default function Settings() {
mutationFn: async () => { mutationFn: async () => {
const res = await fetch('/kitchen/api/food-flags/keywords/reset-defaults', { const res = await fetch('/kitchen/api/food-flags/keywords/reset-defaults', {
method: 'POST', method: 'POST',
headers: { Authorization: `Bearer ${token}` }, credentials: 'include',
}) })
if (!res.ok) throw new Error('Failed to reset keywords') if (!res.ok) throw new Error('Failed to reset keywords')
}, },
@ -1311,7 +1311,7 @@ export default function Settings() {
queryKey: ['ingredient-categories-settings'], queryKey: ['ingredient-categories-settings'],
queryFn: async () => { queryFn: async () => {
const res = await fetch('/kitchen/api/ingredients/categories', { const res = await fetch('/kitchen/api/ingredients/categories', {
headers: { Authorization: `Bearer ${token}` }, credentials: 'include',
}) })
if (!res.ok) return [] if (!res.ok) return []
return res.json() return res.json()
@ -1323,7 +1323,7 @@ export default function Settings() {
mutationFn: async (data: { name: string; sort_order: number }) => { mutationFn: async (data: { name: string; sort_order: number }) => {
const res = await fetch('/kitchen/api/ingredients/categories', { const res = await fetch('/kitchen/api/ingredients/categories', {
method: 'POST', method: 'POST',
headers: { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json' }, credentials: 'include', headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data), body: JSON.stringify(data),
}) })
if (!res.ok) { const err = await res.json().catch(() => null); throw new Error(err?.detail || 'Failed to create category') } if (!res.ok) { const err = await res.json().catch(() => null); throw new Error(err?.detail || 'Failed to create category') }
@ -1344,7 +1344,7 @@ export default function Settings() {
mutationFn: async ({ id, data }: { id: number; data: { name?: string; sort_order?: number } }) => { mutationFn: async ({ id, data }: { id: number; data: { name?: string; sort_order?: number } }) => {
const res = await fetch(`/kitchen/api/ingredients/categories/${id}`, { const res = await fetch(`/kitchen/api/ingredients/categories/${id}`, {
method: 'PATCH', method: 'PATCH',
headers: { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json' }, credentials: 'include', headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data), body: JSON.stringify(data),
}) })
if (!res.ok) { const err = await res.json().catch(() => null); throw new Error(err?.detail || 'Failed to update category') } if (!res.ok) { const err = await res.json().catch(() => null); throw new Error(err?.detail || 'Failed to update category') }
@ -1363,7 +1363,7 @@ export default function Settings() {
mutationFn: async (id: number) => { mutationFn: async (id: number) => {
const res = await fetch(`/kitchen/api/ingredients/categories/${id}`, { const res = await fetch(`/kitchen/api/ingredients/categories/${id}`, {
method: 'DELETE', method: 'DELETE',
headers: { Authorization: `Bearer ${token}` }, credentials: 'include',
}) })
if (!res.ok) throw new Error('Failed to delete category') if (!res.ok) throw new Error('Failed to delete category')
return res.json() return res.json()
@ -1393,7 +1393,7 @@ export default function Settings() {
queryKey: ['recipe-sections-settings'], queryKey: ['recipe-sections-settings'],
queryFn: async () => { queryFn: async () => {
const res = await fetch('/kitchen/api/recipes/menu-sections?section_type=recipe', { const res = await fetch('/kitchen/api/recipes/menu-sections?section_type=recipe', {
headers: { Authorization: `Bearer ${token}` }, credentials: 'include',
}) })
if (!res.ok) return [] if (!res.ok) return []
return res.json() return res.json()
@ -1406,7 +1406,7 @@ export default function Settings() {
queryKey: ['dish-courses-settings'], queryKey: ['dish-courses-settings'],
queryFn: async () => { queryFn: async () => {
const res = await fetch('/kitchen/api/recipes/menu-sections?section_type=dish', { const res = await fetch('/kitchen/api/recipes/menu-sections?section_type=dish', {
headers: { Authorization: `Bearer ${token}` }, credentials: 'include',
}) })
if (!res.ok) return [] if (!res.ok) return []
return res.json() return res.json()
@ -1419,7 +1419,7 @@ export default function Settings() {
mutationFn: async (data: { api_key_enabled: boolean }) => { mutationFn: async (data: { api_key_enabled: boolean }) => {
const res = await fetch('/kitchen/api/settings/api-access', { const res = await fetch('/kitchen/api/settings/api-access', {
method: 'PATCH', method: 'PATCH',
headers: { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json' }, credentials: 'include', headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data), body: JSON.stringify(data),
}) })
if (!res.ok) throw new Error('Failed to save API access settings') if (!res.ok) throw new Error('Failed to save API access settings')
@ -1438,7 +1438,7 @@ export default function Settings() {
mutationFn: async () => { mutationFn: async () => {
const res = await fetch('/kitchen/api/settings/api-access/regenerate', { const res = await fetch('/kitchen/api/settings/api-access/regenerate', {
method: 'POST', method: 'POST',
headers: { Authorization: `Bearer ${token}` }, credentials: 'include',
}) })
if (!res.ok) throw new Error('Failed to regenerate API key') if (!res.ok) throw new Error('Failed to regenerate API key')
return res.json() return res.json()
@ -1462,7 +1462,7 @@ export default function Settings() {
queryKey: ['llm-usage'], queryKey: ['llm-usage'],
queryFn: async () => { queryFn: async () => {
const res = await fetch('/kitchen/api/settings/llm-usage', { const res = await fetch('/kitchen/api/settings/llm-usage', {
headers: { Authorization: `Bearer ${token}` }, credentials: 'include',
}) })
if (!res.ok) return { total_calls: 0, successful_calls: 0, failed_calls: 0, total_input_tokens: 0, total_output_tokens: 0, total_tokens: 0, estimated_cost_usd: 0, cache_entries_this_month: 0 } if (!res.ok) return { total_calls: 0, successful_calls: 0, failed_calls: 0, total_input_tokens: 0, total_output_tokens: 0, total_tokens: 0, estimated_cost_usd: 0, cache_entries_this_month: 0 }
return res.json() return res.json()
@ -1480,7 +1480,7 @@ export default function Settings() {
queryKey: ['llm-models'], queryKey: ['llm-models'],
queryFn: async () => { queryFn: async () => {
const res = await fetch('/kitchen/api/settings/llm-models', { const res = await fetch('/kitchen/api/settings/llm-models', {
headers: { Authorization: `Bearer ${token}` }, credentials: 'include',
}) })
if (!res.ok) return { models: [], default: 'claude-haiku-4-5-20251001', current: 'claude-haiku-4-5-20251001' } if (!res.ok) return { models: [], default: 'claude-haiku-4-5-20251001', current: 'claude-haiku-4-5-20251001' }
return res.json() return res.json()
@ -1493,7 +1493,7 @@ export default function Settings() {
mutationFn: async (data: Record<string, unknown>) => { mutationFn: async (data: Record<string, unknown>) => {
const res = await fetch('/kitchen/api/settings/', { const res = await fetch('/kitchen/api/settings/', {
method: 'PATCH', method: 'PATCH',
headers: { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json' }, credentials: 'include', headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data), body: JSON.stringify(data),
}) })
if (!res.ok) throw new Error('Failed to save LLM settings') if (!res.ok) throw new Error('Failed to save LLM settings')
@ -1512,7 +1512,7 @@ export default function Settings() {
mutationFn: async () => { mutationFn: async () => {
const res = await fetch('/kitchen/api/settings/test-llm', { const res = await fetch('/kitchen/api/settings/test-llm', {
method: 'POST', method: 'POST',
headers: { Authorization: `Bearer ${token}` }, credentials: 'include',
}) })
if (!res.ok) { if (!res.ok) {
const data = await res.json() const data = await res.json()
@ -1548,8 +1548,8 @@ export default function Settings() {
mutationFn: async (data: Partial<SettingsData & { azure_key?: string }>) => { mutationFn: async (data: Partial<SettingsData & { azure_key?: string }>) => {
const res = await fetch('/kitchen/api/settings/', { const res = await fetch('/kitchen/api/settings/', {
method: 'PATCH', method: 'PATCH',
credentials: 'include',
headers: { headers: {
Authorization: `Bearer ${token}`,
'Content-Type': 'application/json', 'Content-Type': 'application/json',
}, },
body: JSON.stringify(data), body: JSON.stringify(data),
@ -1572,7 +1572,7 @@ export default function Settings() {
mutationFn: async () => { mutationFn: async () => {
const res = await fetch('/kitchen/api/settings/test-azure', { const res = await fetch('/kitchen/api/settings/test-azure', {
method: 'POST', method: 'POST',
headers: { Authorization: `Bearer ${token}` }, credentials: 'include',
}) })
if (!res.ok) { if (!res.ok) {
const data = await res.json() const data = await res.json()
@ -1593,8 +1593,8 @@ export default function Settings() {
mutationFn: async (data: { current_password: string; new_password: string }) => { mutationFn: async (data: { current_password: string; new_password: string }) => {
const res = await fetch('/auth/change-password', { const res = await fetch('/auth/change-password', {
method: 'POST', method: 'POST',
credentials: 'include',
headers: { headers: {
Authorization: `Bearer ${token}`,
'Content-Type': 'application/json', 'Content-Type': 'application/json',
}, },
body: JSON.stringify(data), body: JSON.stringify(data),
@ -1621,7 +1621,7 @@ export default function Settings() {
mutationFn: async () => { mutationFn: async () => {
const res = await fetch('/kitchen/api/invoices/reprocess-all', { const res = await fetch('/kitchen/api/invoices/reprocess-all', {
method: 'POST', method: 'POST',
headers: { Authorization: `Bearer ${token}` }, credentials: 'include',
}) })
if (!res.ok) { if (!res.ok) {
const data = await res.json() const data = await res.json()
@ -1643,7 +1643,7 @@ export default function Settings() {
mutationFn: async () => { mutationFn: async () => {
const res = await fetch('/kitchen/api/recipes/cleanup-false-price-changes', { const res = await fetch('/kitchen/api/recipes/cleanup-false-price-changes', {
method: 'POST', method: 'POST',
headers: { Authorization: `Bearer ${token}` }, credentials: 'include',
}) })
if (!res.ok) { if (!res.ok) {
const data = await res.json() const data = await res.json()
@ -1664,7 +1664,7 @@ export default function Settings() {
mutationFn: async () => { mutationFn: async () => {
const res = await fetch('/kitchen/api/recipes/backfill-invoice-references', { const res = await fetch('/kitchen/api/recipes/backfill-invoice-references', {
method: 'POST', method: 'POST',
headers: { Authorization: `Bearer ${token}` }, credentials: 'include',
}) })
if (!res.ok) { if (!res.ok) {
const data = await res.json() const data = await res.json()
@ -1685,7 +1685,7 @@ export default function Settings() {
mutationFn: async () => { mutationFn: async () => {
const res = await fetch('/kitchen/api/suppliers/rematch-fuzzy', { const res = await fetch('/kitchen/api/suppliers/rematch-fuzzy', {
method: 'POST', method: 'POST',
headers: { Authorization: `Bearer ${token}` }, credentials: 'include',
}) })
if (!res.ok) { if (!res.ok) {
const data = await res.json() const data = await res.json()
@ -1709,8 +1709,8 @@ export default function Settings() {
console.log('[Newbook] Sending PATCH with data:', data) console.log('[Newbook] Sending PATCH with data:', data)
const res = await fetch('/kitchen/api/newbook/settings', { const res = await fetch('/kitchen/api/newbook/settings', {
method: 'PATCH', method: 'PATCH',
credentials: 'include',
headers: { headers: {
Authorization: `Bearer ${token}`,
'Content-Type': 'application/json', 'Content-Type': 'application/json',
}, },
body: JSON.stringify(data), body: JSON.stringify(data),
@ -1741,7 +1741,7 @@ export default function Settings() {
mutationFn: async () => { mutationFn: async () => {
const res = await fetch('/kitchen/api/newbook/test-connection', { const res = await fetch('/kitchen/api/newbook/test-connection', {
method: 'POST', method: 'POST',
headers: { Authorization: `Bearer ${token}` }, credentials: 'include',
}) })
if (!res.ok) { if (!res.ok) {
const data = await res.json() const data = await res.json()
@ -1764,8 +1764,8 @@ export default function Settings() {
console.log('[Resos] Sending PATCH with data:', data) console.log('[Resos] Sending PATCH with data:', data)
const res = await fetch('/kitchen/api/resos/settings', { const res = await fetch('/kitchen/api/resos/settings', {
method: 'PATCH', method: 'PATCH',
credentials: 'include',
headers: { headers: {
Authorization: `Bearer ${token}`,
'Content-Type': 'application/json', 'Content-Type': 'application/json',
}, },
body: JSON.stringify(data), body: JSON.stringify(data),
@ -1794,7 +1794,7 @@ export default function Settings() {
mutationFn: async () => { mutationFn: async () => {
const res = await fetch('/kitchen/api/newbook/gl-accounts/fetch', { const res = await fetch('/kitchen/api/newbook/gl-accounts/fetch', {
method: 'POST', method: 'POST',
headers: { Authorization: `Bearer ${token}` }, credentials: 'include',
}) })
if (!res.ok) { if (!res.ok) {
const data = await res.json() const data = await res.json()
@ -1816,7 +1816,7 @@ export default function Settings() {
mutationFn: async () => { mutationFn: async () => {
const res = await fetch('/kitchen/api/newbook/sync/forecast', { const res = await fetch('/kitchen/api/newbook/sync/forecast', {
method: 'POST', method: 'POST',
headers: { Authorization: `Bearer ${token}` }, credentials: 'include',
}) })
if (!res.ok) { if (!res.ok) {
const data = await res.json() const data = await res.json()
@ -1838,8 +1838,8 @@ export default function Settings() {
mutationFn: async (dates: { date_from: string; date_to: string }) => { mutationFn: async (dates: { date_from: string; date_to: string }) => {
const res = await fetch('/kitchen/api/newbook/sync/historical', { const res = await fetch('/kitchen/api/newbook/sync/historical', {
method: 'POST', method: 'POST',
credentials: 'include',
headers: { headers: {
Authorization: `Bearer ${token}`,
'Content-Type': 'application/json', 'Content-Type': 'application/json',
}, },
body: JSON.stringify(dates), body: JSON.stringify(dates),
@ -1865,8 +1865,8 @@ export default function Settings() {
mutationFn: async ({ id, is_tracked }: { id: number; is_tracked: boolean }) => { mutationFn: async ({ id, is_tracked }: { id: number; is_tracked: boolean }) => {
const res = await fetch(`/kitchen/api/newbook/gl-accounts/${id}`, { const res = await fetch(`/kitchen/api/newbook/gl-accounts/${id}`, {
method: 'PATCH', method: 'PATCH',
credentials: 'include',
headers: { headers: {
Authorization: `Bearer ${token}`,
'Content-Type': 'application/json', 'Content-Type': 'application/json',
}, },
body: JSON.stringify({ is_tracked }), body: JSON.stringify({ is_tracked }),
@ -1883,8 +1883,8 @@ export default function Settings() {
mutationFn: async (updates: { id: number; is_tracked: boolean }[]) => { mutationFn: async (updates: { id: number; is_tracked: boolean }[]) => {
const res = await fetch('/kitchen/api/newbook/gl-accounts/bulk-update', { const res = await fetch('/kitchen/api/newbook/gl-accounts/bulk-update', {
method: 'PATCH', method: 'PATCH',
credentials: 'include',
headers: { headers: {
Authorization: `Bearer ${token}`,
'Content-Type': 'application/json', 'Content-Type': 'application/json',
}, },
body: JSON.stringify({ updates }), body: JSON.stringify({ updates }),
@ -1902,7 +1902,7 @@ export default function Settings() {
mutationFn: async () => { mutationFn: async () => {
const res = await fetch('/kitchen/api/newbook/room-categories/fetch', { const res = await fetch('/kitchen/api/newbook/room-categories/fetch', {
method: 'POST', method: 'POST',
headers: { Authorization: `Bearer ${token}` }, credentials: 'include',
}) })
if (!res.ok) { if (!res.ok) {
const data = await res.json() const data = await res.json()
@ -1924,8 +1924,8 @@ export default function Settings() {
mutationFn: async (updates: { id: number; is_included: boolean }[]) => { mutationFn: async (updates: { id: number; is_included: boolean }[]) => {
const res = await fetch('/kitchen/api/newbook/room-categories/bulk-update', { const res = await fetch('/kitchen/api/newbook/room-categories/bulk-update', {
method: 'PATCH', method: 'PATCH',
credentials: 'include',
headers: { headers: {
Authorization: `Bearer ${token}`,
'Content-Type': 'application/json', 'Content-Type': 'application/json',
}, },
body: JSON.stringify({ updates }), body: JSON.stringify({ updates }),
@ -1943,8 +1943,8 @@ export default function Settings() {
mutationFn: async (data: Record<string, unknown>) => { mutationFn: async (data: Record<string, unknown>) => {
const res = await fetch('/kitchen/api/sambapos/settings', { const res = await fetch('/kitchen/api/sambapos/settings', {
method: 'PATCH', method: 'PATCH',
credentials: 'include',
headers: { headers: {
Authorization: `Bearer ${token}`,
'Content-Type': 'application/json', 'Content-Type': 'application/json',
}, },
body: JSON.stringify(data), body: JSON.stringify(data),
@ -1967,7 +1967,7 @@ export default function Settings() {
mutationFn: async () => { mutationFn: async () => {
const res = await fetch('/kitchen/api/sambapos/test-connection', { const res = await fetch('/kitchen/api/sambapos/test-connection', {
method: 'POST', method: 'POST',
headers: { Authorization: `Bearer ${token}` }, credentials: 'include',
}) })
if (!res.ok) { if (!res.ok) {
const data = await res.json() const data = await res.json()
@ -1989,8 +1989,8 @@ export default function Settings() {
mutationFn: async (courses: string[]) => { mutationFn: async (courses: string[]) => {
const res = await fetch('/kitchen/api/sambapos/tracked-categories', { const res = await fetch('/kitchen/api/sambapos/tracked-categories', {
method: 'PATCH', method: 'PATCH',
credentials: 'include',
headers: { headers: {
Authorization: `Bearer ${token}`,
'Content-Type': 'application/json', 'Content-Type': 'application/json',
}, },
body: JSON.stringify({ categories: courses }), body: JSON.stringify({ categories: courses }),
@ -2012,8 +2012,8 @@ export default function Settings() {
mutationFn: async (items: string[]) => { mutationFn: async (items: string[]) => {
const res = await fetch('/kitchen/api/sambapos/excluded-items', { const res = await fetch('/kitchen/api/sambapos/excluded-items', {
method: 'PATCH', method: 'PATCH',
credentials: 'include',
headers: { headers: {
Authorization: `Bearer ${token}`,
'Content-Type': 'application/json', 'Content-Type': 'application/json',
}, },
body: JSON.stringify({ items }), body: JSON.stringify({ items }),
@ -2035,8 +2035,8 @@ export default function Settings() {
mutationFn: async (data: { food_codes: string[]; beverage_codes: string[] }) => { mutationFn: async (data: { food_codes: string[]; beverage_codes: string[] }) => {
const res = await fetch('/kitchen/api/sambapos/gl-codes', { const res = await fetch('/kitchen/api/sambapos/gl-codes', {
method: 'PATCH', method: 'PATCH',
credentials: 'include',
headers: { headers: {
Authorization: `Bearer ${token}`,
'Content-Type': 'application/json', 'Content-Type': 'application/json',
}, },
body: JSON.stringify(data), body: JSON.stringify(data),
@ -2059,8 +2059,8 @@ export default function Settings() {
mutationFn: async (data: Record<string, unknown>) => { mutationFn: async (data: Record<string, unknown>) => {
const res = await fetch('/kitchen/api/kds/settings', { const res = await fetch('/kitchen/api/kds/settings', {
method: 'PATCH', method: 'PATCH',
credentials: 'include',
headers: { headers: {
Authorization: `Bearer ${token}`,
'Content-Type': 'application/json', 'Content-Type': 'application/json',
}, },
body: JSON.stringify(data), body: JSON.stringify(data),
@ -2082,7 +2082,7 @@ export default function Settings() {
const kdsTestMutation = useMutation({ const kdsTestMutation = useMutation({
mutationFn: async () => { mutationFn: async () => {
const res = await fetch('/kitchen/api/kds/test-connection', { const res = await fetch('/kitchen/api/kds/test-connection', {
headers: { Authorization: `Bearer ${token}` }, credentials: 'include',
}) })
const data = await res.json() const data = await res.json()
if (!data.success) { if (!data.success) {
@ -2104,8 +2104,8 @@ export default function Settings() {
mutationFn: async (data: Record<string, unknown>) => { mutationFn: async (data: Record<string, unknown>) => {
const res = await fetch('/kitchen/api/settings/kitchen-details', { const res = await fetch('/kitchen/api/settings/kitchen-details', {
method: 'PATCH', method: 'PATCH',
credentials: 'include',
headers: { headers: {
Authorization: `Bearer ${token}`,
'Content-Type': 'application/json', 'Content-Type': 'application/json',
}, },
body: JSON.stringify(data), body: JSON.stringify(data),
@ -2130,8 +2130,8 @@ export default function Settings() {
mutationFn: async (data: Record<string, unknown>) => { mutationFn: async (data: Record<string, unknown>) => {
const res = await fetch('/kitchen/api/budget/settings', { const res = await fetch('/kitchen/api/budget/settings', {
method: 'PATCH', method: 'PATCH',
credentials: 'include',
headers: { headers: {
Authorization: `Bearer ${token}`,
'Content-Type': 'application/json', 'Content-Type': 'application/json',
}, },
body: JSON.stringify(data), body: JSON.stringify(data),
@ -2157,7 +2157,7 @@ export default function Settings() {
mutationFn: async () => { mutationFn: async () => {
const res = await fetch('/kitchen/api/budget/test-forecast-connection', { const res = await fetch('/kitchen/api/budget/test-forecast-connection', {
method: 'POST', method: 'POST',
headers: { Authorization: `Bearer ${token}` }, credentials: 'include',
}) })
const data = await res.json() const data = await res.json()
if (!data.success) { if (!data.success) {
@ -2179,8 +2179,8 @@ export default function Settings() {
mutationFn: async (data: Record<string, unknown>) => { mutationFn: async (data: Record<string, unknown>) => {
const res = await fetch('/kitchen/api/settings/nextcloud', { const res = await fetch('/kitchen/api/settings/nextcloud', {
method: 'PATCH', method: 'PATCH',
credentials: 'include',
headers: { headers: {
Authorization: `Bearer ${token}`,
'Content-Type': 'application/json', 'Content-Type': 'application/json',
}, },
body: JSON.stringify(data), body: JSON.stringify(data),
@ -2206,7 +2206,7 @@ export default function Settings() {
mutationFn: async () => { mutationFn: async () => {
const res = await fetch('/kitchen/api/settings/nextcloud/test', { const res = await fetch('/kitchen/api/settings/nextcloud/test', {
method: 'POST', method: 'POST',
headers: { Authorization: `Bearer ${token}` }, credentials: 'include',
}) })
if (!res.ok) { if (!res.ok) {
const error = await res.json() const error = await res.json()
@ -2227,7 +2227,7 @@ export default function Settings() {
mutationFn: async () => { mutationFn: async () => {
const res = await fetch('/kitchen/api/settings/nextcloud/archive-all', { const res = await fetch('/kitchen/api/settings/nextcloud/archive-all', {
method: 'POST', method: 'POST',
headers: { Authorization: `Bearer ${token}` }, credentials: 'include',
}) })
if (!res.ok) { if (!res.ok) {
const error = await res.json() const error = await res.json()
@ -2250,8 +2250,8 @@ export default function Settings() {
mutationFn: async (data: Record<string, unknown>) => { mutationFn: async (data: Record<string, unknown>) => {
const res = await fetch('/kitchen/api/backup/settings', { const res = await fetch('/kitchen/api/backup/settings', {
method: 'PATCH', method: 'PATCH',
credentials: 'include',
headers: { headers: {
Authorization: `Bearer ${token}`,
'Content-Type': 'application/json', 'Content-Type': 'application/json',
}, },
body: JSON.stringify(data), body: JSON.stringify(data),
@ -2276,7 +2276,7 @@ export default function Settings() {
mutationFn: async () => { mutationFn: async () => {
const res = await fetch('/kitchen/api/backup/create', { const res = await fetch('/kitchen/api/backup/create', {
method: 'POST', method: 'POST',
headers: { Authorization: `Bearer ${token}` }, credentials: 'include',
}) })
if (!res.ok) { if (!res.ok) {
const error = await res.json() const error = await res.json()
@ -2299,7 +2299,7 @@ export default function Settings() {
mutationFn: async (backupId: number) => { mutationFn: async (backupId: number) => {
const res = await fetch(`/kitchen/api/backup/${backupId}/restore`, { const res = await fetch(`/kitchen/api/backup/${backupId}/restore`, {
method: 'POST', method: 'POST',
headers: { Authorization: `Bearer ${token}` }, credentials: 'include',
}) })
if (!res.ok) { if (!res.ok) {
const error = await res.json() const error = await res.json()
@ -2322,7 +2322,7 @@ export default function Settings() {
mutationFn: async (backupId: number) => { mutationFn: async (backupId: number) => {
const res = await fetch(`/kitchen/api/backup/${backupId}`, { const res = await fetch(`/kitchen/api/backup/${backupId}`, {
method: 'DELETE', method: 'DELETE',
headers: { Authorization: `Bearer ${token}` }, credentials: 'include',
}) })
if (!res.ok) { if (!res.ok) {
const error = await res.json() const error = await res.json()
@ -2346,7 +2346,7 @@ export default function Settings() {
formData.append('file', file) formData.append('file', file)
const res = await fetch('/kitchen/api/backup/upload', { const res = await fetch('/kitchen/api/backup/upload', {
method: 'POST', method: 'POST',
headers: { Authorization: `Bearer ${token}` }, credentials: 'include',
body: formData, body: formData,
}) })
if (!res.ok) { if (!res.ok) {
@ -2370,8 +2370,8 @@ export default function Settings() {
mutationFn: async (data: Record<string, unknown>) => { mutationFn: async (data: Record<string, unknown>) => {
const res = await fetch('/kitchen/api/search/settings', { const res = await fetch('/kitchen/api/search/settings', {
method: 'PATCH', method: 'PATCH',
credentials: 'include',
headers: { headers: {
Authorization: `Bearer ${token}`,
'Content-Type': 'application/json', 'Content-Type': 'application/json',
}, },
body: JSON.stringify(data), body: JSON.stringify(data),
@ -3135,9 +3135,9 @@ export default function Settings() {
const saveRes = await fetch('/kitchen/api/settings/', { const saveRes = await fetch('/kitchen/api/settings/', {
method: 'PATCH', method: 'PATCH',
credentials: 'include',
headers: { headers: {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
Authorization: `Bearer ${token}`
}, },
body: JSON.stringify(savePayload) body: JSON.stringify(savePayload)
}) })
@ -3153,7 +3153,7 @@ export default function Settings() {
setSmtpTestStatus('Testing connection...') setSmtpTestStatus('Testing connection...')
const res = await fetch('/kitchen/api/settings/test-smtp', { const res = await fetch('/kitchen/api/settings/test-smtp', {
method: 'POST', method: 'POST',
headers: { Authorization: `Bearer ${token}` } credentials: 'include'
}) })
if (res.ok) { if (res.ok) {
const data = await res.json() const data = await res.json()
@ -3178,9 +3178,9 @@ export default function Settings() {
try { try {
const res = await fetch('/kitchen/api/settings/', { const res = await fetch('/kitchen/api/settings/', {
method: 'PATCH', method: 'PATCH',
credentials: 'include',
headers: { headers: {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
Authorization: `Bearer ${token}`
}, },
body: JSON.stringify({ body: JSON.stringify({
smtp_host: smtpHost || null, smtp_host: smtpHost || null,
@ -3355,9 +3355,9 @@ export default function Settings() {
try { try {
const res = await fetch('/kitchen/api/imap/test-connection', { const res = await fetch('/kitchen/api/imap/test-connection', {
method: 'POST', method: 'POST',
credentials: 'include',
headers: { headers: {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
Authorization: `Bearer ${token}`
}, },
body: JSON.stringify({ body: JSON.stringify({
imap_host: imapHost || undefined, imap_host: imapHost || undefined,
@ -3388,7 +3388,7 @@ export default function Settings() {
try { try {
const res = await fetch('/kitchen/api/imap/sync-now', { const res = await fetch('/kitchen/api/imap/sync-now', {
method: 'POST', method: 'POST',
headers: { Authorization: `Bearer ${token}` } credentials: 'include'
}) })
const data = await res.json() const data = await res.json()
if (data.success) { if (data.success) {
@ -3418,9 +3418,9 @@ export default function Settings() {
try { try {
const res = await fetch('/kitchen/api/imap/settings', { const res = await fetch('/kitchen/api/imap/settings', {
method: 'PATCH', method: 'PATCH',
credentials: 'include',
headers: { headers: {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
Authorization: `Bearer ${token}`
}, },
body: JSON.stringify({ body: JSON.stringify({
imap_host: imapHost || null, imap_host: imapHost || null,
@ -3624,9 +3624,9 @@ export default function Settings() {
try { try {
const res = await fetch('/kitchen/api/settings/', { const res = await fetch('/kitchen/api/settings/', {
method: 'PATCH', method: 'PATCH',
credentials: 'include',
headers: { headers: {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
Authorization: `Bearer ${token}`
}, },
body: JSON.stringify({ body: JSON.stringify({
dext_email: dextEmail || null, dext_email: dextEmail || null,
@ -3675,7 +3675,7 @@ export default function Settings() {
try { try {
const res = await fetch('/kitchen/api/invoices/bulk/mark-all-dext-sent', { const res = await fetch('/kitchen/api/invoices/bulk/mark-all-dext-sent', {
method: 'POST', method: 'POST',
headers: { Authorization: `Bearer ${token}` } credentials: 'include'
}) })
if (res.ok) { if (res.ok) {
const result = await res.json() const result = await res.json()
@ -4073,7 +4073,7 @@ export default function Settings() {
setResosTestStatus('Testing...') setResosTestStatus('Testing...')
const res = await fetch('/kitchen/api/resos/test-connection', { const res = await fetch('/kitchen/api/resos/test-connection', {
method: 'POST', method: 'POST',
headers: { Authorization: `Bearer ${token}` } credentials: 'include'
}) })
if (res.ok) { if (res.ok) {
setResosTestStatus('✓ Connection successful') setResosTestStatus('✓ Connection successful')
@ -4142,7 +4142,7 @@ export default function Settings() {
onClick={async () => { onClick={async () => {
try { try {
const res = await fetch('/kitchen/api/resos/custom-fields', { const res = await fetch('/kitchen/api/resos/custom-fields', {
headers: { Authorization: `Bearer ${token}` } credentials: 'include'
}) })
if (res.ok) { if (res.ok) {
const data = await res.json() const data = await res.json()
@ -4207,7 +4207,7 @@ export default function Settings() {
// First, sync opening hours to database (POST endpoint) // First, sync opening hours to database (POST endpoint)
const syncRes = await fetch('/kitchen/api/resos/sync/opening-hours', { const syncRes = await fetch('/kitchen/api/resos/sync/opening-hours', {
method: 'POST', method: 'POST',
headers: { Authorization: `Bearer ${token}` } credentials: 'include'
}) })
if (!syncRes.ok) { if (!syncRes.ok) {
@ -4220,7 +4220,7 @@ export default function Settings() {
// Then, fetch opening hours for display (GET endpoint) // Then, fetch opening hours for display (GET endpoint)
const res = await fetch('/kitchen/api/resos/opening-hours', { const res = await fetch('/kitchen/api/resos/opening-hours', {
headers: { Authorization: `Bearer ${token}` } credentials: 'include'
}) })
if (res.ok) { if (res.ok) {
const data = await res.json() const data = await res.json()
@ -4531,7 +4531,7 @@ export default function Settings() {
setResosSaveMessage('Syncing upcoming bookings...') setResosSaveMessage('Syncing upcoming bookings...')
const res = await fetch('/kitchen/api/resos/sync/upcoming', { const res = await fetch('/kitchen/api/resos/sync/upcoming', {
method: 'POST', method: 'POST',
headers: { Authorization: `Bearer ${token}` } credentials: 'include'
}) })
if (res.ok) { if (res.ok) {
const data = await res.json() const data = await res.json()
@ -4559,7 +4559,7 @@ export default function Settings() {
setResosSaveMessage('Syncing forecast...') setResosSaveMessage('Syncing forecast...')
const res = await fetch('/kitchen/api/resos/sync/forecast', { const res = await fetch('/kitchen/api/resos/sync/forecast', {
method: 'POST', method: 'POST',
headers: { Authorization: `Bearer ${token}` } credentials: 'include'
}) })
if (res.ok) { if (res.ok) {
const data = await res.json() const data = await res.json()
@ -4599,8 +4599,8 @@ export default function Settings() {
try { try {
const res = await fetch('/kitchen/api/resos/settings', { const res = await fetch('/kitchen/api/resos/settings', {
method: 'PATCH', method: 'PATCH',
credentials: 'include',
headers: { headers: {
Authorization: `Bearer ${token}`,
'Content-Type': 'application/json' 'Content-Type': 'application/json'
}, },
body: JSON.stringify({ body: JSON.stringify({
@ -4682,7 +4682,7 @@ export default function Settings() {
setResosSaveMessage('Syncing historical data...') setResosSaveMessage('Syncing historical data...')
const res = await fetch(`/kitchen/api/resos/sync/historical?from_date=${historicalResosDateFrom}&to_date=${historicalResosDateTo}`, { const res = await fetch(`/kitchen/api/resos/sync/historical?from_date=${historicalResosDateFrom}&to_date=${historicalResosDateTo}`, {
method: 'POST', method: 'POST',
headers: { Authorization: `Bearer ${token}` } credentials: 'include'
}) })
if (res.ok) { if (res.ok) {
const data = await res.json() const data = await res.json()
@ -6188,7 +6188,7 @@ export default function Settings() {
<div style={styles.actionButtons}> <div style={styles.actionButtons}>
<button <button
onClick={() => { onClick={() => {
window.open(`/kitchen/api/backup/${backup.id}/download?token=${token}`, '_blank') window.open(`/kitchen/api/backup/${backup.id}/download`, '_blank')
}} }}
style={styles.actionBtn} style={styles.actionBtn}
disabled={backup.status !== 'success'} disabled={backup.status !== 'success'}
@ -6249,7 +6249,7 @@ export default function Settings() {
try { try {
const res = await fetch('/kitchen/api/food-flags/seed-defaults', { const res = await fetch('/kitchen/api/food-flags/seed-defaults', {
method: 'POST', method: 'POST',
headers: { Authorization: `Bearer ${token}` }, credentials: 'include',
}) })
const data = await res.json() const data = await res.json()
if (res.ok) { if (res.ok) {
@ -6676,7 +6676,7 @@ export default function Settings() {
try { try {
const res = await fetch('/kitchen/api/ingredients/categories/seed-defaults', { const res = await fetch('/kitchen/api/ingredients/categories/seed-defaults', {
method: 'POST', method: 'POST',
headers: { Authorization: `Bearer ${token}` }, credentials: 'include',
}) })
const data = await res.json() const data = await res.json()
if (res.ok) { if (res.ok) {
@ -6846,7 +6846,7 @@ export default function Settings() {
try { try {
const res = await fetch('/kitchen/api/recipes/menu-sections/seed-defaults?section_type=recipe', { const res = await fetch('/kitchen/api/recipes/menu-sections/seed-defaults?section_type=recipe', {
method: 'POST', method: 'POST',
headers: { Authorization: `Bearer ${token}` }, credentials: 'include',
}) })
const data = await res.json() const data = await res.json()
if (res.ok) { if (res.ok) {
@ -6891,7 +6891,7 @@ export default function Settings() {
<button <button
onClick={async () => { onClick={async () => {
if (confirm(`Delete "${sec.name}"?${sec.recipe_count > 0 ? ` ${sec.recipe_count} recipe(s) will become unsectioned.` : ''}`)) { if (confirm(`Delete "${sec.name}"?${sec.recipe_count > 0 ? ` ${sec.recipe_count} recipe(s) will become unsectioned.` : ''}`)) {
const res = await fetch(`/kitchen/api/recipes/menu-sections/${sec.id}`, { method: 'DELETE', headers: { Authorization: `Bearer ${token}` } }) const res = await fetch(`/kitchen/api/recipes/menu-sections/${sec.id}`, { method: 'DELETE', credentials: 'include' })
if (res.ok) refetchRecipeSections() if (res.ok) refetchRecipeSections()
} }
}} }}
@ -6921,7 +6921,7 @@ export default function Settings() {
if (!name) return if (!name) return
const res = await fetch('/kitchen/api/recipes/menu-sections', { const res = await fetch('/kitchen/api/recipes/menu-sections', {
method: 'POST', method: 'POST',
headers: { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json' }, credentials: 'include', headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ name, section_type: 'recipe', sort_order: recipeSections?.length || 0 }), body: JSON.stringify({ name, section_type: 'recipe', sort_order: recipeSections?.length || 0 }),
}) })
if (res.ok) { input.value = ''; refetchRecipeSections() } if (res.ok) { input.value = ''; refetchRecipeSections() }
@ -6935,7 +6935,7 @@ export default function Settings() {
if (!name) return if (!name) return
const res = await fetch('/kitchen/api/recipes/menu-sections', { const res = await fetch('/kitchen/api/recipes/menu-sections', {
method: 'POST', method: 'POST',
headers: { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json' }, credentials: 'include', headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ name, section_type: 'recipe', sort_order: recipeSections?.length || 0 }), body: JSON.stringify({ name, section_type: 'recipe', sort_order: recipeSections?.length || 0 }),
}) })
if (res.ok) { input.value = ''; refetchRecipeSections() } if (res.ok) { input.value = ''; refetchRecipeSections() }
@ -6957,7 +6957,7 @@ export default function Settings() {
try { try {
const res = await fetch('/kitchen/api/recipes/menu-sections/seed-defaults?section_type=dish', { const res = await fetch('/kitchen/api/recipes/menu-sections/seed-defaults?section_type=dish', {
method: 'POST', method: 'POST',
headers: { Authorization: `Bearer ${token}` }, credentials: 'include',
}) })
const data = await res.json() const data = await res.json()
if (res.ok) { if (res.ok) {
@ -7002,7 +7002,7 @@ export default function Settings() {
<button <button
onClick={async () => { onClick={async () => {
if (confirm(`Delete "${course.name}"?${course.recipe_count > 0 ? ` ${course.recipe_count} dish(es) will become uncategorised.` : ''}`)) { if (confirm(`Delete "${course.name}"?${course.recipe_count > 0 ? ` ${course.recipe_count} dish(es) will become uncategorised.` : ''}`)) {
const res = await fetch(`/kitchen/api/recipes/menu-sections/${course.id}`, { method: 'DELETE', headers: { Authorization: `Bearer ${token}` } }) const res = await fetch(`/kitchen/api/recipes/menu-sections/${course.id}`, { method: 'DELETE', credentials: 'include' })
if (res.ok) refetchDishCourses() if (res.ok) refetchDishCourses()
} }
}} }}
@ -7032,7 +7032,7 @@ export default function Settings() {
if (!name) return if (!name) return
const res = await fetch('/kitchen/api/recipes/menu-sections', { const res = await fetch('/kitchen/api/recipes/menu-sections', {
method: 'POST', method: 'POST',
headers: { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json' }, credentials: 'include', headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ name, section_type: 'dish', sort_order: dishCourses?.length || 0 }), body: JSON.stringify({ name, section_type: 'dish', sort_order: dishCourses?.length || 0 }),
}) })
if (res.ok) { input.value = ''; refetchDishCourses() } if (res.ok) { input.value = ''; refetchDishCourses() }
@ -7046,7 +7046,7 @@ export default function Settings() {
if (!name) return if (!name) return
const res = await fetch('/kitchen/api/recipes/menu-sections', { const res = await fetch('/kitchen/api/recipes/menu-sections', {
method: 'POST', method: 'POST',
headers: { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json' }, credentials: 'include', headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ name, section_type: 'dish', sort_order: dishCourses?.length || 0 }), body: JSON.stringify({ name, section_type: 'dish', sort_order: dishCourses?.length || 0 }),
}) })
if (res.ok) { input.value = ''; refetchDishCourses() } if (res.ok) { input.value = ''; refetchDishCourses() }

View file

@ -137,7 +137,7 @@ export default function UploadApp() {
const res = await fetch('/kitchen/api/invoices/upload', { const res = await fetch('/kitchen/api/invoices/upload', {
method: 'POST', method: 'POST',
headers: { Authorization: `Bearer ${token}` }, credentials: 'include',
body: formData, body: formData,
}) })
@ -222,7 +222,7 @@ export default function UploadApp() {
formData.append('file', pdfFile) formData.append('file', pdfFile)
const res = await fetch('/kitchen/api/invoices/upload', { const res = await fetch('/kitchen/api/invoices/upload', {
method: 'POST', method: 'POST',
headers: { Authorization: `Bearer ${token}` }, credentials: 'include',
body: formData, body: formData,
}) })
if (!res.ok) { if (!res.ok) {

View file

@ -121,12 +121,12 @@ export default function WastageLogbook() {
queryFn: async () => { queryFn: async () => {
const url = queryString ? `/kitchen/api/logbook?${queryString}` : '/kitchen/api/logbook' const url = queryString ? `/kitchen/api/logbook?${queryString}` : '/kitchen/api/logbook'
const res = await fetch(url, { const res = await fetch(url, {
headers: { Authorization: `Bearer ${token}` }, credentials: 'include',
}) })
if (!res.ok) throw new Error('Failed to fetch logbook entries') if (!res.ok) throw new Error('Failed to fetch logbook entries')
return res.json() return res.json()
}, },
enabled: !!token, enabled: true,
}) })
// Fetch summary // Fetch summary
@ -140,19 +140,19 @@ export default function WastageLogbook() {
queryFn: async () => { queryFn: async () => {
const url = summaryString ? `/kitchen/api/logbook/summary?${summaryString}` : '/kitchen/api/logbook/summary' const url = summaryString ? `/kitchen/api/logbook/summary?${summaryString}` : '/kitchen/api/logbook/summary'
const res = await fetch(url, { const res = await fetch(url, {
headers: { Authorization: `Bearer ${token}` }, credentials: 'include',
}) })
if (!res.ok) throw new Error('Failed to fetch summary') if (!res.ok) throw new Error('Failed to fetch summary')
return res.json() return res.json()
}, },
enabled: !!token, enabled: true,
}) })
const deleteMutation = useMutation({ const deleteMutation = useMutation({
mutationFn: async (entryId: number) => { mutationFn: async (entryId: number) => {
const res = await fetch(`/kitchen/api/logbook/${entryId}`, { const res = await fetch(`/kitchen/api/logbook/${entryId}`, {
method: 'DELETE', method: 'DELETE',
headers: { Authorization: `Bearer ${token}` }, credentials: 'include',
}) })
if (!res.ok) throw new Error('Failed to delete entry') if (!res.ok) throw new Error('Failed to delete entry')
return res.json() return res.json()
@ -571,7 +571,7 @@ function CreateEntryModal({
} }
try { try {
const res = await fetch(`/kitchen/api/logbook/products/search?query=${encodeURIComponent(query)}`, { const res = await fetch(`/kitchen/api/logbook/products/search?query=${encodeURIComponent(query)}`, {
headers: { Authorization: `Bearer ${token}` }, credentials: 'include',
}) })
if (res.ok) { if (res.ok) {
const data = await res.json() const data = await res.json()
@ -662,9 +662,9 @@ function CreateEntryModal({
const res = await fetch(endpoint, { const res = await fetch(endpoint, {
method: 'POST', method: 'POST',
credentials: 'include',
headers: { headers: {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
Authorization: `Bearer ${token}`,
}, },
body: JSON.stringify(body), body: JSON.stringify(body),
}) })