feat: initial commit — local services (backend + manager dashboard + waiter PWA)

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>
This commit is contained in:
2026-05-20 14:04:38 +03:00
commit 8ba8c95ecd
209 changed files with 48017 additions and 0 deletions

343
local_backend/print_test.py Normal file
View File

@@ -0,0 +1,343 @@
"""
Printer comprehensive test script — Jolimark TP850UE
Usage: python print_test.py [IP] [PORT]
Default: 10.98.20.25:9100
Prints 6 pages:
Page 1 — ESC ! modes, Font A, English
Page 2 — ESC ! modes, Font B, English
Page 3 — ESC ! modes, Font A, Greek
Page 4 — ESC ! modes, Font B, Greek
Page 5 — GS ! character size multipliers (both fonts)
Page 6 — Beep tests + misc (underline, invert, symbols)
ESC ! (0x1B 0x21 n) correct bit map for TP850UE:
Bit 0 (0x01) — Font B instead of Font A
Bit 3 (0x08) — Emphasize / Bold
Bit 4 (0x10) — Double-height
Bit 5 (0x20) — Double-width
Bit 7 (0x80) — Underline
GS ! (0x1D 0x21 n) character size multiplier:
Low nibble (bits 0-3): height multiplier (0=1x, 1=2x, 2=3x … 7=8x)
High nibble (bits 4-7): width multiplier (0=1x, 1=2x, 2=3x … 7=8x)
e.g. n=0x00 → 1×1, n=0x11 → 2×2, n=0x22 → 3×3, n=0x77 → 8×8
"""
import sys
import time
PRINTER_IP = sys.argv[1] if len(sys.argv) > 1 else "10.98.20.25"
PRINTER_PORT = int(sys.argv[2]) if len(sys.argv) > 2 else 9100
from escpos.printer import Network
# ── Low-level helpers ──────────────────────────────────────────────────────────
def _gr(text: str) -> bytes:
return text.encode('cp737', errors='replace')
def _open():
p = Network(PRINTER_IP, PRINTER_PORT, timeout=10)
p._raw(b'\x1b\x40') # ESC @ — full reset
p._raw(b'\x1b\x74\x1d') # ESC t 29 — CP737 Greek code page
return p
def _t(p, text: str):
p._raw(_gr(text))
def _reset(p):
"""Reset to: Font A, normal size, no bold, left-align."""
p._raw(b'\x1b\x4d\x00') # ESC M 0 — Font A
p._raw(b'\x1b\x21\x00') # ESC ! 0 — normal
p._raw(b'\x1d\x21\x00') # GS ! 0 — 1×1 size
p._raw(b'\x1b\x45\x00') # ESC E 0 — bold off
p._raw(b'\x1b\x61\x00') # ESC a 0 — left align
def _center(p): p._raw(b'\x1b\x61\x01')
def _left(p): p._raw(b'\x1b\x61\x00')
def _divider(p, char="-", width=48):
_left(p)
_t(p, char * width + "\n")
def _page_header(p, title: str):
_center(p)
p._raw(b'\x1b\x21\x28') # double-width + bold (bits 3+5 = 0x28)
_t(p, title + "\n")
_reset(p)
_divider(p, "=")
# ── ESC ! mode table ───────────────────────────────────────────────────────────
#
# Each entry: (esc_bang_byte, esc_e_bold, label)
# esc_bang_byte sets the mode via ESC ! n
# esc_e_bold adds ESC E on top (independent bold layer)
# We test every useful combination so you can see the exact visual result.
ESC_BANG_MODES = [
# (byte, extra_bold, label)
(0x00, False, "0x00 Normal"),
(0x00, True, "0x00 +ESC E Normal + Bold (ESC E)"),
(0x08, False, "0x08 Bold only (bit3)"),
(0x10, False, "0x10 Double-height (bit4)"),
(0x10, True, "0x10 +ESC E Double-height + Bold"),
(0x18, False, "0x18 Double-height + Bold (bits 3+4)"),
(0x20, False, "0x20 Double-width (bit5)"),
(0x20, True, "0x20 +ESC E Double-width + Bold"),
(0x28, False, "0x28 Double-width + Bold (bits 3+5)"),
(0x30, False, "0x30 Double-width + Double-height (bits 4+5)"),
(0x38, False, "0x38 Double-width + Double-height + Bold (bits 3+4+5)"),
]
def _esc_bang_section(p, english: bool):
lang = "EN" if english else "GR"
sample_normal = "TEST PRINT Hello 123" if english else "ΔΟΚΙΜΗ ΕΚΤΥΠΩΣΗΣ"
sample_lower = "test print hello 123" if english else "δοκιμη εκτυπωσης"
for (byte_val, extra_bold, label) in ESC_BANG_MODES:
_left(p)
# Print the label in small normal text first
p._raw(b'\x1b\x21\x00')
p._raw(b'\x1b\x45\x00')
_t(p, f"[{label}]\n")
# Apply mode
p._raw(bytes([0x1b, 0x21, byte_val]))
if extra_bold:
p._raw(b'\x1b\x45\x01')
_t(p, sample_normal + "\n")
_t(p, sample_lower + "\n")
# Reset
_reset(p)
_t(p, "\n")
_divider(p)
# ── Pages 14: ESC ! modes ────────────────────────────────────────────────────
def page_esc_bang(font_b: bool, english: bool):
font_label = "Font B (8x16 small)" if font_b else "Font A (12x24 standard)"
lang_label = "GREEK" if not english else "ENGLISH"
p = _open()
# Select font
p._raw(b'\x1b\x4d\x01' if font_b else b'\x1b\x4d\x00')
_page_header(p, f"ESC! MODES — {lang_label}{font_label[:6]}")
_t(p, f"Font: {font_label}\n")
_divider(p)
_esc_bang_section(p, english)
p._raw(b'\n\n\n')
p.cut()
p.close()
# ── Page 5: GS ! size multipliers ─────────────────────────────────────────────
# Combinations worth seeing: square multipliers + some asymmetric
GS_SIZES = [
(0x00, "1x1 normal"),
(0x01, "1w x 2h"),
(0x10, "2w x 1h"),
(0x11, "2x2"),
(0x22, "3x3"),
(0x33, "4x4"),
(0x44, "5x5"),
(0x55, "6x6"),
(0x02, "1w x 3h"),
(0x20, "3w x 1h"),
(0x21, "3w x 2h"),
(0x12, "2w x 3h"),
]
def page_gs_sizes():
p = _open()
_page_header(p, "GS! SIZE MULTIPLIERS")
_t(p, "GS ! n (0x1D 0x21 n)\n")
_t(p, "Low nibble=height, High nibble=width\n")
_divider(p)
for (byte_val, label) in GS_SIZES:
_left(p)
# Label in tiny normal text
p._raw(b'\x1b\x21\x00')
p._raw(b'\x1d\x21\x00')
_t(p, f"[n=0x{byte_val:02X} {label}]\n")
# Font A sample
p._raw(b'\x1b\x4d\x00')
p._raw(bytes([0x1d, 0x21, byte_val]))
_t(p, "Aa SAMPLE\n")
p._raw(b'\x1d\x21\x00')
# Font B sample on same size
p._raw(b'\x1b\x4d\x01')
p._raw(bytes([0x1d, 0x21, byte_val]))
_t(p, "Bb SMALL\n")
p._raw(b'\x1d\x21\x00')
p._raw(b'\x1b\x4d\x00') # back to Font A
_t(p, "\n")
_divider(p)
# Also show GS ! combined with ESC ! bold
_t(p, "\n")
_divider(p, "=")
p._raw(b'\x1b\x21\x00')
_t(p, "GS! + ESC E bold combined:\n")
_divider(p, "=")
for (byte_val, label) in [(0x11,"2x2"), (0x22,"3x3"), (0x33,"4x4")]:
p._raw(b'\x1b\x21\x00')
p._raw(b'\x1d\x21\x00')
_t(p, f"[{label} + bold]\n")
p._raw(b'\x1b\x45\x01')
p._raw(bytes([0x1d, 0x21, byte_val]))
_t(p, "BOLD LARGE\n")
p._raw(b'\x1b\x45\x00')
p._raw(b'\x1d\x21\x00')
_t(p, "\n")
p._raw(b'\n\n\n')
p.cut()
p.close()
# ── Page 6: Beep + misc ────────────────────────────────────────────────────────
def page_beep_misc():
p = _open()
_page_header(p, "BEEP + MISC TESTS")
# ── Beep section ──
_t(p, "BEEP TESTS\n")
_divider(p, "-")
_t(p, "Sending beeps now...\n\n")
# BEL — single beep (0x07)
_t(p, "[1] BEL single beep (0x07)\n")
p._raw(b'\x07')
time.sleep(0.5)
# ESC BEL n1 n2 n3 — beep for appointment
# n1=beep length (100ms units), n2=intermission (100ms), n3=count
_t(p, "[2] ESC BEL: 1 beep, 200ms long\n")
p._raw(bytes([0x1b, 0x07, 2, 2, 1])) # 200ms on, 200ms off, 1 beep
time.sleep(0.8)
_t(p, "[3] ESC BEL: 3 short beeps\n")
p._raw(bytes([0x1b, 0x07, 1, 1, 3])) # 100ms on, 100ms off, 3 beeps
time.sleep(1.5)
_t(p, "[4] ESC BEL: 1 long beep (500ms)\n")
p._raw(bytes([0x1b, 0x07, 5, 2, 1])) # 500ms on, 200ms off, 1 beep
time.sleep(1.2)
_t(p, "[5] GS BEL: 2 beeps\n")
p._raw(bytes([0x1d, 0x07, 2, 3, 2])) # 2 beeps, 300ms long, 200ms off
time.sleep(1.5)
_t(p, "Beep tests done.\n")
_divider(p)
# ── Underline ──
_t(p, "\nUNDERLINE\n")
_divider(p, "-")
for ul in [1, 2]:
p._raw(bytes([0x1b, 0x2d, ul]))
_t(p, f"Underline mode {ul}: Hello World 123\n")
p._raw(b'\x1b\x2d\x00')
_t(p, "\n")
_divider(p)
# ── White-on-black invert ──
_t(p, "\nWHITE-ON-BLACK (GS B)\n")
_divider(p, "-")
p._raw(b'\x1d\x42\x01')
_t(p, " INVERTED NORMAL \n")
p._raw(b'\x1d\x21\x11') # 2x2 inverted
_t(p, " INVERTED 2x2 \n")
p._raw(b'\x1d\x21\x00')
p._raw(b'\x1d\x42\x00')
_t(p, "Normal after invert\n")
_divider(p)
# ── 90-degree rotation ──
_t(p, "\n90-DEGREE ROTATION (ESC V)\n")
_divider(p, "-")
p._raw(b'\x1b\x56\x01')
_t(p, "ROTATED TEXT\n")
p._raw(b'\x1b\x56\x00')
_t(p, "Normal again\n")
_divider(p)
# ── CP737 useful symbols at normal size ──
_t(p, "\nUSEFUL CP737 SYMBOLS\n")
_divider(p, "-")
symbols = [
(0xFB, "tick / checkmark"),
(0xFE, "filled square"),
(0xF9, "middle dot"),
(0xFA, "small bullet"),
(0xF8, "degree"),
(0xDB, "full block"),
(0xDC, "lower half block"),
(0xDF, "upper half block"),
(0xB0, "light shade"),
(0xB1, "medium shade"),
(0xB2, "dark shade"),
(0xC4, "thin horiz line"),
(0xCD, "double horiz line"),
(0xBA, "vertical bar"),
(0xC9, "top-left corner dbl"),
(0xBB, "top-right corner dbl"),
(0xC8, "bot-left corner dbl"),
(0xBC, "bot-right corner dbl"),
]
for code, desc in symbols:
p._raw(bytes([code, 0x20, code, 0x20, code, 0x20]))
_t(p, f" {desc}\n")
_divider(p)
p._raw(b'\n\n\n')
p.cut()
p.close()
# ── Main ───────────────────────────────────────────────────────────────────────
def main():
print(f"Connecting to {PRINTER_IP}:{PRINTER_PORT}")
print("Printing 6 pages...\n")
page_esc_bang(font_b=False, english=True)
print("Page 1 done — ESC! modes, Font A, English")
page_esc_bang(font_b=True, english=True)
print("Page 2 done — ESC! modes, Font B, English")
page_esc_bang(font_b=False, english=False)
print("Page 3 done — ESC! modes, Font A, Greek")
page_esc_bang(font_b=True, english=False)
print("Page 4 done — ESC! modes, Font B, Greek")
page_gs_sizes()
print("Page 5 done — GS! size multipliers")
page_beep_misc()
print("Page 6 done — Beep tests + misc")
print("\nAll done.")
if __name__ == "__main__":
main()