Add calendar sync step files: API, cron, and event handlers for bidirectional Advoware-Google Calendar sync

This commit is contained in:
root
2025-10-22 23:37:28 +00:00
parent 0a4317c44a
commit 2f9203cac2
3 changed files with 770 additions and 0 deletions

View File

@@ -0,0 +1,49 @@
import json
config = {
'type': 'api',
'name': 'Calendar Sync API Trigger',
'description': 'API-Endpunkt zum manuellen Auslösen des Calendar Sync',
'path': '/advoware/calendar/sync',
'method': 'POST',
'emits': ['calendar.sync.triggered']
}
async def handler(req, context):
try:
# Konfiguration aus Request-Body
body = req.get('body', {})
full_content = body.get('full_content', True)
print(f"Calendar Sync API aufgerufen, full_content: {full_content}")
# Emit Event für den Sync
await context.emit({
"topic": "calendar.sync.triggered",
"data": {
"body": {
"full_content": full_content,
"triggered_by": "api"
}
}
})
return {
'status': 200,
'body': {
'status': 'triggered',
'message': 'Calendar sync wurde ausgelöst',
'full_content': full_content,
'triggered_by': 'api'
}
}
except Exception as e:
context.logger.error(f"Fehler beim API-Trigger: {e}")
return {
'status': 500,
'body': {
'error': 'Internal server error',
'details': str(e)
}
}

View File

@@ -0,0 +1,37 @@
import json
config = {
'type': 'cron',
'name': 'Calendar Sync Cron Job',
'description': 'Führt den Calendar Sync alle 15 Minuten automatisch aus',
'cron': '*/15 * * * *', # Alle 15 Minuten
'emits': ['calendar.sync.triggered']
}
async def handler(event, context):
try:
context.logger.info("Calendar Sync Cron: Starte automatische Synchronisation alle 15 Minuten")
# Emit Event für den Sync
await context.emit({
"topic": "calendar.sync.triggered",
"data": {
"body": {
"full_content": True, # Cron verwendet immer volle Details
"triggered_by": "cron"
}
}
})
context.logger.info("Calendar Sync Cron: Event wurde emittiert")
return {
'status': 'completed',
'triggered_by': 'cron'
}
except Exception as e:
context.logger.error(f"Fehler beim Cron-Job: {e}")
return {
'status': 'error',
'error': str(e)
}

View File

@@ -0,0 +1,684 @@
from services.advoware import AdvowareAPI
from config import Config
from googleapiclient.discovery import build
from google.oauth2 import service_account
from googleapiclient.errors import HttpError
import json
import datetime
import redis
import os
import hashlib
import asyncio
import random
# Salt für Token-Generierung laden
CALENDAR_SYNC_SALT = os.getenv('CALENDAR_SYNC_SALT')
if not CALENDAR_SYNC_SALT:
raise ValueError("CALENDAR_SYNC_SALT environment variable not set")
def generate_change_token(frnr):
"""Generiert einen Change-Token mit Salt für eine frNr"""
return hashlib.md5((str(frnr) + CALENDAR_SYNC_SALT).encode()).hexdigest()
config = {
'type': 'event',
'name': 'Calendar Sync Event Handler',
'description': 'Führt den Advoware-Google Calendar Sync aus bei Events',
'subscribes': ['calendar.sync.triggered'],
'emits': []
}
async def google_api_call_with_backoff(call_func, *args, **kwargs):
"""Führt Google API Call mit exponentiellem Backoff bei 403/429 aus"""
max_retries = 5
base_delay = 1 # seconds
for attempt in range(max_retries):
try:
return call_func(*args, **kwargs)
except HttpError as e:
if e.resp.status in [403, 429]:
if attempt == max_retries - 1:
raise
delay = base_delay * (2 ** attempt) + random.uniform(0, 1)
await asyncio.sleep(delay)
else:
raise
SCOPES = Config.GOOGLE_CALENDAR_SCOPES
async def get_google_service(context):
"""Initialisiert Google Calendar API Service mit Service Account"""
try:
service_account_path = Config.GOOGLE_CALENDAR_SERVICE_ACCOUNT_PATH
if not os.path.exists(service_account_path):
context.logger.error(f"Service Account Datei nicht gefunden: {service_account_path}")
context.logger.error("Bitte erstellen Sie einen Google Service Account und legen Sie die service-account.json in das Projekt-Verzeichnis.")
return None
context.logger.info("Initialisiere Google Calendar API mit Service Account...")
creds = service_account.Credentials.from_service_account_file(
service_account_path, scopes=SCOPES)
service = build('calendar', 'v3', credentials=creds)
context.logger.info("Google Calendar API erfolgreich initialisiert")
return service
except Exception as e:
context.logger.error(f"Fehler bei Google Service Account Authentifizierung: {e}")
context.logger.error("Überprüfen Sie die service-account.json Datei und die Berechtigungen.")
return None
async def get_advoware_employees(context):
"""Ruft alle Mitarbeiter von Advoware ab"""
advoware = AdvowareAPI(context)
try:
# Annahme: Mitarbeiter-Endpoint existiert ähnlich wie andere
result = await advoware.api_call('api/v1/advonet/Mitarbeiter', params={'aktiv': 'true'})
context.logger.info(f"Advoware Mitarbeiter abgerufen: {len(result) if isinstance(result, list) else 'unbekannt'}")
return result if isinstance(result, list) else []
except Exception as e:
context.logger.error(f"Fehler beim Abrufen der Mitarbeiter: {e}")
return []
async def ensure_google_calendar(service, employee_kuerzel, context):
"""Stellt sicher, dass ein Google Calendar für den Mitarbeiter existiert"""
calendar_name = f"AW-{employee_kuerzel}"
try:
# Bestehende Kalender prüfen
calendar_list = await google_api_call_with_backoff(service.calendarList().list().execute)
for calendar in calendar_list.get('items', []):
if calendar['summary'] == calendar_name:
context.logger.info(f"Google Calendar '{calendar_name}' existiert bereits")
calendar_id = calendar['id']
# Kalender mit Hauptaccount teilen (auch für bestehende)
acl_rule = {
'scope': {
'type': 'user',
'value': 'lehmannundpartner@gmail.com'
},
'role': 'owner'
}
try:
await google_api_call_with_backoff(service.acl().insert(calendarId=calendar_id, body=acl_rule).execute)
context.logger.info(f"Kalender '{calendar_name}' mit lehmannundpartner@gmail.com geteilt")
except Exception as e:
context.logger.info(f"ACL für '{calendar_name}' bereits vorhanden oder Fehler: {e}")
return calendar_id
# Neuen Kalender erstellen
calendar_body = {
'summary': calendar_name,
'description': f'Advoware Termine für Mitarbeiter {employee_kuerzel}',
'timeZone': 'Europe/Berlin'
}
created_calendar = await google_api_call_with_backoff(service.calendars().insert(body=calendar_body).execute)
calendar_id = created_calendar['id']
context.logger.info(f"Google Calendar '{calendar_name}' erstellt mit ID: {calendar_id}")
# Kalender mit Hauptaccount teilen
acl_rule = {
'scope': {
'type': 'user',
'value': 'lehmannundpartner@gmail.com'
},
'role': 'owner'
}
await google_api_call_with_backoff(service.acl().insert(calendarId=calendar_id, body=acl_rule).execute)
context.logger.info(f"Kalender '{calendar_name}' mit lehmannundpartner@gmail.com geteilt")
return calendar_id
except Exception as e:
context.logger.error(f"Fehler bei Google Calendar für {employee_kuerzel}: {e}")
return None
async def get_advoware_appointments(employee_kuerzel, context):
"""Ruft Termine eines Mitarbeiters aus Advoware ab"""
advoware = AdvowareAPI(context)
# Zeitraum: aktuelles Jahr + 2 Jahre
from_date = datetime.datetime.now().strftime('%Y-01-01T00:00:00Z')
to_date = (datetime.datetime.now() + datetime.timedelta(days=730)).strftime('%Y-12-31T23:59:59Z')
try:
params = {
'kuerzel': employee_kuerzel,
'from': from_date,
'to': to_date
}
result = await advoware.api_call('api/v1/advonet/Termine', method='GET', params=params)
appointments = result if isinstance(result, list) else []
context.logger.info(f"Advoware Termine für {employee_kuerzel}: {len(appointments)} gefunden")
return appointments
except Exception as e:
context.logger.error(f"Fehler beim Abrufen der Termine für {employee_kuerzel}: {e}")
return []
async def get_google_events(service, calendar_id, context):
"""Ruft Events aus Google Calendar ab"""
try:
now = datetime.datetime.utcnow()
from_date = now.strftime('%Y-01-01T00:00:00Z')
to_date = (now + datetime.timedelta(days=730)).strftime('%Y-12-31T23:59:59Z')
events_result = await google_api_call_with_backoff(service.events().list(
calendarId=calendar_id,
timeMin=from_date,
timeMax=to_date,
singleEvents=True,
orderBy='startTime'
).execute)
events = events_result.get('items', [])
context.logger.info(f"Google Calendar Events: {len(events)} gefunden")
return events
except Exception as e:
context.logger.error(f"Fehler beim Abrufen der Google Events: {e}")
return []
async def sync_appointment_to_google(service, calendar_id, appointment, full_content, context):
"""Synchronisiert einen Advoware-Termin zu Google Calendar"""
try:
context.logger.info(f"Sync Advoware Termin {appointment.get('frNr')} zu Google starten")
context.logger.debug(f"Termin Daten: {appointment}")
# Advoware sendet bereits datetime strings
datum = appointment.get('datum', '') # z.B. '2025-05-30T16:30:00'
datum_bis = appointment.get('datumBis', '') # z.B. '2025-05-30T00:00:00'
# Extrahiere Datum und Zeit separat
if 'T' in datum:
start_date = datum.split('T')[0] # '2025-05-30'
start_time = datum.split('T')[1] # '16:30:00'
else:
start_date = datum
start_time = appointment.get('uhrzeitVon') or appointment.get('uhrzeit') or '09:00:00'
if 'T' in datum_bis and datum_bis != '0001-01-01T00:00:00': # Advoware default
end_date = datum_bis.split('T')[0]
end_time = datum_bis.split('T')[1]
# Prüfen ob Endzeit 00:00:00 ist (Advoware default für ganztägige Termine)
if end_time == '00:00:00':
# Berechne Endzeit als Startzeit + 1 Stunde
start_hour = int(start_time.split(':')[0])
end_hour = (start_hour + 1) % 24
end_time = f"{end_hour:02d}:{start_time.split(':')[1]}:{start_time.split(':')[2]}"
else:
end_date = start_date
# Standarddauer: 1 Stunde
start_hour = int(start_time.split(':')[0])
end_hour = (start_hour + 1) % 24
end_time = f"{end_hour:02d}:{start_time.split(':')[1]}:{start_time.split(':')[2]}"
# Vollständiges Event oder nur "blocked" - immer "blocked"
summary = "Advoware blocked"
description = f"Advoware Termin ID: {appointment.get('frNr')}"
location = ""
# Google Calendar erwartet RFC3339 formatierte Datetimes
start_datetime = f"{start_date}T{start_time}+01:00" # +01:00 für Europe/Berlin
end_datetime = f"{end_date}T{end_time}+01:00"
context.logger.info(f"Erstelle Google Event: Start {start_datetime}, End {end_datetime}")
event_body = {
'summary': summary,
'description': description,
'location': location,
'start': {
'dateTime': start_datetime,
'timeZone': 'Europe/Berlin',
},
'end': {
'dateTime': end_datetime,
'timeZone': 'Europe/Berlin',
},
'extendedProperties': {
'private': {
'advoware_frnr': str(appointment.get('frNr'))
}
}
}
# Event erstellen
context.logger.info("Sende Google Calendar Insert Request...")
created_event = await google_api_call_with_backoff(service.events().insert(calendarId=calendar_id, body=event_body).execute)
context.logger.info(f"Termin {appointment.get('frNr')} zu Google Calendar hinzugefügt, Event ID: {created_event.get('id')}")
return created_event
except Exception as e:
context.logger.error(f"Fehler beim Sync zu Google für Termin {appointment.get('frNr')}: {e}")
return None
async def sync_event_to_advoware(service, calendar_id, event, employee_kuerzel, context):
"""Synchronisiert ein Google Event zu Advoware (nur wenn change_allowed_token validiert)"""
try:
context.logger.info(f"Sync Google Event {event.get('id')} zu Advoware starten: {event.get('summary')}")
context.logger.debug(f"Event Daten: {event}")
# Prüfen ob bereits eine frNr vorhanden (aus description parsen)
description = event.get('description', '')
frnr = None
change_token = None
if 'frNr: ' in description and 'sync-token: ' in description:
frnr_parts = description.split('frNr: ')
if len(frnr_parts) > 1:
frnr = frnr_parts[1].split('\n')[0]
token_parts = description.split('sync-token: ')
if len(token_parts) > 1:
change_token = token_parts[1].split('\n')[0]
context.logger.info(f"Parsed frNr: {frnr}, Token: {change_token}")
if frnr and change_token:
# Validierung des Tokens
expected_token = generate_change_token(frnr)
if change_token != expected_token:
context.logger.info(f"Token für Google Event {event.get('id')} nicht validiert, überspringe Update")
return None
# Termin aus Advoware abrufen zum Vergleich
advoware = AdvowareAPI(context)
result = await advoware.api_call('api/v1/advonet/Termine', params={'frnr': frnr})
existing_appointment = result[0] if result and isinstance(result, list) and len(result) > 0 else None
if not existing_appointment:
context.logger.error(f"Termin {frnr} nicht in Advoware gefunden")
return None
# Daten aus Google Event extrahieren
start = event.get('start', {}).get('dateTime') or event.get('start', {}).get('date', '')
end = event.get('end', {}).get('dateTime') or event.get('end', {}).get('date', '')
summary = event.get('summary', '')
description = event.get('description', '')
location = event.get('location', '')
# Mit Advoware-Daten vergleichen
advoware_datum = existing_appointment.get('datum', '')
advoware_uhrzeit_bis = existing_appointment.get('uhrzeitBis', '')
advoware_text = existing_appointment.get('text', '')
advoware_notiz = existing_appointment.get('notiz', '')
advoware_ort = existing_appointment.get('ort', '')
# Berechne erwartete Werte aus Google
expected_datum = start.split('+')[0] if '+' in start else start
expected_uhrzeit_bis = end.split('T')[1].split('+')[0] if 'T' in end else '09:00:00'
# Für all-day Events (nur date, keine Zeit) setze Default-Start-Zeit
if 'T' not in start:
expected_datum = f"{start}T09:00:00"
context.logger.info(f"All-day Event erkannt, setze Default-Start-Zeit auf 09:00:00")
expected_text = summary
expected_notiz = description
expected_ort = location
context.logger.debug(f"Vergleiche: Advoware datum={advoware_datum}, expected={expected_datum}")
context.logger.debug(f"Vergleiche: Advoware uhrzeit_bis={advoware_uhrzeit_bis}, expected={expected_uhrzeit_bis}")
context.logger.debug(f"Vergleiche: Advoware text={advoware_text}, expected={expected_text}")
context.logger.debug(f"Vergleiche: Advoware notiz={advoware_notiz}, expected={expected_notiz}")
context.logger.debug(f"Vergleiche: Advoware ort={advoware_ort}, expected={expected_ort}")
if (advoware_datum == expected_datum and
advoware_uhrzeit_bis == expected_uhrzeit_bis and
advoware_text == expected_text and
advoware_notiz == expected_notiz and
advoware_ort == expected_ort):
context.logger.info(f"Google Event {event.get('id')} unverändert, überspringe Advoware Update")
return None
context.logger.info(f"Termin {frnr} geändert, aktualisiere in Advoware...")
# Update Termin in Advoware
# Kein Anhängen von sync_info bei bestehenden Terminen
updated_description = description
appointment_data = {
'frNr': int(frnr),
'text': summary,
'notiz': updated_description,
'ort': location,
'datum': start.split('+')[0] if '+' in start else start,
'uhrzeitBis': end.split('T')[1].split('+')[0] if 'T' in end else '09:00:00',
'datumBis': end.split('+')[0] if '+' in end else end,
'sb': employee_kuerzel,
'anwalt': employee_kuerzel,
'vorbereitungsDauer': '00:00:00'
}
await update_advoware_appointment(advoware, frnr, change_token, appointment_data, context)
return frnr
else:
context.logger.info(f"Neuer Termin aus Google, erstelle in Advoware...")
# Neuen Termin in Advoware erstellen
advoware = AdvowareAPI(context)
# Start/End aus Google Event extrahieren
start = event.get('start', {}).get('dateTime') or event.get('start', {}).get('date', '')
end = event.get('end', {}).get('dateTime') or event.get('end', {}).get('date', '')
# Advoware-Termin erstellen
appointment_data = {
'text': event.get('summary', 'Google Calendar Termin'),
'notiz': event.get('description', ''),
'ort': event.get('location', ''),
'datum': start.split('+')[0] if '+' in start else start,
'uhrzeitBis': end.split('T')[1].split('+')[0] if 'T' in end else '09:00:00',
'datumBis': end.split('+')[0] if '+' in end else end,
'sb': employee_kuerzel,
'anwalt': employee_kuerzel,
'vorbereitungsDauer': '00:00:00'
}
context.logger.info("Sende Advoware POST Request...")
result = await advoware.api_call('api/v1/advonet/Termine', method='POST', json_data=appointment_data)
context.logger.info(f"POST Termine Response: {result}")
if result and isinstance(result, dict):
new_frnr = result.get('frNr') or result.get('frnr')
if new_frnr:
context.logger.info(f"Neuer Advoware Termin erstellt: {new_frnr}")
# Sync-Token berechnen
token = generate_change_token(new_frnr)
# Sync-Info für beide Systeme vorbereiten
sync_info = f"\n\n## no change below this line ##\nfrNr: {new_frnr}\nsync-token: {token}"
existing_description = event.get('description', '')
new_description = existing_description + sync_info
# Sofort frNr und sync-token in Advoware schreiben (PUT)
context.logger.info(f"Schreibe sync-info in Advoware Termin {new_frnr}...")
update_data = {'frNr': int(new_frnr), 'notiz': new_description}
await advoware.api_call('api/v1/advonet/Termine', method='PUT', json_data=update_data)
context.logger.info(f"Sync-info in Advoware eingefügt")
# Sofort frNr und sync-token in Google schreiben
context.logger.info(f"Schreibe sync-info in Google Event {event.get('id')}...")
event['description'] = new_description
await google_api_call_with_backoff(service.events().update(calendarId=calendar_id, eventId=event['id'], body=event).execute)
context.logger.info(f"Sync-info in Google aktualisiert")
return new_frnr
else:
context.logger.error(f"Keine frNr in POST Response: {result}")
else:
context.logger.error(f"Ungültige POST Response: {result}")
except Exception as e:
context.logger.error(f"Fehler beim Sync zu Advoware für Google Event {event.get('id')}: {e}")
return None
async def update_google_event(service, calendar_id, existing_event, appointment, context):
"""Aktualisiert ein bestehendes Google Event mit Advoware-Daten, nur wenn Änderungen vorliegen"""
try:
context.logger.info(f"Aktualisiere Google Event für Advoware Termin {appointment.get('frNr')}")
context.logger.debug(f"Appointment Daten: {appointment}")
# Gleiche Logik wie in sync_appointment_to_google für datetime und body
datum = appointment.get('datum', '')
datum_bis = appointment.get('datumBis', '')
if 'T' in datum:
start_date = datum.split('T')[0]
start_time = datum.split('T')[1]
else:
start_date = datum
start_time = appointment.get('uhrzeitVon') or appointment.get('uhrzeit') or '09:00:00'
if 'T' in datum_bis and datum_bis != '0001-01-01T00:00:00':
end_date = datum_bis.split('T')[0]
end_time = datum_bis.split('T')[1]
if end_time == '00:00:00':
start_hour = int(start_time.split(':')[0])
end_hour = (start_hour + 1) % 24
end_time = f"{end_hour:02d}:{start_time.split(':')[1]}:{start_time.split(':')[2]}"
else:
end_date = start_date
start_hour = int(start_time.split(':')[0])
end_hour = (start_hour + 1) % 24
end_time = f"{end_hour:02d}:{start_time.split(':')[1]}:{start_time.split(':')[2]}"
summary = "Advoware blocked"
description = f"Advoware Termin ID: {appointment.get('frNr')}"
location = ""
start_datetime = f"{start_date}T{start_time}+01:00"
end_datetime = f"{end_date}T{end_time}+01:00"
# Prüfe ob es ein Google-initiales Event ist (hat sync-token in description)
is_google_initial = 'sync-token: ' in existing_event.get('description', '')
if is_google_initial:
# Behalte die originale Summary
summary = existing_event.get('summary', summary)
context.logger.debug(f"Google-initiales Event, behalte Summary: {summary}")
# Vergleiche mit bestehendem Event
current_start = existing_event.get('start', {}).get('dateTime', '')
current_end = existing_event.get('end', {}).get('dateTime', '')
current_summary = existing_event.get('summary', '')
current_description = existing_event.get('description', '')
current_location = existing_event.get('location', '')
context.logger.debug(f"Vergleiche Start: {current_start} vs {start_datetime}")
context.logger.debug(f"Vergleiche End: {current_end} vs {end_datetime}")
context.logger.debug(f"Vergleiche Summary: {current_summary} vs {summary}")
context.logger.debug(f"Vergleiche Description: {current_description} vs {description}")
context.logger.debug(f"Vergleiche Location: {current_location} vs {location}")
if (current_start == start_datetime and
current_end == end_datetime and
current_summary == summary and
current_description == description and
current_location == location):
context.logger.info(f"Termin {appointment.get('frNr')} unverändert, überspringe Google Update")
return None
context.logger.info(f"Termin {appointment.get('frNr')} geändert, aktualisiere in Google...")
event_body = {
'summary': summary,
'description': description,
'location': location,
'start': {
'dateTime': start_datetime,
'timeZone': 'Europe/Berlin',
},
'end': {
'dateTime': end_datetime,
'timeZone': 'Europe/Berlin',
}
}
# Event aktualisieren
context.logger.info("Sende Google Calendar Update Request...")
updated_event = await google_api_call_with_backoff(service.events().update(calendarId=calendar_id, eventId=existing_event['id'], body=event_body).execute)
context.logger.info(f"Termin {appointment.get('frNr')} in Google Calendar aktualisiert")
return updated_event
except Exception as e:
context.logger.error(f"Fehler beim Update des Google Events für Termin {appointment.get('frNr')}: {e}")
return None
async def update_advoware_appointment(advoware, frnr, change_token, data, context):
"""Aktualisiert einen Advoware-Termin mit Token-Validierung"""
context.logger.info(f"Aktualisiere Advoware Termin {frnr}...")
expected_token = generate_change_token(frnr)
if change_token != expected_token:
context.logger.error(f"Token-Validierung fehlgeschlagen für Update von Termin {frnr}: expected {expected_token}, got {change_token}")
raise ValueError("Invalid change token")
# Entferne frNr aus data, da es in der URL ist
data_copy = data.copy()
context.logger.debug(f"Sende PUT Request mit Daten: {data_copy}")
result = await advoware.api_call('api/v1/advonet/Termine', method='PUT', json_data=data_copy)
context.logger.info(f"Advoware Termin {frnr} aktualisiert")
return result
async def delete_advoware_appointment(advoware, frnr, change_token, context):
"""Löscht einen Advoware-Termin mit Token-Validierung"""
context.logger.info(f"Lösche Advoware Termin {frnr}...")
expected_token = generate_change_token(frnr)
if change_token != expected_token:
context.logger.error(f"Token-Validierung fehlgeschlagen für Löschung von Termin {frnr}: expected {expected_token}, got {change_token}")
raise ValueError("Invalid change token")
context.logger.debug("Sende DELETE Request...")
result = await advoware.api_call('api/v1/advonet/Termine', method='DELETE', params={'frnr': frnr})
context.logger.info(f"Advoware Termin {frnr} gelöscht")
return result
async def handler(event, context):
try:
# Konfiguration aus Event-Daten
data = event.get('data', {})
body = data.get('body', {})
full_content = body.get('full_content', True) # Default: volle Termindetails
context.logger.info(f"=== Calendar Sync Event gestartet ===")
context.logger.info(f"Event Data: {data}")
context.logger.info(f"Full Content: {full_content}, Triggered By: {body.get('triggered_by')}")
# Google Calendar Service initialisieren
context.logger.info("Initialisiere Google Calendar Service...")
service = await get_google_service(context)
if not service:
context.logger.warn("Google Calendar Service nicht verfügbar. Sync wird übersprungen.")
return {
'status': 200,
'body': {
'status': 'skipped',
'reason': 'Google Calendar credentials not configured',
'total_synced': 0
}
}
# Alle Mitarbeiter abrufen
context.logger.info("Rufe Advoware Mitarbeiter ab...")
employees = await get_advoware_employees(context)
if not employees:
context.logger.error("Keine Mitarbeiter gefunden. Sync abgebrochen.")
return {'status': 500, 'body': {'error': 'Keine Mitarbeiter gefunden'}}
total_synced = 0
deleted_frnrs = set() # Sammle frNr von gelöschten Terminen
for employee in employees:
kuerzel = employee.get('kuerzel') or employee.get('anwalt')
if not kuerzel:
context.logger.warn(f"Mitarbeiter ohne Kürzel übersprungen: {employee}")
continue
# Zum Testen nur "AI" synchronisieren
if kuerzel != 'AI':
context.logger.info(f"Mitarbeiter {kuerzel} übersprungen (nur AI im Testmodus)")
continue
context.logger.info(f"=== Verarbeite Mitarbeiter: {kuerzel} ===")
# Google Calendar sicherstellen
calendar_id = await ensure_google_calendar(service, kuerzel, context)
if not calendar_id:
context.logger.error(f"Google Calendar für {kuerzel} konnte nicht erstellt werden. Überspringe.")
continue
# Termine aus beiden Systemen abrufen
context.logger.info(f"Rufe Advoware Termine für {kuerzel} ab...")
advoware_appointments = await get_advoware_appointments(kuerzel, context)
context.logger.info(f"Rufe Google Events für {kuerzel} ab...")
google_events = await get_google_events(service, calendar_id, context)
# Advoware → Google syncen
context.logger.info("=== Advoware → Google Sync starten ===")
frnr_to_event = {}
for event in google_events:
desc = event.get('description', '')
if 'frNr: ' in desc:
parts = desc.split('frNr: ')
if len(parts) > 1:
frnr = parts[1].split('\n')[0]
frnr_to_event[frnr] = event
context.logger.debug(f"Google Event {event.get('id')} zu frNr {frnr} zugeordnet")
for appointment in advoware_appointments:
frnr = str(appointment.get('frNr'))
if frnr in deleted_frnrs:
context.logger.info(f"Termin {frnr} wurde gerade gelöscht, überspringe Google Sync")
continue
if frnr in frnr_to_event:
# Termin existiert bereits, aktualisieren
context.logger.info(f"Termin {frnr} existiert in Google, aktualisiere...")
existing_event = frnr_to_event[frnr]
await update_google_event(service, calendar_id, existing_event, appointment, context)
total_synced += 1
else:
# Neuen Termin erstellen
context.logger.info(f"Termin {frnr} neu in Advoware, erstelle in Google...")
await sync_appointment_to_google(service, calendar_id, appointment, full_content, context)
total_synced += 1
# Google → Advoware syncen
context.logger.info("=== Google → Advoware Sync starten ===")
for event in google_events:
context.logger.info(f"Verarbeite Google Event {event.get('id')}: {event.get('summary')}")
await sync_event_to_advoware(service, calendar_id, event, kuerzel, context)
# Löschungen handhaben: Google-initiale Termine, die in Google gelöscht wurden
context.logger.info("=== Löschungen behandeln ===")
google_frnrs_with_token = set()
for event in google_events:
desc = event.get('description', '')
if 'frNr: ' in desc and 'sync-token: ' in desc:
frnr_parts = desc.split('frNr: ')
if len(frnr_parts) > 1:
frnr = frnr_parts[1].split('\n')[0]
google_frnrs_with_token.add(frnr)
context.logger.debug(f"Google Event mit Token gefunden: frNr {frnr}")
for appointment in advoware_appointments:
notiz = appointment.get('notiz', '')
if '## no change below this line ##' in notiz and 'sync-token: ' in notiz:
frnr = str(appointment.get('frNr'))
# Token aus notiz extrahieren
parts = notiz.split('sync-token: ')
if len(parts) > 1:
stored_token = parts[1].split('\n')[0]
expected_token = generate_change_token(frnr)
context.logger.debug(f"Prüfe Löschung für Termin {frnr}: stored_token={stored_token}, expected={expected_token}, in_google={frnr in google_frnrs_with_token}")
if stored_token == expected_token and frnr not in google_frnrs_with_token:
# Lösche in Advoware
context.logger.info(f"Termin {frnr} in Google gelöscht, lösche in Advoware...")
advoware = AdvowareAPI(context)
await delete_advoware_appointment(advoware, frnr, stored_token, context)
deleted_frnrs.add(frnr)
else:
context.logger.debug(f"Termin {frnr} nicht gelöscht: Token mismatch oder noch in Google")
else:
context.logger.warn(f"Ungültiger sync-token in Termin {frnr}")
else:
context.logger.debug(f"Termin {frnr} hat keinen Token, überspringe Löschprüfung")
context.logger.info(f"=== Advoware Calendar Sync abgeschlossen. {total_synced} Termine synchronisiert. Gelöschte Termine: {len(deleted_frnrs)} ===")
return {
'status': 200,
'body': {
'status': 'completed',
'total_synced': total_synced,
'employees_processed': len([e for e in employees if e.get('kuerzel') or e.get('anwalt')]),
'deleted_appointments': len(deleted_frnrs)
}
}
except Exception as e:
context.logger.error(f"Fehler beim Advoware Calendar Sync: {e}")
return {
'status': 500,
'body': {
'error': 'Internal server error',
'details': str(e)
}
}