feat(calllog): Tags einer Gesprächsnotiz zuordnen (n:m)
Backend: - Neues CallLogTag-Model (Tabelle call_logs_tags) mit Relationship auf CallLog (cascade delete-orphan); to_dict liefert 'tags'. - Migration _db_V01_11 (idempotent via create_all). - _set_call_log_tags in create/update_call_log: ersetzt Tag-Zuordnungen, filtert is_parent-Tags, eliminiert Duplikate. - _parse_date wandelt ISO-Strings in date-Objekte (fixt autoflush- Crash bei Date-Spalten). Frontend: - iCallLog um tags (Lesen) und tag_ids (Schreiben) ergänzt. - Neues generisches MultiSelect-Widget (widgets/select.ts): Badges + Edit-Button, inline Modal mit Suchfeld, Checkbox-Liste, OK-Button; konfigurierbare Texte (placeholder, modalTitle, editTooltip). - Call_Log-Formular: Tag-Auswahl via MultiSelect, Submit via FormData.getAll; Tag-Badges in der Listenansicht hinter dem Betreff. Generated by Mistral Vibe. Co-Authored-By: Mistral Vibe <vibe@mistral.ai>
This commit is contained in:
1 parent
5abcb2d96c
commit
d30adf1579
6 files changed
+421
-9
No files matched your search
@@ -259,12 +259,19 @@ class CallLog(dbapp.Model):
|
||||
erledigt = Column(Boolean, nullable=False, default=False)
|
||||
|
||||
projekt = relationship('Projekt')
|
||||
# n:m-Beziehung zu Tags über die Verknüpfungstabelle call_logs_tags.
|
||||
# cascade='all, delete-orphan' stellt sicher, dass die Zuordnungen
|
||||
# automatisch gelöscht werden, wenn die Gesprächsnotiz gelöscht wird.
|
||||
tags_rel = relationship(
|
||||
'CallLogTag', cascade='all, delete-orphan', back_populates='call_log'
|
||||
)
|
||||
|
||||
def to_dict(self):
|
||||
"""
|
||||
Liefert die Gesprächsnotiz als Dictionary. Die Tabellenspalten werden
|
||||
über columns2dict serialisiert; projekt_name und kunden_name werden
|
||||
zusätzlich aus der Projekt-Relationship eingefügt.
|
||||
zusätzlich aus der Projekt-Relationship eingefügt. Die zugeordneten
|
||||
Tags werden als Liste von Tag-Dictionaries unter 'tags' ergänzt.
|
||||
"""
|
||||
result = columns2dict(self)
|
||||
if self.projekt:
|
||||
@@ -273,9 +280,33 @@ class CallLog(dbapp.Model):
|
||||
else:
|
||||
result['projekt_name'] = None
|
||||
result['kunden_name'] = None
|
||||
# Zugeordnete Tags serialisieren (nur is_parent=False sind zuordenbar).
|
||||
result['tags'] = [clt.tag.to_dict() for clt in self.tags_rel]
|
||||
return result
|
||||
|
||||
|
||||
class CallLogTag(dbapp.Model):
|
||||
"""
|
||||
Verknüpfungstabelle für die n:m-Beziehung zwischen Gesprächsnotizen
|
||||
(CallLog) und Tags. Jede Zeile verknüpft eine Gesprächsnotiz mit einem
|
||||
Tag. Der UniqueConstraint stellt sicher, dass jeder Tag nur einmal pro
|
||||
Gesprächsnotiz zugeordnet werden kann.
|
||||
"""
|
||||
__tablename__ = 'call_logs_tags'
|
||||
__bind_key__ = BINDKEY
|
||||
__table_args__ = (
|
||||
UniqueConstraint('call_log_id', 'tag_id', name='uq_calllog_tag'),
|
||||
Index('ix_calllogtag_call_log_id', 'call_log_id'),
|
||||
Index('ix_calllogtag_tag_id', 'tag_id'),
|
||||
)
|
||||
id = Column(Integer, primary_key=True)
|
||||
call_log_id = Column(Integer, ForeignKey('call_logs.id'), nullable=False)
|
||||
tag_id = Column(Integer, ForeignKey('tags.id'), nullable=False)
|
||||
|
||||
call_log = relationship('CallLog', back_populates='tags_rel')
|
||||
tag = relationship('Tag')
|
||||
|
||||
|
||||
class Tag(dbapp.Model):
|
||||
"""
|
||||
Hierarchischer Tag (Taxonomie). Tags koennen beliebig verschachtelt
|
||||
|
||||
@@ -9,14 +9,68 @@ dort über den Decorator authorisation_required.
|
||||
:date: 2026-09-04
|
||||
:version: 1.0
|
||||
"""
|
||||
from datetime import date
|
||||
|
||||
from db import dbapp
|
||||
from crm.model import CallLog, Projekt, ProjektUser
|
||||
from crm.model import CallLog, CallLogTag, 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_call_logs(projekt_id: int | None = None, user_id: int | None = None):
|
||||
"""
|
||||
Liefert Gesprächsnotizen, sortiert nach Datum absteigend.
|
||||
@@ -94,15 +148,16 @@ def create_call_log(data: dict) -> CallLog:
|
||||
)
|
||||
call_log = CallLog()
|
||||
call_log.projekt_id = int(projekt_id)
|
||||
call_log.datum = (data.get("datum") or "").strip() or None
|
||||
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 = (data.get("wiedervorlage_datum") 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
|
||||
@@ -127,15 +182,16 @@ def update_call_log(data: dict) -> CallLog:
|
||||
htmlstatus=HtmlStatusCode.Bad_Request,
|
||||
)
|
||||
call_log.projekt_id = int(projekt_id)
|
||||
call_log.datum = (data.get("datum") or "").strip() or None
|
||||
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 = (data.get("wiedervorlage_datum") 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
|
||||
|
||||
|
||||
@@ -232,6 +232,17 @@ def _db_V01_10():
|
||||
session.commit()
|
||||
|
||||
|
||||
def _db_V01_11():
|
||||
"""
|
||||
Legt die Verknüpfungstabelle 'call_logs_tags' für die n:m-Beziehung
|
||||
zwischen Gesprächsnotizen und Tags an. create_all erzeugt nur fehlende
|
||||
Tabellen, bestehende bleiben unangetastet (idempotent).
|
||||
"""
|
||||
dbapp.create_all(bind_key=BINDKEY)
|
||||
_updatesysteminfo('01.11')
|
||||
session.commit()
|
||||
|
||||
|
||||
def checkupdates():
|
||||
"""
|
||||
Ermittelt die aktuelle DB-Version aus der Systeminfo-Tabelle und führt alle
|
||||
|
||||
Reference in new issue
Block a user