"""Hilfsfunktionen für client-seitige Zeitstempel und Öffnungszeiten.""" import re 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.""" 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'