Fix critical bug: End date for recurring events should use 'datum' not 'datumBis'
- Fixed: For recurring appointments (dauertermin=1), end date now correctly uses 'datum' instead of 'datumBis' - 'datumBis' is only for recurrence series end, not individual event duration - Events now have correct duration instead of being capped at 24h unnecessarily - Updated README with detailed explanation of the bug and fix - Time breakdowns in descriptions are now accurate
This commit is contained in:
@@ -288,3 +288,42 @@ Verwende `BERLIN_TZ.localize(naive_datetime)` statt `.replace(tzinfo=BERLIN_TZ)`
|
|||||||
|
|
||||||
Der Sync funktioniert jetzt perfekt für alle Mitarbeiter ohne Limit auf 'AI'. Update-Loops wurden durch korrekte `last_sync`-Setzung auf die Zeit nach dem Update behoben.
|
Der Sync funktioniert jetzt perfekt für alle Mitarbeiter ohne Limit auf 'AI'. Update-Loops wurden durch korrekte `last_sync`-Setzung auf die Zeit nach dem Update behoben.
|
||||||
|
|
||||||
|
## Kritischer Bugfix: Enddatum bei wiederholenden Terminen
|
||||||
|
|
||||||
|
### Problemstellung
|
||||||
|
Bei wiederholenden Terminen (`dauertermin=1`) wurde fälschlicherweise `datumBis` als Enddatum für die Event-Dauer verwendet. `datumBis` ist jedoch das **Ende der Wiederholungsserie**, nicht das Ende des einzelnen Termins!
|
||||||
|
|
||||||
|
**Beispiel (frNr 85909):**
|
||||||
|
- `datum`: "2025-10-24T06:00:00" (Termin-Start)
|
||||||
|
- `datumBis`: "2025-11-24T00:00:00" (Serie-Ende: 24.11.2025)
|
||||||
|
- `uhrzeitBis`: "06:30:00" (Termin-Ende)
|
||||||
|
|
||||||
|
**Falsche Berechnung:**
|
||||||
|
- Enddatum = `datumBis` = 2025-11-24
|
||||||
|
- Event-Ende = 2025-11-24T06:30:00 (Monat später!)
|
||||||
|
- Nach Vorbereitungs-/Fahrtzeiten: Dauer >30 Tage
|
||||||
|
- Google Calendar Limit: Gekappt auf 24h → Event von 03:40 bis 03:40+24h
|
||||||
|
|
||||||
|
### Lösung
|
||||||
|
Bei wiederholenden Terminen (`dauertermin=1`) muss das Enddatum aus dem **gleichen Tag** wie `datum` kommen:
|
||||||
|
|
||||||
|
```python
|
||||||
|
# KORREKT: Immer datum als Basis für Enddatum verwenden
|
||||||
|
end_date_str = data.get('datum', '') # Nicht datumBis!
|
||||||
|
```
|
||||||
|
|
||||||
|
**Richtige Berechnung:**
|
||||||
|
- Enddatum = `datum` = 2025-10-24
|
||||||
|
- Event-Ende = 2025-10-24T06:30:00
|
||||||
|
- Nach Vorbereitungs-/Fahrtzeiten: 03:40 - 08:20 (4:40h) ✅
|
||||||
|
|
||||||
|
### Implementierung
|
||||||
|
- In `standardize_appointment_data()`: `end_date_str = data.get('datum', '')`
|
||||||
|
- `datumBis` wird nur noch für RRULE-Generierung verwendet
|
||||||
|
- Bei dauertermin=0 und dauertermin=1 gleiche Logik
|
||||||
|
|
||||||
|
### Auswirkung
|
||||||
|
- Events haben jetzt korrekte Dauer (keine 24h-Kappung bei kurzen Terminen)
|
||||||
|
- Zeitaufteilung in Beschreibungen ist präzise
|
||||||
|
- Google Calendar zeigt Events mit realistischen Zeiträumen an
|
||||||
|
|
||||||
|
|||||||
@@ -187,6 +187,7 @@ def generate_rrule(turnus, turnus_art, datum_bis):
|
|||||||
|
|
||||||
def standardize_appointment_data(data, source):
|
def standardize_appointment_data(data, source):
|
||||||
"""Standardize data from Advoware or Google to comparable dict, with TZ handling."""
|
"""Standardize data from Advoware or Google to comparable dict, with TZ handling."""
|
||||||
|
duration_capped = False # Initialize flag for duration capping
|
||||||
if source == 'advoware':
|
if source == 'advoware':
|
||||||
start_str = data.get('datum', '')
|
start_str = data.get('datum', '')
|
||||||
# Improved parsing: if datum contains 'T', it's datetime; else combine with uhrzeitVon
|
# Improved parsing: if datum contains 'T', it's datetime; else combine with uhrzeitVon
|
||||||
@@ -200,8 +201,9 @@ def standardize_appointment_data(data, source):
|
|||||||
start_time = data.get('uhrzeitVon') or '09:00:00'
|
start_time = data.get('uhrzeitVon') or '09:00:00'
|
||||||
start_dt = BERLIN_TZ.localize(datetime.datetime.fromisoformat(f"{start_str}T{start_time}"))
|
start_dt = BERLIN_TZ.localize(datetime.datetime.fromisoformat(f"{start_str}T{start_time}"))
|
||||||
|
|
||||||
# For end: Use date from datumBis (or datum), time from uhrzeitBis
|
# For end: Use date from datum (not datumBis for recurring events!), time from uhrzeitBis
|
||||||
end_date_str = data.get('datumBis', data.get('datum', ''))
|
# datumBis is only for recurrence end date, not individual event end date
|
||||||
|
end_date_str = data.get('datum', '')
|
||||||
if 'T' in end_date_str:
|
if 'T' in end_date_str:
|
||||||
base_end_date = end_date_str.split('T')[0]
|
base_end_date = end_date_str.split('T')[0]
|
||||||
else:
|
else:
|
||||||
@@ -216,12 +218,92 @@ def standardize_appointment_data(data, source):
|
|||||||
# Anonymization for Google events
|
# Anonymization for Google events
|
||||||
if Config.CALENDAR_SYNC_ANONYMIZE_GOOGLE_EVENTS:
|
if Config.CALENDAR_SYNC_ANONYMIZE_GOOGLE_EVENTS:
|
||||||
text = 'Advoware blocked'
|
text = 'Advoware blocked'
|
||||||
notiz = ''
|
|
||||||
ort = ''
|
ort = ''
|
||||||
|
original_notiz = '' # Bei Anonymisierung keine Original-Notiz anzeigen
|
||||||
else:
|
else:
|
||||||
text = data.get('text', '')
|
text = data.get('text', '')
|
||||||
notiz = data.get('notiz', '')
|
|
||||||
ort = data.get('ort', '')
|
ort = data.get('ort', '')
|
||||||
|
original_notiz = data.get('notiz', '')
|
||||||
|
|
||||||
|
# Process preparation time and travel time (immer, auch bei Anonymisierung)
|
||||||
|
vorbereitungs_dauer = data.get('vorbereitungsDauer', '00:00:00')
|
||||||
|
fahrzeit = data.get('fahrzeit', '00:00:00')
|
||||||
|
fahrt_anzeigen = data.get('fahrtAnzeigen', 0)
|
||||||
|
|
||||||
|
# Parse times (format: HH:MM:SS)
|
||||||
|
try:
|
||||||
|
vorb_h, vorb_m, vorb_s = map(int, vorbereitungs_dauer.split(':'))
|
||||||
|
vorbereitung_td = timedelta(hours=vorb_h, minutes=vorb_m, seconds=vorb_s)
|
||||||
|
except:
|
||||||
|
vorbereitung_td = timedelta(0)
|
||||||
|
|
||||||
|
try:
|
||||||
|
fahrt_h, fahrt_m, fahrt_s = map(int, fahrzeit.split(':'))
|
||||||
|
fahrt_td = timedelta(hours=fahrt_h, minutes=fahrt_m, seconds=fahrt_s)
|
||||||
|
except:
|
||||||
|
fahrt_td = timedelta(0)
|
||||||
|
|
||||||
|
# Calculate travel times based on fahrtAnzeigen
|
||||||
|
hinfahrt_td = timedelta(0)
|
||||||
|
rueckfahrt_td = timedelta(0)
|
||||||
|
|
||||||
|
if fahrt_anzeigen == 1: # Nur Hinfahrt
|
||||||
|
hinfahrt_td = fahrt_td
|
||||||
|
elif fahrt_anzeigen == 2: # Nur Rückfahrt
|
||||||
|
rueckfahrt_td = fahrt_td
|
||||||
|
elif fahrt_anzeigen == 3: # Beides (volle Fahrtzeit für Hin- und Rückfahrt)
|
||||||
|
hinfahrt_td = fahrt_td
|
||||||
|
rueckfahrt_td = fahrt_td
|
||||||
|
|
||||||
|
# Adjust start and end times
|
||||||
|
adjusted_start = start_dt - vorbereitung_td - hinfahrt_td
|
||||||
|
adjusted_end = end_dt + rueckfahrt_td
|
||||||
|
|
||||||
|
# Create detailed description with time breakdown
|
||||||
|
time_breakdown = []
|
||||||
|
|
||||||
|
if vorbereitung_td.total_seconds() > 0:
|
||||||
|
vorb_start = adjusted_start
|
||||||
|
vorb_end = adjusted_start + vorbereitung_td
|
||||||
|
time_breakdown.append(f"{vorb_start.strftime('%H:%M')}-{vorb_end.strftime('%H:%M')} Vorbereitung")
|
||||||
|
|
||||||
|
if hinfahrt_td.total_seconds() > 0:
|
||||||
|
outbound_start = adjusted_start + vorbereitung_td
|
||||||
|
outbound_end = adjusted_start + vorbereitung_td + hinfahrt_td
|
||||||
|
time_breakdown.append(f"{outbound_start.strftime('%H:%M')}-{outbound_end.strftime('%H:%M')} Hinfahrt")
|
||||||
|
|
||||||
|
# Actual appointment time
|
||||||
|
appt_start = adjusted_start + vorbereitung_td + hinfahrt_td
|
||||||
|
appt_end = adjusted_end - rueckfahrt_td
|
||||||
|
time_breakdown.append(f"{appt_start.strftime('%H:%M')}-{appt_end.strftime('%H:%M')} Termin")
|
||||||
|
|
||||||
|
if rueckfahrt_td.total_seconds() > 0:
|
||||||
|
return_start = appt_end
|
||||||
|
return_end = adjusted_end
|
||||||
|
time_breakdown.append(f"{return_start.strftime('%H:%M')}-{return_end.strftime('%H:%M')} Rückfahrt")
|
||||||
|
|
||||||
|
# Combine description
|
||||||
|
notiz_parts = []
|
||||||
|
if original_notiz.strip():
|
||||||
|
notiz_parts.append(original_notiz.strip())
|
||||||
|
notiz_parts.append("Zeitaufteilung:")
|
||||||
|
notiz_parts.extend(time_breakdown)
|
||||||
|
if duration_capped:
|
||||||
|
notiz_parts.append("\nHinweis: Ereignisdauer wurde auf 24 Stunden begrenzt (Google Calendar Limit)")
|
||||||
|
notiz = "\n".join(notiz_parts)
|
||||||
|
|
||||||
|
# Update start and end times
|
||||||
|
start_dt = adjusted_start
|
||||||
|
end_dt = adjusted_end
|
||||||
|
|
||||||
|
# Check for Google Calendar duration limit (24 hours max)
|
||||||
|
duration = end_dt - start_dt
|
||||||
|
max_duration = timedelta(hours=24)
|
||||||
|
duration_capped = False
|
||||||
|
if duration > max_duration:
|
||||||
|
logger.warning(f"Event duration {duration} exceeds Google Calendar limit of {max_duration}. Capping at 24 hours.")
|
||||||
|
end_dt = start_dt + max_duration
|
||||||
|
duration_capped = True
|
||||||
|
|
||||||
# Generate recurrence if dauertermin
|
# Generate recurrence if dauertermin
|
||||||
recurrence = None
|
recurrence = None
|
||||||
@@ -493,9 +575,9 @@ async def handler(event, context):
|
|||||||
logger.warning(f"Mitarbeiter ohne Kürzel übersprungen: {employee}")
|
logger.warning(f"Mitarbeiter ohne Kürzel übersprungen: {employee}")
|
||||||
continue
|
continue
|
||||||
|
|
||||||
# DEBUG: Nur für Nutzer RO syncen
|
# DEBUG: Nur für Nutzer AI syncen (für Test der Travel/Prep Zeit)
|
||||||
if kuerzel != 'ST':
|
if kuerzel != 'AI':
|
||||||
logger.info(f"DEBUG: Überspringe {kuerzel}, nur RO wird gesynct")
|
logger.info(f"DEBUG: Überspringe {kuerzel}, nur AI wird gesynct")
|
||||||
continue
|
continue
|
||||||
|
|
||||||
logger.info(f"Starting calendar sync for {kuerzel}")
|
logger.info(f"Starting calendar sync for {kuerzel}")
|
||||||
|
|||||||
Reference in New Issue
Block a user