1163 lines
42 KiB
Python
1163 lines
42 KiB
Python
#!/usr/bin/env python3
|
|
"""Reservierungssystem - Flask Backend"""
|
|
|
|
import os
|
|
import json
|
|
from datetime import datetime, timedelta
|
|
from functools import wraps
|
|
|
|
from flask import Flask, request, jsonify, render_template, send_from_directory, g
|
|
from flask_cors import CORS
|
|
|
|
from database import get_db, init_db, generate_booking_number, log_change
|
|
from auth import require_auth, verify_captcha
|
|
from login_routes import auth_bp
|
|
from utils.ollama_client import (
|
|
parse_email_with_ollama,
|
|
generate_confirmation_email,
|
|
suggest_table_for_group
|
|
)
|
|
|
|
app = Flask(__name__,
|
|
template_folder='templates',
|
|
static_folder='static')
|
|
CORS(app)
|
|
|
|
# Security config
|
|
app.secret_key = os.environ.get('SESSION_SECRET', 'dev-secret-change-in-production')
|
|
|
|
# Register auth blueprint
|
|
app.register_blueprint(auth_bp)
|
|
|
|
@app.before_request
|
|
def check_auth():
|
|
# Public endpoints don't require auth
|
|
public_endpoints = ['/', '/api/captcha', '/api/rooms', '/api/availability',
|
|
'/api/health', '/api/admin/login']
|
|
if request.path in public_endpoints or request.path.startswith('/static/'):
|
|
return None
|
|
|
|
# Admin endpoints require login
|
|
if request.path.startswith('/api/admin/') and session.get('user_role') != 'admin':
|
|
return jsonify({"error": "Unauthorized"}), 401
|
|
|
|
# Konfiguration
|
|
DEFAULT_OPEN_HOUR = 10 # 10:00
|
|
DEFAULT_CLOSE_HOUR = 23 # 23:00
|
|
RESERVATION_DURATION = 120 # Minuten
|
|
|
|
def init_app():
|
|
"""App initialisieren"""
|
|
init_db()
|
|
|
|
# Standard-Daten einfügen
|
|
with get_db() as db:
|
|
# Default Raum
|
|
room = db.execute("SELECT id FROM rooms LIMIT 1").fetchone()
|
|
if not room:
|
|
cursor = db.execute(
|
|
"INSERT INTO rooms (name, capacity, color) VALUES (?, ?, ?)",
|
|
("Hauptsaal", 50, "#3498db")
|
|
)
|
|
room_id = cursor.lastrowid
|
|
|
|
# Default Bereich
|
|
cursor = db.execute(
|
|
"INSERT INTO areas (room_id, name) VALUES (?, ?)",
|
|
(room_id, "Fensterbereich")
|
|
)
|
|
area_id = cursor.lastrowid
|
|
|
|
# Beispiel-Tische
|
|
tables = [
|
|
("T1", 2, 50, 50),
|
|
("T2", 4, 200, 50),
|
|
("T3", 4, 350, 50),
|
|
("T4", 6, 500, 50),
|
|
("T5", 8, 50, 200),
|
|
("T6", 2, 200, 200),
|
|
]
|
|
for name, seats, x, y in tables:
|
|
db.execute(
|
|
"""INSERT INTO tables (area_id, name, seats, x, y)
|
|
VALUES (?, ?, ?, ?, ?)""",
|
|
(area_id, name, seats, x, y)
|
|
)
|
|
|
|
db.commit()
|
|
print("✓ Standard-Daten erstellt")
|
|
|
|
# API-Routen
|
|
|
|
@app.route('/')
|
|
def index():
|
|
"""Hauptseite - Dashboard"""
|
|
return render_template('index.html')
|
|
|
|
@app.route('/api/health')
|
|
def health():
|
|
"""Health-Check"""
|
|
return jsonify({"status": "ok", "timestamp": datetime.now().isoformat()})
|
|
|
|
# Dashboard-Daten
|
|
@app.route('/api/dashboard')
|
|
def dashboard():
|
|
"""Dashboard-Daten laden"""
|
|
today = datetime.now().date().isoformat()
|
|
|
|
with get_db() as db:
|
|
# Heutige Reservierungen
|
|
today_count = db.execute(
|
|
"SELECT COUNT(*) FROM reservations WHERE date = ? AND status IN ('confirmed', 'pending')",
|
|
(today,)
|
|
).fetchone()[0]
|
|
|
|
# Unbearbeitete E-Mails
|
|
pending_emails = db.execute(
|
|
"SELECT COUNT(*) FROM emails WHERE status = 'needs_review'"
|
|
).fetchone()[0]
|
|
|
|
# Freie Tische heute
|
|
total_tables = db.execute(
|
|
"SELECT COUNT(*) FROM tables WHERE is_active = 1"
|
|
).fetchone()[0]
|
|
|
|
# Heutige Gäste gesamt
|
|
guests_today = db.execute(
|
|
"SELECT COALESCE(SUM(guests), 0) FROM reservations WHERE date = ? AND status IN ('confirmed', 'pending')",
|
|
(today,)
|
|
).fetchone()[0]
|
|
|
|
# Heutige Reservierungen (Liste)
|
|
today_reservations = db.execute('''
|
|
SELECT r.*, g.name as guest_name, g.phone
|
|
FROM reservations r
|
|
LEFT JOIN guests g ON r.guest_id = g.id
|
|
WHERE r.date = ? AND r.status IN ('confirmed', 'pending')
|
|
ORDER BY r.time_from
|
|
''', (today,)).fetchall()
|
|
|
|
return jsonify({
|
|
"today_count": today_count,
|
|
"pending_emails": pending_emails,
|
|
"total_tables": total_tables,
|
|
"guests_today": guests_today,
|
|
"today_reservations": [dict(r) for r in today_reservations]
|
|
})
|
|
|
|
# Gäste-Verwaltung
|
|
@app.route('/api/guests', methods=['GET', 'POST'])
|
|
def guests():
|
|
"""Gäste auflisten oder neuen Gast erstellen"""
|
|
if request.method == 'GET':
|
|
search = request.args.get('search', '')
|
|
with get_db() as db:
|
|
if search:
|
|
rows = db.execute('''
|
|
SELECT g.*, t.name as preferred_table
|
|
FROM guests g
|
|
LEFT JOIN tables t ON g.preferred_table_id = t.id
|
|
WHERE g.name LIKE ? OR g.phone LIKE ? OR g.email LIKE ?
|
|
ORDER BY g.name
|
|
''', (f'%{search}%', f'%{search}%', f'%{search}%')).fetchall()
|
|
else:
|
|
rows = db.execute('''
|
|
SELECT g.*, t.name as preferred_table
|
|
FROM guests g
|
|
LEFT JOIN tables t ON g.preferred_table_id = t.id
|
|
ORDER BY g.name
|
|
''').fetchall()
|
|
return jsonify([dict(r) for r in rows])
|
|
|
|
# POST: Neuen Gast erstellen
|
|
data = request.get_json()
|
|
with get_db() as db:
|
|
cursor = db.execute('''
|
|
INSERT INTO guests (name, phone, email, notes)
|
|
VALUES (?, ?, ?, ?)
|
|
''', (data.get('name'), data.get('phone'), data.get('email'), data.get('notes')))
|
|
db.commit()
|
|
return jsonify({"id": cursor.lastrowid, "message": "Gast erstellt"})
|
|
|
|
@app.route('/api/guests/<int:guest_id>', methods=['GET', 'PUT', 'DELETE'])
|
|
def guest_detail(guest_id):
|
|
"""Einzelnen Gast verwalten"""
|
|
with get_db() as db:
|
|
if request.method == 'GET':
|
|
# Gast mit Historie
|
|
guest = db.execute('SELECT * FROM guests WHERE id = ?', (guest_id,)).fetchone()
|
|
if not guest:
|
|
return jsonify({"error": "Gast nicht gefunden"}), 404
|
|
|
|
# Letzte Reservierungen
|
|
history = db.execute('''
|
|
SELECT booking_number, date, time_from, guests
|
|
FROM reservations
|
|
WHERE guest_id = ?
|
|
ORDER BY date DESC
|
|
LIMIT 5
|
|
''', (guest_id,)).fetchall()
|
|
|
|
result = dict(guest)
|
|
result['reservation_history'] = [dict(h) for h in history]
|
|
return jsonify(result)
|
|
|
|
elif request.method == 'PUT':
|
|
data = request.get_json()
|
|
db.execute('''
|
|
UPDATE guests
|
|
SET name = ?, phone = ?, email = ?, preferred_table_id = ?, notes = ?
|
|
WHERE id = ?
|
|
''', (data.get('name'), data.get('phone'), data.get('email'),
|
|
data.get('preferred_table_id'), data.get('notes'), guest_id))
|
|
db.commit()
|
|
return jsonify({"message": "Gast aktualisiert"})
|
|
|
|
elif request.method == 'DELETE':
|
|
db.execute('DELETE FROM guests WHERE id = ?', (guest_id,))
|
|
db.commit()
|
|
return jsonify({"message": "Gast gelöscht"})
|
|
|
|
# Tische und Räume
|
|
@app.route('/api/rooms')
|
|
def rooms():
|
|
"""Alle Räume mit Bereichen und Tischen"""
|
|
with get_db() as db:
|
|
rooms = db.execute('SELECT * FROM rooms').fetchall()
|
|
result = []
|
|
|
|
for room in rooms:
|
|
room_dict = dict(room)
|
|
areas = db.execute('SELECT * FROM areas WHERE room_id = ?', (room['id'],)).fetchall()
|
|
room_dict['areas'] = []
|
|
|
|
for area in areas:
|
|
area_dict = dict(area)
|
|
tables = db.execute(
|
|
'SELECT * FROM tables WHERE area_id = ? AND is_active = 1',
|
|
(area['id'],)
|
|
).fetchall()
|
|
area_dict['tables'] = [dict(t) for t in tables]
|
|
room_dict['areas'].append(area_dict)
|
|
|
|
result.append(room_dict)
|
|
|
|
return jsonify(result)
|
|
|
|
# Reservierungen
|
|
@app.route('/api/reservations', methods=['GET', 'POST'])
|
|
def reservations():
|
|
"""Reservierungen auflisten oder neu erstellen"""
|
|
if request.method == 'GET':
|
|
date = request.args.get('date')
|
|
status = request.args.get('status', 'confirmed,pending')
|
|
|
|
with get_db() as db:
|
|
query = '''
|
|
SELECT r.*, g.name as guest_name, g.phone, g.email
|
|
FROM reservations r
|
|
LEFT JOIN guests g ON r.guest_id = g.id
|
|
WHERE r.status IN ({})
|
|
'''.format(','.join(f'"{s}"' for s in status.split(',')))
|
|
|
|
params = []
|
|
if date:
|
|
query += ' AND r.date = ?'
|
|
params.append(date)
|
|
|
|
query += ' ORDER BY r.time_from'
|
|
|
|
rows = db.execute(query, params).fetchall()
|
|
return jsonify([dict(r) for r in rows])
|
|
|
|
# POST: Neue Reservierung
|
|
data = request.get_json()
|
|
|
|
# Fix: time -> time_from/time_to Mapping
|
|
# Frontend sendet 'time', Backend erwartet 'time_from'/'time_to'
|
|
if 'time' in data and 'time_from' not in data:
|
|
data['time_from'] = data['time']
|
|
|
|
# time_to automatisch +2h berechnen wenn nicht angegeben
|
|
if 'time_to' not in data or not data['time_to']:
|
|
from datetime import datetime as dt
|
|
try:
|
|
tf = dt.strptime(data['time_from'], '%H:%M')
|
|
data['time_to'] = (tf + timedelta(minutes=120)).strftime('%H:%M')
|
|
except:
|
|
data['time_to'] = '22:00' # Default
|
|
|
|
# Gast finden oder erstellen
|
|
guest_id = data.get('guest_id')
|
|
if not guest_id and data.get('email'):
|
|
with get_db() as db:
|
|
guest = db.execute('SELECT id FROM guests WHERE email = ?',
|
|
(data['email'],)).fetchone()
|
|
if guest:
|
|
guest_id = guest['id']
|
|
|
|
# Buchungsnummer generieren
|
|
booking_number = generate_booking_number()
|
|
|
|
with get_db() as db:
|
|
cursor = db.execute('''
|
|
INSERT INTO reservations
|
|
(booking_number, guest_id, table_ids, date, time_from, time_to, guests,
|
|
occasion, notes, source, phone_caller_name, created_by)
|
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
''', (
|
|
booking_number,
|
|
guest_id,
|
|
json.dumps(data.get('table_ids', [])),
|
|
data.get('date'),
|
|
data.get('time_from'),
|
|
data.get('time_to'),
|
|
data.get('guests'),
|
|
data.get('occasion'),
|
|
data.get('notes'),
|
|
data.get('source', 'web'),
|
|
data.get('phone_caller_name'),
|
|
data.get('created_by', 'system')
|
|
))
|
|
|
|
# Gast-Zähler aktualisieren
|
|
if guest_id:
|
|
db.execute('''
|
|
UPDATE guests
|
|
SET visit_count = visit_count + 1, last_visit = ?
|
|
WHERE id = ?
|
|
''', (data.get('date'), guest_id))
|
|
|
|
db.commit()
|
|
|
|
return jsonify({
|
|
"id": cursor.lastrowid,
|
|
"booking_number": booking_number,
|
|
"message": "Reservierung erstellt"
|
|
})
|
|
|
|
@app.route('/api/reservations/<int:res_id>', methods=['GET', 'PUT', 'DELETE'])
|
|
def reservation_detail(res_id):
|
|
"""Einzelne Reservierung verwalten"""
|
|
with get_db() as db:
|
|
if request.method == 'GET':
|
|
# Reservierung mit Historie
|
|
res = db.execute('''
|
|
SELECT r.*, g.name as guest_name, g.phone, g.email
|
|
FROM reservations r
|
|
LEFT JOIN guests g ON r.guest_id = g.id
|
|
WHERE r.id = ?
|
|
''', (res_id,)).fetchone()
|
|
|
|
if not res:
|
|
return jsonify({"error": "Reservierung nicht gefunden"}), 404
|
|
|
|
# Änderungshistorie
|
|
history = db.execute('''
|
|
SELECT * FROM reservation_history
|
|
WHERE reservation_id = ?
|
|
ORDER BY changed_at DESC
|
|
''', (res_id,)).fetchall()
|
|
|
|
result = dict(res)
|
|
result['change_history'] = [dict(h) for h in history]
|
|
return jsonify(result)
|
|
|
|
elif request.method == 'PUT':
|
|
data = request.get_json()
|
|
|
|
# Alte Werte für Historie
|
|
old = db.execute('SELECT * FROM reservations WHERE id = ?', (res_id,)).fetchone()
|
|
|
|
# Felder aktualisieren
|
|
update_fields = []
|
|
params = []
|
|
|
|
if 'date' in data:
|
|
update_fields.append('date = ?')
|
|
params.append(data['date'])
|
|
log_change(db, res_id, old['booking_number'], 'date', old['date'], data['date'])
|
|
|
|
if 'time_from' in data:
|
|
update_fields.append('time_from = ?')
|
|
params.append(data['time_from'])
|
|
log_change(db, res_id, old['booking_number'], 'time_from', old['time_from'], data['time_from'])
|
|
|
|
if 'guests' in data:
|
|
update_fields.append('guests = ?')
|
|
params.append(data['guests'])
|
|
log_change(db, res_id, old['booking_number'], 'guests', old['guests'], data['guests'])
|
|
|
|
if 'table_ids' in data:
|
|
update_fields.append('table_ids = ?')
|
|
params.append(json.dumps(data['table_ids']))
|
|
|
|
if 'status' in data:
|
|
update_fields.append('status = ?')
|
|
params.append(data['status'])
|
|
|
|
if update_fields:
|
|
params.append(res_id)
|
|
db.execute(f'''
|
|
UPDATE reservations
|
|
SET {', '.join(update_fields)}
|
|
WHERE id = ?
|
|
''', params)
|
|
db.commit()
|
|
|
|
return jsonify({"message": "Reservierung aktualisiert"})
|
|
|
|
elif request.method == 'DELETE':
|
|
db.execute('UPDATE reservations SET status = "cancelled" WHERE id = ?', (res_id,))
|
|
db.commit()
|
|
return jsonify({"message": "Reservierung storniert"})
|
|
|
|
# E-Mail-Verarbeitung
|
|
@app.route('/api/emails', methods=['GET', 'POST'])
|
|
def emails():
|
|
"""E-Mails verwalten"""
|
|
if request.method == 'GET':
|
|
status = request.args.get('status', 'needs_review')
|
|
with get_db() as db:
|
|
rows = db.execute('''
|
|
SELECT e.*, r.booking_number as linked_booking
|
|
FROM emails e
|
|
LEFT JOIN reservations r ON e.linked_reservation_id = r.id
|
|
WHERE e.status = ?
|
|
ORDER BY e.received_at DESC
|
|
''', (status,)).fetchall()
|
|
return jsonify([dict(r) for r in rows])
|
|
|
|
# POST: Neue E-Mail verarbeiten
|
|
data = request.get_json()
|
|
|
|
# Mit Ollama parsen
|
|
parsed = parse_email_with_ollama(
|
|
data.get('subject', ''),
|
|
data.get('body', ''),
|
|
data.get('sender_name', '')
|
|
)
|
|
|
|
with get_db() as db:
|
|
cursor = db.execute('''
|
|
INSERT INTO emails
|
|
(message_id, subject, body, sender, sender_email, parsed_json,
|
|
confidence, action_type, status, booking_number_found, received_at)
|
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
''', (
|
|
data.get('message_id'),
|
|
data.get('subject'),
|
|
data.get('body'),
|
|
data.get('sender_name'),
|
|
data.get('sender_email'),
|
|
json.dumps(parsed),
|
|
parsed.get('confidence', 0),
|
|
parsed.get('intent', 'unknown'),
|
|
'auto_processed' if not parsed.get('needs_review') else 'needs_review',
|
|
parsed.get('booking_number'),
|
|
datetime.now()
|
|
))
|
|
db.commit()
|
|
|
|
email_id = cursor.lastrowid
|
|
|
|
# Wenn keine Review nötig, automatisch verarbeiten
|
|
if not parsed.get('needs_review') and parsed.get('intent') in ['new', 'modification']:
|
|
process_email_reservation(db, email_id, parsed)
|
|
|
|
return jsonify({
|
|
"email_id": email_id,
|
|
"parsed": parsed,
|
|
"status": "processed" if not parsed.get('needs_review') else "needs_review"
|
|
})
|
|
|
|
def process_email_reservation(db, email_id, parsed_data):
|
|
"""E-Mail automatisch in Reservierung umwandeln"""
|
|
|
|
intent = parsed_data.get('intent')
|
|
booking_number = parsed_data.get('booking_number')
|
|
|
|
if intent == 'modification' and booking_number:
|
|
# Bestehende Reservierung ändern
|
|
existing = db.execute(
|
|
'SELECT id FROM reservations WHERE booking_number = ?',
|
|
(booking_number,)
|
|
).fetchone()
|
|
|
|
if existing:
|
|
# Felder aktualisieren
|
|
updates = []
|
|
params = []
|
|
|
|
if parsed_data.get('date'):
|
|
updates.append('date = ?')
|
|
params.append(parsed_data['date'])
|
|
if parsed_data.get('time'):
|
|
updates.append('time_from = ?')
|
|
params.append(parsed_data['time'])
|
|
if parsed_data.get('guests'):
|
|
updates.append('guests = ?')
|
|
params.append(parsed_data['guests'])
|
|
|
|
if updates:
|
|
params.extend([existing['id']])
|
|
db.execute(f'''
|
|
UPDATE reservations
|
|
SET {', '.join(updates)}
|
|
WHERE id = ?
|
|
''', params)
|
|
|
|
db.execute('''
|
|
UPDATE emails
|
|
SET linked_reservation_id = ?, status = 'confirmed'
|
|
WHERE id = ?
|
|
''', (existing['id'], email_id))
|
|
|
|
elif intent == 'new':
|
|
# Neue Reservierung erstellen
|
|
# Gast finden oder erstellen
|
|
guest_id = None
|
|
if parsed_data.get('email'):
|
|
guest = db.execute(
|
|
'SELECT id FROM guests WHERE email = ?',
|
|
(parsed_data['email'],)
|
|
).fetchone()
|
|
if guest:
|
|
guest_id = guest['id']
|
|
else:
|
|
cursor = db.execute('''
|
|
INSERT INTO guests (name, phone, email)
|
|
VALUES (?, ?, ?)
|
|
''', (parsed_data.get('name'), parsed_data.get('phone'), parsed_data['email']))
|
|
guest_id = cursor.lastrowid
|
|
|
|
# Neue Buchungsnummer
|
|
new_booking = generate_booking_number()
|
|
|
|
cursor = db.execute('''
|
|
INSERT INTO reservations
|
|
(booking_number, guest_id, table_ids, date, time_from, guests,
|
|
occasion, notes, source, status)
|
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
''', (
|
|
new_booking,
|
|
guest_id,
|
|
json.dumps([]), # Tische später zuweisen
|
|
parsed_data.get('date'),
|
|
parsed_data.get('time'),
|
|
parsed_data.get('guests', 2),
|
|
parsed_data.get('occasion'),
|
|
parsed_data.get('notes'),
|
|
'email',
|
|
'confirmed'
|
|
))
|
|
|
|
res_id = cursor.lastrowid
|
|
|
|
db.execute('''
|
|
UPDATE emails
|
|
SET linked_reservation_id = ?, status = 'confirmed'
|
|
WHERE id = ?
|
|
''', (res_id, email_id))
|
|
|
|
# Verfügbarkeit prüfen
|
|
@app.route('/api/availability')
|
|
def check_availability():
|
|
"""Verfügbare Tische für Zeitfenster prüfen"""
|
|
date = request.args.get('date')
|
|
time_from = request.args.get('time_from')
|
|
time_to = request.args.get('time_to')
|
|
guests = int(request.args.get('guests', 2))
|
|
|
|
if not all([date, time_from]):
|
|
return jsonify({"error": "Datum und Uhrzeit erforderlich"}), 400
|
|
|
|
if not time_to:
|
|
# Standard-Dauer 2 Stunden
|
|
from datetime import datetime as dt
|
|
tf = dt.strptime(time_from, '%H:%M')
|
|
time_to = (tf + timedelta(minutes=120)).strftime('%H:%M')
|
|
|
|
with get_db() as db:
|
|
# Alle aktiven Tische laden
|
|
all_tables = db.execute('''
|
|
SELECT t.*, a.name as area_name
|
|
FROM tables t
|
|
JOIN areas a ON t.area_id = a.id
|
|
WHERE t.is_active = 1
|
|
''').fetchall()
|
|
|
|
# Belegte Tische in diesem Zeitraum
|
|
occupied = db.execute('''
|
|
SELECT DISTINCT json_each.value as table_id
|
|
FROM reservations r, json_each(r.table_ids)
|
|
WHERE r.date = ?
|
|
AND r.status IN ('confirmed', 'pending')
|
|
AND NOT (r.time_to <= ? OR r.time_from >= ?)
|
|
''', (date, time_from, time_to)).fetchall()
|
|
|
|
occupied_ids = {row['table_id'] for row in occupied}
|
|
|
|
# Verfügbare Tische filtern
|
|
available = [dict(t) for t in all_tables
|
|
if t['id'] not in occupied_ids and t['seats'] >= guests]
|
|
|
|
# Nach Größe sortieren (beste Passung zuerst)
|
|
available.sort(key=lambda x: (x['seats'] - guests, x['seats']))
|
|
|
|
return jsonify({
|
|
"available_tables": available,
|
|
"total": len(all_tables),
|
|
"occupied": len(occupied_ids)
|
|
})
|
|
|
|
# Statistiken
|
|
@app.route('/api/stats')
|
|
def statistics():
|
|
"""Statistiken für Dashboard"""
|
|
from_date = request.args.get('from', (datetime.now() - timedelta(days=30)).date().isoformat())
|
|
to_date = request.args.get('to', datetime.now().date().isoformat())
|
|
|
|
with get_db() as db:
|
|
# Gesamtreservierungen
|
|
total = db.execute('''
|
|
SELECT COUNT(*) as count, COALESCE(SUM(guests), 0) as guests
|
|
FROM reservations
|
|
WHERE date BETWEEN ? AND ?
|
|
AND status IN ('confirmed', 'completed')
|
|
''', (from_date, to_date)).fetchone()
|
|
|
|
# Durchschnittliche Gruppengröße
|
|
avg_size = db.execute('''
|
|
SELECT AVG(guests) as avg
|
|
FROM reservations
|
|
WHERE date BETWEEN ? AND ?
|
|
AND status IN ('confirmed', 'completed')
|
|
''', (from_date, to_date)).fetchone()
|
|
|
|
# Nach Wochentag
|
|
by_day = db.execute('''
|
|
SELECT strftime('%w', date) as day, COUNT(*) as count
|
|
FROM reservations
|
|
WHERE date BETWEEN ? AND ?
|
|
AND status IN ('confirmed', 'completed')
|
|
GROUP BY day
|
|
''', (from_date, to_date)).fetchall()
|
|
|
|
# Stammgäste (mehr als 2 Besuche)
|
|
regulars = db.execute('''
|
|
SELECT COUNT(*) FROM guests WHERE visit_count >= 2
|
|
''').fetchone()[0]
|
|
|
|
return jsonify({
|
|
"period": {"from": from_date, "to": to_date},
|
|
"total_reservations": total['count'],
|
|
"total_guests": total['guests'],
|
|
"average_group_size": round(avg_size['avg'], 1) if avg_size['avg'] else 0,
|
|
"by_day": {row['day']: row['count'] for row in by_day},
|
|
"regular_guests": regulars
|
|
})
|
|
|
|
if __name__ == '__main__':
|
|
init_app()
|
|
port = int(os.environ.get('PORT', 8080))
|
|
print(f"🍽️ Reservierungssystem starting on port {port}")
|
|
app.run(host='0.0.0.0', port=port, debug=False)# NEUE API ENDPOINTS FÜR ROOM BOOKINGS
|
|
# Diese werden ans Ende der main.py angehängt
|
|
|
|
# =============================================================================
|
|
# ROOM BOOKING APIs (NEU)
|
|
# =============================================================================
|
|
|
|
@app.route('/api/rooms-with-bookings', methods=['GET'])
|
|
def rooms_with_bookings():
|
|
"""Räume mit aktuellen Raumbuchungen laden"""
|
|
date = request.args.get('date', datetime.now().date().isoformat())
|
|
|
|
with get_db() as db:
|
|
rooms = db.execute('SELECT * FROM rooms').fetchall()
|
|
result = []
|
|
|
|
for room in rooms:
|
|
room_dict = dict(room)
|
|
|
|
# Bereiche und Tische laden
|
|
areas = db.execute('SELECT * FROM areas WHERE room_id = ?', (room['id'],)).fetchall()
|
|
room_dict['areas'] = []
|
|
for area in areas:
|
|
area_dict = dict(area)
|
|
tables = db.execute(
|
|
'SELECT * FROM tables WHERE area_id = ? AND is_active = 1',
|
|
(area['id'],)
|
|
).fetchall()
|
|
area_dict['tables'] = [dict(t) for t in tables]
|
|
room_dict['areas'].append(area_dict)
|
|
|
|
# Raumbuchungen für das Datum laden
|
|
bookings = db.execute('''
|
|
SELECT * FROM room_bookings
|
|
WHERE room_id = ? AND date = ? AND status = 'confirmed'
|
|
ORDER BY time_from
|
|
''', (room['id'], date)).fetchall()
|
|
room_dict['bookings'] = [dict(b) for b in bookings]
|
|
|
|
result.append(room_dict)
|
|
|
|
return jsonify(result)
|
|
|
|
@app.route('/api/room-bookings', methods=['GET', 'POST'])
|
|
def room_bookings():
|
|
"""Raumbuchungen auflisten oder erstellen"""
|
|
if request.method == 'GET':
|
|
date = request.args.get('date')
|
|
room_id = request.args.get('room_id')
|
|
|
|
with get_db() as db:
|
|
query = '''
|
|
SELECT rb.*, r.name as room_name, r.capacity as room_capacity
|
|
FROM room_bookings rb
|
|
JOIN rooms r ON rb.room_id = r.id
|
|
WHERE 1=1
|
|
'''
|
|
params = []
|
|
|
|
if date:
|
|
query += ' AND rb.date = ?'
|
|
params.append(date)
|
|
if room_id:
|
|
query += ' AND rb.room_id = ?'
|
|
params.append(room_id)
|
|
|
|
query += ' ORDER BY rb.date DESC, rb.time_from'
|
|
|
|
rows = db.execute(query, params).fetchall()
|
|
return jsonify([dict(r) for r in rows])
|
|
|
|
# POST: Neue Raumbuchung erstellen
|
|
data = request.get_json()
|
|
|
|
# Validierung
|
|
required = ['room_id', 'date', 'time_from', 'time_to', 'guests', 'name']
|
|
for field in required:
|
|
if not data.get(field):
|
|
return jsonify({"error": f"{field} ist erforderlich"}), 400
|
|
|
|
with get_db() as db:
|
|
# Prüfe Verfügbarkeit
|
|
existing = db.execute('''
|
|
SELECT COUNT(*) FROM room_bookings
|
|
WHERE room_id = ? AND date = ? AND status = 'confirmed'
|
|
AND (
|
|
(time_from < ? AND time_to > ?) OR
|
|
(time_from < ? AND time_to > ?) OR
|
|
(time_from >= ? AND time_to <= ?)
|
|
)
|
|
''', (
|
|
data['room_id'], data['date'],
|
|
data['time_to'], data['time_from'],
|
|
data['time_to'], data['time_from'],
|
|
data['time_from'], data['time_to']
|
|
)).fetchone()[0]
|
|
|
|
if existing > 0:
|
|
return jsonify({"error": "Raum ist im gewählten Zeitraum bereits belegt"}), 409
|
|
|
|
# Kapazität prüfen
|
|
room = db.execute('SELECT capacity FROM rooms WHERE id = ?',
|
|
(data['room_id'],)).fetchone()
|
|
if room and data['guests'] > room['capacity']:
|
|
return jsonify({"error": f"Maximale Kapazität: {room['capacity']} Personen"}), 400
|
|
|
|
# Buchung erstellen
|
|
cursor = db.execute('''
|
|
INSERT INTO room_bookings
|
|
(room_id, date, time_from, time_to, guests, name, phone, email,
|
|
event_type, notes, status)
|
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
''', (
|
|
data['room_id'], data['date'], data['time_from'], data['time_to'],
|
|
data['guests'], data['name'], data.get('phone'), data.get('email'),
|
|
data.get('event_type'), data.get('notes'),
|
|
data.get('status', 'confirmed')
|
|
))
|
|
db.commit()
|
|
|
|
return jsonify({
|
|
"id": cursor.lastrowid,
|
|
"message": "Raumbuchung erstellt"
|
|
}), 201
|
|
|
|
@app.route('/api/room-bookings/<int:booking_id>', methods=['GET', 'PUT', 'DELETE'])
|
|
def room_booking_detail(booking_id):
|
|
"""Einzelne Raumbuchung verwalten"""
|
|
with get_db() as db:
|
|
if request.method == 'GET':
|
|
booking = db.execute('''
|
|
SELECT rb.*, r.name as room_name, r.capacity as room_capacity
|
|
FROM room_bookings rb
|
|
JOIN rooms r ON rb.room_id = r.id
|
|
WHERE rb.id = ?
|
|
''', (booking_id,)).fetchone()
|
|
|
|
if not booking:
|
|
return jsonify({"error": "Raumbuchung nicht gefunden"}), 404
|
|
|
|
return jsonify(dict(booking))
|
|
|
|
elif request.method == 'PUT':
|
|
data = request.get_json()
|
|
|
|
update_fields = []
|
|
params = []
|
|
|
|
if 'date' in data:
|
|
update_fields.append('date = ?')
|
|
params.append(data['date'])
|
|
if 'time_from' in data:
|
|
update_fields.append('time_from = ?')
|
|
params.append(data['time_from'])
|
|
if 'time_to' in data:
|
|
update_fields.append('time_to = ?')
|
|
params.append(data['time_to'])
|
|
if 'guests' in data:
|
|
update_fields.append('guests = ?')
|
|
params.append(data['guests'])
|
|
if 'name' in data:
|
|
update_fields.append('name = ?')
|
|
params.append(data['name'])
|
|
if 'phone' in data:
|
|
update_fields.append('phone = ?')
|
|
params.append(data['phone'])
|
|
if 'email' in data:
|
|
update_fields.append('email = ?')
|
|
params.append(data['email'])
|
|
if 'event_type' in data:
|
|
update_fields.append('event_type = ?')
|
|
params.append(data['event_type'])
|
|
if 'notes' in data:
|
|
update_fields.append('notes = ?')
|
|
params.append(data['notes'])
|
|
if 'status' in data:
|
|
update_fields.append('status = ?')
|
|
params.append(data['status'])
|
|
|
|
if update_fields:
|
|
params.append(booking_id)
|
|
db.execute(f'''
|
|
UPDATE room_bookings
|
|
SET {', '.join(update_fields)}
|
|
WHERE id = ?
|
|
''', params)
|
|
db.commit()
|
|
|
|
return jsonify({"message": "Raumbuchung aktualisiert"})
|
|
|
|
elif request.method == 'DELETE':
|
|
db.execute('DELETE FROM room_bookings WHERE id = ?', (booking_id,))
|
|
db.commit()
|
|
return jsonify({"message": "Raumbuchung gelöscht"})
|
|
|
|
@app.route('/api/room-availability', methods=['GET'])
|
|
def room_availability():
|
|
"""Verfügbarkeit für alle Räume prüfen"""
|
|
date = request.args.get('date', datetime.now().date().isoformat())
|
|
time_from = request.args.get('time_from', '10:00')
|
|
time_to = request.args.get('time_to', '12:00')
|
|
|
|
with get_db() as db:
|
|
rooms = db.execute('SELECT * FROM rooms').fetchall()
|
|
result = []
|
|
|
|
for room in rooms:
|
|
# Prüfe ob Raum belegt ist
|
|
existing = db.execute('''
|
|
SELECT COUNT(*) FROM room_bookings
|
|
WHERE room_id = ? AND date = ? AND status = 'confirmed'
|
|
AND (
|
|
(time_from < ? AND time_to > ?) OR
|
|
(time_from < ? AND time_to > ?) OR
|
|
(time_from >= ? AND time_to <= ?)
|
|
)
|
|
''', (room['id'], date, time_to, time_from, time_to, time_from,
|
|
time_from, time_to)).fetchone()[0]
|
|
|
|
# Lade Tische im Raum
|
|
areas = db.execute('SELECT id FROM areas WHERE room_id = ?', (room['id'],)).fetchall()
|
|
area_ids = [a['id'] for a in areas]
|
|
|
|
table_count = 0
|
|
if area_ids:
|
|
placeholders = ','.join('?' for _ in area_ids)
|
|
table_count = db.execute(f'''
|
|
SELECT COUNT(*) FROM tables
|
|
WHERE area_id IN ({placeholders}) AND is_active = 1
|
|
''', area_ids).fetchone()[0]
|
|
|
|
result.append({
|
|
'room_id': room['id'],
|
|
'room_name': room['name'],
|
|
'capacity': room['capacity'],
|
|
'color': room['color'],
|
|
'available': existing == 0,
|
|
'table_count': table_count,
|
|
'date': date,
|
|
'time_from': time_from,
|
|
'time_to': time_to
|
|
})
|
|
|
|
return jsonify(result)
|
|
|
|
@app.route('/api/tables/<int:area_id>', methods=['GET', 'POST'])
|
|
def tables_by_area(area_id):
|
|
"""Tische für einen Bereich verwalten"""
|
|
if request.method == 'GET':
|
|
with get_db() as db:
|
|
tables = db.execute(
|
|
'SELECT * FROM tables WHERE area_id = ? ORDER BY name',
|
|
(area_id,)
|
|
).fetchall()
|
|
return jsonify([dict(t) for t in tables])
|
|
|
|
# POST: Neuen Tisch erstellen
|
|
data = request.get_json()
|
|
|
|
with get_db() as db:
|
|
cursor = db.execute('''
|
|
INSERT INTO tables (area_id, name, x, y, width, height, seats, shape, is_combinable)
|
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
''', (
|
|
area_id,
|
|
data.get('name', 'Neuer Tisch'),
|
|
data.get('x', 0),
|
|
data.get('y', 0),
|
|
data.get('width', 100),
|
|
data.get('height', 100),
|
|
data.get('seats', 4),
|
|
data.get('shape', 'rect'),
|
|
data.get('is_combinable', 1)
|
|
))
|
|
db.commit()
|
|
return jsonify({"id": cursor.lastrowid, "message": "Tisch erstellt"}), 201
|
|
|
|
@app.route('/api/tables/<int:table_id>', methods=['PUT', 'DELETE'])
|
|
def table_detail(table_id):
|
|
"""Einzelnen Tisch verwalten"""
|
|
with get_db() as db:
|
|
if request.method == 'PUT':
|
|
data = request.get_json()
|
|
|
|
update_fields = []
|
|
params = []
|
|
|
|
if 'name' in data:
|
|
update_fields.append('name = ?')
|
|
params.append(data['name'])
|
|
if 'x' in data:
|
|
update_fields.append('x = ?')
|
|
params.append(data['x'])
|
|
if 'y' in data:
|
|
update_fields.append('y = ?')
|
|
params.append(data['y'])
|
|
if 'seats' in data:
|
|
update_fields.append('seats = ?')
|
|
params.append(data['seats'])
|
|
if 'is_active' in data:
|
|
update_fields.append('is_active = ?')
|
|
params.append(data['is_active'])
|
|
|
|
if update_fields:
|
|
params.append(table_id)
|
|
db.execute(f'''
|
|
UPDATE tables SET {', '.join(update_fields)} WHERE id = ?
|
|
''', params)
|
|
db.commit()
|
|
|
|
return jsonify({"message": "Tisch aktualisiert"})
|
|
|
|
elif request.method == 'DELETE':
|
|
db.execute('UPDATE tables SET is_active = 0 WHERE id = ?', (table_id,))
|
|
db.commit()
|
|
return jsonify({"message": "Tisch deaktiviert"})
|
|
|
|
# =============================================================================
|
|
# ERWEITERTE RESERVIERUNGS-ENDPOINTS
|
|
# =============================================================================
|
|
|
|
@app.route('/api/reservations/check-availability', methods=['POST'])
|
|
def check_reservation_availability():
|
|
"""Verfügbarkeit für Reservierung prüfen (erweitert)"""
|
|
data = request.get_json()
|
|
|
|
date = data.get('date')
|
|
time_from = data.get('time_from') or data.get('time') + ':00' if data.get('time') else None
|
|
time_to = data.get('time_to', '23:00')
|
|
guests = data.get('guests', 2)
|
|
preferred_room_id = data.get('room_id')
|
|
|
|
if not all([date, time_from]):
|
|
return jsonify({"error": "Datum und Uhrzeit erforderlich"}), 400
|
|
|
|
with get_db() as db:
|
|
# Basis-Query für verfügbare Tische
|
|
base_query = '''
|
|
SELECT t.*, a.name as area_name, r.id as room_id, r.name as room_name
|
|
FROM tables t
|
|
JOIN areas a ON t.area_id = a.id
|
|
JOIN rooms r ON a.room_id = r.id
|
|
WHERE t.is_active = 1
|
|
'''
|
|
params = []
|
|
|
|
if preferred_room_id:
|
|
base_query += ' AND r.id = ?'
|
|
params.append(preferred_room_id)
|
|
|
|
base_query += ' AND t.seats >= ?'
|
|
params.append(guests)
|
|
|
|
all_tables = db.execute(base_query, params).fetchall()
|
|
|
|
# Belegte Tische finden
|
|
occupied_query = '''
|
|
SELECT DISTINCT json_each.value as table_id
|
|
FROM reservations r, json_each(r.table_ids)
|
|
WHERE r.date = ?
|
|
AND r.status IN ('confirmed', 'pending')
|
|
AND NOT (r.time_to <= ? OR r.time_from >= ?)
|
|
'''
|
|
occupied = db.execute(occupied_query, (date, time_from, time_to)).fetchall()
|
|
occupied_ids = {str(row['table_id']) for row in occupied}
|
|
|
|
# Verfügbare Tische filtern
|
|
available = []
|
|
for t in all_tables:
|
|
if str(t['id']) not in occupied_ids:
|
|
table_dict = dict(t)
|
|
table_dict['is_available'] = True
|
|
available.append(table_dict)
|
|
|
|
# Nach Raum gruppieren
|
|
rooms_with_tables = {}
|
|
for t in available:
|
|
room_id = t['room_id']
|
|
if room_id not in rooms_with_tables:
|
|
rooms_with_tables[room_id] = {
|
|
'room_id': room_id,
|
|
'room_name': t['room_name'],
|
|
'tables': []
|
|
}
|
|
rooms_with_tables[room_id]['tables'].append(t)
|
|
|
|
return jsonify({
|
|
"date": date,
|
|
"time_from": time_from,
|
|
"time_to": time_to,
|
|
"guests": guests,
|
|
"available_tables": available,
|
|
"available_count": len(available),
|
|
"rooms": list(rooms_with_tables.values())
|
|
})
|
|
|
|
@app.route('/api/dashboard/extended', methods=['GET'])
|
|
def dashboard_extended():
|
|
"""Erweitertes Dashboard mit Raumbuchungen"""
|
|
today = datetime.now().date().isoformat()
|
|
|
|
with get_db() as db:
|
|
# Standard-Dashboard-Daten
|
|
today_count = db.execute(
|
|
"SELECT COUNT(*) FROM reservations WHERE date = ? AND status IN ('confirmed', 'pending')",
|
|
(today,)
|
|
).fetchone()[0]
|
|
|
|
# Raumbuchungen heute
|
|
room_bookings_count = db.execute(
|
|
"SELECT COUNT(*) FROM room_bookings WHERE date = ? AND status = 'confirmed'",
|
|
(today,)
|
|
).fetchone()[0]
|
|
|
|
# Unbearbeitete E-Mails
|
|
pending_emails = db.execute(
|
|
"SELECT COUNT(*) FROM emails WHERE status = 'needs_review'"
|
|
).fetchone()[0]
|
|
|
|
# Freie Tische
|
|
total_tables = db.execute(
|
|
"SELECT COUNT(*) FROM tables WHERE is_active = 1"
|
|
).fetchone()[0]
|
|
|
|
# Heutige Gäste
|
|
guests_today = db.execute('''
|
|
SELECT COALESCE(SUM(guests), 0) FROM reservations
|
|
WHERE date = ? AND status IN ('confirmed', 'pending')
|
|
''', (today,)).fetchone()[0]
|
|
|
|
# Raumbuchungs-Gäste
|
|
room_guests = db.execute('''
|
|
SELECT COALESCE(SUM(guests), 0) FROM room_bookings
|
|
WHERE date = ? AND status = 'confirmed'
|
|
''', (today,)).fetchone()[0]
|
|
|
|
# Aktuelle Reservierungen
|
|
today_reservations = db.execute('''
|
|
SELECT r.*, g.name as guest_name, g.phone
|
|
FROM reservations r
|
|
LEFT JOIN guests g ON r.guest_id = g.id
|
|
WHERE r.date = ? AND r.status IN ('confirmed', 'pending')
|
|
ORDER BY r.time_from
|
|
''', (today,)).fetchall()
|
|
|
|
# Aktuelle Raumbuchungen
|
|
today_room_bookings = db.execute('''
|
|
SELECT rb.*, r.name as room_name
|
|
FROM room_bookings rb
|
|
JOIN rooms r ON rb.room_id = r.id
|
|
WHERE rb.date = ? AND rb.status = 'confirmed'
|
|
ORDER BY rb.time_from
|
|
''', (today,)).fetchall()
|
|
|
|
# Räume mit Auslastung
|
|
rooms = db.execute('SELECT * FROM rooms').fetchall()
|
|
rooms_data = []
|
|
for room in rooms:
|
|
# Tische im Raum zählen
|
|
area_ids = db.execute('SELECT id FROM areas WHERE room_id = ?',
|
|
(room['id'],)).fetchall()
|
|
table_count = 0
|
|
if area_ids:
|
|
ids = [a['id'] for a in area_ids]
|
|
placeholders = ','.join('?' for _ in ids)
|
|
table_count = db.execute(f'''
|
|
SELECT COUNT(*) FROM tables
|
|
WHERE area_id IN ({placeholders}) AND is_active = 1
|
|
''', ids).fetchone()[0]
|
|
|
|
# Aktuelle Buchungen
|
|
bookings = db.execute('''
|
|
SELECT COUNT(*) FROM room_bookings
|
|
WHERE room_id = ? AND date = ? AND status = 'confirmed'
|
|
''', (room['id'], today)).fetchone()[0]
|
|
|
|
rooms_data.append({
|
|
'id': room['id'],
|
|
'name': room['name'],
|
|
'capacity': room['capacity'],
|
|
'color': room['color'],
|
|
'table_count': table_count,
|
|
'bookings_today': bookings
|
|
})
|
|
|
|
return jsonify({
|
|
"today": today,
|
|
"reservations_count": today_count,
|
|
"room_bookings_count": room_bookings_count,
|
|
"pending_emails": pending_emails,
|
|
"total_tables": total_tables,
|
|
"guests_today": guests_today,
|
|
"room_guests_today": room_guests,
|
|
"total_guests_today": guests_today + room_guests,
|
|
"reservations": [dict(r) for r in today_reservations],
|
|
"room_bookings": [dict(r) for r in today_room_bookings],
|
|
"rooms": rooms_data
|
|
})
|