""" Geschäftslogik für die Entität CallLog (Gesprächsnotiz). Diese Funktionen kapseln den DB-Zugriff und die fachlichen Validierungen. Aufgerufen werden sie aus crm/routes.py; die Autorisierungsprüfung erfolgt dort über den Decorator authorisation_required. :author: Mistral Vibe :date: 2026-09-04 :version: 1.0 """ from datetime import date from db import dbapp from crm.model import CallLog, CallLogTag, Kontaktadresse, Projekt, ProjektUser, Tag from auth.model.errors import ApplicationError, HtmlStatusCode session = dbapp.session def _parse_date(value: str | None) -> date | None: """ Wandelt einen ISO-Datum-String (yyyy-MM-dd) in ein date-Objekt um. Leere Strings und None werden zu None konvertiert (Spalte ist nullable). """ if not value: return None value = value.strip() if not value: return None try: return date.fromisoformat(value) except ValueError: return None def _set_call_log_tags(call_log: CallLog, tag_ids: list) -> None: """ Ersetzt die Tag-Zuordnungen einer Gesprächsnotiz durch die übergebene Liste von Tag-IDs. Bestehende Zuordnungen werden gelöscht, danach werden die neuen Zuordnungen angelegt. Nur Tags, die existieren und nicht reine Gruppierungsknoten (is_parent=True) sind, werden zugeordnet. Duplikate in der Liste werden durch ein Set eliminiert. """ # Bestehende Zuordnungen entfernen. for clt in list(call_log.tags_rel): session.delete(clt) call_log.tags_rel = [] # Neue Zuordnungen anlegen. seen = set() for tag_id in tag_ids or []: try: tag_id = int(tag_id) except (TypeError, ValueError): continue if tag_id in seen: continue seen.add(tag_id) tag = Tag.query.get(tag_id) if tag is None or tag.is_parent: # Nicht zuordenbar — überspringen. continue clt = CallLogTag() clt.call_log = call_log clt.tag = tag session.add(clt) def _get_validierte_kontaktadresse_id(data: dict, projekt_id) -> int: """ Liest kontaktadresse_id aus dem data-Dict und validiert sie: - Pflichtfeld: fehlt sie oder ist sie leer, wird Bad Request geliefert. - Die Kontaktadresse muss existieren und zum selben Projekt gehören. Liefert die geprüfte ID als int zurück. """ kontaktadresse_id = data.get("kontaktadresse_id") if not kontaktadresse_id: raise ApplicationError( "Es muss eine Kontaktadresse ausgewählt werden.", htmlstatus=HtmlStatusCode.Bad_Request, ) kontaktadresse = Kontaktadresse.query.get(int(kontaktadresse_id)) if kontaktadresse is None or kontaktadresse.projekt_id != int(projekt_id): raise ApplicationError( "Die ausgewählte Kontaktadresse existiert nicht oder gehört nicht zum Projekt.", htmlstatus=HtmlStatusCode.Bad_Request, ) return kontaktadresse.id def get_call_logs(projekt_id: int | None = None, user_id: int | None = None): """ Liefert Gesprächsnotizen, sortiert nach Datum absteigend. Ist projekt_id gesetzt, werden nur die Notizen dieses Projekts zurückgegeben (serverseitige Filterung). Ohne projekt_id werden alle Notizen geliefert. Ist user_id gesetzt, werden nur Notizen von Projekten geliefert, denen der Nutzer über projekt_users zugeordnet ist (Sichtbarkeits-Filterung). SysAdmin und AppAdmin übergeben user_id=None und sehen alle Notizen. """ query = CallLog.query if projekt_id is not None: query = query.filter(CallLog.projekt_id == projekt_id) if user_id is not None: # Nur Notizen von Projekten, denen der Nutzer zugeordnet ist. query = query.filter( CallLog.projekt_id.in_( dbapp.session.query(ProjektUser.projekt_id).filter( ProjektUser.user_id == user_id ) ) ) return query.order_by(CallLog.datum.desc()).all() def get_call_log(call_log_id: int, user_id: int | None = None) -> CallLog: """ Liefert eine einzelne Gesprächsnotiz anhand ihrer ID. Wirft ApplicationError (404), falls die Notiz nicht existiert. Ist user_id gesetzt, wird zusätzlich geprüft, ob der Nutzer dem Projekt der Notiz zugeordnet ist; sonst wird 404 geliefert. """ call_log = CallLog.query.get(call_log_id) if call_log is None: raise ApplicationError( "Gesprächsnotiz mit der ID {} wurde nicht gefunden.".format(call_log_id), htmlstatus=HtmlStatusCode.Not_Found, ) if user_id is not None: zuordnung = ProjektUser.query.filter( ProjektUser.projekt_id == call_log.projekt_id, ProjektUser.user_id == user_id, ).first() if zuordnung is None: raise ApplicationError( "Gesprächsnotiz mit der ID {} wurde nicht gefunden.".format(call_log_id), htmlstatus=HtmlStatusCode.Not_Found, ) return call_log def create_call_log(data: dict) -> CallLog: """ Legt eine neue Gesprächsnotiz an. Die vom Client übergebene ID wird ignoriert; die ID wird serverseitig per Auto-Increment vergeben. - data: dict mit den Feldern projekt_id, kontaktadresse_id, datum, uhrzeit_beginn, dauer, betreff, name_ansprechpartner, telefon, notiz, wiedervorlage_datum, erledigt, tag_ids """ projekt_id = data.get("projekt_id") if projekt_id is None: raise ApplicationError( "Es muss ein Projekt ausgewählt werden.", htmlstatus=HtmlStatusCode.Bad_Request, ) projekt = Projekt.query.get(int(projekt_id)) if projekt is None: raise ApplicationError( "Das ausgewählte Projekt existiert nicht.", htmlstatus=HtmlStatusCode.Bad_Request, ) kontaktadresse_id = _get_validierte_kontaktadresse_id(data, projekt_id) call_log = CallLog() call_log.projekt_id = int(projekt_id) call_log.kontaktadresse_id = kontaktadresse_id call_log.datum = _parse_date(data.get("datum")) call_log.uhrzeit_beginn = (data.get("uhrzeit_beginn") or "").strip() or None call_log.dauer = (data.get("dauer") or "").strip() or None call_log.betreff = (data.get("betreff") or "").strip() or None call_log.name_ansprechpartner = (data.get("name_ansprechpartner") or "").strip() or None call_log.telefon = (data.get("telefon") or "").strip() or None call_log.notiz = (data.get("notiz") or "").strip() or None call_log.wiedervorlage_datum = _parse_date(data.get("wiedervorlage_datum")) call_log.erledigt = bool(data.get("erledigt", False)) _set_call_log_tags(call_log, data.get("tag_ids")) session.add(call_log) session.commit() return call_log def update_call_log(data: dict) -> CallLog: """ Aktualisiert eine bestehende Gesprächsnotiz. Die ID im data-Dict bestimmt den zu ändernden Datensatz. """ call_log = get_call_log(data["id"]) projekt_id = data.get("projekt_id") if projekt_id is None: raise ApplicationError( "Es muss ein Projekt ausgewählt werden.", htmlstatus=HtmlStatusCode.Bad_Request, ) projekt = Projekt.query.get(int(projekt_id)) if projekt is None: raise ApplicationError( "Das ausgewählte Projekt existiert nicht.", htmlstatus=HtmlStatusCode.Bad_Request, ) kontaktadresse_id = _get_validierte_kontaktadresse_id(data, projekt_id) call_log.projekt_id = int(projekt_id) call_log.kontaktadresse_id = kontaktadresse_id call_log.datum = _parse_date(data.get("datum")) call_log.uhrzeit_beginn = (data.get("uhrzeit_beginn") or "").strip() or None call_log.dauer = (data.get("dauer") or "").strip() or None call_log.betreff = (data.get("betreff") or "").strip() or None call_log.name_ansprechpartner = (data.get("name_ansprechpartner") or "").strip() or None call_log.telefon = (data.get("telefon") or "").strip() or None call_log.notiz = (data.get("notiz") or "").strip() or None call_log.wiedervorlage_datum = _parse_date(data.get("wiedervorlage_datum")) call_log.erledigt = bool(data.get("erledigt", False)) _set_call_log_tags(call_log, data.get("tag_ids")) session.commit() return call_log def delete_call_log(call_log_id: int): """ Löscht eine Gesprächsnotiz anhand ihrer ID. Wirft ApplicationError (404), falls die Notiz nicht existiert. """ call_log = get_call_log(call_log_id) session.delete(call_log) session.commit()