diff --git a/local_backend/routers/settings.py b/local_backend/routers/settings.py index 6cdbc29..e15218f 100644 --- a/local_backend/routers/settings.py +++ b/local_backend/routers/settings.py @@ -16,6 +16,7 @@ VALID_SETTINGS = { "business_day.force_close_allowed": "Allow force-closing business day with open tables", "system.timezone": "IANA timezone name used by the backend container (e.g. Europe/Athens). Requires container restart to take effect.", "ui.table_colours": "JSON blob of table card colour scheme (light + dark modes) for the Waiter PWA.", + "dev.spoof_printing": "When enabled, all print jobs are silently dropped. Devices behave as if printing succeeded.", } DEFAULTS = { @@ -24,6 +25,7 @@ DEFAULTS = { "business_day.force_close_allowed": "true", "system.timezone": "Europe/Athens", "ui.table_colours": "", + "dev.spoof_printing": "false", } diff --git a/local_backend/services/printer_service.py b/local_backend/services/printer_service.py index 976d842..9dd1b09 100644 --- a/local_backend/services/printer_service.py +++ b/local_backend/services/printer_service.py @@ -20,6 +20,7 @@ from database import SessionLocal from models.order import Order, OrderItem, PrintLog from models.printer import Printer from models.product import Product +from models.settings import PosSettings logger = logging.getLogger(__name__) @@ -73,7 +74,19 @@ def check_printer(ip: str, port: int) -> bool: return False +def is_spoof_mode() -> bool: + """Stateless check — opens its own DB session. For use outside route_and_print.""" + db = SessionLocal() + try: + return _is_spoof_mode(db) + finally: + db.close() + + def send_test_print(ip: str, port: int, name: str) -> Tuple[bool, str]: + if is_spoof_mode(): + logger.info("Spoof printing ON — dropping test print for %s", name) + return True, "" try: p = _get_printer(ip, port) p._raw(b'\x1b\x61\x01') @@ -164,6 +177,9 @@ def _print_kitchen_ticket(p: Network, order: Order, items: List[OrderItem], db: def print_waiter_report(ip: str, port: int, report: dict, mode: str): """Print a waiter shift/period report. mode='simple'|'extensive'.""" + if is_spoof_mode(): + logger.info("Spoof printing ON — dropping waiter report print") + return try: p = _get_printer(ip, port) @@ -222,6 +238,9 @@ def print_waiter_report(ip: str, port: int, report: dict, mode: str): def print_printer_report(ip: str, port: int, report: dict, mode: str): """Print a per-printer totals report. mode='simple'|'extensive'.""" + if is_spoof_mode(): + logger.info("Spoof printing ON — dropping printer report print") + return try: p = _get_printer(ip, port) @@ -282,6 +301,9 @@ def print_printer_report(ip: str, port: int, report: dict, mode: str): def print_order_receipt(ip: str, port: int, receipt: dict): """Print a manager-triggered order receipt.""" + if is_spoof_mode(): + logger.info("Spoof printing ON — dropping order receipt print") + return try: p = _get_printer(ip, port) @@ -329,6 +351,9 @@ def print_order_receipt(ip: str, port: int, receipt: dict): def print_order_synopsis(ip: str, port: int, synopsis: dict): """Print a waiter-triggered order synopsis (not a kitchen ticket).""" + if is_spoof_mode(): + logger.info("Spoof printing ON — dropping order synopsis print") + return try: p = _get_printer(ip, port) @@ -408,7 +433,21 @@ def route_and_print_sync(order_id: int, item_ids: List[int], db: Session) -> Lis return _do_route_and_print(order_id, item_ids, db) +def _is_spoof_mode(db: Session) -> bool: + row = db.query(PosSettings).filter(PosSettings.key == "dev.spoof_printing").first() + return row is not None and row.value == "true" + + def _do_route_and_print(order_id: int, item_ids: List[int], db: Session) -> List[dict]: + if _is_spoof_mode(db): + logger.info("Spoof printing ON — dropping print job for order %s", order_id) + for item_id in item_ids: + item = db.query(OrderItem).filter(OrderItem.id == item_id).first() + if item: + item.printed = True + db.commit() + return [{"printer_name": "spoof", "success": True, "error": None}] + results = [] order = db.query(Order).filter(Order.id == order_id).first() diff --git a/manager_dashboard/src/pages/Settings/SettingsPage.jsx b/manager_dashboard/src/pages/Settings/SettingsPage.jsx index 5a0d71a..9445ac7 100644 --- a/manager_dashboard/src/pages/Settings/SettingsPage.jsx +++ b/manager_dashboard/src/pages/Settings/SettingsPage.jsx @@ -1,10 +1,12 @@ import { useState } from 'react' import AppInfoTab from './tabs/AppInfoTab' import ColoursTab from './tabs/ColoursTab' +import DevelopmentTab from './tabs/DevelopmentTab' const TABS = [ - { key: 'app-info', label: 'App Info' }, - { key: 'colours', label: 'UI Personalization' }, + { key: 'app-info', label: 'App Info' }, + { key: 'colours', label: 'UI Personalization' }, + { key: 'development', label: 'Development' }, ] export default function SettingsPage() { @@ -44,8 +46,9 @@ export default function SettingsPage() { {/* Tab content */} - {activeTab === 'app-info' && } - {activeTab === 'colours' && } + {activeTab === 'app-info' && } + {activeTab === 'colours' && } + {activeTab === 'development' && } ) } diff --git a/manager_dashboard/src/pages/Settings/tabs/DevelopmentTab.jsx b/manager_dashboard/src/pages/Settings/tabs/DevelopmentTab.jsx new file mode 100644 index 0000000..773f0be --- /dev/null +++ b/manager_dashboard/src/pages/Settings/tabs/DevelopmentTab.jsx @@ -0,0 +1,86 @@ +import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query' +import toast from 'react-hot-toast' +import client from '../../../api/client' + +function Toggle({ checked, onChange, disabled }) { + return ( + + ) +} + +export default function DevelopmentTab() { + const qc = useQueryClient() + + const { data: settings, isLoading } = useQuery({ + queryKey: ['settings'], + queryFn: () => client.get('/api/settings/').then(r => r.data), + }) + + const mutation = useMutation({ + mutationFn: ({ key, value }) => client.put(`/api/settings/${key}`, { value }), + onSuccess: () => { qc.invalidateQueries({ queryKey: ['settings'] }) }, + onError: () => toast.error('Failed to update setting'), + }) + + const spoofOn = settings?.['dev.spoof_printing']?.value === 'true' + + function toggleSpoof(val) { + mutation.mutate({ key: 'dev.spoof_printing', value: val ? 'true' : 'false' }) + toast.success(val ? 'Spoof printing ON — printers are silenced' : 'Spoof printing OFF — printers active') + } + + if (isLoading) return

Loading…

+ + return ( +
+
+ These settings are intended for testing only. Do not leave them enabled in production. +
+ +
+
+
+ Spoof Printer Mode +
+
+ All print jobs are silently dropped. Devices behave as if printing succeeded — no errors, nothing printed. +
+
+ +
+ + {spoofOn && ( +
+ Spoof mode is active — printers are silenced. +
+ )} +
+ ) +} diff --git a/simple-pos-system.zip b/simple-pos-system.zip new file mode 100644 index 0000000..208b3a2 Binary files /dev/null and b/simple-pos-system.zip differ diff --git a/waiter_pwa/dev-dist/sw.js b/waiter_pwa/dev-dist/sw.js index 9d827aa..24f463e 100644 --- a/waiter_pwa/dev-dist/sw.js +++ b/waiter_pwa/dev-dist/sw.js @@ -82,7 +82,7 @@ define(['./workbox-5a5d9309'], (function (workbox) { 'use strict'; "revision": "3ca0b8505b4bec776b69afdba2768812" }, { "url": "index.html", - "revision": "0.7tvu7c24jlg" + "revision": "0.qb5i81hq8" }], {}); workbox.cleanupOutdatedCaches(); workbox.registerRoute(new workbox.NavigationRoute(workbox.createHandlerBoundToURL("index.html"), { diff --git a/waiter_pwa/vite.config.js b/waiter_pwa/vite.config.js index 109fdfd..8b1f3dc 100644 --- a/waiter_pwa/vite.config.js +++ b/waiter_pwa/vite.config.js @@ -3,6 +3,9 @@ import react from '@vitejs/plugin-react' import { VitePWA } from 'vite-plugin-pwa' export default defineConfig({ + server: { + allowedHosts: 'all', + }, plugins: [ react(), VitePWA({