Teil 3: Terminvorschläge + KI-Limit-Bypass für Admins/Mods — SW by-v435, APP_VER 414
- timeutils: next_appointment_slot() parst OSM opening_hours, findet Slot - GET /health/terminvorschlaege: fällige/überfällige Einträge (30-Tage-Horizont) Impfung/Tierarzt nutzen Praxis-Öffnungszeiten, Rest nächster Werktag 09:00 - Frontend: Terminvorschlags-Karten, bestätigbares Modal, legt Event an - ki.py: Admins, Moderatoren, Media Manager bypassen CLOUD_WEEKLY_LIMIT
This commit is contained in:
parent
570dcd4e93
commit
c935d3fbd4
7 changed files with 300 additions and 9 deletions
|
|
@ -1,15 +1,93 @@
|
|||
"""Hilfsfunktionen für client-seitige Zeitstempel."""
|
||||
"""Hilfsfunktionen für client-seitige Zeitstempel und Öffnungszeiten."""
|
||||
import re
|
||||
from datetime import datetime
|
||||
from datetime import datetime, date, timedelta
|
||||
|
||||
|
||||
def safe_client_time(client_time: str | None) -> str:
|
||||
"""Gibt client_time zurück falls valides ISO-Datetime, sonst UTC-Now.
|
||||
|
||||
Schützt gegen Injection: nur YYYY-MM-DD HH:MM[:SS] erlaubt.
|
||||
"""
|
||||
"""Gibt client_time zurück falls valides ISO-Datetime, sonst UTC-Now."""
|
||||
if client_time and re.match(
|
||||
r'^\d{4}-\d{2}-\d{2}[T ]\d{2}:\d{2}(:\d{2})?$', client_time
|
||||
):
|
||||
return client_time.replace('T', ' ')[:19]
|
||||
return datetime.utcnow().strftime("%Y-%m-%d %H:%M:%S")
|
||||
|
||||
|
||||
# OSM-Wochentag-Kürzel → Python-weekday (Mo=0 … So=6)
|
||||
_DAY = {'Mo': 0, 'Di': 1, 'Mi': 2, 'Do': 3, 'Fr': 4, 'Sa': 5, 'So': 6}
|
||||
_DAY_EN = {'Mo': 0, 'Tu': 1, 'We': 2, 'Th': 3, 'Fr': 4, 'Sa': 5, 'Su': 6}
|
||||
|
||||
|
||||
def _parse_oh_segments(oh: str) -> list[tuple[set[int], str, str]]:
|
||||
"""Parst OSM-opening_hours in [(weekdays, open_time, close_time), …].
|
||||
|
||||
Unterstützt: "Mo-Fr 08:00-18:00; Sa 09:00-13:00", "24/7", "Mo,Mi 09:00-17:00"
|
||||
"""
|
||||
if not oh:
|
||||
return []
|
||||
oh = oh.strip()
|
||||
if oh.lower() in ('24/7', '24/7 open'):
|
||||
return [({0, 1, 2, 3, 4, 5, 6}, '09:00', '18:00')]
|
||||
|
||||
segments = []
|
||||
for part in oh.split(';'):
|
||||
part = part.strip()
|
||||
m = re.match(
|
||||
r'^([A-Za-z,\-]+)\s+(\d{2}:\d{2})-(\d{2}:\d{2})$', part
|
||||
)
|
||||
if not m:
|
||||
continue
|
||||
day_spec, open_t, close_t = m.group(1), m.group(2), m.group(3)
|
||||
days: set[int] = set()
|
||||
for chunk in day_spec.split(','):
|
||||
chunk = chunk.strip()
|
||||
if '-' in chunk:
|
||||
parts = chunk.split('-')
|
||||
if len(parts) == 2:
|
||||
d1 = _DAY.get(parts[0], _DAY_EN.get(parts[0]))
|
||||
d2 = _DAY.get(parts[1], _DAY_EN.get(parts[1]))
|
||||
if d1 is not None and d2 is not None:
|
||||
days.update(range(d1, d2 + 1))
|
||||
else:
|
||||
d = _DAY.get(chunk, _DAY_EN.get(chunk))
|
||||
if d is not None:
|
||||
days.add(d)
|
||||
if days:
|
||||
segments.append((days, open_t, close_t))
|
||||
return segments
|
||||
|
||||
|
||||
def next_appointment_slot(
|
||||
opening_hours: str | None,
|
||||
start_from: date | None = None,
|
||||
prefer_morning: bool = True,
|
||||
) -> tuple[str, str]:
|
||||
"""Gibt (datum_str, uhrzeit_str) für den nächsten Slot zurück.
|
||||
|
||||
Sucht ab morgen (oder start_from) innerhalb der Öffnungszeiten.
|
||||
Fallback: nächster Werktag 09:00 wenn keine Öffnungszeiten vorhanden.
|
||||
Uhrzeit = Öffnungszeit + 1h (Puffer), min. 09:00, max. 11:00 (Vormittag).
|
||||
"""
|
||||
base = (start_from or date.today()) + timedelta(days=1)
|
||||
segments = _parse_oh_segments(opening_hours or '')
|
||||
|
||||
if not segments:
|
||||
# Fallback: nächster Mo–Fr 09:00
|
||||
d = base
|
||||
for _ in range(14):
|
||||
if d.weekday() < 5: # Mo–Fr
|
||||
return d.isoformat(), '09:00'
|
||||
d += timedelta(days=1)
|
||||
return base.isoformat(), '09:00'
|
||||
|
||||
for _ in range(14):
|
||||
wd = base.weekday()
|
||||
for days, open_t, close_t in segments:
|
||||
if wd in days:
|
||||
# Slot: Öffnungszeit + 1h, mindestens 09:00, höchstens 11:00
|
||||
h, m = map(int, open_t.split(':'))
|
||||
slot_h = max(9, h + 1)
|
||||
slot_h = min(slot_h, 11)
|
||||
return base.isoformat(), f'{slot_h:02d}:00'
|
||||
base += timedelta(days=1)
|
||||
|
||||
return base.isoformat(), '09:00'
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue