Files
reservierungssystem/app/database.py
T

184 lines
7.3 KiB
Python

#!/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))