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 shutil
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 sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy import select
@ -282,15 +282,21 @@ async def delete_backup(
@router.get("/{backup_id}/download")
async def download_backup(
backup_id: int,
token: str,
request: Request,
token: Optional[str] = Query(None),
db: AsyncSession = Depends(get_db)
):
"""Download a backup file. Auth via token query param for direct browser downloads."""
from auth import get_current_user, require_cap_from_token
current_user = await get_current_user_from_token(token, db)
"""Download a backup file. Cookie auth preferred; ?token= accepted as fallback."""
current_user = None
if token:
current_user = await get_current_user_from_token(token, db)
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:
raise HTTPException(status_code=403, detail="Admin only")

View file

@ -6,7 +6,7 @@ from datetime import date, datetime
from decimal import Decimal
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 sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy import select, func, text, and_, or_, delete, update
@ -783,14 +783,20 @@ async def get_ingredient(
@router.get("/{ingredient_id}/label-image")
async def get_label_image(
ingredient_id: int,
token: str,
request: Request,
token: Optional[str] = Query(None),
db: AsyncSession = Depends(get_db),
):
"""Serve the stored label image for a prepackaged ingredient.
Uses token query param for auth (allows window.open / img src usage)."""
"""Serve the stored label image for a prepackaged ingredient. Cookie auth preferred; ?token= accepted as fallback."""
import os
from auth import get_current_user, require_cap_from_token
user = await get_current_user_from_token(token, db)
user = None
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:
raise HTTPException(401, "Not authenticated")
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 {}
DATA_DIR = "/app/data"
DATA_DIR = os.getenv("DATA_DIR", "/app/data")
# Response Models
@ -549,7 +549,11 @@ async def upload_invoice(
status=InvoiceStatus.PENDING
)
db.add(invoice)
await db.commit()
try:
await db.commit()
except Exception:
os.remove(filepath)
raise
await db.refresh(invoice)
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:
logger.warning(f"Ingredient price auto-update failed (non-critical): {e}")
# Run duplicate detection
detector = DuplicateDetector(db, kitchen_id)
duplicates = await detector.check_duplicates(invoice)
# Run duplicate detection (non-critical — log and continue on failure)
try:
detector = DuplicateDetector(db, kitchen_id)
duplicates = await detector.check_duplicates(invoice)
if duplicates["firm_duplicate"]:
invoice.duplicate_status = "firm_duplicate"
invoice.duplicate_of_id = duplicates["firm_duplicate"].id
elif duplicates["possible_duplicates"]:
invoice.duplicate_status = "possible_duplicate"
invoice.duplicate_of_id = duplicates["possible_duplicates"][0].id
if duplicates["firm_duplicate"]:
invoice.duplicate_status = "firm_duplicate"
invoice.duplicate_of_id = duplicates["firm_duplicate"].id
elif duplicates["possible_duplicates"]:
invoice.duplicate_status = "possible_duplicate"
invoice.duplicate_of_id = duplicates["possible_duplicates"][0].id
if duplicates["related_documents"]:
invoice.related_document_id = duplicates["related_documents"][0].id
if duplicates["related_documents"]:
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
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}")
except Exception as e:
logger.error(f"OCR processing error for invoice {invoice_id}: {e}")
stmt = select(Invoice).where(Invoice.id == invoice_id)
db_result = await db.execute(stmt)
invoice = db_result.scalar_one()
invoice.status = InvoiceStatus.PROCESSED
invoice.ocr_raw_text = f"Error: {str(e)}"
await db.commit()
logger.error(f"OCR processing error for invoice {invoice_id}: {e}", exc_info=True)
try:
await db.rollback()
stmt = select(Invoice).where(Invoice.id == invoice_id)
db_result = await db.execute(stmt)
invoice = db_result.scalar_one()
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)
@ -2106,19 +2117,26 @@ async def get_invoice_ocr_data(
async def get_line_item_preview(
invoice_id: int,
line_number: int,
token: str,
request: Request,
token: Optional[str] = Query(None),
db: AsyncSession = Depends(get_db)
):
"""Get a cropped image preview of a specific line item from the invoice OCR bounding box."""
import json as json_module
import io
from auth import get_current_user, require_cap_from_token
from starlette.responses import Response
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:
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)
@ -2220,13 +2238,13 @@ async def get_line_item_field_preview(
invoice_id: int,
line_number: int,
field_name: str,
token: str,
request: Request,
token: Optional[str] = Query(None),
db: AsyncSession = Depends(get_db),
):
"""Get a cropped image preview of a specific field within a line item (e.g. product_code)."""
import json as json_module
import io
from auth import get_current_user, require_cap_from_token
from starlette.responses import Response
from services.file_archival_service import FileArchivalService
@ -2234,9 +2252,16 @@ async def get_line_item_field_preview(
if not azure_key:
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:
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)
@ -2387,11 +2412,11 @@ async def parse_dates_from_ocr(
def _generalize_invoice_number_pattern(sample: str) -> str:
"""
r"""
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.
e.g. 'ID304574' r'\bID\d{5,7}\b'
'INV-00123' r'\bINV-\d{4,6}\b'
e.g. 'ID304574' -> r'\bID\d{5,7}\b'
'INV-00123' -> r'\bINV-\d{4,6}\b'
"""
parts = []
i = 0

View file

@ -10,7 +10,7 @@ from decimal import Decimal
from typing import Optional
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 sqlalchemy.ext.asyncio import AsyncSession
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")
async def preview_purchase_order(
po_id: int,
token: Optional[str] = None,
request: Request,
token: Optional[str] = Query(None),
db: AsyncSession = Depends(get_db),
):
"""Return a print-friendly HTML preview of the purchase order (query-param auth)."""
if not token:
raise HTTPException(status_code=401, detail="Token required — use ?token=your_jwt_token")
current_user = await get_current_user_from_token(token, db)
"""Return a print-friendly HTML preview of the purchase order. Cookie auth preferred; ?token= accepted as fallback."""
current_user = None
if token:
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:
raise HTTPException(status_code=401, detail="Not authenticated")
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 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 sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy import select, func, text, and_, delete
@ -35,7 +35,7 @@ logger = logging.getLogger(__name__)
router = APIRouter()
DATA_DIR = "/app/data"
DATA_DIR = os.getenv("DATA_DIR", "/app/data")
# ── Pydantic schemas ─────────────────────────────────────────────────────────
@ -686,16 +686,33 @@ async def list_recipes(
result = await db.execute(query.order_by(Recipe.name))
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 = []
for r in recipes:
# Get latest cost snapshot
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()
snap = snap_map.get(r.id)
# Get flag summary (lightweight)
from api.food_flags import compute_recipe_flags
@ -1529,13 +1546,18 @@ async def upload_image(
async def serve_image(
recipe_id: int,
image_id: int,
request: Request,
token: Optional[str] = Query(None),
db: AsyncSession = Depends(get_db),
):
# img tags can't send Authorization header, so auth via query param
if not token:
raise HTTPException(401, "Not authenticated")
user = await get_current_user_from_token(token, db)
user = None
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:
raise HTTPException(401, "Not authenticated")
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
portions_needed_raw = float(sr.portions_needed) * scale_factor
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
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
@ -2098,14 +2122,19 @@ async def backfill_invoice_references(
@router.get("/{recipe_id}/print")
async def print_recipe(
recipe_id: int,
request: Request,
format: str = Query("full"), # "full" | "kitchen"
token: Optional[str] = Query(None),
db: AsyncSession = Depends(get_db),
):
# window.open() can't send Authorization header, so auth via query param
if not token:
raise HTTPException(status_code=401, detail="Not authenticated")
user = await get_current_user_from_token(token, db)
user = None
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:
raise HTTPException(status_code=401, detail="Not authenticated")
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
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)
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."""
esc = html_escape
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:
plating_images = recipe_data.get("images", [])[:1]
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;" />'
time_info = ""

View file

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

View file

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

View file

@ -255,7 +255,7 @@ export default function AllowancesReport() {
queryKey: ['allowances-summary', submittedFromDate, submittedToDate],
queryFn: async () => {
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')
return res.json()
@ -269,7 +269,7 @@ export default function AllowancesReport() {
queryKey: ['allowances-daily', submittedFromDate, submittedToDate],
queryFn: async () => {
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')
return res.json()
@ -283,7 +283,7 @@ export default function AllowancesReport() {
queryKey: ['disputes-period-summary', submittedFromDate, submittedToDate],
queryFn: async () => {
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')
return res.json()

View file

@ -255,12 +255,12 @@ export default function Budget() {
queryKey: ['budget', 'weekly', weekOffset],
queryFn: async () => {
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')
return res.json()
},
enabled: !!token,
enabled: true,
})
// Fetch prior 2 weeks for chart comparison
@ -268,23 +268,23 @@ export default function Budget() {
queryKey: ['budget', 'weekly', weekOffset - 1],
queryFn: async () => {
const res = await fetch(`/kitchen/api/budget/weekly?week_offset=${weekOffset - 1}`, {
headers: { Authorization: `Bearer ${token}` },
credentials: 'include',
})
if (!res.ok) return null
return res.json()
},
enabled: !!token,
enabled: true,
})
const { data: prevWeek2 } = useQuery<WeeklyBudgetResponse>({
queryKey: ['budget', 'weekly', weekOffset - 2],
queryFn: async () => {
const res = await fetch(`/kitchen/api/budget/weekly?week_offset=${weekOffset - 2}`, {
headers: { Authorization: `Bearer ${token}` },
credentials: 'include',
})
if (!res.ok) return null
return res.json()
},
enabled: !!token,
enabled: true,
})
const goToPreviousWeek = () => setWeekOffset((prev) => prev - 1)
@ -299,7 +299,7 @@ export default function Budget() {
queryKey: ['cover-overrides', 'weekly', weekOffset],
queryFn: async () => {
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')
return res.json()
@ -313,7 +313,7 @@ export default function Budget() {
queryFn: async () => {
const res = await fetch(
`/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')
return res.json()
@ -336,7 +336,7 @@ export default function Budget() {
queryFn: async () => {
const res = await fetch(
`/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 {}
const data = await res.json()
@ -349,7 +349,7 @@ export default function Budget() {
mutationFn: async () => {
const res = await fetch('/kitchen/api/cover-overrides/snapshot', {
method: 'POST',
headers: { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json' },
credentials: 'include', headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ week_offset: weekOffset }),
})
if (!res.ok) throw new Error('Failed to create snapshot')
@ -385,7 +385,7 @@ export default function Budget() {
const [overrideDate, period] = key.split('|')
return fetch('/kitchen/api/cover-overrides', {
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 }),
})
}))
@ -400,7 +400,7 @@ export default function Budget() {
const deleteOverride = async (id: number) => {
await fetch(`/kitchen/api/cover-overrides/${id}`, {
method: 'DELETE',
headers: { Authorization: `Bearer ${token}` },
credentials: 'include',
})
refetchOverrides()
refetch()
@ -409,7 +409,7 @@ export default function Budget() {
const saveSpendRate = async (period: string, food: number | null, drinks: number | null) => {
await fetch('/kitchen/api/cover-overrides/spend-rates', {
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 }),
})
refetchOverrides()
@ -806,7 +806,7 @@ export default function Budget() {
}}
onBlur={(e) => {
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
saveSpendRate(sr.period, netVal, null)
}

View file

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

View file

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

View file

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

View file

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

View file

@ -289,7 +289,7 @@ export default function DishEditor() {
queryKey: ['recipe', recipeId],
queryFn: async () => {
const res = await fetch(`/kitchen/api/recipes/${recipeId}`, {
headers: { Authorization: `Bearer ${token}` },
credentials: 'include',
})
if (!res.ok) throw new Error('Not found')
return res.json()
@ -302,11 +302,11 @@ export default function DishEditor() {
queryKey: ['dish-courses'],
queryFn: async () => {
const res = await fetch('/kitchen/api/recipes/menu-sections?section_type=dish', {
headers: { Authorization: `Bearer ${token}` },
credentials: 'include',
})
return res.json()
},
enabled: !!token,
enabled: true,
})
// Fetch costing (base, unscaled)
@ -314,7 +314,7 @@ export default function DishEditor() {
queryKey: ['recipe-cost', recipeId],
queryFn: async () => {
const res = await fetch(`/kitchen/api/recipes/${recipeId}/costing`, {
headers: { Authorization: `Bearer ${token}` },
credentials: 'include',
})
return res.json()
},
@ -326,7 +326,7 @@ export default function DishEditor() {
queryKey: ['recipe-cost-scaled', recipeId, scalePortions],
queryFn: async () => {
const res = await fetch(`/kitchen/api/recipes/${recipeId}/costing?scale_to=${scalePortions}`, {
headers: { Authorization: `Bearer ${token}` },
credentials: 'include',
})
return res.json()
},
@ -338,7 +338,7 @@ export default function DishEditor() {
queryKey: ['recipe-flags', recipeId],
queryFn: async () => {
const res = await fetch(`/kitchen/api/food-flags/recipes/${recipeId}/flags`, {
headers: { Authorization: `Bearer ${token}` },
credentials: 'include',
})
return res.json()
},
@ -350,11 +350,11 @@ export default function DishEditor() {
queryKey: ['food-flag-categories'],
queryFn: async () => {
const res = await fetch('/kitchen/api/food-flags/categories', {
headers: { Authorization: `Bearer ${token}` },
credentials: 'include',
})
return res.json()
},
enabled: !!token,
enabled: true,
})
// Fetch change log
@ -362,7 +362,7 @@ export default function DishEditor() {
queryKey: ['recipe-changelog', recipeId],
queryFn: async () => {
const res = await fetch(`/kitchen/api/recipes/${recipeId}/change-log`, {
headers: { Authorization: `Bearer ${token}` },
credentials: 'include',
})
return res.json()
},
@ -374,7 +374,7 @@ export default function DishEditor() {
queryKey: ['recipe-cost-trend', recipeId],
queryFn: async () => {
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')
return res.json()
@ -387,7 +387,7 @@ export default function DishEditor() {
queryKey: ['dish-menus', recipeId],
queryFn: async () => {
const res = await fetch(`/kitchen/api/menus/dish/${recipeId}/menus`, {
headers: { Authorization: `Bearer ${token}` },
credentials: 'include',
})
if (!res.ok) return []
return res.json()
@ -400,7 +400,7 @@ export default function DishEditor() {
queryKey: ['ingredient-edit', editIngId],
queryFn: async () => {
const res = await fetch(`/kitchen/api/ingredients/${editIngId}`, {
headers: { Authorization: `Bearer ${token}` },
credentials: 'include',
})
if (!res.ok) throw new Error('Not found')
const data = await res.json()
@ -426,7 +426,7 @@ export default function DishEditor() {
queryKey: ['recipes-list-for-sub'],
queryFn: async () => {
const res = await fetch('/kitchen/api/recipes?recipe_type=component', {
headers: { Authorization: `Bearer ${token}` },
credentials: 'include',
})
return res.json()
},
@ -439,7 +439,7 @@ export default function DishEditor() {
queryKey: ['sambapos-menu-items-portions'],
queryFn: async () => {
const res = await fetch('/kitchen/api/sambapos/menu-items-with-portions', {
headers: { Authorization: `Bearer ${token}` },
credentials: 'include',
})
if (!res.ok) return []
return res.json()
@ -468,7 +468,7 @@ export default function DishEditor() {
const timer = setTimeout(async () => {
try {
const res = await fetch(`/kitchen/api/ingredients/suggest?description=${encodeURIComponent(ingSearch)}`, {
headers: { Authorization: `Bearer ${token}` },
credentials: 'include',
})
if (res.ok) {
const data = await res.json()
@ -486,7 +486,7 @@ export default function DishEditor() {
mutationFn: async (data: Record<string, unknown>) => {
const res = await fetch(`/kitchen/api/recipes/${recipeId}`, {
method: 'PATCH',
headers: { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json' },
credentials: 'include', headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data),
})
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 }) => {
const res = await fetch(`/kitchen/api/recipes/${recipeId}/ingredients`, {
method: 'POST',
headers: { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json' },
credentials: 'include', headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data),
})
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 }) => {
const res = await fetch(`/kitchen/api/recipes/recipe-ingredients/${riId}`, {
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 }),
})
if (!res.ok) throw new Error('Failed to update')
@ -547,7 +547,7 @@ export default function DishEditor() {
mutationFn: async (riId: number) => {
const res = await fetch(`/kitchen/api/recipes/recipe-ingredients/${riId}`, {
method: 'DELETE',
headers: { Authorization: `Bearer ${token}` },
credentials: 'include',
})
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 }) => {
const res = await fetch(`/kitchen/api/recipes/${recipeId}/sub-recipes`, {
method: 'POST',
headers: { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json' },
credentials: 'include', headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data),
})
if (!res.ok) {
@ -585,7 +585,7 @@ export default function DishEditor() {
mutationFn: async (srId: number) => {
const res = await fetch(`/kitchen/api/recipes/recipe-sub-recipes/${srId}`, {
method: 'DELETE',
headers: { Authorization: `Bearer ${token}` },
credentials: 'include',
})
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 }) => {
const res = await fetch(`/kitchen/api/recipes/${recipeId}/steps`, {
method: 'POST',
headers: { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json' },
credentials: 'include', headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data),
})
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 } }) => {
const res = await fetch(`/kitchen/api/recipes/recipe-steps/${stepId}`, {
method: 'PATCH',
headers: { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json' },
credentials: 'include', headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data),
})
if (!res.ok) throw new Error('Failed')
@ -638,7 +638,7 @@ export default function DishEditor() {
mutationFn: async (stepId: number) => {
const res = await fetch(`/kitchen/api/recipes/recipe-steps/${stepId}`, {
method: 'DELETE',
headers: { Authorization: `Bearer ${token}` },
credentials: 'include',
})
if (!res.ok) throw new Error('Failed')
},
@ -654,7 +654,7 @@ export default function DishEditor() {
formData.append('image_type', image_type)
const res = await fetch(`/kitchen/api/recipes/${recipeId}/images`, {
method: 'POST',
headers: { Authorization: `Bearer ${token}` },
credentials: 'include',
body: formData,
})
if (!res.ok) throw new Error('Failed to upload image')
@ -672,7 +672,7 @@ export default function DishEditor() {
mutationFn: async (imageId: number) => {
const res = await fetch(`/kitchen/api/recipes/${recipeId}/images/${imageId}`, {
method: 'DELETE',
headers: { Authorization: `Bearer ${token}` },
credentials: 'include',
})
if (!res.ok) throw new Error('Failed to delete image')
},
@ -686,7 +686,7 @@ export default function DishEditor() {
mutationFn: async (ingredientIds: number[]) => {
const res = await fetch(`/kitchen/api/recipes/${recipeId}/ingredients/reorder`, {
method: 'PATCH',
headers: { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json' },
credentials: 'include', headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ ingredient_ids: ingredientIds }),
})
if (!res.ok) throw new Error('Failed to reorder')
@ -701,7 +701,7 @@ export default function DishEditor() {
mutationFn: async (subRecipeIds: number[]) => {
const res = await fetch(`/kitchen/api/recipes/${recipeId}/sub-recipes/reorder`, {
method: 'PATCH',
headers: { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json' },
credentials: 'include', headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ sub_recipe_ids: subRecipeIds }),
})
if (!res.ok) throw new Error('Failed to reorder')
@ -716,7 +716,7 @@ export default function DishEditor() {
mutationFn: async (stepIds: number[]) => {
const res = await fetch(`/kitchen/api/recipes/${recipeId}/steps/reorder`, {
method: 'PATCH',
headers: { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json' },
credentials: 'include', headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ step_ids: stepIds }),
})
if (!res.ok) throw new Error('Failed to reorder steps')
@ -931,7 +931,7 @@ export default function DishEditor() {
try {
const res = await fetch(`/kitchen/api/food-flags/recipes/${recipeId}/text-dismissals`, {
method: 'POST',
headers: { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json' },
credentials: 'include', headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
food_flag_id: s.flag_id,
dismissed_by_name: user?.name || user?.email || 'Unknown',
@ -1345,10 +1345,10 @@ export default function DishEditor() {
{recipe.images.map(img => (
<div key={img.id} style={styles.imageCard}>
<img
src={`/kitchen/api/recipes/${recipeId}/images/${img.id}?token=${token}`}
src={`/kitchen/api/recipes/${recipeId}/images/${img.id}`}
alt={img.caption || 'Dish image'}
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' }}>
{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'],
queryFn: async () => {
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')
return res.json()
},
enabled: !!token,
enabled: true,
})
const { data: recipes, isLoading } = useQuery<RecipeItem[]>({
@ -138,12 +138,12 @@ export default function DishList() {
if (sectionFilter) params.set('menu_section_id', sectionFilter)
if (showArchived) params.set('archived', 'true')
const res = await fetch(`/kitchen/api/recipes?${params}`, {
headers: { Authorization: `Bearer ${token}` },
credentials: 'include',
})
if (!res.ok) throw new Error('Failed to fetch dishes')
return res.json()
},
enabled: !!token,
enabled: true,
})
// Price impact data for badge overlay
@ -151,12 +151,12 @@ export default function DishList() {
queryKey: ['price-impact-dishes', costChangeDays],
queryFn: async () => {
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')
return res.json()
},
enabled: !!token,
enabled: true,
})
// Build lookup: recipe_id → impact item (dishes only)
@ -168,7 +168,7 @@ export default function DishList() {
queryKey: ['cost-trend', expandedCostId],
queryFn: async () => {
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')
return res.json()
@ -180,7 +180,7 @@ export default function DishList() {
mutationFn: async (data: Record<string, unknown>) => {
const res = await fetch('/kitchen/api/recipes', {
method: 'POST',
headers: { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json' },
credentials: 'include', headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data),
})
if (!res.ok) throw new Error('Failed to create dish')
@ -197,7 +197,7 @@ export default function DishList() {
mutationFn: async (id: number) => {
const res = await fetch(`/kitchen/api/recipes/${id}/duplicate`, {
method: 'POST',
headers: { Authorization: `Bearer ${token}` },
credentials: 'include',
})
if (!res.ok) throw new Error('Failed to duplicate')
return res.json()
@ -212,7 +212,7 @@ export default function DishList() {
mutationFn: async (id: number) => {
const res = await fetch(`/kitchen/api/recipes/${id}`, {
method: 'DELETE',
headers: { Authorization: `Bearer ${token}` },
credentials: 'include',
})
if (!res.ok) throw new Error('Failed to archive')
},
@ -223,7 +223,7 @@ export default function DishList() {
mutationFn: async (id: number) => {
const res = await fetch(`/kitchen/api/recipes/${id}`, {
method: 'PATCH',
headers: { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json' },
credentials: 'include', headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ is_archived: false }),
})
if (!res.ok) throw new Error('Failed to unarchive')
@ -235,7 +235,7 @@ export default function DishList() {
mutationFn: async (name: string) => {
const res = await fetch('/kitchen/api/recipes/menu-sections', {
method: 'POST',
headers: { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json' },
credentials: 'include', headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ name, section_type: 'dish' }),
})
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 }) => {
const res = await fetch(`/kitchen/api/recipes/menu-sections/${id}`, {
method: 'PATCH',
headers: { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json' },
credentials: 'include', headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ name }),
})
if (!res.ok) throw new Error('Failed to update course')
@ -270,7 +270,7 @@ export default function DishList() {
mutationFn: async (id: number) => {
const res = await fetch(`/kitchen/api/recipes/menu-sections/${id}`, {
method: 'DELETE',
headers: { Authorization: `Bearer ${token}` },
credentials: 'include',
})
if (!res.ok) throw new Error('Failed to delete course')
return res.json()
@ -285,7 +285,7 @@ export default function DishList() {
queryKey: ['food-flag-categories'],
queryFn: async () => {
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')
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 }>({
queryKey: ['settings'],
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 {}
return res.json()
},
enabled: !!token,
enabled: true,
staleTime: 60000,
})
@ -137,21 +137,21 @@ export default function DisputeDetailModal({ disputeId, onClose, onUpdate }: Dis
queryKey: ['dispute', disputeId],
queryFn: async () => {
const res = await fetch(`/kitchen/api/disputes/${disputeId}`, {
headers: { Authorization: `Bearer ${token}` },
credentials: 'include',
})
if (!res.ok) throw new Error('Failed to fetch dispute')
return res.json()
},
enabled: !!token,
enabled: true,
})
const updateMutation = useMutation({
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}`, {
method: 'PATCH',
credentials: 'include',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${token}`,
},
body: JSON.stringify(data),
})
@ -174,8 +174,8 @@ export default function DisputeDetailModal({ disputeId, onClose, onUpdate }: Dis
mutationFn: async () => {
const res = await fetch(`/kitchen/api/disputes/${disputeId}`, {
method: 'DELETE',
credentials: 'include',
headers: {
Authorization: `Bearer ${token}`,
},
})
if (!res.ok) {
@ -253,7 +253,7 @@ export default function DisputeDetailModal({ disputeId, onClose, onUpdate }: Dis
try {
const res = await fetch(`/kitchen/api/disputes/${disputeId}/draft-email`, {
method: 'POST',
headers: { Authorization: `Bearer ${token}` },
credentials: 'include',
})
if (!res.ok) {
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}`, {
method: 'POST',
credentials: 'include',
headers: {
Authorization: `Bearer ${token}`,
},
body: formData,
})

View file

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

View file

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

View file

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

View file

@ -158,14 +158,22 @@ export default function GPReport() {
const monthOptions = getMonthOptions()
// Allowances checkbox state - default: all checked EXCEPT wastage
const [allowancesSelection, setAllowancesSelection] = useState({
wastage: false, // Wastage: unchecked by default
transfer: true, // Transfer: checked by default
staffFood: true, // Staff Food: checked by default
manualAdjustment: true, // Manual Adjustment: checked by default
disputes: true, // Open Disputes: checked by default
cdDeductions: true, // Distributed Deductions: checked by default
cdReallocations: true // Distributed Reallocations: checked by default
const _defaultAllowances = {
wastage: false,
transfer: true,
staffFood: true,
manualAdjustment: true,
disputes: true,
cdDeductions: true,
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
@ -318,7 +326,7 @@ export default function GPReport() {
queryKey: ['gp-range', submittedFromDate, submittedToDate],
queryFn: async () => {
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')
return res.json()
@ -332,7 +340,7 @@ export default function GPReport() {
queryKey: ['gp-daily', submittedFromDate, submittedToDate],
queryFn: async () => {
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')
return res.json()
@ -346,7 +354,7 @@ export default function GPReport() {
queryKey: ['gp-top-sellers', submittedFromDate, submittedToDate],
queryFn: async () => {
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) {
// 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
const gpWithSelectedAllowances = salesNum > 0
? ((salesNum - adjustedPurchases + selectedAllowancesTotal) / salesNum * 100)
? Math.min((salesNum - adjustedPurchases + selectedAllowancesTotal) / salesNum * 100, 100)
: 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
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 (
@ -626,7 +684,14 @@ export default function GPReport() {
</div>
{/* 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 */}
<div style={styles.mainContent}>
@ -1252,9 +1317,18 @@ const styles: Record<string, React.CSSProperties> = {
fontSize: '1.1rem',
fontWeight: 'bold',
color: '#1a1a2e',
marginBottom: '1rem',
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: {
display: 'flex',
gap: '1.5rem',

View file

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

View file

@ -166,7 +166,7 @@ export default function IngredientModal({
queryKey: ['ingredient-categories'],
queryFn: async () => {
const res = await fetch('/kitchen/api/ingredients/categories', {
headers: { Authorization: `Bearer ${token}` },
credentials: 'include',
})
if (!res.ok) throw new Error('Failed to fetch categories')
return res.json()
@ -177,7 +177,7 @@ export default function IngredientModal({
const { data: liSuppliers } = useQuery<Array<{ id: number; name: string }>>({
queryKey: ['suppliers'],
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 []
const data = await res.json()
return data.suppliers || data || []
@ -200,7 +200,7 @@ export default function IngredientModal({
queryKey: ['ingredient-sources', editingIngredient?.id],
queryFn: async () => {
const res = await fetch(`/kitchen/api/ingredients/${editingIngredient!.id}/sources`, {
headers: { Authorization: `Bearer ${token}` },
credentials: 'include',
})
if (!res.ok) return []
return res.json()
@ -217,7 +217,7 @@ export default function IngredientModal({
if (liSupplierId) params.set('supplier_id', liSupplierId)
params.set('limit', '100')
const res = await fetch(`/kitchen/api/search/line-items?${params}`, {
headers: { Authorization: `Bearer ${token}` },
credentials: 'include',
})
if (!res.ok) return { items: [], total_count: 0 }
return res.json()
@ -231,7 +231,7 @@ export default function IngredientModal({
queryKey: ['settings-llm-check'],
queryFn: async () => {
const res = await fetch('/kitchen/api/settings/', {
headers: { Authorization: `Bearer ${token}` },
credentials: 'include',
})
if (!res.ok) return { llm_enabled: false }
return res.json()
@ -254,8 +254,8 @@ export default function IngredientModal({
try {
const res = await fetch('/kitchen/api/food-flags/analyse-label', {
method: 'POST',
credentials: 'include',
headers: {
Authorization: `Bearer ${token}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({ ingredients_text: debouncedProductIngredients }),
@ -295,7 +295,7 @@ export default function IngredientModal({
setYieldHintLoading(true)
try {
const res = await fetch(`/kitchen/api/ingredients/ai-estimate-yield?name=${encodeURIComponent(debouncedFormName.trim())}`, {
headers: { Authorization: `Bearer ${token}` },
credentials: 'include',
})
if (res.ok && !cancelled) {
const data = await res.json()
@ -390,7 +390,7 @@ export default function IngredientModal({
const params = new URLSearchParams({ [lookup.paramName]: productCode })
if (force) params.set('force', 'true')
const res = await fetch(`${lookup.endpoint}?${params}`, {
headers: { Authorization: `Bearer ${token}` },
credentials: 'include',
})
if (res.ok) {
const data = await res.json()
@ -438,7 +438,7 @@ export default function IngredientModal({
: '/kitchen/api/food-flags/scan-label'
const res = await fetch(url, {
method: 'POST',
headers: { Authorization: `Bearer ${token}` },
credentials: 'include',
body: formData,
})
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
const srcRes = await fetch(`/kitchen/api/ingredients/${ingredientId}/sources`, {
method: 'POST',
headers: { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json' },
credentials: 'include', headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(sourceData),
})
if (!srcRes.ok) {
@ -486,7 +486,7 @@ export default function IngredientModal({
try {
await fetch(`/kitchen/api/ingredients/${ingredientId}/flags`, {
method: 'PUT',
headers: { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json' },
credentials: 'include', headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ food_flag_ids: pendingFlagIds }),
})
} catch { /* ignore */ }
@ -495,7 +495,7 @@ export default function IngredientModal({
try {
await fetch(`/kitchen/api/ingredients/${ingredientId}/flags/none`, {
method: 'POST',
headers: { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json' },
credentials: 'include', headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ category_id: catId }),
})
} catch { /* ignore */ }
@ -505,7 +505,7 @@ export default function IngredientModal({
try {
await fetch(`/kitchen/api/ingredients/${ingredientId}/flags/dismissals/batch`, {
method: 'POST',
headers: { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json' },
credentials: 'include', headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ dismissals: pendingDismissals }),
})
} catch { /* ignore */ }
@ -529,7 +529,7 @@ export default function IngredientModal({
setFormFree(editingIngredient.is_free || false)
setFormPrepackaged(editingIngredient.is_prepackaged || false)
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)
} else {
const name = prePopulateName || ''
@ -551,7 +551,7 @@ export default function IngredientModal({
// LLM FEATURE — AI pack size deduction when regex can't parse
setAiPackLoading(true)
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 => {
if (data?.pack_quantity && data?.unit_size) {
setLiPackQty(data.pack_quantity)
@ -590,7 +590,7 @@ export default function IngredientModal({
const timer = setTimeout(async () => {
try {
const res = await fetch(`/kitchen/api/ingredients/check-duplicate?name=${encodeURIComponent(formName)}`, {
headers: { Authorization: `Bearer ${token}` },
credentials: 'include',
})
if (res.ok) {
const data = await res.json()
@ -607,7 +607,7 @@ export default function IngredientModal({
mutationFn: async (data: Record<string, unknown>) => {
const res = await fetch('/kitchen/api/ingredients', {
method: 'POST',
headers: { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json' },
credentials: 'include', headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data),
})
if (!res.ok) {
@ -630,7 +630,7 @@ export default function IngredientModal({
mutationFn: async ({ id, data }: { id: number; data: Record<string, unknown> }) => {
const res = await fetch(`/kitchen/api/ingredients/${id}`, {
method: 'PATCH',
headers: { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json' },
credentials: 'include', headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data),
})
if (!res.ok) throw new Error('Failed to update')
@ -988,7 +988,7 @@ export default function IngredientModal({
title="Click to enlarge"
>
<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"
style={{ width: '100%', height: 'auto', display: 'block' }}
onError={(e) => { (e.target as HTMLImageElement).parentElement!.style.display = 'none' }}
@ -1058,7 +1058,7 @@ export default function IngredientModal({
{selectedLi.most_recent_line_number != null && (
<div style={{ flex: '0 0 auto', maxWidth: '120px', borderRadius: '4px', overflow: 'hidden', border: '1px solid #e0e0e0' }}>
<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"
style={{ width: '100%', height: 'auto', display: 'block' }}
onError={(e) => { (e.target as HTMLImageElement).parentElement!.style.display = 'none' }}
@ -1246,7 +1246,7 @@ export default function IngredientModal({
{'\u2715'}
</button>
<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"
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'],
queryFn: async () => {
const res = await fetch('/kitchen/api/ingredients/categories', {
headers: { Authorization: `Bearer ${token}` },
credentials: 'include',
})
if (!res.ok) throw new Error('Failed to fetch categories')
return res.json()
},
enabled: !!token,
enabled: true,
})
// Ingredients list
@ -85,12 +85,12 @@ export default function Ingredients() {
if (showUnmapped) params.set('unmapped', 'true')
if (showArchived) params.set('archived', 'true')
const res = await fetch(`/kitchen/api/ingredients?${params}`, {
headers: { Authorization: `Bearer ${token}` },
credentials: 'include',
})
if (!res.ok) throw new Error('Failed to fetch ingredients')
return res.json()
},
enabled: !!token,
enabled: true,
})
// Sources for expanded ingredient
@ -98,7 +98,7 @@ export default function Ingredients() {
queryKey: ['ingredient-sources', expandedId],
queryFn: async () => {
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')
return res.json()
@ -110,7 +110,7 @@ export default function Ingredients() {
mutationFn: async (id: number) => {
const res = await fetch(`/kitchen/api/ingredients/${id}`, {
method: 'DELETE',
headers: { Authorization: `Bearer ${token}` },
credentials: 'include',
})
if (!res.ok) throw new Error('Failed to archive')
},
@ -121,7 +121,7 @@ export default function Ingredients() {
mutationFn: async (id: number) => {
const res = await fetch(`/kitchen/api/ingredients/${id}`, {
method: 'PATCH',
headers: { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json' },
credentials: 'include', headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ is_archived: false }),
})
if (!res.ok) throw new Error('Failed to unarchive')

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -49,12 +49,12 @@ export default function PriceImpact() {
queryKey: ['price-impact', days],
queryFn: async () => {
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')
return res.json()
},
enabled: !!token,
enabled: true,
})
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 }>({
queryKey: ['settings'],
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 }
return res.json()
},
@ -72,7 +72,7 @@ export default function PublishToMenuModal({
try {
const res = await fetch('/kitchen/api/menus/generate-description', {
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: [] }),
})
if (res.ok) {
@ -88,7 +88,7 @@ export default function PublishToMenuModal({
queryKey: ['dishes-for-menu'],
queryFn: async () => {
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')
return res.json()
@ -101,7 +101,7 @@ export default function PublishToMenuModal({
queryKey: ['menus-for-publish'],
queryFn: async () => {
const res = await fetch('/kitchen/api/menus', {
headers: { Authorization: `Bearer ${token}` },
credentials: 'include',
})
if (!res.ok) throw new Error('Failed to fetch menus')
return res.json()
@ -114,7 +114,7 @@ export default function PublishToMenuModal({
queryKey: ['menu-divisions', selectedMenuId],
queryFn: async () => {
const res = await fetch(`/kitchen/api/menus/${selectedMenuId}`, {
headers: { Authorization: `Bearer ${token}` },
credentials: 'include',
})
if (!res.ok) throw new Error('Failed')
return res.json()
@ -130,7 +130,7 @@ export default function PublishToMenuModal({
queryKey: ['recipe-flags-for-publish', selectedRecipeId],
queryFn: async () => {
const res = await fetch(`/kitchen/api/food-flags/recipes/${selectedRecipeId}/flags`, {
headers: { Authorization: `Bearer ${token}` },
credentials: 'include',
})
if (!res.ok) throw new Error('Failed')
return res.json()
@ -160,7 +160,7 @@ export default function PublishToMenuModal({
mutationFn: async () => {
const res = await fetch(`/kitchen/api/menus/${selectedMenuId}/items`, {
method: 'POST',
headers: { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json' },
credentials: 'include', headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
recipe_id: selectedRecipeId,
division_id: selectedDivId,

View file

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

View file

@ -282,7 +282,7 @@ export default function Purchases() {
queryKey: ['purchases-range', submittedFromDate, submittedToDate],
queryFn: async () => {
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')
return res.json()
@ -294,7 +294,7 @@ export default function Purchases() {
queryKey: ['daily-dispute-stats', submittedFromDate, submittedToDate],
queryFn: async () => {
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')
return res.json()
@ -306,7 +306,7 @@ export default function Purchases() {
queryKey: ['daily-allowance-stats', submittedFromDate, submittedToDate],
queryFn: async () => {
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')
return res.json()
@ -380,7 +380,7 @@ export default function Purchases() {
queryKey: ['weekly-chart-data', weeklyChartDateRange.from, weeklyChartDateRange.to],
queryFn: async () => {
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')
return res.json()

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -32,7 +32,7 @@ export default function Suppliers() {
queryKey: ['suppliers'],
queryFn: async () => {
const res = await fetch('/kitchen/api/suppliers/', {
headers: { Authorization: `Bearer ${token}` },
credentials: 'include',
})
if (!res.ok) throw new Error('Failed to fetch suppliers')
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 }) => {
const res = await fetch('/kitchen/api/suppliers/', {
method: 'POST',
credentials: 'include',
headers: {
Authorization: `Bearer ${token}`,
'Content-Type': 'application/json',
},
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 }) => {
const res = await fetch(`/kitchen/api/suppliers/${id}`, {
method: 'PATCH',
credentials: 'include',
headers: {
Authorization: `Bearer ${token}`,
'Content-Type': 'application/json',
},
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) => {
const res = await fetch(`/kitchen/api/suppliers/${id}`, {
method: 'DELETE',
headers: { Authorization: `Bearer ${token}` },
credentials: 'include',
})
if (!res.ok) throw new Error('Failed to delete supplier')
return res.json()

View file

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

View file

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

View file

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

View file

@ -152,7 +152,7 @@ export default function BookingsStats() {
queryKey: ['resos-stats', submittedFromDate, submittedToDate],
queryFn: async () => {
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')
return res.json()
@ -183,7 +183,7 @@ export default function BookingsStats() {
queryFn: async () => {
const prevDates = getPreviousPeriodDates()
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')
return res.json()
@ -196,7 +196,7 @@ export default function BookingsStats() {
queryKey: ['resos-bookings', selectedDate],
queryFn: async () => {
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')
return res.json()

View file

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

View file

@ -67,7 +67,7 @@ export default function ResidentsTableChart() {
queryKey: ['residents-table-chart', 'v2', startDate], // v2 to invalidate old cache
queryFn: async () => {
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')
return res.json()

View file

@ -116,12 +116,12 @@ export default function ResosData() {
queryKey: ['resos-settings'],
queryFn: async () => {
const res = await fetch('/kitchen/api/resos/settings', {
headers: { Authorization: `Bearer ${token}` },
credentials: 'include',
})
if (!res.ok) throw new Error('Failed to fetch settings')
return res.json()
},
enabled: !!token,
enabled: true,
})
// 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 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')
return res.json()
},
enabled: !!token,
enabled: true,
})
// 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 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')
return res.json()
},
enabled: !!token,
enabled: true,
})
// Fetch calendar events for the month
@ -168,7 +168,7 @@ export default function ResosData() {
const lastDay = new Date(year, month, 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}`, {
headers: { Authorization: `Bearer ${token}` }
credentials: 'include'
})
if (!res.ok) throw new Error('Failed to fetch calendar events')
return res.json()
@ -182,7 +182,7 @@ export default function ResosData() {
queryFn: async () => {
if (!selectedDate) return []
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')
return res.json()
@ -195,7 +195,7 @@ export default function ResosData() {
queryKey: ['resos-all-opening-hours'],
queryFn: async () => {
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')
return res.json()
@ -231,7 +231,7 @@ export default function ResosData() {
queryFn: async () => {
if (!selectedDate) return []
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')
return res.json()
@ -1324,7 +1324,7 @@ export default function ResosData() {
if (confirm('Delete this event?')) {
await fetch(`/kitchen/api/calendar-events/${editingEvent.id}`, {
method: 'DELETE',
headers: { Authorization: `Bearer ${token}` }
credentials: 'include'
})
setShowEventModal(false)
refetchEvents()
@ -1365,8 +1365,8 @@ export default function ResosData() {
await fetch(url, {
method,
credentials: 'include',
headers: {
Authorization: `Bearer ${token}`,
'Content-Type': 'application/json'
},
body: JSON.stringify(eventForm)

View file

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

View file

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

View file

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