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>
393 lines
23 KiB
Python
393 lines
23 KiB
Python
import os
|
||
from contextlib import asynccontextmanager
|
||
from fastapi import FastAPI
|
||
from fastapi.middleware.cors import CORSMiddleware
|
||
from fastapi.staticfiles import StaticFiles
|
||
|
||
from database import engine, Base
|
||
from middleware.license_check import LicenseCheckMiddleware
|
||
from services.cloud_sync import start_cloud_sync
|
||
|
||
# Import all models so SQLAlchemy can create their tables
|
||
import models.user # noqa: F401 — also registers WaiterZone
|
||
import models.table # noqa: F401
|
||
import models.printer # noqa: F401
|
||
import models.product # noqa: F401
|
||
import models.order # noqa: F401 — also registers OrderAuditLog, OrderDiscount
|
||
import models.business_day # noqa: F401
|
||
import models.shift # noqa: F401 — registers WaiterShift, ShiftBreak
|
||
import models.settings # noqa: F401
|
||
import models.flag # noqa: F401 — registers TableFlagDef, TableFlagAssignment
|
||
import models.message # noqa: F401 — registers StaffMessage, StaffMessageAck, QuickMessageTemplate
|
||
import models.reservation # noqa: F401
|
||
import models.notes # noqa: F401 — registers SiteNote, SiteTodo
|
||
import models.expenses # noqa: F401 — registers Contact, Expense, ExpensePayment
|
||
|
||
from routers import auth, tables, products, orders, waiters, reports, system, setup as setup_router
|
||
from routers import business_day as business_day_router
|
||
from routers import shifts as shifts_router
|
||
from routers import settings as settings_router
|
||
from routers import flags as flags_router
|
||
from routers import messages as messages_router
|
||
from routers import sse as sse_router
|
||
from routers import data_transfer as data_transfer_router
|
||
from routers import connect_orders as connect_orders_router
|
||
from routers import reservations as reservations_router
|
||
from routers import notes as notes_router
|
||
from routers.expenses import contacts_router, expenses_router
|
||
|
||
|
||
def _run_migrations():
|
||
"""Apply additive schema changes that create_all won't handle.
|
||
Each migration gets its own connection so a no-op (column already exists)
|
||
doesn't leave a dirty transaction that blocks subsequent migrations."""
|
||
from sqlalchemy import text
|
||
|
||
migrations = [
|
||
"ALTER TABLE product_ingredients ADD COLUMN extra_cost REAL NOT NULL DEFAULT 0.0",
|
||
"ALTER TABLE products ADD COLUMN image_url VARCHAR",
|
||
"ALTER TABLE tables ADD COLUMN group_id INTEGER REFERENCES table_groups(id)",
|
||
"ALTER TABLE table_groups ADD COLUMN prefix VARCHAR",
|
||
"ALTER TABLE table_groups ADD COLUMN color VARCHAR",
|
||
"ALTER TABLE products ADD COLUMN sort_order INTEGER NOT NULL DEFAULT 0",
|
||
"ALTER TABLE product_preference_sets ADD COLUMN default_choice_id INTEGER",
|
||
"ALTER TABLE product_preference_choices ADD COLUMN sub_choices TEXT",
|
||
"ALTER TABLE product_preference_choices ADD COLUMN disables_subset INTEGER NOT NULL DEFAULT 0",
|
||
"ALTER TABLE product_preference_sets ADD COLUMN shared_subset TEXT",
|
||
"ALTER TABLE product_options ADD COLUMN sub_choices TEXT",
|
||
# Zone-based access control
|
||
"""CREATE TABLE IF NOT EXISTS waiter_zones (
|
||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||
waiter_id INTEGER NOT NULL REFERENCES users(id),
|
||
group_id INTEGER REFERENCES table_groups(id),
|
||
assigned_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
||
)""",
|
||
# Payment tracking on items
|
||
"ALTER TABLE order_items ADD COLUMN paid_by INTEGER REFERENCES users(id)",
|
||
"ALTER TABLE order_items ADD COLUMN paid_at DATETIME",
|
||
"ALTER TABLE order_items ADD COLUMN payment_method VARCHAR",
|
||
# Full audit log
|
||
"""CREATE TABLE IF NOT EXISTS order_audit_log (
|
||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||
order_id INTEGER NOT NULL REFERENCES orders(id),
|
||
event_type VARCHAR NOT NULL,
|
||
waiter_id INTEGER REFERENCES users(id),
|
||
item_ids TEXT,
|
||
amount REAL,
|
||
payment_method VARCHAR,
|
||
note TEXT,
|
||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
||
)""",
|
||
# Waiter profile fields
|
||
"ALTER TABLE users ADD COLUMN full_name VARCHAR",
|
||
"ALTER TABLE users ADD COLUMN nickname VARCHAR",
|
||
"ALTER TABLE users ADD COLUMN mobile_phone VARCHAR",
|
||
"ALTER TABLE users ADD COLUMN avatar_url VARCHAR",
|
||
# Quick options (flat, allow_multiple)
|
||
"""CREATE TABLE IF NOT EXISTS product_quick_options (
|
||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||
product_id INTEGER NOT NULL REFERENCES products(id),
|
||
name VARCHAR NOT NULL,
|
||
price REAL NOT NULL DEFAULT 0.0,
|
||
allow_multiple INTEGER NOT NULL DEFAULT 0,
|
||
sort_order INTEGER NOT NULL DEFAULT 0
|
||
)""",
|
||
# allow_multiple flag on extras (product_options)
|
||
"ALTER TABLE product_options ADD COLUMN allow_multiple INTEGER NOT NULL DEFAULT 0",
|
||
# Discounts table (future-proofed, schema ready now)
|
||
"""CREATE TABLE IF NOT EXISTS order_discounts (
|
||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||
order_id INTEGER NOT NULL REFERENCES orders(id),
|
||
item_id INTEGER REFERENCES order_items(id),
|
||
discount_type VARCHAR NOT NULL,
|
||
discount_value REAL NOT NULL,
|
||
applied_by INTEGER NOT NULL REFERENCES users(id),
|
||
applied_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||
reason TEXT
|
||
)""",
|
||
# Business day scoping on orders
|
||
"ALTER TABLE orders ADD COLUMN business_day_id INTEGER REFERENCES business_days(id)",
|
||
# Shift attribution on paid items
|
||
"ALTER TABLE order_items ADD COLUMN paid_in_shift_id INTEGER REFERENCES waiter_shifts(id)",
|
||
# Seed default POS settings (INSERT OR IGNORE = no-op if already exists)
|
||
"INSERT OR IGNORE INTO pos_settings (key, value, updated_at) VALUES ('shifts.waiter_self_start', 'true', CURRENT_TIMESTAMP)",
|
||
"INSERT OR IGNORE INTO pos_settings (key, value, updated_at) VALUES ('shifts.waiter_self_end', 'true', CURRENT_TIMESTAMP)",
|
||
"INSERT OR IGNORE INTO pos_settings (key, value, updated_at) VALUES ('business_day.force_close_allowed', 'true', CURRENT_TIMESTAMP)",
|
||
"INSERT OR IGNORE INTO pos_settings (key, value, updated_at) VALUES ('flags.display_mode', 'both', CURRENT_TIMESTAMP)",
|
||
# Table flags
|
||
"""CREATE TABLE IF NOT EXISTS table_flag_defs (
|
||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||
name VARCHAR NOT NULL,
|
||
emoji VARCHAR,
|
||
color VARCHAR DEFAULT '#6b7280',
|
||
text_color VARCHAR DEFAULT NULL,
|
||
sort_order INTEGER NOT NULL DEFAULT 0,
|
||
is_active INTEGER NOT NULL DEFAULT 1,
|
||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
||
)""",
|
||
# Migration: add text_color if upgrading from older schema
|
||
"ALTER TABLE table_flag_defs ADD COLUMN text_color VARCHAR DEFAULT NULL",
|
||
"""CREATE TABLE IF NOT EXISTS table_flag_assignments (
|
||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||
table_id INTEGER NOT NULL REFERENCES tables(id),
|
||
flag_id INTEGER NOT NULL REFERENCES table_flag_defs(id),
|
||
assigned_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||
assigned_by INTEGER REFERENCES users(id)
|
||
)""",
|
||
# Staff messaging
|
||
"""CREATE TABLE IF NOT EXISTS quick_message_templates (
|
||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||
body VARCHAR NOT NULL,
|
||
sort_order INTEGER NOT NULL DEFAULT 0,
|
||
is_active INTEGER NOT NULL DEFAULT 1,
|
||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
||
)""",
|
||
"""CREATE TABLE IF NOT EXISTS staff_messages (
|
||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||
sender_id INTEGER NOT NULL REFERENCES users(id),
|
||
body TEXT NOT NULL,
|
||
target_waiter_ids TEXT NOT NULL DEFAULT '[]',
|
||
table_ids TEXT NOT NULL DEFAULT '[]',
|
||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
||
)""",
|
||
"""CREATE TABLE IF NOT EXISTS staff_message_acks (
|
||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||
message_id INTEGER NOT NULL REFERENCES staff_messages(id),
|
||
waiter_id INTEGER NOT NULL REFERENCES users(id),
|
||
acked_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
||
)""",
|
||
# Seed default flag definitions
|
||
"INSERT OR IGNORE INTO table_flag_defs (id, name, emoji, color, sort_order) VALUES (1, 'Χρειάζεται καθάρισμα', '🧹', '#ef4444', 1)",
|
||
"INSERT OR IGNORE INTO table_flag_defs (id, name, emoji, color, sort_order) VALUES (2, 'Χρειάζεται Βοήθεια', '🆘', '#f97316', 2)",
|
||
"INSERT OR IGNORE INTO table_flag_defs (id, name, emoji, color, sort_order) VALUES (3, 'Χρειάζεται Σερβιτόρο', '🔔', '#eab308', 3)",
|
||
"INSERT OR IGNORE INTO table_flag_defs (id, name, emoji, color, sort_order) VALUES (4, 'Περιμένει να πληρώσει', '💳', '#3b82f6', 4)",
|
||
"INSERT OR IGNORE INTO table_flag_defs (id, name, emoji, color, sort_order) VALUES (5, 'VIP', '⭐', '#8b5cf6', 5)",
|
||
"INSERT OR IGNORE INTO table_flag_defs (id, name, emoji, color, sort_order) VALUES (6, 'Ευγενικός Πελάτης', '😊', '#22c55e', 6)",
|
||
"INSERT OR IGNORE INTO table_flag_defs (id, name, emoji, color, sort_order) VALUES (7, 'Αγενής Πελάτης', '😤', '#dc2626', 7)",
|
||
"INSERT OR IGNORE INTO table_flag_defs (id, name, emoji, color, sort_order) VALUES (8, 'Αλλεργίες', '⚠️', '#f59e0b', 8)",
|
||
"INSERT OR IGNORE INTO table_flag_defs (id, name, emoji, color, sort_order) VALUES (9, 'Παιδιά στο τραπέζι', '👶', '#06b6d4', 9)",
|
||
"INSERT OR IGNORE INTO table_flag_defs (id, name, emoji, color, sort_order) VALUES (10, 'Επέτειος / Γενέθλια', '🎂', '#ec4899', 10)",
|
||
# Seed default quick message templates
|
||
"INSERT OR IGNORE INTO quick_message_templates (id, body, sort_order) VALUES (1, 'Σε χρειάζομαι τώρα', 1)",
|
||
"INSERT OR IGNORE INTO quick_message_templates (id, body, sort_order) VALUES (2, 'Πάρε διάλειμμα', 2)",
|
||
"INSERT OR IGNORE INTO quick_message_templates (id, body, sort_order) VALUES (3, 'Ετοιμάσου για κλείσιμο', 3)",
|
||
"INSERT OR IGNORE INTO quick_message_templates (id, body, sort_order) VALUES (4, 'Ήρθε νέος πελάτης', 4)",
|
||
"INSERT OR IGNORE INTO quick_message_templates (id, body, sort_order) VALUES (5, 'Ο πελάτης περιμένει να πληρώσει', 5)",
|
||
# Product lifecycle status (active / archived)
|
||
"ALTER TABLE products ADD COLUMN lifecycle_status VARCHAR NOT NULL DEFAULT 'active'",
|
||
# Favorite flags + ordering on all product sub-item types
|
||
"ALTER TABLE product_quick_options ADD COLUMN is_favorite INTEGER NOT NULL DEFAULT 0",
|
||
"ALTER TABLE product_quick_options ADD COLUMN favorite_sort_order INTEGER NOT NULL DEFAULT 0",
|
||
"ALTER TABLE product_options ADD COLUMN is_favorite INTEGER NOT NULL DEFAULT 0",
|
||
"ALTER TABLE product_options ADD COLUMN favorite_sort_order INTEGER NOT NULL DEFAULT 0",
|
||
"ALTER TABLE product_ingredients ADD COLUMN is_favorite INTEGER NOT NULL DEFAULT 0",
|
||
"ALTER TABLE product_ingredients ADD COLUMN favorite_sort_order INTEGER NOT NULL DEFAULT 0",
|
||
"ALTER TABLE product_preference_sets ADD COLUMN is_favorite INTEGER NOT NULL DEFAULT 0",
|
||
"ALTER TABLE product_preference_sets ADD COLUMN favorite_sort_order INTEGER NOT NULL DEFAULT 0",
|
||
# Sub-category support
|
||
"ALTER TABLE categories ADD COLUMN parent_id INTEGER REFERENCES categories(id)",
|
||
"ALTER TABLE categories ADD COLUMN general_sort_order INTEGER NOT NULL DEFAULT 0",
|
||
# Auto-expand flag for sub-categories on the PWA accordion
|
||
"ALTER TABLE categories ADD COLUMN auto_expanded INTEGER NOT NULL DEFAULT 0",
|
||
# Printer protocol field
|
||
"ALTER TABLE printers ADD COLUMN protocol VARCHAR NOT NULL DEFAULT 'escpos_tcp'",
|
||
# Compact (half-width) display flag for quick options
|
||
"ALTER TABLE product_quick_options ADD COLUMN is_compact INTEGER NOT NULL DEFAULT 0",
|
||
# Print layout + per-type font settings
|
||
"INSERT OR IGNORE INTO pos_settings (key, value, updated_at) VALUES ('print.ticket_mode', 'detailed', CURRENT_TIMESTAMP)",
|
||
"INSERT OR IGNORE INTO pos_settings (key, value, updated_at) VALUES ('print.font_order_number', '48:1:0', CURRENT_TIMESTAMP)",
|
||
"INSERT OR IGNORE INTO pos_settings (key, value, updated_at) VALUES ('print.font_meta', '0:0:0', CURRENT_TIMESTAMP)",
|
||
"INSERT OR IGNORE INTO pos_settings (key, value, updated_at) VALUES ('print.font_item_name', '16:1:0', CURRENT_TIMESTAMP)",
|
||
"INSERT OR IGNORE INTO pos_settings (key, value, updated_at) VALUES ('print.font_quick', '0:0:0', CURRENT_TIMESTAMP)",
|
||
"INSERT OR IGNORE INTO pos_settings (key, value, updated_at) VALUES ('print.font_pref', '0:0:0', CURRENT_TIMESTAMP)",
|
||
"INSERT OR IGNORE INTO pos_settings (key, value, updated_at) VALUES ('print.font_extra', '0:0:0', CURRENT_TIMESTAMP)",
|
||
"INSERT OR IGNORE INTO pos_settings (key, value, updated_at) VALUES ('print.font_ingredient', '0:0:0', CURRENT_TIMESTAMP)",
|
||
"INSERT OR IGNORE INTO pos_settings (key, value, updated_at) VALUES ('print.font_item_note', '0:0:0', CURRENT_TIMESTAMP)",
|
||
"INSERT OR IGNORE INTO pos_settings (key, value, updated_at) VALUES ('print.font_order_note', '0:1:0', CURRENT_TIMESTAMP)",
|
||
# Offline/emergency payment tracking
|
||
"ALTER TABLE order_audit_log ADD COLUMN offline_uuid VARCHAR",
|
||
"ALTER TABLE order_audit_log ADD COLUMN offline_at VARCHAR",
|
||
"ALTER TABLE order_audit_log ADD COLUMN is_duplicate INTEGER NOT NULL DEFAULT 0",
|
||
# Cancellation tracking on order items (for reports)
|
||
"ALTER TABLE order_items ADD COLUMN cancelled_by INTEGER REFERENCES users(id)",
|
||
"ALTER TABLE order_items ADD COLUMN cancel_reason TEXT",
|
||
"ALTER TABLE order_items ADD COLUMN cancelled_at DATETIME",
|
||
# Manager account fields (added for setup wizard / future password login)
|
||
"ALTER TABLE users ADD COLUMN password_hash VARCHAR",
|
||
"ALTER TABLE users ADD COLUMN email VARCHAR",
|
||
# Venue identity settings
|
||
"INSERT OR IGNORE INTO pos_settings (key, value, updated_at) VALUES ('venue.name', '', CURRENT_TIMESTAMP)",
|
||
"INSERT OR IGNORE INTO pos_settings (key, value, updated_at) VALUES ('venue.type', '', CURRENT_TIMESTAMP)",
|
||
# Security settings
|
||
"INSERT OR IGNORE INTO pos_settings (key, value, updated_at) VALUES ('security.login_method', 'password', CURRENT_TIMESTAMP)",
|
||
"INSERT OR IGNORE INTO pos_settings (key, value, updated_at) VALUES ('security.autofill_username', 'true', CURRENT_TIMESTAMP)",
|
||
"INSERT OR IGNORE INTO pos_settings (key, value, updated_at) VALUES ('security.auto_lock', 'false', CURRENT_TIMESTAMP)",
|
||
"INSERT OR IGNORE INTO pos_settings (key, value, updated_at) VALUES ('security.auto_lock_seconds', '300', CURRENT_TIMESTAMP)",
|
||
"INSERT OR IGNORE INTO pos_settings (key, value, updated_at) VALUES ('security.auto_logout', 'false', CURRENT_TIMESTAMP)",
|
||
"INSERT OR IGNORE INTO pos_settings (key, value, updated_at) VALUES ('security.auto_logout_seconds', '1800', CURRENT_TIMESTAMP)",
|
||
# Xenia Connect — online order fields on orders table
|
||
"ALTER TABLE orders ADD COLUMN source VARCHAR NOT NULL DEFAULT 'pos'",
|
||
"ALTER TABLE orders ADD COLUMN online_order_ref VARCHAR",
|
||
"ALTER TABLE orders ADD COLUMN online_order_cloud_id INTEGER",
|
||
"ALTER TABLE orders ADD COLUMN online_status VARCHAR",
|
||
"ALTER TABLE orders ADD COLUMN online_customer_name VARCHAR",
|
||
"ALTER TABLE orders ADD COLUMN online_customer_phone VARCHAR",
|
||
"ALTER TABLE orders ADD COLUMN online_customer_address TEXT",
|
||
"ALTER TABLE orders ADD COLUMN online_customer_notes TEXT",
|
||
"ALTER TABLE orders ADD COLUMN online_order_type VARCHAR",
|
||
# Xenia Connect — digital menu fields on products
|
||
"ALTER TABLE products ADD COLUMN digital_visible INTEGER NOT NULL DEFAULT 1",
|
||
"ALTER TABLE products ADD COLUMN digital_available INTEGER NOT NULL DEFAULT 1",
|
||
"ALTER TABLE products ADD COLUMN digital_name VARCHAR",
|
||
"ALTER TABLE products ADD COLUMN digital_description VARCHAR",
|
||
"ALTER TABLE products ADD COLUMN digital_price REAL",
|
||
"ALTER TABLE products ADD COLUMN digital_discount REAL NOT NULL DEFAULT 0.0",
|
||
"ALTER TABLE products ADD COLUMN digital_image_url VARCHAR",
|
||
# Per-printer line width (chars per line) — default 48 for 80mm Jolimark
|
||
"ALTER TABLE printers ADD COLUMN line_width INTEGER NOT NULL DEFAULT 48",
|
||
# Product description (plain-text, for menu display)
|
||
"ALTER TABLE products ADD COLUMN description TEXT",
|
||
# Staff note (internal memo on a waiter's profile)
|
||
"ALTER TABLE users ADD COLUMN note VARCHAR",
|
||
# Reservations table
|
||
"""CREATE TABLE IF NOT EXISTS reservations (
|
||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||
guest_name VARCHAR NOT NULL,
|
||
party_size INTEGER NOT NULL,
|
||
phone VARCHAR,
|
||
email VARCHAR,
|
||
note TEXT,
|
||
reserved_for DATETIME NOT NULL,
|
||
table_id INTEGER REFERENCES tables(id),
|
||
status VARCHAR NOT NULL DEFAULT 'pending',
|
||
source VARCHAR NOT NULL DEFAULT 'manager_app',
|
||
created_by_user_id INTEGER REFERENCES users(id),
|
||
online_customer_id VARCHAR,
|
||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
||
)""",
|
||
# Phase 2A — product cost tracking
|
||
"ALTER TABLE products ADD COLUMN cost_simple REAL",
|
||
"ALTER TABLE products ADD COLUMN cost_breakdown TEXT",
|
||
"ALTER TABLE order_items ADD COLUMN unit_cost REAL",
|
||
# Phase 2B — staff payroll
|
||
"ALTER TABLE users ADD COLUMN hourly_rate REAL",
|
||
"ALTER TABLE waiter_shifts ADD COLUMN hourly_rate_snapshot REAL",
|
||
# Phase 2D — expense tracking + supplier contacts
|
||
"""CREATE TABLE IF NOT EXISTS contacts (
|
||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||
name VARCHAR NOT NULL,
|
||
type VARCHAR NOT NULL DEFAULT 'supplier',
|
||
phone VARCHAR,
|
||
email VARCHAR,
|
||
address TEXT,
|
||
notes TEXT,
|
||
is_active INTEGER NOT NULL DEFAULT 1,
|
||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
||
)""",
|
||
"""CREATE TABLE IF NOT EXISTS expenses (
|
||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||
description VARCHAR NOT NULL,
|
||
category VARCHAR NOT NULL,
|
||
contact_id INTEGER REFERENCES contacts(id),
|
||
total_amount REAL NOT NULL,
|
||
paid_amount REAL NOT NULL DEFAULT 0.0,
|
||
due_date DATETIME,
|
||
business_day_id INTEGER REFERENCES business_days(id),
|
||
created_by_id INTEGER NOT NULL REFERENCES users(id),
|
||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||
notes TEXT
|
||
)""",
|
||
"""CREATE TABLE IF NOT EXISTS expense_payments (
|
||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||
expense_id INTEGER NOT NULL REFERENCES expenses(id),
|
||
amount REAL NOT NULL,
|
||
paid_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||
paid_by_id INTEGER NOT NULL REFERENCES users(id),
|
||
notes TEXT
|
||
)""",
|
||
# Phase 2C — notes & todos
|
||
"""CREATE TABLE IF NOT EXISTS site_notes (
|
||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||
body TEXT NOT NULL,
|
||
created_by_id INTEGER NOT NULL REFERENCES users(id),
|
||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||
is_pinned INTEGER NOT NULL DEFAULT 0
|
||
)""",
|
||
"""CREATE TABLE IF NOT EXISTS site_todos (
|
||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||
body TEXT NOT NULL,
|
||
is_done INTEGER NOT NULL DEFAULT 0,
|
||
done_at DATETIME,
|
||
done_by_id INTEGER REFERENCES users(id),
|
||
created_by_id INTEGER NOT NULL REFERENCES users(id),
|
||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||
priority VARCHAR NOT NULL DEFAULT 'normal'
|
||
)""",
|
||
]
|
||
for sql in migrations:
|
||
try:
|
||
with engine.connect() as conn:
|
||
conn.execute(text(sql))
|
||
conn.commit()
|
||
except Exception:
|
||
pass
|
||
|
||
|
||
@asynccontextmanager
|
||
async def lifespan(app: FastAPI):
|
||
import asyncio
|
||
from services.sse_bus import init_loop
|
||
from services.reservation_tasks import start_reservation_tasks
|
||
init_loop(asyncio.get_running_loop())
|
||
Base.metadata.create_all(bind=engine)
|
||
_run_migrations()
|
||
sync_task = await start_cloud_sync()
|
||
reservation_task = await start_reservation_tasks()
|
||
yield
|
||
sync_task.cancel()
|
||
reservation_task.cancel()
|
||
|
||
|
||
app = FastAPI(title="POS Local Backend", lifespan=lifespan)
|
||
|
||
app.add_middleware(
|
||
CORSMiddleware,
|
||
allow_origins=["*"],
|
||
allow_methods=["*"],
|
||
allow_headers=["*"],
|
||
)
|
||
app.add_middleware(LicenseCheckMiddleware)
|
||
|
||
# Serve product images as static files
|
||
IMAGE_DIR = "/app/data/product_images"
|
||
os.makedirs(IMAGE_DIR, exist_ok=True)
|
||
app.mount("/static/product_images", StaticFiles(directory=IMAGE_DIR), name="product_images")
|
||
|
||
# Serve waiter avatars as static files
|
||
AVATAR_DIR = "/app/data/avatars"
|
||
os.makedirs(AVATAR_DIR, exist_ok=True)
|
||
app.mount("/static/avatars", StaticFiles(directory=AVATAR_DIR), name="avatars")
|
||
|
||
app.include_router(setup_router.router, prefix="/api/setup", tags=["setup"])
|
||
app.include_router(auth.router, prefix="/api/auth", tags=["auth"])
|
||
app.include_router(tables.router, prefix="/api/tables", tags=["tables"])
|
||
app.include_router(products.router, prefix="/api/products", tags=["products"])
|
||
app.include_router(orders.router, prefix="/api/orders", tags=["orders"])
|
||
app.include_router(waiters.router, prefix="/api/waiters", tags=["waiters"])
|
||
app.include_router(reports.router, prefix="/api/reports", tags=["reports"])
|
||
app.include_router(system.router, prefix="/api/system", tags=["system"])
|
||
app.include_router(business_day_router.router, prefix="/api/business-day", tags=["business-day"])
|
||
app.include_router(shifts_router.router, prefix="/api/shifts", tags=["shifts"])
|
||
app.include_router(settings_router.router, prefix="/api/settings", tags=["settings"])
|
||
app.include_router(flags_router.router, prefix="/api/flags", tags=["flags"])
|
||
app.include_router(messages_router.router, prefix="/api/messages", tags=["messages"])
|
||
app.include_router(sse_router.router, prefix="/api/sse", tags=["sse"])
|
||
app.include_router(data_transfer_router.router, prefix="/api/data-transfer", tags=["data-transfer"])
|
||
app.include_router(connect_orders_router.router, prefix="/api/connect", tags=["connect"])
|
||
app.include_router(reservations_router.router, prefix="/api/reservations", tags=["reservations"])
|
||
app.include_router(notes_router.router, prefix="/api/notes", tags=["notes"])
|
||
app.include_router(contacts_router, prefix="/api/contacts", tags=["contacts"])
|
||
app.include_router(expenses_router, prefix="/api/expenses", tags=["expenses"])
|