- Add tz.py with local_strftime/to_local helpers that read system.timezone
from DB and convert UTC datetimes to venue local time before formatting
- Fix all strftime() calls in orders.py, reports.py, printer_service.py
that were formatting UTC datetimes without timezone conversion
- Fix get_order endpoint returning raw dicts without Z suffix on datetimes,
causing JS new Date() to treat timestamps as local instead of UTC
- Fix fmtDate() in tokens.js that stripped the T separator before parsing,
breaking UTC-to-local conversion for all report date displays
- Make open/partially_paid table chips more visually distinct on dashboard
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Backend reports: fix cost/profit calculation to handle null unit_cost
with has_gap flag; expose total_cost in product & workday summary
- StaffTab: add hourly_rate field in payroll section (admin-only)
- ProductFormModal: major refactor of form layout and structure
- ProductsTab: minor tweaks aligned with form changes
- Report pages (Today, WorkDaySummary, CategoryPerformance,
ProductPerformance, RevenueTrends): UI improvements and cost data
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- usePhase2Features.js hook: manages localStorage-based feature flags for all 8 Phase 2
sidebar entries (notes, expenses, contacts, customers, tabs, waste, kds, schedule)
All default to enabled=true. setFeatureEnabled() writes to localStorage and dispatches
a 'phase2features' event so all subscribers re-render immediately without page reload
- Phase2FeaturesTab.jsx: new Settings tab 'Λειτουργίες' — toggle each Phase 2 page on/off
with master 'Ενεργοποίηση όλων' / 'Απενεργοποίηση όλων' buttons
Note shown: changes are localStorage-only, instant, no reload needed, no data deleted
- SettingsPage.jsx: adds 'Λειτουργίες' and reinstates 'Developer' tab (was missing import)
- Sidebar.jsx: subscribes to phase2features + storage events; filters ALL_NAV to remove
disabled Phase 2 entries; each phase2 item has a phase2 id that isFeatureEnabled() checks
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Backend:
- GET /api/reports/discounts: read-only audit of all OrderDiscount records
Filters: ?from=&to= or ?business_day_id=, ?applied_by=
Returns:
- discounts[]: id, order_id, table_name, applied_by_name, applied_at, discount_type,
discount_value, discount_amount (computed euro value), order_total_before,
item_id (null = whole-order), reason
- total_discount_value: sum of all euro discount amounts
- order_count: unique orders that received a discount
- by_waiter[]: grouped summary (waiter_name, count, total_value)
discount_amount computation: fixed → discount_value directly;
percent → computed from order or item total at query time
Frontend:
- DiscountsLog.jsx: read-only report under Reports → Λειτουργίες → Εκπτώσεις
- Filter bar: range/workday toggle, date range, waiter filter
- 3 stat cards: total discount value, orders with discounts, count of entries
- By-waiter summary table (hidden when only one waiter)
- Full audit table: date, order + table, waiter, type badge (% blue / € amber),
order total before, euro amount discounted in amber, reason
- Totals footer row
- No create/edit/delete — pure audit log
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Backend — /api/business-day/summary now returns full financial summary:
- COGS: trackable cost (sum of unit_cost snapshots on paid items), uncosted item count
and revenue, gross_profit, cogs_has_gap flag
- Labor: total_labor_cost (sum of shift_pay for the day), labor_untracked_shifts count
- Waste: waste_item_count + waste_estimated_cost from waste_log for this business day
- Expenses: expenses_total, expenses_paid_today, expenses_due for expenses logged today
- Tabbed revenue: revenue from items with status='tabbed' (deferred, not yet collected)
- Net estimate: revenue - COGS - labor - waste - expenses_paid; net_has_unknowns flag
when any component has gaps (uncosted items or untracked shifts)
- compute_shift_pay import hoisted out of loop (bug fix)
Frontend — WorkdaySummaryModal OverviewTab fully expanded:
- New FinancialRow helper for consistent two-column P&L rows with accent/warn/indent
- REVENUE section: total sales, tabbed deduction, net collected
- COGS section: trackable cost, uncosted items warning, gross profit with gap indicator
- LABOR section: tracked labor cost, untracked shifts warning
- WASTE section: item count + estimated cost (hidden when zero)
- EXPENSES section: total, paid today, still due (hidden when zero)
- NET ESTIMATE: prominent bottom line, amber warning when unknowns exist
- Payment breakdown bar (existing)
- Cash reconciliation summary when store cash was counted
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Backend:
- New model: ScheduledShift (scheduled_shifts table)
Planning data separate from waiter_shifts (actual data) — linked conceptually by user+date
start_time/end_time stored as "HH:MM" strings (SQLite has no native Time type)
- New router /api/schedule/:
- GET / — list by date range + optional user_id filter
- POST / — create scheduled shift (validates user exists)
- PUT /{id} — update times/notes
- DELETE /{id} — remove
- GET /week?week_start= — full week view: scheduled shifts with actual WaiterShift
comparison for the same user+date, estimated weekly labor cost, waiters list with rates
- Each ScheduledShiftOut includes duration_hours (handles overnight) + estimated_pay
(duration × hourly_rate, null if rate not set)
- Migration: CREATE TABLE IF NOT EXISTS scheduled_shifts
Frontend:
- SchedulePage (/schedule): weekly calendar grid — rows = staff, columns = Mon–Sun
- Week navigation (← Προηγ. / Αυτή η εβδομάδα / Επόμ. →)
- Estimated weekly labor cost in header when shifts have rates
- ShiftSlot cells: blue = scheduled only, green = scheduled + actual shift happened
Shows scheduled times + actual start/end from real WaiterShift data
- Click empty cell → AddShiftModal (pre-selects that row's waiter, any date in week)
- "+" row at bottom for waiters not yet scheduled this week
- Delete button (✕) per slot with confirm
- Legend explaining the color scheme
- Sidebar: CalendarDays icon for Πρόγραμμα (after KDS)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Backend:
- New router /api/kds/:
- GET /items — returns all active order items for open orders, grouped by printer zone
(zone_id, zone_name, items array with table_name, product_name, qty, notes, added_at)
Zones sorted by zone_id asc, no-zone column last
- PUT /orders/{order_id}/items/{item_id}/status — mark item 'ready' (active→ready only)
Broadcasts item_status_changed SSE event to all connected clients
- No migration needed — 'ready' is a new valid string value for order_items.status
- pay_items endpoint: now accepts 'ready' items as payable (status.in_(['active','ready']))
- active_remaining count for order status also includes 'ready' items
Frontend:
- KdsPage (/kds): full-screen dark layout, columns per printer zone
- Zone columns: dark header with zone name + pending count, scrollable item cards
- Item cards: table name badge, elapsed time (colour-coded: green<8m, amber<15m, red>=15m),
product name + quantity, notes in amber, tap anywhere to mark ready
- Optimistic marking: card dims while request in flight, disappears on success
- SSE live updates: listens for order_updated / item_status_changed / order_paid / order_closed
- Fallback poll every 30s; manual ↻ Ανανέωση button; live clock in top bar
- Empty state: ✓ message when no pending items
- Sidebar: ChefHat icon for KDS (after Αποβλήτα)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
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 <noreply@anthropic.com>
Backend:
- New models: Tab, TabEntry, TabPayment (3 new tables)
- Tab: open|closed|forgiven, one open tab per customer enforced
- TabEntry: what went ON the tab (per-item snapshot with description)
- TabPayment: what came OFF the tab; balance = sum(entries) - sum(payments), never stored
- New router /api/tabs/:
- GET / — list open tabs (sorted by balance desc); ?tab_status= override
- GET /{id} — full tab detail with entries + payments
- POST / — open new tab for a customer (400 if one already exists)
- POST /{id}/entries — add entry directly (amount + description)
- POST /{id}/pay — record payment; validates amount <= balance
- POST /{id}/close — close tab (requires balance = 0)
- POST /{id}/forgive — write off remaining balance
- GET /customer/{customer_id} — all tabs for a customer (open first)
- POST /api/orders/{id}/items/{item_id}/tab:
- Requires order has a customer assigned
- Finds or auto-creates open tab for that customer
- Sets order_item.status = "tabbed" (new valid status value)
- Creates TabEntry with auto-generated description
- Broadcasts order_updated SSE event
- Migrations: CREATE TABLE IF NOT EXISTS for tabs, tab_entries, tab_payments
Frontend:
- TabsPage (/tabs): open tabs list sorted by balance; each card shows charges/payments
history, live balance, Πληρωμή modal (defaults to full balance), Κλείσιμο button
(only shown when balance=0), Χάρισμα Υπολοίπου with confirm modal + reason
- CustomersPage CustomerDetail: open tab banner showing balance + entry count
(appears between stats and contact info when customer has an open tab)
- OrderDetailPage: 📋 Καρτέλα button appears on active items when order has a customer;
tabbed items show a 📋 Καρτέλα badge; tabItem mutation calls /items/{id}/tab
- Sidebar: CreditCard icon for Καρτέλες (after Πελάτες)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Backend:
- New model: Customer (customers table) — name, nickname, phone, email, notes, is_active
- Order model: add customer_id nullable FK → customers
- New router: /api/customers/ — list (with ?search=), create, get, update, soft-delete,
GET /{id}/orders (visit history with totals)
- Each CustomerOut includes visit_count + total_spent (computed from closed/paid orders)
- PUT /api/orders/{id}/customer — assign or unassign a customer; manager-only;
validates customer is active; broadcasts order_updated SSE event
- GET /api/orders/{id} now returns customer_id, customer_name (with nickname), customer_phone
- Migration: CREATE TABLE customers + ALTER TABLE orders ADD COLUMN customer_id
Frontend:
- CustomersPage (/customers): searchable list (name/nickname/phone), avatar initials,
visit count + total_spent on each row; click → detail panel slides in showing stats
(visits, total spent, avg ticket), contact info, notes, full visit history
- OrderDetailPage: new Πελάτης card — shows assigned customer in blue if set;
for open orders shows "+ Ανάθεση πελάτη" button → inline search dropdown → assign;
Αφαίρεση button to unassign
- Sidebar: Users icon for Πελάτες (between Επαφές and Ρυθμίσεις)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Backend:
- New models: Contact, Expense, ExpensePayment (3 new tables)
- Expense status (paid/partial/due) is always computed from paid_amount vs total_amount — never stored
- paid_amount recomputed from sum of all payments on each payment write (no client trust)
- New router /api/contacts/: list, create, update, soft-delete (is_active=false)
- New router /api/expenses/: list (filter by status/category/contact), create, update, delete,
record payment, summary (total_due, total_paid, by_category)
- Migrations: CREATE TABLE IF NOT EXISTS for contacts, expenses, expense_payments
Frontend:
- ContactsPage (/contacts): searchable table, type badge (Προμηθευτής/Προσωπικό/Κοινή Χρεία/Άλλο),
create/edit modal, soft-delete
- ExpensesPage (/expenses): filter bar (status + category), expandable rows with payment history,
summary header showing total outstanding, inline Payment modal (defaults to full due amount),
create/edit expense modal
- Sidebar: Receipt icon for Έξοδα, BookUser icon for Επαφές
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- New models: SiteNote, SiteTodo (site_notes, site_todos tables)
- New schemas: NoteOut, TodoOut with creator/done-by name enrichment
- New router: /api/notes/ — full CRUD for notes and todos
- Notes: create, list (pinned first), update (body + pin), delete
- Todos: create, list (undone high-priority first), toggle done, edit, delete
- Marking done records done_at + done_by_id; unchecking clears both
- Migrations: CREATE TABLE IF NOT EXISTS for both tables (additive, safe)
- NotesPage.jsx: two-column layout — notes left, todos right
- Notes: inline click-to-edit, pin toggle, Ctrl+Enter to save, pinned section on top
- Todos: inline edit, high-priority flag, completed collapse toggle
- /notes route added to App.jsx
- NotebookPen sidebar entry between Διαχείριση and Ρυθμίσεις
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Products: add cost_simple (flat €) and cost_breakdown (JSON line items)
- OrderItems: add unit_cost snapshot written at time of order creation
- Snapshot logic: breakdown sum takes priority over cost_simple; NULL if neither set
- Product schema: serialize/deserialize cost_breakdown as JSON; expose in API
- Product performance report: add trackable_profit, uncosted_revenue, has_gap per product
- Manager UI: cost section in product edit form (simple / detailed toggle with live margin %)
- Products list: show margin % or cost-error badge per product
- Product performance report: profit column + gap warning banner
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- New reservations module: model, schema, router (CRUD + status updates + upcoming alerts)
and background task for auto-expiring stale reservations
- Reports: print_products, print_categories, print_tables analytics endpoints
plus meta_products and business_day_summary for workday close/view flow
- printer_service: configurable font sizes/weights, donut/bar chart print layout helpers,
analytics print blocks per printer
- tables/schemas: surfaced color, zone, and other new fields on Table, Product, User, Printer
- demo_seed.py for quick dev DB population; wipe_database.py utility
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- connect_orders.py: add GET /api/connect/orders?status=active|history endpoints
- api/client.js: add getIncomingOrders, getActiveOrders, getOrderHistory,
acceptOnlineOrder, rejectOnlineOrder, updateOnlineOrderStatus
- OnlineOrdersPage.jsx: three-tab page (Incoming / Active / History) with
order cards, action buttons (accept/reject/status progression),
20 s polling, SSE listener for online_order_received/updated events,
and optimistic UI updates
- App.jsx: add /online-orders route
- Sidebar.jsx: add Online Orders nav item with red badge showing pending count,
polling every 20 s independently of the page
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Adds a new "Digital Menu" tab with three sections:
- Visibility: digital_visible / digital_available toggles
- Display overrides: digital_name, digital_description, digital_image_url
- Pricing: digital_price / digital_discount with live customer-price preview
All 7 fields are wired into form state (buildFormFromProduct) and
included in the PUT request body (buildBody).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
_connect_loop now waits up to 30 seconds for the heartbeat to return
site_numeric_id, then immediately pushes the menu snapshot and stats
before entering the regular poll loop. Previously the first push
wouldn't fire until 5 minutes after startup, making the menu
unavailable until then.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Adds _push_stats_snapshot() to cloud_sync.py. Every 5 minutes
(piggybacked on the existing _connect_loop push tick alongside the
menu snapshot) it queries the local DB and POSTs a JSON stats blob
to POST /api/remote/snapshot (site API key auth).
Stats collected:
- open_tables: count of open/partially_paid POS orders
- today_revenue: sum of active+paid item prices on orders
closed today
- today_orders: count of paid/closed POS orders today
- online_orders_pending: online orders awaiting acceptance
- online_orders_today: all online orders opened today
- current_shift: active waiter shift info (waiter_id, started_at)
if a business day is open; null otherwise
- as_of: UTC timestamp of the snapshot
The remote manager dashboard reads this via
GET /api/remote/sites/{id}/snapshot (manager JWT).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The Phase 4 _mirror_status_to_cloud function had no way to look up the
cloud order's numeric id once it was marked synced (not in the pending
list anymore), so status updates from local staff could silently fail.
Fix:
- models/order.py: online_order_cloud_id INTEGER column added to Order
- main.py: migration for the new column
- schemas/order.py: online_order_cloud_id exposed in OrderOut
- cloud_sync.py: stores cloud_order["id"] as online_order_cloud_id
when creating the local order during the pull
- connect_orders.py: _mirror_status_to_cloud now takes the integer
cloud id directly — no more pending-list lookup; function body
reduced from ~50 lines to ~15
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
4.1 — cloud_sync.py
- _push_menu_snapshot(): serializes digital-visible products+categories
and POSTs to cloud /api/menu/sync every 5 minutes
- _pull_pending_orders(): polls cloud /api/orders/pending/{site_id}
every CONNECT_SYNC_INTERVAL_SECONDS (default 30s); creates local
Order + OrderItem rows, marks synced on cloud, broadcasts SSE event
- _connect_loop(): second asyncio task running the fast poll loop;
piggybacked menu push fires every 5 min regardless of poll interval
- _sync_once(): captures site_numeric_id from heartbeat response and
stores it in license_state so Connect loops can use it
- start_cloud_sync(): now creates and returns both tasks
4.2 — orders model/schema/migrations
- models/order.py: table_id made nullable (online orders have no
table); 7 new online_* columns added to Order
- schemas/order.py: OrderOut table_id Optional, all 7 online_* fields
added
- main.py: 8 additive ALTER TABLE migrations for orders table
4.3 — routers/connect_orders.py (NEW)
GET /api/connect/orders/incoming — pending online orders (any auth)
POST /api/connect/orders/{id}/accept — accept (manager)
POST /api/connect/orders/{id}/reject — reject with optional reason (manager)
POST /api/connect/orders/{id}/status — progress through lifecycle (manager)
All state changes mirror to cloud via background task and broadcast SSE
4.4 — main.py router registration
connect_orders router registered at /api/connect
config.py
CONNECT_SYNC_INTERVAL_SECONDS setting added (default 30)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Backend (reports.py):
- Cache order DB objects to avoid N+1 queries per log entry
- Track seen_order_totals keyed by order_id so reprinting the same
order doesn't inflate the total; returns total_amount pre-computed
Frontend (PrinterHistory.jsx):
- Remove client-side accumulation that caused the same issue; use
total_amount directly from the API response
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Backend:
- tables.py: PUT /api/tables/groups/reorder accepts an ordered list
of group IDs and updates sort_order accordingly
Frontend (TablesConfigTab):
- Zone tabs are now draggable for group tabs (not All/Ungrouped)
- HTML5 drag-and-drop with visual drop-target highlight and a
grab cursor; fires reorderGroups mutation on drop
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Adds configurable buzzer control so the printer alerts kitchen staff
on each new ticket print.
Backend:
- printer_service.py: ESC 0x07 beep command fired before paper cut,
reads print.beep_on_ticket / print.beep_pattern settings; supports
single/double/triple/long presets and a custom:n1:n2:n3 format
- settings.py: registers the two new print.beep_* settings with
defaults (beep_on_ticket=true, beep_pattern=double)
- system.py: POST /api/system/printers/test-beep endpoint for
live testing from the dashboard
Frontend (PrintFontsTab):
- BeepSection component with preset grid, custom n1/n2/n3 inputs,
and a live test-beep button targeting any configured printer
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Adds seven digital-menu-specific columns to the products table so
managers can control the public Xenia Connect menu independently of POS
data:
- digital_visible (INTEGER, default 1) — hide product from menu
- digital_available (INTEGER, default 1) — show as out-of-stock
- digital_name (VARCHAR, nullable) — override display name
- digital_description (VARCHAR, nullable)
- digital_price (REAL, nullable) — override price
- digital_discount (REAL, default 0.0) — percentage discount
- digital_image_url (VARCHAR, nullable) — menu-specific image
Changes:
- main.py: 7 additive ALTER TABLE migrations (safe on existing DBs)
- models/product.py: matching SQLAlchemy columns on Product
- schemas/product.py: ProductBase carries defaults (visible/available
default True); ProductUpdate has all fields Optional; ProductOut
inherits everything via ProductBase
No router changes needed — PUT /api/products/{id} picks up the new
fields from ProductUpdate automatically.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- .env.example: set REGISTRY=registry.bonamin.gr, CLOUD_URL=https://xenia-admin.bonamin.gr, DATA_PATH=/opt/xenia/data
- install.sh: auto-create .env from example, prompt for SITE_ID/SITE_KEY/SECRET_KEY,
clarify DNS subdomain requirements, add backend API proxy block to nginx config
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Includes all work to date:
- local_backend: FastAPI backend with products, orders, tables, shifts, cloud sync
- manager_dashboard: React manager UI with product/category management, reports, settings
- waiter_pwa: React PWA for waiter devices
- Category reparent endpoint and UI
- Waiter domain: local_ip sent on heartbeat, waiter_domain persisted from cloud response
- QR code modal in AppInfoTab for waiter domain
- Product form: number input spinners removed, category pre-selected on new product
- Category row: count badge moved to far right
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>