"""Slot-Helper: Konflikt-Pruefung, Belegung, Validierung. Reine Daten-Operationen ohne Flask-Abhaengigkeit. Wird von SuS- und Admin-Routen gleichermassen genutzt. Konvention: Zeitfelder in der DB sind ISO-Strings (datetime.isoformat()), in Python arbeiten wir mit datetime-Objekten. """ from datetime import datetime, timedelta # Validierungs-Defaults MIN_DAUER = timedelta(minutes=15) MAX_DAUER = timedelta(hours=24) # Slot-Statuswerte, die gegen Konflikte zaehlen (alles ausser storniert/beendet). BELEGT_STATUS = ("reserviert", "aktiv") def parse_datetime_local(wert: str) -> datetime | None: """Liest den Wert eines (YYYY-MM-DDTHH:MM).""" if not wert: return None try: return datetime.fromisoformat(wert) except ValueError: return None def validiere_zeitraum(start: datetime, ende: datetime, jetzt: datetime | None = None) -> str | None: """Prueft den Zeitraum eines neuen Slots. None = OK, sonst Fehlertext (deutsch).""" if not start or not ende: return "Start- und Endzeit muessen gesetzt sein." if ende <= start: return "Die Endzeit muss nach der Startzeit liegen." jetzt = jetzt or datetime.now() # Toleranz: 5 Minuten in der Vergangenheit erlauben, damit "jetzt"-Buchungen klappen if start < jetzt - timedelta(minutes=5): return "Der Slot darf nicht in der Vergangenheit beginnen." dauer = ende - start if dauer < MIN_DAUER: return f"Mindestdauer sind {int(MIN_DAUER.total_seconds() // 60)} Minuten." if dauer > MAX_DAUER: return f"Maximaldauer sind {int(MAX_DAUER.total_seconds() // 3600)} Stunden." return None def konflikt_check(conn, drucker_id: int, start: datetime, ende: datetime, exclude_slot_id: int | None = None): """Liefert eine Liste kollidierender Slots (sqlite3.Row) auf dem Drucker. Standard-Intervall-Ueberschneidung: Konflikt wenn start_a < ende_b UND start_b < ende_a. Stornierte/beendete Slots blockieren nicht. """ sql = ( "SELECT id, profil_id, start_zeit, ende_zeit, status, notiz " "FROM slot WHERE drucker_id = ? " f"AND status IN ({','.join('?' for _ in BELEGT_STATUS)}) " "AND start_zeit < ? AND ende_zeit > ?" ) params = [drucker_id, *BELEGT_STATUS, ende.isoformat(), start.isoformat()] if exclude_slot_id is not None: sql += " AND id != ?" params.append(exclude_slot_id) return conn.execute(sql, params).fetchall() def aktuelle_belegung(conn, drucker_id: int, jetzt: datetime | None = None): """Liefert den gerade aktiven Slot auf dem Drucker (oder None).""" jetzt = jetzt or datetime.now() return conn.execute( f"SELECT s.id, s.profil_id, s.start_zeit, s.ende_zeit, s.status, s.notiz, " f" p.vorname, p.tier " f"FROM slot s LEFT JOIN profil p ON p.id = s.profil_id " f"WHERE s.drucker_id = ? AND s.status IN ({','.join('?' for _ in BELEGT_STATUS)}) " f"AND s.start_zeit <= ? AND s.ende_zeit > ? " f"ORDER BY s.start_zeit DESC LIMIT 1", [drucker_id, *BELEGT_STATUS, jetzt.isoformat(), jetzt.isoformat()] ).fetchone() def naechste_slots(conn, drucker_id: int, n: int = 5, ab: datetime | None = None): """Liefert die naechsten n bevorstehenden Slots (start in der Zukunft).""" ab = ab or datetime.now() return conn.execute( f"SELECT s.id, s.profil_id, s.start_zeit, s.ende_zeit, s.status, s.notiz, " f" p.vorname, p.tier " f"FROM slot s LEFT JOIN profil p ON p.id = s.profil_id " f"WHERE s.drucker_id = ? AND s.status IN ({','.join('?' for _ in BELEGT_STATUS)}) " f"AND s.start_zeit >= ? " f"ORDER BY s.start_zeit ASC LIMIT ?", [drucker_id, *BELEGT_STATUS, ab.isoformat(), n] ).fetchall() def profil_darf_reservieren(conn, profil_id: int) -> bool: """Prueft das Lehrer-Freischalt-Flag fuer dieses Profil.""" if not profil_id: return False row = conn.execute( "SELECT reservieren_freigeschaltet FROM profil WHERE id = ?", (profil_id,) ).fetchone() return bool(row and row["reservieren_freigeschaltet"]) def slots_eines_profils(conn, profil_id: int, kommend_nur: bool = True): """Slots eines SuS — default nur bevorstehende und aktive, nicht beendete.""" sql = ( "SELECT s.id, s.drucker_id, s.start_zeit, s.ende_zeit, s.status, s.notiz, " " d.name AS drucker_name, d.modell " "FROM slot s LEFT JOIN drucker d ON d.id = s.drucker_id " "WHERE s.profil_id = ? " ) params: list = [profil_id] if kommend_nur: sql += "AND s.status IN ('reserviert', 'aktiv') AND s.ende_zeit >= ? " params.append(datetime.now().isoformat()) sql += "ORDER BY s.start_zeit ASC" return conn.execute(sql, params).fetchall()