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:
parent
6f6e16c88f
commit
ba075276b1
57 changed files with 15427 additions and 15274 deletions
|
|
@ -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")
|
||||
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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 = ""
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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')
|
||||
|
|
|
|||
|
|
@ -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(),
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
})
|
||||
|
|
|
|||
|
|
@ -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
|
||||
})
|
||||
|
||||
|
|
|
|||
|
|
@ -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>}
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
})
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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')
|
||||
|
|
|
|||
|
|
@ -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')
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -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] })
|
||||
|
|
|
|||
|
|
@ -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)' }}
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -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')
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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({
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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 (
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -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 (
|
||||
|
|
|
|||
|
|
@ -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')
|
||||
|
|
|
|||
|
|
@ -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) || []
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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) => {
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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>}
|
||||
|
|
|
|||
|
|
@ -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')
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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'))
|
||||
|
|
|
|||
|
|
@ -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) => {
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
})
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
})
|
||||
|
||||
|
|
|
|||
|
|
@ -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' }))
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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 = () => {
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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() }
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
})
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue