#!/usr/bin/env python3 """Datenbank-Modelle für Reservierungssystem""" import sqlite3 import json from datetime import datetime, timedelta from contextlib import contextmanager DB_PATH = "/data/reservations.db" @contextmanager def get_db(): conn = sqlite3.connect(DB_PATH) conn.row_factory = sqlite3.Row try: yield conn conn.commit() except: conn.rollback() raise finally: conn.close() def init_db(): """Datenbank initialisieren""" with get_db() as db: db.executescript(''' -- Gäste-Adressbuch CREATE TABLE IF NOT EXISTS guests ( id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL, phone TEXT, email TEXT UNIQUE, preferred_table_id INTEGER, notes TEXT, visit_count INTEGER DEFAULT 0, last_visit DATE, created_at DATETIME DEFAULT CURRENT_TIMESTAMP ); -- Räume CREATE TABLE IF NOT EXISTS rooms ( id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL, capacity INTEGER, color TEXT DEFAULT '#3498db' ); -- Bereiche innerhalb von Räumen CREATE TABLE IF NOT EXISTS areas ( id INTEGER PRIMARY KEY AUTOINCREMENT, room_id INTEGER NOT NULL, name TEXT NOT NULL, available_from TIME DEFAULT '10:00', available_to TIME DEFAULT '23:00', FOREIGN KEY (room_id) REFERENCES rooms(id) ON DELETE CASCADE ); -- Tische CREATE TABLE IF NOT EXISTS tables ( id INTEGER PRIMARY KEY AUTOINCREMENT, area_id INTEGER NOT NULL, name TEXT NOT NULL, x INTEGER DEFAULT 0, y INTEGER DEFAULT 0, width INTEGER DEFAULT 100, height INTEGER DEFAULT 100, shape TEXT DEFAULT 'rect' CHECK(shape IN ('rect', 'circle', 'oval')), seats INTEGER DEFAULT 4, is_combinable BOOLEAN DEFAULT 1, is_active BOOLEAN DEFAULT 1, FOREIGN KEY (area_id) REFERENCES areas(id) ON DELETE CASCADE ); -- Tisch-Kombinationen (Zusammenlegungen) CREATE TABLE IF NOT EXISTS table_combinations ( id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL, parent_table_id INTEGER NOT NULL, child_table_ids TEXT NOT NULL, -- JSON-Array total_seats INTEGER, is_active BOOLEAN DEFAULT 1, FOREIGN KEY (parent_table_id) REFERENCES tables(id) ON DELETE CASCADE ); -- Reservierungen CREATE TABLE IF NOT EXISTS reservations ( id INTEGER PRIMARY KEY AUTOINCREMENT, booking_number TEXT UNIQUE NOT NULL, -- RES-20250512-001 guest_id INTEGER, table_ids TEXT NOT NULL, -- JSON-Array date DATE NOT NULL, time_from TIME NOT NULL, time_to TIME, guests INTEGER NOT NULL, occasion TEXT, notes TEXT, source TEXT DEFAULT 'email' CHECK(source IN ('email', 'phone', 'web', 'walk-in')), phone_caller_name TEXT, -- Für Telefon-Buchungen status TEXT DEFAULT 'confirmed' CHECK(status IN ('confirmed', 'pending', 'cancelled', 'completed')), email_thread_id TEXT, created_by TEXT, created_at DATETIME DEFAULT CURRENT_TIMESTAMP, updated_at DATETIME DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (guest_id) REFERENCES guests(id) ON DELETE SET NULL ); -- Änderungshistorie CREATE TABLE IF NOT EXISTS reservation_history ( id INTEGER PRIMARY KEY AUTOINCREMENT, reservation_id INTEGER NOT NULL, booking_number TEXT NOT NULL, changed_field TEXT NOT NULL, old_value TEXT, new_value TEXT, changed_by TEXT, changed_at DATETIME DEFAULT CURRENT_TIMESTAMP, reason TEXT, FOREIGN KEY (reservation_id) REFERENCES reservations(id) ON DELETE CASCADE ); -- E-Mails CREATE TABLE IF NOT EXISTS emails ( id INTEGER PRIMARY KEY AUTOINCREMENT, message_id TEXT UNIQUE NOT NULL, thread_id TEXT, subject TEXT, body TEXT, sender TEXT, sender_email TEXT, parsed_json TEXT, -- JSON mit extrahierten Daten confidence REAL, action_type TEXT CHECK(action_type IN ('new', 'modification', 'cancellation', 'unknown')), status TEXT DEFAULT 'new' CHECK(status IN ('new', 'auto_processed', 'needs_review', 'confirmed', 'failed')), linked_reservation_id INTEGER, booking_number_found TEXT, -- Extrahierte Buchungsnummer auto_reply_sent BOOLEAN DEFAULT 0, auto_reply_status TEXT, received_at DATETIME, processed_at DATETIME, FOREIGN KEY (linked_reservation_id) REFERENCES reservations(id) ON DELETE SET NULL ); -- Indizes CREATE INDEX IF NOT EXISTS idx_reservations_date ON reservations(date); CREATE INDEX IF NOT EXISTS idx_reservations_booking ON reservations(booking_number); CREATE INDEX IF NOT EXISTS idx_reservations_guest ON reservations(guest_id); CREATE INDEX IF NOT EXISTS idx_reservations_status ON reservations(status); CREATE INDEX IF NOT EXISTS idx_emails_thread ON emails(thread_id); CREATE INDEX IF NOT EXISTS idx_emails_status ON emails(status); CREATE INDEX IF NOT EXISTS idx_emails_booking ON emails(booking_number_found); CREATE INDEX IF NOT EXISTS idx_history_booking ON reservation_history(booking_number); -- Trigger für updated_at CREATE TRIGGER IF NOT EXISTS update_reservations_timestamp AFTER UPDATE ON reservations BEGIN UPDATE reservations SET updated_at = CURRENT_TIMESTAMP WHERE id = NEW.id; END; ''') def generate_booking_number(date=None): """Generiere Buchungsnummer RES-YYYY-MM-DD-XXX""" if date is None: date = datetime.now() date_str = date.strftime('%Y-%m-%d') with get_db() as db: # Zähle bestehende Reservierungen an diesem Tag count = db.execute( "SELECT COUNT(*) FROM reservations WHERE date = ?", (date_str,) ).fetchone()[0] return f"RES-{date_str}-{count + 1:03d}" def log_change(db, reservation_id, booking_number, field, old_val, new_val, reason=None): """Änderung in Historie speichern""" db.execute(''' INSERT INTO reservation_history (reservation_id, booking_number, changed_field, old_value, new_value, reason) VALUES (?, ?, ?, ?, ?, ?) ''', (reservation_id, booking_number, field, str(old_val), str(new_val), reason))