From 9cfbde0e3ee17a0d6b59b2fa29a2bc01cde6ab4c Mon Sep 17 00:00:00 2001 From: bonamin Date: Mon, 8 Jun 2026 03:19:33 +0300 Subject: [PATCH] =?UTF-8?q?feat:=20Phase=202H=20=E2=80=94=20void/waste=20l?= =?UTF-8?q?og?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Backend: - New model: WasteLog (waste_log table) — product, quantity (float), reason, reason_notes, unit_cost_snapshot (copied at log time from product's effective cost), total_cost (stored), logged_by, business_day_id - Cost snapshot: same logic as Phase 2A — breakdown sum first, fallback to cost_simple, null if neither - New router /api/waste/: - GET / — list entries (?business_day_id= or ?from=&to=), ordered by logged_at desc - POST / — log waste; snapshots cost; attaches to open business day automatically - DELETE /{id} — manager only; only allowed within the same open business day - GET /summary — totals by reason + by product for a period - product_performance report: waste_qty + waste_cost added per product from the same period; products with only waste (no sales) also included - Migration: CREATE TABLE IF NOT EXISTS waste_log Frontend: - WastePage (/waste): fast log form (product picker, quantity, reason chips, notes); today's entries list below (scoped to active business day); cost shown per entry; delete button for today's entries; history section behind toggle with date-range picker - ProductPerformance report: Απόβλητα column showing waste_qty + cost in amber - Sidebar: Trash2 icon for Αποβλήτα (after Καρτέλες) Co-Authored-By: Claude Sonnet 4.6 --- local_backend/main.py | 16 + local_backend/models/waste.py | 26 ++ local_backend/routers/reports.py | 37 +++ local_backend/routers/waste.py | 182 +++++++++++ local_backend/schemas/waste.py | 37 +++ manager_dashboard/src/App.jsx | 2 + manager_dashboard/src/components/Sidebar.jsx | 3 +- manager_dashboard/src/pages/WastePage.jsx | 296 ++++++++++++++++++ .../reports/restaurant/ProductPerformance.jsx | 9 + 9 files changed, 607 insertions(+), 1 deletion(-) create mode 100644 local_backend/models/waste.py create mode 100644 local_backend/routers/waste.py create mode 100644 local_backend/schemas/waste.py create mode 100644 manager_dashboard/src/pages/WastePage.jsx diff --git a/local_backend/main.py b/local_backend/main.py index 722f131..d05058c 100644 --- a/local_backend/main.py +++ b/local_backend/main.py @@ -24,6 +24,7 @@ import models.notes # noqa: F401 — registers SiteNote, SiteTodo import models.expenses # noqa: F401 — registers Contact, Expense, ExpensePayment import models.customers # noqa: F401 — registers Customer import models.tabs # noqa: F401 — registers Tab, TabEntry, TabPayment +import models.waste # noqa: F401 — registers WasteLog from routers import auth, tables, products, orders, waiters, reports, system, setup as setup_router from routers import business_day as business_day_router @@ -39,6 +40,7 @@ from routers import notes as notes_router from routers.expenses import contacts_router, expenses_router from routers import customers as customers_router from routers import tabs as tabs_router +from routers import waste as waste_router def _run_migrations(): @@ -277,6 +279,19 @@ def _run_migrations(): # Phase 2B — staff payroll "ALTER TABLE users ADD COLUMN hourly_rate REAL", "ALTER TABLE waiter_shifts ADD COLUMN hourly_rate_snapshot REAL", + # Phase 2H — void/waste log + """CREATE TABLE IF NOT EXISTS waste_log ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + product_id INTEGER NOT NULL REFERENCES products(id), + quantity REAL NOT NULL, + unit_cost_snapshot REAL, + total_cost REAL, + reason VARCHAR NOT NULL, + reason_notes TEXT, + logged_by_id INTEGER NOT NULL REFERENCES users(id), + business_day_id INTEGER REFERENCES business_days(id), + logged_at DATETIME DEFAULT CURRENT_TIMESTAMP + )""", # Phase 2G — pay-later tab system """CREATE TABLE IF NOT EXISTS tabs ( id INTEGER PRIMARY KEY AUTOINCREMENT, @@ -444,3 +459,4 @@ app.include_router(contacts_router, prefix="/api/contacts", tag app.include_router(expenses_router, prefix="/api/expenses", tags=["expenses"]) app.include_router(customers_router.router, prefix="/api/customers", tags=["customers"]) app.include_router(tabs_router.router, prefix="/api/tabs", tags=["tabs"]) +app.include_router(waste_router.router, prefix="/api/waste", tags=["waste"]) diff --git a/local_backend/models/waste.py b/local_backend/models/waste.py new file mode 100644 index 0000000..153f5a1 --- /dev/null +++ b/local_backend/models/waste.py @@ -0,0 +1,26 @@ +from sqlalchemy import Column, Integer, String, Float, DateTime, ForeignKey, Text +from sqlalchemy.orm import relationship +from datetime import datetime, timezone +from database import Base + + +def _utcnow(): + return datetime.now(timezone.utc) + + +class WasteLog(Base): + __tablename__ = "waste_log" + + id = Column(Integer, primary_key=True, index=True) + product_id = Column(Integer, ForeignKey("products.id"), nullable=False) + quantity = Column(Float, nullable=False) # supports decimals + unit_cost_snapshot = Column(Float, nullable=True) # product's effective cost at log time + total_cost = Column(Float, nullable=True) # unit_cost_snapshot * quantity + reason = Column(String, nullable=False) # kitchen_error|dropped|expired|other + reason_notes = Column(Text, nullable=True) + logged_by_id = Column(Integer, ForeignKey("users.id"), nullable=False) + business_day_id = Column(Integer, ForeignKey("business_days.id"), nullable=True) + logged_at = Column(DateTime(timezone=True), default=_utcnow) + + product = relationship("Product", foreign_keys=[product_id]) + logged_by = relationship("User", foreign_keys=[logged_by_id]) diff --git a/local_backend/routers/reports.py b/local_backend/routers/reports.py index 97ab09b..cdb00be 100644 --- a/local_backend/routers/reports.py +++ b/local_backend/routers/reports.py @@ -904,6 +904,7 @@ def product_performance( user: User = Depends(require_manager), ): from models.product import Product + from models.waste import WasteLog q = db.query(OrderItem).filter(OrderItem.status.in_(["active", "paid"])) if from_dt: @@ -916,6 +917,24 @@ def product_performance( items = q.all() products_db = {p.id: p for p in db.query(Product).all()} + # Phase 2H: load waste for the same period + wq = db.query(WasteLog) + if business_day_id: + wq = wq.filter(WasteLog.business_day_id == business_day_id) + elif from_dt and to_dt: + wq = wq.filter( + WasteLog.logged_at >= datetime.fromisoformat(from_dt), + WasteLog.logged_at <= datetime.fromisoformat(to_dt), + ) + waste_entries = wq.all() + waste_by_product: dict = {} + for w in waste_entries: + pid = w.product_id + if pid not in waste_by_product: + waste_by_product[pid] = {"waste_qty": 0.0, "waste_cost": 0.0} + waste_by_product[pid]["waste_qty"] += w.quantity + waste_by_product[pid]["waste_cost"] += w.total_cost or 0.0 + summary: dict = {} for item in items: pid = item.product_id @@ -945,13 +964,31 @@ def product_performance( summary[pid]["uncosted_revenue"] += revenue summary[pid]["uncosted_items"] += qty + # Also include products that only have waste (not sold) in the period + for pid, wdata in waste_by_product.items(): + if pid not in summary: + product = products_db.get(pid) + if category_id and (not product or product.category_id != category_id): + continue + summary[pid] = { + "product_id": pid, + "product_name": product.name if product else f"#{pid}", + "category_id": product.category_id if product else None, + "qty_sold": 0, "revenue": 0.0, "trackable_profit": 0.0, + "uncosted_revenue": 0.0, "uncosted_items": 0, "order_ids": set(), + } + result = [] for entry in summary.values(): + pid = entry["product_id"] + wdata = waste_by_product.get(pid, {"waste_qty": 0.0, "waste_cost": 0.0}) entry["order_count"] = len(entry.pop("order_ids")) entry["revenue"] = round(entry["revenue"], 2) entry["trackable_profit"] = round(entry["trackable_profit"], 2) entry["uncosted_revenue"] = round(entry["uncosted_revenue"], 2) entry["has_gap"] = entry["uncosted_items"] > 0 + entry["waste_qty"] = round(wdata["waste_qty"], 3) + entry["waste_cost"] = round(wdata["waste_cost"], 2) result.append(entry) result.sort(key=lambda x: x["qty_sold"], reverse=True) diff --git a/local_backend/routers/waste.py b/local_backend/routers/waste.py new file mode 100644 index 0000000..583a1f9 --- /dev/null +++ b/local_backend/routers/waste.py @@ -0,0 +1,182 @@ +import json +from fastapi import APIRouter, Depends, HTTPException, status, Query +from sqlalchemy.orm import Session +from typing import Optional + +from database import get_db +from models.waste import WasteLog +from models.product import Product +from models.business_day import BusinessDay +from models.user import User +from schemas.waste import WasteLogCreate, WasteLogOut, VALID_REASONS, REASON_LABELS +from routers.deps import require_manager + +router = APIRouter() + + +def _effective_cost(product: Product) -> Optional[float]: + """Return product's current effective cost (breakdown sum or simple), or None.""" + if product.cost_breakdown: + try: + entries = json.loads(product.cost_breakdown) + total = sum(e.get("amount", 0.0) for e in entries if isinstance(e, dict)) + if total > 0: + return total + except Exception: + pass + if product.cost_simple: + return product.cost_simple + return None + + +def _waste_out(w: WasteLog) -> dict: + product_name = w.product.name if w.product else f"#{w.product_id}" + logger_name = None + if w.logged_by: + logger_name = w.logged_by.full_name or w.logged_by.username + return { + "id": w.id, + "product_id": w.product_id, + "product_name": product_name, + "quantity": w.quantity, + "unit_cost_snapshot": w.unit_cost_snapshot, + "total_cost": w.total_cost, + "reason": w.reason, + "reason_label": REASON_LABELS.get(w.reason, w.reason), + "reason_notes": w.reason_notes, + "logged_by_id": w.logged_by_id, + "logged_by_name": logger_name, + "business_day_id": w.business_day_id, + "logged_at": w.logged_at, + } + + +@router.get("/") +def list_waste( + business_day_id: Optional[int] = None, + from_dt: Optional[str] = Query(default=None, alias="from"), + to_dt: Optional[str] = Query(default=None, alias="to"), + db: Session = Depends(get_db), + user: User = Depends(require_manager), +): + from datetime import datetime + q = db.query(WasteLog).order_by(WasteLog.logged_at.desc()) + if business_day_id: + q = q.filter(WasteLog.business_day_id == business_day_id) + elif from_dt and to_dt: + q = q.filter( + WasteLog.logged_at >= datetime.fromisoformat(from_dt), + WasteLog.logged_at <= datetime.fromisoformat(to_dt), + ) + entries = q.all() + return {"entries": [_waste_out(w) for w in entries]} + + +@router.post("/", status_code=status.HTTP_201_CREATED) +def log_waste( + body: WasteLogCreate, + db: Session = Depends(get_db), + user: User = Depends(require_manager), +): + if body.reason not in VALID_REASONS: + raise HTTPException(status_code=400, detail=f"Invalid reason. Valid: {sorted(VALID_REASONS)}") + if body.quantity <= 0: + raise HTTPException(status_code=400, detail="Quantity must be positive") + + product = db.query(Product).filter(Product.id == body.product_id).first() + if not product: + raise HTTPException(status_code=404, detail="Product not found") + + # Snapshot current effective cost + unit_cost = _effective_cost(product) + total_cost = round(unit_cost * body.quantity, 2) if unit_cost is not None else None + + # Attach to open business day if any + active_day = db.query(BusinessDay).filter(BusinessDay.status == "open").first() + + entry = WasteLog( + product_id=body.product_id, + quantity=body.quantity, + reason=body.reason, + reason_notes=body.reason_notes, + unit_cost_snapshot=unit_cost, + total_cost=total_cost, + logged_by_id=user.id, + business_day_id=active_day.id if active_day else None, + ) + db.add(entry) + db.commit() + db.refresh(entry) + return _waste_out(entry) + + +@router.delete("/{entry_id}", status_code=status.HTTP_204_NO_CONTENT) +def delete_waste( + entry_id: int, + db: Session = Depends(get_db), + user: User = Depends(require_manager), +): + entry = db.query(WasteLog).filter(WasteLog.id == entry_id).first() + if not entry: + raise HTTPException(status_code=404, detail="Waste entry not found") + + # Only allow deletion within the same business day + active_day = db.query(BusinessDay).filter(BusinessDay.status == "open").first() + if active_day and entry.business_day_id != active_day.id: + raise HTTPException(status_code=403, detail="Can only delete waste entries from the current open business day") + + db.delete(entry) + db.commit() + + +@router.get("/summary") +def waste_summary( + business_day_id: Optional[int] = None, + from_dt: Optional[str] = Query(default=None, alias="from"), + to_dt: Optional[str] = Query(default=None, alias="to"), + db: Session = Depends(get_db), + user: User = Depends(require_manager), +): + from datetime import datetime + q = db.query(WasteLog) + if business_day_id: + q = q.filter(WasteLog.business_day_id == business_day_id) + elif from_dt and to_dt: + q = q.filter( + WasteLog.logged_at >= datetime.fromisoformat(from_dt), + WasteLog.logged_at <= datetime.fromisoformat(to_dt), + ) + entries = q.all() + + total_items = len(entries) + total_cost = round(sum(e.total_cost for e in entries if e.total_cost is not None), 2) + uncosted_count = sum(1 for e in entries if e.total_cost is None) + + by_reason: dict = {} + by_product: dict = {} + for e in entries: + r = e.reason + if r not in by_reason: + by_reason[r] = {"reason": r, "label": REASON_LABELS.get(r, r), "count": 0, "total_cost": 0.0} + by_reason[r]["count"] += 1 + by_reason[r]["total_cost"] += e.total_cost or 0.0 + + pid = e.product_id + pname = e.product.name if e.product else f"#{pid}" + if pid not in by_product: + by_product[pid] = {"product_id": pid, "product_name": pname, "qty": 0.0, "total_cost": 0.0} + by_product[pid]["qty"] += e.quantity + by_product[pid]["total_cost"] += e.total_cost or 0.0 + + for v in by_reason.values(): + v["total_cost"] = round(v["total_cost"], 2) + for v in by_product.values(): + v["total_cost"] = round(v["total_cost"], 2) + + return { + "total_items": total_items, + "total_cost": total_cost, + "uncosted_count": uncosted_count, + "by_reason": sorted(by_reason.values(), key=lambda x: -x["count"]), + "by_product": sorted(by_product.values(), key=lambda x: -x["total_cost"]), + } diff --git a/local_backend/schemas/waste.py b/local_backend/schemas/waste.py new file mode 100644 index 0000000..a290406 --- /dev/null +++ b/local_backend/schemas/waste.py @@ -0,0 +1,37 @@ +from pydantic import BaseModel +from typing import Optional +from datetime import datetime + +VALID_REASONS = {"kitchen_error", "dropped", "expired", "other"} + +REASON_LABELS = { + "kitchen_error": "Λάθος κουζίνας", + "dropped": "Έπεσε", + "expired": "Έληξε", + "other": "Άλλο", +} + + +class WasteLogCreate(BaseModel): + product_id: int + quantity: float + reason: str + reason_notes: Optional[str] = None + + +class WasteLogOut(BaseModel): + id: int + product_id: int + product_name: Optional[str] = None + quantity: float + unit_cost_snapshot: Optional[float] = None + total_cost: Optional[float] = None + reason: str + reason_label: Optional[str] = None + reason_notes: Optional[str] = None + logged_by_id: int + logged_by_name: Optional[str] = None + business_day_id: Optional[int] = None + logged_at: datetime + + model_config = {"from_attributes": True} diff --git a/manager_dashboard/src/App.jsx b/manager_dashboard/src/App.jsx index 2e8ea8a..26764b9 100644 --- a/manager_dashboard/src/App.jsx +++ b/manager_dashboard/src/App.jsx @@ -16,6 +16,7 @@ import ContactsPage from './pages/ContactsPage' import ExpensesPage from './pages/ExpensesPage' import CustomersPage from './pages/CustomersPage' import TabsPage from './pages/TabsPage' +import WastePage from './pages/WastePage' import client from './api/client' function Spinner() { @@ -91,6 +92,7 @@ export default function App() { } /> } /> } /> + } /> } /> } /> } /> diff --git a/manager_dashboard/src/components/Sidebar.jsx b/manager_dashboard/src/components/Sidebar.jsx index 80bc116..ac1d47f 100644 --- a/manager_dashboard/src/components/Sidebar.jsx +++ b/manager_dashboard/src/components/Sidebar.jsx @@ -1,6 +1,6 @@ import { NavLink } from 'react-router-dom' import { useState, useEffect, useRef } from 'react' -import { BarChart2, LayoutGrid, ClipboardList, Package, Settings, ChevronRight, ChevronLeft, ShoppingBag, NotebookPen, Receipt, BookUser, Users, CreditCard } from 'lucide-react' +import { BarChart2, LayoutGrid, ClipboardList, Package, Settings, ChevronRight, ChevronLeft, ShoppingBag, NotebookPen, Receipt, BookUser, Users, CreditCard, Trash2 } from 'lucide-react' import { getIncomingOrders } from '../api/client' export default function Sidebar() { @@ -31,6 +31,7 @@ export default function Sidebar() { { to: '/contacts', icon: BookUser, label: 'Επαφές' }, { to: '/customers', icon: Users, label: 'Πελάτες' }, { to: '/tabs', icon: CreditCard, label: 'Καρτέλες' }, + { to: '/waste', icon: Trash2, label: 'Αποβλήτα' }, { to: '/settings', icon: Settings, label: 'Ρυθμίσεις' }, ] diff --git a/manager_dashboard/src/pages/WastePage.jsx b/manager_dashboard/src/pages/WastePage.jsx new file mode 100644 index 0000000..63f6bc4 --- /dev/null +++ b/manager_dashboard/src/pages/WastePage.jsx @@ -0,0 +1,296 @@ +import { useState } from 'react' +import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query' +import toast from 'react-hot-toast' +import client from '../api/client' + +// ── Constants ───────────────────────────────────────────────────────────────── + +const REASONS = [ + { id: 'kitchen_error', label: 'Λάθος κουζίνας', color: '#dc2626' }, + { id: 'dropped', label: 'Έπεσε', color: '#d97706' }, + { id: 'expired', label: 'Έληξε', color: '#7c3aed' }, + { id: 'other', label: 'Άλλο', color: '#6b7280' }, +] + +// ── Helpers ─────────────────────────────────────────────────────────────────── + +function fmt(n) { + if (n == null) return '—' + return n.toLocaleString('el-GR', { minimumFractionDigits: 2, maximumFractionDigits: 2 }) + ' €' +} + +function fmtDateTime(iso) { + if (!iso) return '—' + return new Date(iso).toLocaleString('el-GR', { day: '2-digit', month: '2-digit', hour: '2-digit', minute: '2-digit' }) +} + +// ── Log form ────────────────────────────────────────────────────────────────── + +function LogForm({ products, onLog, isPending }) { + const [productId, setProductId] = useState('') + const [qty, setQty] = useState('1') + const [reason, setReason] = useState('kitchen_error') + const [notes, setNotes] = useState('') + + const canLog = productId && parseFloat(qty) > 0 + + function submit() { + if (!canLog) return + onLog({ + product_id: Number(productId), + quantity: parseFloat(qty), + reason, + reason_notes: notes.trim() || null, + }) + setProductId('') + setQty('1') + setReason('kitchen_error') + setNotes('') + } + + const inputStyle = { + width: '100%', padding: '8px 11px', border: '1px solid #e5e7eb', + borderRadius: 8, fontSize: 13.5, outline: 'none', fontFamily: 'inherit', background: 'white', + boxSizing: 'border-box', + } + + return ( +
+
+ Καταγραφή Απόβλητου +
+
+
+ + +
+
+ + setQty(e.target.value)} /> +
+
+ +
+ +
+ {REASONS.map(r => ( + + ))} +
+
+ +
+ + setNotes(e.target.value)} + placeholder="π.χ. Ο πελάτης επέστρεψε ελαττωματικό…" /> +
+ + +
+ ) +} + +// ── Waste entry row ─────────────────────────────────────────────────────────── + +function WasteRow({ entry, canDelete, onDelete }) { + const reasonCfg = REASONS.find(r => r.id === entry.reason) || { label: entry.reason, color: '#6b7280' } + return ( +
+
+
+ {entry.product_name} + ×{entry.quantity} + + {reasonCfg.label} + +
+
+ {fmtDateTime(entry.logged_at)} · {entry.logged_by_name} + {entry.reason_notes && — {entry.reason_notes}} +
+
+
+ {entry.total_cost != null ? ( +
{fmt(entry.total_cost)}
+ ) : ( +
χωρίς κόστος
+ )} + {entry.unit_cost_snapshot != null && ( +
{fmt(entry.unit_cost_snapshot)}/τεμ.
+ )} +
+ {canDelete && ( + + )} +
+ ) +} + +// ── Main page ───────────────────────────────────────────────────────────────── + +export default function WastePage() { + const qc = useQueryClient() + const [showHistory, setShowHistory] = useState(false) + const [histFrom, setHistFrom] = useState(() => new Date().toISOString().slice(0, 10)) + const [histTo, setHistTo] = useState(() => new Date().toISOString().slice(0, 10)) + + const { data: productsData = [] } = useQuery({ + queryKey: ['meta-products'], + queryFn: () => client.get('/api/reports/meta/products').then(r => r.data.products), + staleTime: 5 * 60_000, + }) + + const { data: bdData } = useQuery({ + queryKey: ['business-day'], + queryFn: () => client.get('/api/business-day/current').then(r => r.data), + staleTime: 30_000, + }) + const activeDayId = bdData?.id ?? null + + // Today's entries — scoped to active business day (or all if no active day) + const { data: todayData } = useQuery({ + queryKey: ['waste-today', activeDayId], + queryFn: () => client.get('/api/waste/', { params: activeDayId ? { business_day_id: activeDayId } : {} }).then(r => r.data), + staleTime: 15_000, + }) + + // Historical entries + const { data: histData, refetch: refetchHist } = useQuery({ + queryKey: ['waste-history', histFrom, histTo], + queryFn: () => client.get('/api/waste/', { params: { from: histFrom + 'T00:00:00', to: histTo + 'T23:59:59' } }).then(r => r.data), + staleTime: 30_000, + enabled: showHistory, + }) + + const todayEntries = todayData?.entries || [] + const histEntries = histData?.entries || [] + + const todayCost = todayEntries.reduce((s, e) => s + (e.total_cost || 0), 0) + const todayCount = todayEntries.length + + const logWaste = useMutation({ + mutationFn: (body) => client.post('/api/waste/', body), + onSuccess: () => { + qc.invalidateQueries({ queryKey: ['waste-today'] }) + qc.invalidateQueries({ queryKey: ['waste-history'] }) + toast.success('Καταγράφηκε') + }, + onError: (e) => toast.error(e?.response?.data?.detail || 'Σφάλμα'), + }) + + const deleteWaste = useMutation({ + mutationFn: (id) => client.delete(`/api/waste/${id}`), + onSuccess: () => { + qc.invalidateQueries({ queryKey: ['waste-today'] }) + qc.invalidateQueries({ queryKey: ['waste-history'] }) + toast.success('Διαγράφηκε') + }, + onError: (e) => toast.error(e?.response?.data?.detail || 'Σφάλμα'), + }) + + return ( +
+ {/* Header */} +
+
+

Αποβλήτα / Φθορές

+

+ Σήμερα: {todayCount} καταχωρήσεις + {todayCost > 0 && · εκτιμώμενο κόστος {todayCost.toLocaleString('el-GR', { minimumFractionDigits: 2, maximumFractionDigits: 2 })} €} +

+
+
+ +
+ {/* Log form */} + logWaste.mutate(body)} isPending={logWaste.isPending} /> + + {/* Today's entries */} +
+
+ {activeDayId ? 'Σήμερα' : 'Τελευταίες καταχωρήσεις'} +
+ {todayEntries.length === 0 ? ( +
Καμία καταχώρηση ακόμα.
+ ) : ( +
+ {todayEntries.map(e => ( + deleteWaste.mutate(id)} /> + ))} +
+ )} +
+ + {/* History toggle */} +
+ + + {showHistory && ( +
+
+
+ + setHistFrom(e.target.value)} + style={{ padding: '5px 8px', border: '1px solid #e5e7eb', borderRadius: 7, fontSize: 13, outline: 'none' }} /> +
+
+ + setHistTo(e.target.value)} + style={{ padding: '5px 8px', border: '1px solid #e5e7eb', borderRadius: 7, fontSize: 13, outline: 'none' }} /> +
+ +
+ + {histEntries.length === 0 ? ( +
Δεν βρέθηκαν καταχωρήσεις.
+ ) : ( +
+ {histEntries.map(e => ( + {}} /> + ))} +
+ {histEntries.length} καταχωρήσεις · εκτιμώμενο κόστος{' '} + + {histEntries.reduce((s, e) => s + (e.total_cost || 0), 0).toLocaleString('el-GR', { minimumFractionDigits: 2 })} € + +
+
+ )} +
+ )} +
+
+
+ ) +} diff --git a/manager_dashboard/src/pages/reports/restaurant/ProductPerformance.jsx b/manager_dashboard/src/pages/reports/restaurant/ProductPerformance.jsx index 2cdaca4..85063b1 100644 --- a/manager_dashboard/src/pages/reports/restaurant/ProductPerformance.jsx +++ b/manager_dashboard/src/pages/reports/restaurant/ProductPerformance.jsx @@ -379,6 +379,7 @@ export default function ProductPerformance() { Τεμάχια Έσοδα Κέρδος + Απόβλητα % Συνόλου Παραγγελίες @@ -403,6 +404,14 @@ export default function ProductPerformance() { )} + + {p.waste_qty > 0 ? ( + + {p.waste_qty} + {p.waste_cost > 0 && ({fmtEUR(p.waste_cost)})} + + ) : } + {pct.toFixed(1)}% {fmtNum(p.order_count)}