feat(calllog): kontaktadresse_id als Pflicht-FK + is_erledigt()-Methoden
- Migration V01_13: Spalte kontaktadresse_id auf call_logs mit Backfill über die erste Kontaktadresse des Projekts; Commit vor dem Systeminfo-Update (Muster wie V01_08) gegen 'database is locked'. - CallLog gehört zu genau einer Kontaktadresse (FK, Pflichtfeld); create/update validieren Existenz und Projekt-Zugehörigkeit. - is_erledigt() auf Kontaktadresse (mind. ein erledigend-Tag reicht) und Projekt (False ohne Kontakte, sonst alle erledigt); beide in to_dict() serialisiert. - Client: kontaktadresse_id in Notizmaske/Submit (required), Interfaces um is_erledigt erweitert. Generated by Mistral Vibe. Co-Authored-By: Mistral Vibe <vibe@mistral.ai>
This commit is contained in:
1 parent
24b492c5c1
commit
977b64292c
9 files changed
+174
-18
No files matched your search
@@ -148,15 +148,33 @@ class Projekt(dbapp.Model):
|
|||||||
startdatum = Column(Date, nullable=True)
|
startdatum = Column(Date, nullable=True)
|
||||||
|
|
||||||
kunde = relationship('Kunde')
|
kunde = relationship('Kunde')
|
||||||
|
# Kontaktadressen dieses Projekts (Akquise-Ziele). Die Rückrichtung
|
||||||
|
# ist Kontaktadresse.projekt (back_populates).
|
||||||
|
kontaktadressen = relationship('Kontaktadresse', back_populates='projekt')
|
||||||
|
|
||||||
|
def is_erledigt(self):
|
||||||
|
"""
|
||||||
|
Prüft dynamisch, ob das Projekt als erledigt gilt.
|
||||||
|
|
||||||
|
True, wenn das Projekt Kontaktadressen hat und alle davon erledigt
|
||||||
|
sind. Ein Projekt ohne Kontaktadressen ist offen (False), da
|
||||||
|
Kontaktadressen erst nach dem Anlegen des Projekts erfasst werden
|
||||||
|
können — ein neues Projekt ist somit zunächst offen.
|
||||||
|
"""
|
||||||
|
if not self.kontaktadressen:
|
||||||
|
return False
|
||||||
|
return all(k.is_erledigt() for k in self.kontaktadressen)
|
||||||
|
|
||||||
def to_dict(self):
|
def to_dict(self):
|
||||||
"""
|
"""
|
||||||
Liefert die Projektdaten als Dictionary. Die Tabellenspalten werden
|
Liefert die Projektdaten als Dictionary. Die Tabellenspalten werden
|
||||||
über columns2dict serialisiert; kunden_name wird zusätzlich aus der
|
über columns2dict serialisiert; kunden_name wird zusätzlich aus der
|
||||||
Kunde-Relationship eingefügt.
|
Kunde-Relationship eingefügt. is_erledigt wird dynamisch aus den
|
||||||
|
zugeordneten Kontaktadressen berechnet.
|
||||||
"""
|
"""
|
||||||
result = columns2dict(self)
|
result = columns2dict(self)
|
||||||
result['kunden_name'] = self.kunde.name if self.kunde else None
|
result['kunden_name'] = self.kunde.name if self.kunde else None
|
||||||
|
result['is_erledigt'] = self.is_erledigt()
|
||||||
return result
|
return result
|
||||||
|
|
||||||
|
|
||||||
@@ -220,34 +238,64 @@ class Kontaktadresse(dbapp.Model):
|
|||||||
land = Column(String(80), nullable=True)
|
land = Column(String(80), nullable=True)
|
||||||
notiz = Column(String(2000), nullable=True)
|
notiz = Column(String(2000), nullable=True)
|
||||||
|
|
||||||
projekt = relationship('Projekt')
|
projekt = relationship('Projekt', back_populates='kontaktadressen')
|
||||||
|
# Gesprächsnotizen, die zu diesem Kontakt erfasst wurden. Die
|
||||||
|
# Rückrichtung ist CallLog.kontaktadresse (back_populates); die
|
||||||
|
# Join-Bedingung ergibt sich aus dem Fremdschlüssel
|
||||||
|
# call_logs.kontaktadresse_id -> kontaktadressen.id.
|
||||||
|
call_logs = relationship('CallLog', back_populates='kontaktadresse')
|
||||||
|
|
||||||
|
def is_erledigt(self):
|
||||||
|
"""
|
||||||
|
Prüft dynamisch, ob der Kontakt als erledigt gilt.
|
||||||
|
|
||||||
|
True, sobald mindestens eine Gesprächsnotiz des Kontakts einen Tag
|
||||||
|
mit is_erledigend=True hat. Spätere Notizen ohne erledigend-Tag
|
||||||
|
setzen den Status nicht zurück. False bei keinen Notizen oder wenn
|
||||||
|
keine Notiz einen erledigend-Tag hat.
|
||||||
|
"""
|
||||||
|
return any(
|
||||||
|
clt.tag.is_erledigend
|
||||||
|
for notiz in self.call_logs
|
||||||
|
for clt in notiz.tags_rel
|
||||||
|
)
|
||||||
|
|
||||||
def to_dict(self):
|
def to_dict(self):
|
||||||
"""
|
"""
|
||||||
Liefert die Kontaktdaten als Dictionary. Die Tabellenspalten werden
|
Liefert die Kontaktdaten als Dictionary. Die Tabellenspalten werden
|
||||||
über columns2dict serialisiert; projekt_name wird zusätzlich aus der
|
über columns2dict serialisiert; projekt_name wird zusätzlich aus der
|
||||||
Projekt-Relationship eingefügt.
|
Projekt-Relationship eingefügt. is_erledigt wird dynamisch aus den
|
||||||
|
Gesprächsnotizen des Kontakts berechnet.
|
||||||
"""
|
"""
|
||||||
result = columns2dict(self)
|
result = columns2dict(self)
|
||||||
result['projekt_name'] = self.projekt.projektname if self.projekt else None
|
result['projekt_name'] = self.projekt.projektname if self.projekt else None
|
||||||
|
result['is_erledigt'] = self.is_erledigt()
|
||||||
return result
|
return result
|
||||||
|
|
||||||
|
|
||||||
class CallLog(dbapp.Model):
|
class CallLog(dbapp.Model):
|
||||||
"""
|
"""
|
||||||
Gesprächsnotiz für ein Projekt. Jede Gesprächsnotiz ist genau einem
|
Gesprächsnotiz für einen Kontakt. Jede Gesprächsnotiz ist genau einem
|
||||||
Projekt zugeordnet (projekt_id). Der Projektname (projekt_name) und
|
Projekt (projekt_id) und genau einer Kontaktadresse (kontaktadresse_id)
|
||||||
der Kundenname (kunden_name) sind keine eigenen Spalten, sondern
|
zugeordnet. Der Projektname (projekt_name) und der Kundenname
|
||||||
werden über die Projekt-Relationship abgeleitet und nur in to_dict()
|
(kunden_name) sind keine eigenen Spalten, sondern werden über die
|
||||||
für die JSON-Serialisierung ergänzt.
|
Projekt-Relationship abgeleitet und nur in to_dict() für die
|
||||||
|
JSON-Serialisierung ergänzt.
|
||||||
"""
|
"""
|
||||||
__tablename__ = 'call_logs'
|
__tablename__ = 'call_logs'
|
||||||
__bind_key__ = BINDKEY
|
__bind_key__ = BINDKEY
|
||||||
__table_args__ = (
|
__table_args__ = (
|
||||||
Index('ix_call_logs_projekt_id', 'projekt_id'),
|
Index('ix_call_logs_projekt_id', 'projekt_id'),
|
||||||
|
Index('ix_call_logs_kontaktadresse_id', 'kontaktadresse_id'),
|
||||||
)
|
)
|
||||||
id = Column(Integer, primary_key=True)
|
id = Column(Integer, primary_key=True)
|
||||||
projekt_id = Column(Integer, ForeignKey('projekte.id'), nullable=False)
|
projekt_id = Column(Integer, ForeignKey('projekte.id'), nullable=False)
|
||||||
|
# Jede Gesprächsnotiz gehört zu genau einer Kontaktadresse
|
||||||
|
# (Pflichtfeld). nullable=False im Modell; in bestehenden Datenbanken
|
||||||
|
# ist die physische Spalte nullable (SQLite kann kein nachträgliches
|
||||||
|
# NOT NULL) — die Pflicht wird über die Server-Validierung in
|
||||||
|
# crm.model.call_log durchgesetzt.
|
||||||
|
kontaktadresse_id = Column(Integer, ForeignKey('kontaktadressen.id'), nullable=False)
|
||||||
datum = Column(Date, nullable=True)
|
datum = Column(Date, nullable=True)
|
||||||
uhrzeit_beginn = Column(String(10), nullable=True)
|
uhrzeit_beginn = Column(String(10), nullable=True)
|
||||||
dauer = Column(String(10), nullable=True)
|
dauer = Column(String(10), nullable=True)
|
||||||
@@ -259,6 +307,7 @@ class CallLog(dbapp.Model):
|
|||||||
erledigt = Column(Boolean, nullable=False, default=False)
|
erledigt = Column(Boolean, nullable=False, default=False)
|
||||||
|
|
||||||
projekt = relationship('Projekt')
|
projekt = relationship('Projekt')
|
||||||
|
kontaktadresse = relationship('Kontaktadresse', back_populates='call_logs')
|
||||||
# n:m-Beziehung zu Tags über die Verknüpfungstabelle call_logs_tags.
|
# n:m-Beziehung zu Tags über die Verknüpfungstabelle call_logs_tags.
|
||||||
# cascade='all, delete-orphan' stellt sicher, dass die Zuordnungen
|
# cascade='all, delete-orphan' stellt sicher, dass die Zuordnungen
|
||||||
# automatisch gelöscht werden, wenn die Gesprächsnotiz gelöscht wird.
|
# automatisch gelöscht werden, wenn die Gesprächsnotiz gelöscht wird.
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ dort über den Decorator authorisation_required.
|
|||||||
from datetime import date
|
from datetime import date
|
||||||
|
|
||||||
from db import dbapp
|
from db import dbapp
|
||||||
from crm.model import CallLog, CallLogTag, Projekt, ProjektUser, Tag
|
from crm.model import CallLog, CallLogTag, Kontaktadresse, Projekt, ProjektUser, Tag
|
||||||
|
|
||||||
from auth.model.errors import ApplicationError, HtmlStatusCode
|
from auth.model.errors import ApplicationError, HtmlStatusCode
|
||||||
|
|
||||||
@@ -71,6 +71,29 @@ def _set_call_log_tags(call_log: CallLog, tag_ids: list) -> None:
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
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):
|
def get_call_logs(projekt_id: int | None = None, user_id: int | None = None):
|
||||||
"""
|
"""
|
||||||
Liefert Gesprächsnotizen, sortiert nach Datum absteigend.
|
Liefert Gesprächsnotizen, sortiert nach Datum absteigend.
|
||||||
@@ -130,9 +153,9 @@ def create_call_log(data: dict) -> CallLog:
|
|||||||
Legt eine neue Gesprächsnotiz an. Die vom Client übergebene ID wird
|
Legt eine neue Gesprächsnotiz an. Die vom Client übergebene ID wird
|
||||||
ignoriert; die ID wird serverseitig per Auto-Increment vergeben.
|
ignoriert; die ID wird serverseitig per Auto-Increment vergeben.
|
||||||
|
|
||||||
- data: dict mit den Feldern projekt_id, datum, uhrzeit_beginn, dauer,
|
- data: dict mit den Feldern projekt_id, kontaktadresse_id, datum,
|
||||||
betreff, name_ansprechpartner, telefon, notiz,
|
uhrzeit_beginn, dauer, betreff, name_ansprechpartner, telefon,
|
||||||
wiedervorlage_datum, erledigt
|
notiz, wiedervorlage_datum, erledigt, tag_ids
|
||||||
"""
|
"""
|
||||||
projekt_id = data.get("projekt_id")
|
projekt_id = data.get("projekt_id")
|
||||||
if projekt_id is None:
|
if projekt_id is None:
|
||||||
@@ -146,8 +169,10 @@ def create_call_log(data: dict) -> CallLog:
|
|||||||
"Das ausgewählte Projekt existiert nicht.",
|
"Das ausgewählte Projekt existiert nicht.",
|
||||||
htmlstatus=HtmlStatusCode.Bad_Request,
|
htmlstatus=HtmlStatusCode.Bad_Request,
|
||||||
)
|
)
|
||||||
|
kontaktadresse_id = _get_validierte_kontaktadresse_id(data, projekt_id)
|
||||||
call_log = CallLog()
|
call_log = CallLog()
|
||||||
call_log.projekt_id = int(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.datum = _parse_date(data.get("datum"))
|
||||||
call_log.uhrzeit_beginn = (data.get("uhrzeit_beginn") or "").strip() or None
|
call_log.uhrzeit_beginn = (data.get("uhrzeit_beginn") or "").strip() or None
|
||||||
call_log.dauer = (data.get("dauer") or "").strip() or None
|
call_log.dauer = (data.get("dauer") or "").strip() or None
|
||||||
@@ -181,7 +206,9 @@ def update_call_log(data: dict) -> CallLog:
|
|||||||
"Das ausgewählte Projekt existiert nicht.",
|
"Das ausgewählte Projekt existiert nicht.",
|
||||||
htmlstatus=HtmlStatusCode.Bad_Request,
|
htmlstatus=HtmlStatusCode.Bad_Request,
|
||||||
)
|
)
|
||||||
|
kontaktadresse_id = _get_validierte_kontaktadresse_id(data, projekt_id)
|
||||||
call_log.projekt_id = int(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.datum = _parse_date(data.get("datum"))
|
||||||
call_log.uhrzeit_beginn = (data.get("uhrzeit_beginn") or "").strip() or None
|
call_log.uhrzeit_beginn = (data.get("uhrzeit_beginn") or "").strip() or None
|
||||||
call_log.dauer = (data.get("dauer") or "").strip() or None
|
call_log.dauer = (data.get("dauer") or "").strip() or None
|
||||||
|
|||||||
@@ -275,6 +275,56 @@ def _db_V01_12():
|
|||||||
session.commit()
|
session.commit()
|
||||||
|
|
||||||
|
|
||||||
|
def _db_V01_13():
|
||||||
|
"""
|
||||||
|
Fügt der Tabelle 'call_logs' die Spalte 'kontaktadresse_id' hinzu:
|
||||||
|
Jede Gesprächsnotiz gehört zu genau einer Kontaktadresse
|
||||||
|
(Fremdschlüssel -> kontaktadressen.id, Pflichtfeld).
|
||||||
|
|
||||||
|
Da SQLite kein nachträgliches NOT NULL auf einer bestehenden Spalte
|
||||||
|
setzen kann, wird die Spalte als nullable angelegt und anschließend
|
||||||
|
backgefüllt: Für bestehende CallLogs ohne Kontakt-Bezug wird die
|
||||||
|
erste Kontaktadresse des gleichen Projekts eingetragen. Das NOT NULL
|
||||||
|
ist im Modell deklariert und wird über die Server-Validierung in
|
||||||
|
crm.model.call_log durchgesetzt.
|
||||||
|
|
||||||
|
Idempotent: Bei Neuinstallation hat create_all die Spalte bereits
|
||||||
|
angelegt — in diesem Fall wird das ALTER TABLE übersprungen. Der
|
||||||
|
Backfill und der Index sind ebenfalls idempotent.
|
||||||
|
"""
|
||||||
|
from sqlalchemy import text
|
||||||
|
|
||||||
|
result = session.execute(text("PRAGMA table_info(call_logs)"))
|
||||||
|
spalten = {row[1] for row in result}
|
||||||
|
if 'kontaktadresse_id' not in spalten:
|
||||||
|
session.execute(text(
|
||||||
|
"ALTER TABLE call_logs ADD COLUMN kontaktadresse_id INTEGER"
|
||||||
|
))
|
||||||
|
# Backfill: erste Kontaktadresse des Projekts eintragen, falls
|
||||||
|
# noch keine kontaktadresse_id gesetzt ist.
|
||||||
|
session.execute(text(
|
||||||
|
"UPDATE call_logs SET kontaktadresse_id = ("
|
||||||
|
" SELECT ka.id FROM kontaktadressen ka"
|
||||||
|
" WHERE ka.projekt_id = call_logs.projekt_id"
|
||||||
|
" LIMIT 1"
|
||||||
|
") WHERE kontaktadresse_id IS NULL"
|
||||||
|
))
|
||||||
|
session.execute(text(
|
||||||
|
"CREATE INDEX IF NOT EXISTS ix_call_logs_kontaktadresse_id"
|
||||||
|
" ON call_logs (kontaktadresse_id)"
|
||||||
|
))
|
||||||
|
# Roh-SQL-Änderungen committen, BEVOR die Systeminfo aktualisiert wird
|
||||||
|
# (dasselbe Muster wie _db_V01_08): Das Backfill-UPDATE öffnet eine
|
||||||
|
# Schreib-Transaktion auf der Default-Bind-Verbindung —
|
||||||
|
# SQLALCHEMY_DATABASE_URI zeigt auf dieselbe Datei wie der appdb-Bind.
|
||||||
|
# Das ORM-Update der Systeminfo läuft über die appdb-Bind-Verbindung;
|
||||||
|
# ohne dieses Commit blockiert die offene Transaktion das UPDATE
|
||||||
|
# ('database is locked').
|
||||||
|
session.commit()
|
||||||
|
_updatesysteminfo('01.13')
|
||||||
|
session.commit()
|
||||||
|
|
||||||
|
|
||||||
def checkupdates():
|
def checkupdates():
|
||||||
"""
|
"""
|
||||||
Ermittelt die aktuelle DB-Version aus der Systeminfo-Tabelle und führt alle
|
Ermittelt die aktuelle DB-Version aus der Systeminfo-Tabelle und führt alle
|
||||||
|
|||||||
@@ -189,6 +189,7 @@ export const Call_Log: m.ClosureComponent = (vnode) => {
|
|||||||
let currentLog: {
|
let currentLog: {
|
||||||
id: number
|
id: number
|
||||||
projekt_id: number
|
projekt_id: number
|
||||||
|
kontaktadresse_id: number
|
||||||
projekt_name: string
|
projekt_name: string
|
||||||
kunden_name: string
|
kunden_name: string
|
||||||
datum: string
|
datum: string
|
||||||
@@ -203,6 +204,7 @@ export const Call_Log: m.ClosureComponent = (vnode) => {
|
|||||||
} = {
|
} = {
|
||||||
id: -1,
|
id: -1,
|
||||||
projekt_id: -1,
|
projekt_id: -1,
|
||||||
|
kontaktadresse_id: -1,
|
||||||
projekt_name: "",
|
projekt_name: "",
|
||||||
kunden_name: "",
|
kunden_name: "",
|
||||||
datum: "",
|
datum: "",
|
||||||
@@ -315,7 +317,7 @@ export const Call_Log: m.ClosureComponent = (vnode) => {
|
|||||||
fetchCallLogById(id).then((result) => {
|
fetchCallLogById(id).then((result) => {
|
||||||
currentLog = result
|
currentLog = result
|
||||||
projektVoreingestellt = true
|
projektVoreingestellt = true
|
||||||
ansprechpartnerVoreingestellt = currentLog.name_ansprechpartner !== ""
|
ansprechpartnerVoreingestellt = currentLog.kontaktadresse_id > 0
|
||||||
selectedTagIds = (result.tags || []).map((t: iTag) => t.id)
|
selectedTagIds = (result.tags || []).map((t: iTag) => t.id)
|
||||||
}).catch((e) => showServerError(e))
|
}).catch((e) => showServerError(e))
|
||||||
} else {
|
} else {
|
||||||
@@ -332,7 +334,10 @@ export const Call_Log: m.ClosureComponent = (vnode) => {
|
|||||||
currentLog.notiz = routeParams && routeParams.notiz ? routeParams.notiz : ""
|
currentLog.notiz = routeParams && routeParams.notiz ? routeParams.notiz : ""
|
||||||
currentLog.wiedervorlage_datum = routeParams && routeParams.wiedervorlage_datum ? routeParams.wiedervorlage_datum : ""
|
currentLog.wiedervorlage_datum = routeParams && routeParams.wiedervorlage_datum ? routeParams.wiedervorlage_datum : ""
|
||||||
currentLog.erledigt = routeParams && (routeParams.erledigt === true || routeParams.erledigt === "true")
|
currentLog.erledigt = routeParams && (routeParams.erledigt === true || routeParams.erledigt === "true")
|
||||||
ansprechpartnerVoreingestellt = currentLog.name_ansprechpartner !== ""
|
// Kontaktadresse-ID aus den Route-Params übernehmen, wenn die
|
||||||
|
// Maske aus der Kontaktadressenliste geöffnet wurde.
|
||||||
|
currentLog.kontaktadresse_id = routeParams && routeParams.kontaktadresse_id ? parseInt(routeParams.kontaktadresse_id) : -1
|
||||||
|
ansprechpartnerVoreingestellt = currentLog.kontaktadresse_id > 0
|
||||||
// Bei Neuanlage Startzeit merken und Dauer-Timer starten.
|
// Bei Neuanlage Startzeit merken und Dauer-Timer starten.
|
||||||
startZeit = new Date()
|
startZeit = new Date()
|
||||||
timerId = window.setInterval(() => {
|
timerId = window.setInterval(() => {
|
||||||
@@ -386,6 +391,12 @@ export const Call_Log: m.ClosureComponent = (vnode) => {
|
|||||||
projekt_id: projektVoreingestellt
|
projekt_id: projektVoreingestellt
|
||||||
? currentLog.projekt_id
|
? currentLog.projekt_id
|
||||||
: parseInt((formData.get("projekt_id_select") as string) || "-1"),
|
: parseInt((formData.get("projekt_id_select") as string) || "-1"),
|
||||||
|
// Pflichtfeld: voreingestellt aus dem currentLog, sonst
|
||||||
|
// aus dem Select-Widget (kontaktadresse_id ist im Server
|
||||||
|
// ein Pflichtfeld, ohne wird Bad Request geliefert).
|
||||||
|
kontaktadresse_id: ansprechpartnerVoreingestellt
|
||||||
|
? currentLog.kontaktadresse_id
|
||||||
|
: parseInt((formData.get("kontaktadresse_id") as string) || "-1"),
|
||||||
projekt_name: "",
|
projekt_name: "",
|
||||||
kunden_name: "",
|
kunden_name: "",
|
||||||
datum: currentLog.datum || "",
|
datum: currentLog.datum || "",
|
||||||
@@ -501,6 +512,7 @@ export const Call_Log: m.ClosureComponent = (vnode) => {
|
|||||||
name: "kontaktadresse_id",
|
name: "kontaktadresse_id",
|
||||||
value: -1,
|
value: -1,
|
||||||
options: kontaktadressenOptionen,
|
options: kontaktadressenOptionen,
|
||||||
|
required: true,
|
||||||
onchanged: (kontaktadresseId: number) => {
|
onchanged: (kontaktadresseId: number) => {
|
||||||
const ka = kontaktadressenListe.find((k: iKontaktadresse) => k.id === kontaktadresseId)
|
const ka = kontaktadressenListe.find((k: iKontaktadresse) => k.id === kontaktadresseId)
|
||||||
if (ka) {
|
if (ka) {
|
||||||
|
|||||||
@@ -63,6 +63,8 @@ export const kontaktadresseliste: m.ClosureComponent = (vnode) => {
|
|||||||
id: -1,
|
id: -1,
|
||||||
projekt_id: kontaktadresse.projekt_id,
|
projekt_id: kontaktadresse.projekt_id,
|
||||||
projekt_name: kontaktadresse.projekt_name,
|
projekt_name: kontaktadresse.projekt_name,
|
||||||
|
// Kontaktadresse als Pflichtfeld an die Notizmaske durchreichen
|
||||||
|
kontaktadresse_id: kontaktadresse.id,
|
||||||
datum: datum,
|
datum: datum,
|
||||||
uhrzeit_beginn: uhrzeit,
|
uhrzeit_beginn: uhrzeit,
|
||||||
dauer: "",
|
dauer: "",
|
||||||
@@ -246,7 +248,9 @@ export const Kontaktadresse: m.ClosureComponent = (vnode) => {
|
|||||||
plz: (formData.get("plz") as string) || "",
|
plz: (formData.get("plz") as string) || "",
|
||||||
ort: (formData.get("ort") as string) || "",
|
ort: (formData.get("ort") as string) || "",
|
||||||
land: (formData.get("land") as string) || "",
|
land: (formData.get("land") as string) || "",
|
||||||
notiz: (formData.get("notiz") as string) || ""
|
notiz: (formData.get("notiz") as string) || "",
|
||||||
|
// Server-seitig dynamisch berechnet; Platzhalter beim Senden.
|
||||||
|
is_erledigt: false
|
||||||
}
|
}
|
||||||
if (currentKontaktadresse.id === -1) {
|
if (currentKontaktadresse.id === -1) {
|
||||||
createKontaktadresseREST(gespeicherteKontaktadresse).then(() => guard.navigateAfterSave()).catch((e) => { showServerError(e); guard.markDirty() })
|
createKontaktadresseREST(gespeicherteKontaktadresse).then(() => guard.navigateAfterSave()).catch((e) => { showServerError(e); guard.markDirty() })
|
||||||
|
|||||||
@@ -239,7 +239,9 @@ export const Projekt: m.ClosureComponent = (vnode) => {
|
|||||||
projektname: (formData.get("projektname") as string) || "",
|
projektname: (formData.get("projektname") as string) || "",
|
||||||
zweck: (formData.get("zweck") as string) || "",
|
zweck: (formData.get("zweck") as string) || "",
|
||||||
verantwortlicher: (formData.get("verantwortlicher") as string) || "",
|
verantwortlicher: (formData.get("verantwortlicher") as string) || "",
|
||||||
startdatum: (formData.get("startdatum") as string) || ""
|
startdatum: (formData.get("startdatum") as string) || "",
|
||||||
|
// Server-seitig dynamisch berechnet; Platzhalter beim Senden.
|
||||||
|
is_erledigt: false
|
||||||
}
|
}
|
||||||
if (currentProjekt.id === -1) {
|
if (currentProjekt.id === -1) {
|
||||||
createProjektREST(gespeichertesProjekt).then(() => guard.navigateAfterSave()).catch((e) => { showServerError(e); guard.markDirty() })
|
createProjektREST(gespeichertesProjekt).then(() => guard.navigateAfterSave()).catch((e) => { showServerError(e); guard.markDirty() })
|
||||||
|
|||||||
@@ -6,12 +6,15 @@ import {iTag} from "./tag"
|
|||||||
* Die Feldnamen entsprechen den serverseitigen ORM-Spalten (snake_case).
|
* Die Feldnamen entsprechen den serverseitigen ORM-Spalten (snake_case).
|
||||||
* projekt_name und kunden_name sind keine ORM-Spalten, sondern werden
|
* projekt_name und kunden_name sind keine ORM-Spalten, sondern werden
|
||||||
* serverseitig aus der Projekt-Relationship abgeleitet und im JSON ergänzt.
|
* serverseitig aus der Projekt-Relationship abgeleitet und im JSON ergänzt.
|
||||||
|
* kontaktadresse_id ist Pflichtfeld: Jede Notiz gehört zu genau einer
|
||||||
|
* Kontaktadresse (Fremdschlüssel auf kontaktadressen.id).
|
||||||
* tags (vom Server beim Lesen) und tag_ids (vom Client beim Schreiben)
|
* tags (vom Server beim Lesen) und tag_ids (vom Client beim Schreiben)
|
||||||
* steuern die n:m-Beziehung zu den Tags.
|
* steuern die n:m-Beziehung zu den Tags.
|
||||||
*/
|
*/
|
||||||
export interface iCallLog {
|
export interface iCallLog {
|
||||||
id: number
|
id: number
|
||||||
projekt_id: number
|
projekt_id: number
|
||||||
|
kontaktadresse_id: number
|
||||||
projekt_name: string
|
projekt_name: string
|
||||||
kunden_name: string
|
kunden_name: string
|
||||||
datum: string
|
datum: string
|
||||||
|
|||||||
@@ -5,7 +5,10 @@ import m from "mithril"
|
|||||||
* Jede Kontaktadresse ist genau einem Projekt zugeordnet (FK projekt_id).
|
* Jede Kontaktadresse ist genau einem Projekt zugeordnet (FK projekt_id).
|
||||||
* Die Feldnamen entsprechen den serverseitigen ORM-Spalten (snake_case).
|
* Die Feldnamen entsprechen den serverseitigen ORM-Spalten (snake_case).
|
||||||
* projekt_name ist keine ORM-Spalte, sondern wird serverseitig aus der
|
* projekt_name ist keine ORM-Spalte, sondern wird serverseitig aus der
|
||||||
* Projekt-Relationship abgeleitet und im JSON ergänzt.
|
* Projekt-Relationship abgeleitet und im JSON ergänzt. is_erledigt ist
|
||||||
|
* ebenfalls keine Spalte, sondern wird serverseitig dynamisch berechnet:
|
||||||
|
* True, sobald mindestens eine Gesprächsnotiz des Kontakts einen Tag mit
|
||||||
|
* is_erledigend=True hat.
|
||||||
*/
|
*/
|
||||||
export interface iKontaktadresse {
|
export interface iKontaktadresse {
|
||||||
id: number
|
id: number
|
||||||
@@ -22,6 +25,8 @@ export interface iKontaktadresse {
|
|||||||
ort: string
|
ort: string
|
||||||
land: string
|
land: string
|
||||||
notiz: string
|
notiz: string
|
||||||
|
/** Dynamisch berechneter Erledigt-Status (keine ORM-Spalte). */
|
||||||
|
is_erledigt: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ------------------------------------------------------------------ */
|
/* ------------------------------------------------------------------ */
|
||||||
|
|||||||
@@ -4,7 +4,9 @@ import m from "mithril"
|
|||||||
* Datenmodell für ein Projekt.
|
* Datenmodell für ein Projekt.
|
||||||
* Die Feldnamen entsprechen den serverseitigen ORM-Spalten (snake_case).
|
* Die Feldnamen entsprechen den serverseitigen ORM-Spalten (snake_case).
|
||||||
* kunden_name ist keine ORM-Spalte, sondern wird serverseitig aus der
|
* kunden_name ist keine ORM-Spalte, sondern wird serverseitig aus der
|
||||||
* Kunde-Relationship abgeleitet und im JSON ergänzt.
|
* Kunde-Relationship abgeleitet und im JSON ergänzt. is_erledigt ist
|
||||||
|
* ebenfalls keine Spalte, sondern wird serverseitig dynamisch berechnet:
|
||||||
|
* True, wenn das Projekt Kontaktadressen hat und alle davon erledigt sind.
|
||||||
*/
|
*/
|
||||||
export interface iProjekt {
|
export interface iProjekt {
|
||||||
id: number
|
id: number
|
||||||
@@ -14,6 +16,8 @@ export interface iProjekt {
|
|||||||
zweck: string
|
zweck: string
|
||||||
verantwortlicher: string
|
verantwortlicher: string
|
||||||
startdatum: string
|
startdatum: string
|
||||||
|
/** Dynamisch berechneter Erledigt-Status (keine ORM-Spalte). */
|
||||||
|
is_erledigt: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ------------------------------------------------------------------ */
|
/* ------------------------------------------------------------------ */
|
||||||
|
|||||||
Reference in new issue
Block a user