Added Printer Spoofer for testing purposes
This commit is contained in:
@@ -16,6 +16,7 @@ VALID_SETTINGS = {
|
|||||||
"business_day.force_close_allowed": "Allow force-closing business day with open tables",
|
"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.",
|
"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.",
|
"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 = {
|
DEFAULTS = {
|
||||||
@@ -24,6 +25,7 @@ DEFAULTS = {
|
|||||||
"business_day.force_close_allowed": "true",
|
"business_day.force_close_allowed": "true",
|
||||||
"system.timezone": "Europe/Athens",
|
"system.timezone": "Europe/Athens",
|
||||||
"ui.table_colours": "",
|
"ui.table_colours": "",
|
||||||
|
"dev.spoof_printing": "false",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ from database import SessionLocal
|
|||||||
from models.order import Order, OrderItem, PrintLog
|
from models.order import Order, OrderItem, PrintLog
|
||||||
from models.printer import Printer
|
from models.printer import Printer
|
||||||
from models.product import Product
|
from models.product import Product
|
||||||
|
from models.settings import PosSettings
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
@@ -73,7 +74,19 @@ def check_printer(ip: str, port: int) -> bool:
|
|||||||
return False
|
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]:
|
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:
|
try:
|
||||||
p = _get_printer(ip, port)
|
p = _get_printer(ip, port)
|
||||||
p._raw(b'\x1b\x61\x01')
|
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):
|
def print_waiter_report(ip: str, port: int, report: dict, mode: str):
|
||||||
"""Print a waiter shift/period report. mode='simple'|'extensive'."""
|
"""Print a waiter shift/period report. mode='simple'|'extensive'."""
|
||||||
|
if is_spoof_mode():
|
||||||
|
logger.info("Spoof printing ON — dropping waiter report print")
|
||||||
|
return
|
||||||
try:
|
try:
|
||||||
p = _get_printer(ip, port)
|
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):
|
def print_printer_report(ip: str, port: int, report: dict, mode: str):
|
||||||
"""Print a per-printer totals report. mode='simple'|'extensive'."""
|
"""Print a per-printer totals report. mode='simple'|'extensive'."""
|
||||||
|
if is_spoof_mode():
|
||||||
|
logger.info("Spoof printing ON — dropping printer report print")
|
||||||
|
return
|
||||||
try:
|
try:
|
||||||
p = _get_printer(ip, port)
|
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):
|
def print_order_receipt(ip: str, port: int, receipt: dict):
|
||||||
"""Print a manager-triggered order receipt."""
|
"""Print a manager-triggered order receipt."""
|
||||||
|
if is_spoof_mode():
|
||||||
|
logger.info("Spoof printing ON — dropping order receipt print")
|
||||||
|
return
|
||||||
try:
|
try:
|
||||||
p = _get_printer(ip, port)
|
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):
|
def print_order_synopsis(ip: str, port: int, synopsis: dict):
|
||||||
"""Print a waiter-triggered order synopsis (not a kitchen ticket)."""
|
"""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:
|
try:
|
||||||
p = _get_printer(ip, port)
|
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)
|
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]:
|
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 = []
|
results = []
|
||||||
|
|
||||||
order = db.query(Order).filter(Order.id == order_id).first()
|
order = db.query(Order).filter(Order.id == order_id).first()
|
||||||
|
|||||||
@@ -1,10 +1,12 @@
|
|||||||
import { useState } from 'react'
|
import { useState } from 'react'
|
||||||
import AppInfoTab from './tabs/AppInfoTab'
|
import AppInfoTab from './tabs/AppInfoTab'
|
||||||
import ColoursTab from './tabs/ColoursTab'
|
import ColoursTab from './tabs/ColoursTab'
|
||||||
|
import DevelopmentTab from './tabs/DevelopmentTab'
|
||||||
|
|
||||||
const TABS = [
|
const TABS = [
|
||||||
{ key: 'app-info', label: 'App Info' },
|
{ key: 'app-info', label: 'App Info' },
|
||||||
{ key: 'colours', label: 'UI Personalization' },
|
{ key: 'colours', label: 'UI Personalization' },
|
||||||
|
{ key: 'development', label: 'Development' },
|
||||||
]
|
]
|
||||||
|
|
||||||
export default function SettingsPage() {
|
export default function SettingsPage() {
|
||||||
@@ -44,8 +46,9 @@ export default function SettingsPage() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Tab content */}
|
{/* Tab content */}
|
||||||
{activeTab === 'app-info' && <AppInfoTab />}
|
{activeTab === 'app-info' && <AppInfoTab />}
|
||||||
{activeTab === 'colours' && <ColoursTab />}
|
{activeTab === 'colours' && <ColoursTab />}
|
||||||
|
{activeTab === 'development' && <DevelopmentTab />}
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
86
manager_dashboard/src/pages/Settings/tabs/DevelopmentTab.jsx
Normal file
86
manager_dashboard/src/pages/Settings/tabs/DevelopmentTab.jsx
Normal file
@@ -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 (
|
||||||
|
<button
|
||||||
|
role="switch"
|
||||||
|
aria-checked={checked}
|
||||||
|
onClick={() => !disabled && onChange(!checked)}
|
||||||
|
style={{
|
||||||
|
width: 44, height: 24, borderRadius: 999, border: 'none', cursor: disabled ? 'not-allowed' : 'pointer',
|
||||||
|
background: checked ? '#dc2626' : '#d1d5db',
|
||||||
|
position: 'relative', transition: 'background 150ms', flexShrink: 0, opacity: disabled ? 0.5 : 1,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<span style={{
|
||||||
|
position: 'absolute', top: 3, left: checked ? 23 : 3,
|
||||||
|
width: 18, height: 18, borderRadius: '50%', background: 'white',
|
||||||
|
transition: 'left 150ms', boxShadow: '0 1px 3px rgba(0,0,0,0.2)',
|
||||||
|
}} />
|
||||||
|
</button>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
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 <p style={{ color: '#6b7280', fontSize: 14 }}>Loading…</p>
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div style={{ maxWidth: 560 }}>
|
||||||
|
<div style={{
|
||||||
|
background: '#fef2f2', border: '1px solid #fca5a5',
|
||||||
|
borderRadius: 10, padding: '12px 16px', marginBottom: 24,
|
||||||
|
fontSize: 13, color: '#991b1b',
|
||||||
|
}}>
|
||||||
|
These settings are intended for testing only. Do not leave them enabled in production.
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style={{
|
||||||
|
background: 'white', border: '1px solid #e5e7eb',
|
||||||
|
borderRadius: 10, padding: '16px 20px',
|
||||||
|
display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: 16,
|
||||||
|
}}>
|
||||||
|
<div>
|
||||||
|
<div style={{ fontWeight: 600, fontSize: 14, color: '#111827' }}>
|
||||||
|
Spoof Printer Mode
|
||||||
|
</div>
|
||||||
|
<div style={{ fontSize: 13, color: '#6b7280', marginTop: 3 }}>
|
||||||
|
All print jobs are silently dropped. Devices behave as if printing succeeded — no errors, nothing printed.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<Toggle checked={spoofOn} onChange={toggleSpoof} disabled={mutation.isPending} />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{spoofOn && (
|
||||||
|
<div style={{
|
||||||
|
marginTop: 12, padding: '10px 14px',
|
||||||
|
background: '#fff7ed', border: '1px solid #fed7aa',
|
||||||
|
borderRadius: 8, fontSize: 13, color: '#92400e', fontWeight: 500,
|
||||||
|
}}>
|
||||||
|
Spoof mode is active — printers are silenced.
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
BIN
simple-pos-system.zip
Normal file
BIN
simple-pos-system.zip
Normal file
Binary file not shown.
@@ -82,7 +82,7 @@ define(['./workbox-5a5d9309'], (function (workbox) { 'use strict';
|
|||||||
"revision": "3ca0b8505b4bec776b69afdba2768812"
|
"revision": "3ca0b8505b4bec776b69afdba2768812"
|
||||||
}, {
|
}, {
|
||||||
"url": "index.html",
|
"url": "index.html",
|
||||||
"revision": "0.7tvu7c24jlg"
|
"revision": "0.qb5i81hq8"
|
||||||
}], {});
|
}], {});
|
||||||
workbox.cleanupOutdatedCaches();
|
workbox.cleanupOutdatedCaches();
|
||||||
workbox.registerRoute(new workbox.NavigationRoute(workbox.createHandlerBoundToURL("index.html"), {
|
workbox.registerRoute(new workbox.NavigationRoute(workbox.createHandlerBoundToURL("index.html"), {
|
||||||
|
|||||||
@@ -3,6 +3,9 @@ import react from '@vitejs/plugin-react'
|
|||||||
import { VitePWA } from 'vite-plugin-pwa'
|
import { VitePWA } from 'vite-plugin-pwa'
|
||||||
|
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
|
server: {
|
||||||
|
allowedHosts: 'all',
|
||||||
|
},
|
||||||
plugins: [
|
plugins: [
|
||||||
react(),
|
react(),
|
||||||
VitePWA({
|
VitePWA({
|
||||||
|
|||||||
Reference in New Issue
Block a user