feat(tag): Workflow-Felder für Telefon-Akquise (is_erledigend, Wiedervorlage)

Tag-Modell um is_erledigend, loest_wiedervorlage_aus und
wiedervorlage_abstand_tage erweitert. Migration V01_12 idempotent
angelegt, Geschäftslogik mit Validierung, Client-Maske mit Controls
und Workflow-Badges. Farb-Default Grau bei fehlender Farbe, parent_id
im onchange direkt in currentTag übernehmen.

Generated by Mistral Vibe.
Co-Authored-By: Mistral Vibe <vibe@mistral.ai>
This commit is contained in:
gg committed 2026-09-20 20:20:52 +02:00
1 parent 1d8201f46a
commit 6f86bc9808
5 files changed
+206 -4

No files matched your search

+9
View File
@@ -325,6 +325,15 @@ class Tag(dbapp.Model):
color = Column(String(20), nullable=True)
parent_id = Column(Integer, ForeignKey('tags.id'), nullable=True)
is_parent = Column(Boolean, nullable=False, default=False)
# Felder für die Telefon-Akquise-Workflow-Logik:
# is_erledigend: True = Kontakt gilt als erledigt, wenn dieser Tag
# zugewiesen wird.
# loest_wiedervorlage_aus: True = Tag löst eine Wiedervorlage aus.
# wiedervorlage_abstand_tage: Tage bis zur Wiedervorlage (nur relevant,
# wenn loest_wiedervorlage_aus True ist).
is_erledigend = Column(Boolean, nullable=False, default=False)
loest_wiedervorlage_aus = Column(Boolean, nullable=False, default=False)
wiedervorlage_abstand_tage = Column(Integer, nullable=True)
# Selbst-Referenzierende Relationship: parent verweist auf den
# uebergeordneten Tag, children auf alle direkten Kinder. Die
+32
View File
@@ -243,6 +243,38 @@ def _db_V01_11():
session.commit()
def _db_V01_12():
"""
Fügt der Tabelle 'tags' die drei neuen Spalten für die
Telefon-Akquise-Workflow-Logik hinzu:
- is_erledigend (BOOLEAN NOT NULL DEFAULT 0)
- loest_wiedervorlage_aus (BOOLEAN NOT NULL DEFAULT 0)
- wiedervorlage_abstand_tage (INTEGER)
Idempotent: Bei Neuinstallation hat create_all die Spalten bereits
angelegt — in diesem Fall wird das jeweilige ALTER TABLE übersprungen.
"""
from sqlalchemy import text
result = session.execute(text("PRAGMA table_info(tags)"))
spalten = {row[1] for row in result}
if 'is_erledigend' not in spalten:
session.execute(text(
"ALTER TABLE tags ADD COLUMN is_erledigend BOOLEAN NOT NULL DEFAULT 0"
))
if 'loest_wiedervorlage_aus' not in spalten:
session.execute(text(
"ALTER TABLE tags ADD COLUMN loest_wiedervorlage_aus BOOLEAN NOT NULL DEFAULT 0"
))
if 'wiedervorlage_abstand_tage' not in spalten:
session.execute(text(
"ALTER TABLE tags ADD COLUMN wiedervorlage_abstand_tage INTEGER"
))
_updatesysteminfo('01.12')
session.commit()
def checkupdates():
"""
Ermittelt die aktuelle DB-Version aus der Systeminfo-Tabelle und führt alle
+44
View File
@@ -20,6 +20,48 @@ from auth.model.errors import ApplicationError, HtmlStatusCode
session = dbapp.session
def _apply_workflow_felder(tag: Tag, data: dict) -> None:
"""
Übernimmt die Wiedervorlage- und Erledigt-Felder aus dem data-Dict in
den Tag. Für Parent-Tags (is_parent=True) werden die Felder auf False
bzw. None gesetzt, da sie dort keine Wirkung haben (Parent-Tags sind
nicht zuordenbar).
Validierung: Ist loest_wiedervorlage_aus True, muss wiedervorlage_abstand_tage
einen gültigen positiven Integer-Wert haben; sonst wird ein
ApplicationError (Bad_Request) geworfen.
"""
is_parent = bool(data.get("is_parent", tag.is_parent))
if is_parent:
# Parent-Tags: Workflow-Felder sind nicht relevant.
tag.is_erledigend = False
tag.loest_wiedervorlage_aus = False
tag.wiedervorlage_abstand_tage = None
return
tag.is_erledigend = bool(data.get("is_erledigend", False))
loest = bool(data.get("loest_wiedervorlage_aus", False))
tag.loest_wiedervorlage_aus = loest
abstand_raw = data.get("wiedervorlage_abstand_tage")
abstand = None
if abstand_raw is not None and str(abstand_raw).strip() != "":
try:
abstand = int(abstand_raw)
except (TypeError, ValueError):
abstand = None
if loest:
if abstand is None or abstand < 0:
raise ApplicationError(
"Wiedervorlage-Abstand in Tagen ist erforderlich, wenn "
"Wiedervorlage ausgelöst wird.",
htmlstatus=HtmlStatusCode.Bad_Request,
)
tag.wiedervorlage_abstand_tage = abstand
else:
tag.wiedervorlage_abstand_tage = None
def get_tags(parent_id: int | None = None) -> list[Tag]:
"""
Liefert Tags, sortiert nach name (alphabetisch).
@@ -76,6 +118,7 @@ def create_tag(data: dict) -> Tag:
tag.color = (data.get("color") or "").strip() or None
tag.parent_id = parent_id
tag.is_parent = bool(data.get("is_parent", False))
_apply_workflow_felder(tag, data)
session.add(tag)
session.commit()
return tag
@@ -124,6 +167,7 @@ def update_tag(data: dict) -> Tag:
tag.color = (data.get("color") or "").strip() or None
tag.parent_id = parent_id
tag.is_parent = bool(data.get("is_parent", False))
_apply_workflow_felder(tag, data)
session.commit()
return tag
+115 -4
View File
@@ -77,6 +77,9 @@ export const Tag: m.ClosureComponent = () => {
color: "",
parent_id: null,
is_parent: false,
is_erledigend: false,
loest_wiedervorlage_aus: false,
wiedervorlage_abstand_tage: null,
}
// Liste aller Parent-Tags fuer die Eltern-Auswahl (nur Parent-Tags
@@ -109,6 +112,9 @@ export const Tag: m.ClosureComponent = () => {
color: routeParams.color || "",
parent_id: routeParams.parent_id ? parseInt(routeParams.parent_id) : null,
is_parent: routeParams.is_parent ? routeParams.is_parent === "true" : false,
is_erledigend: routeParams.is_erledigend ? routeParams.is_erledigend === "true" : false,
loest_wiedervorlage_aus: routeParams.loest_wiedervorlage_aus ? routeParams.loest_wiedervorlage_aus === "true" : false,
wiedervorlage_abstand_tage: routeParams.wiedervorlage_abstand_tage ? parseInt(routeParams.wiedervorlage_abstand_tage) : null,
}
}
// Parent-Tag-Liste vom Server laden (nur is_parent-Tags sind
@@ -129,8 +135,11 @@ export const Tag: m.ClosureComponent = () => {
name: (formData.get("name") as string) || "",
description: (formData.get("description") as string) || "",
color: currentTag.color || "",
parent_id: formData.get("parent_id") ? parseInt(formData.get("parent_id") as string) : null,
parent_id: currentTag.parent_id,
is_parent: currentTag.is_parent,
is_erledigend: currentTag.is_erledigend,
loest_wiedervorlage_aus: currentTag.loest_wiedervorlage_aus,
wiedervorlage_abstand_tage: currentTag.wiedervorlage_abstand_tage,
}
if (currentTag.id === -1) {
createTagREST(gespeicherterTag).then(() => guard.navigateAfterSave()).catch((e) => { showServerError(e); guard.markDirty() })
@@ -168,7 +177,11 @@ export const Tag: m.ClosureComponent = () => {
m("select.w3-select", {
name: "parent_id",
value: currentTag.parent_id || "",
onchange: () => { guard.markDirty() }
onchange: (e: Event) => {
const val = (e.target as HTMLSelectElement).value
currentTag.parent_id = val ? parseInt(val) : null
guard.markDirty()
}
},
m("option", {value: ""}, "-- Kein übergeordneter Tag --"),
allParentTags.map(tag =>
@@ -176,6 +189,62 @@ export const Tag: m.ClosureComponent = () => {
)
),
// Workflow-Felder (nur für echte, zuordenbare Tags sichtbar).
// Parent-Tags sind Gruppierungsknoten und nicht zuordenbar,
// daher sind diese Felder für sie irrelevant.
currentTag.is_parent ? null : [
m("div", {style: {'margin-top': '16px', 'margin-bottom': '5px'}},
m("label",
m("input.w3-check", {
type: "checkbox",
name: "is_erledigend",
checked: currentTag.is_erledigend,
style: {'margin-right': '8px'},
onclick: (e: Event) => {
currentTag.is_erledigend = (e.target as HTMLInputElement).checked
guard.markDirty()
}
}),
"Kontakt als erledigt markieren"
)
),
m("div", {style: {'margin-bottom': '5px'}},
m("label",
m("input.w3-check", {
type: "checkbox",
name: "loest_wiedervorlage_aus",
checked: currentTag.loest_wiedervorlage_aus,
style: {'margin-right': '8px'},
onclick: (e: Event) => {
currentTag.loest_wiedervorlage_aus = (e.target as HTMLInputElement).checked
if (!currentTag.loest_wiedervorlage_aus) {
currentTag.wiedervorlage_abstand_tage = null
}
guard.markDirty()
}
}),
"Löst Wiedervorlage aus"
)
),
m("label", "Wiedervorlage-Abstand (Tage)"),
m("input.w3-input", {
type: "number",
name: "wiedervorlage_abstand_tage",
value: currentTag.wiedervorlage_abstand_tage ?? "",
min: 0,
disabled: !currentTag.loest_wiedervorlage_aus,
style: {
'margin-bottom': '12px',
opacity: currentTag.loest_wiedervorlage_aus ? '1' : '0.5'
},
oninput: (e: Event) => {
const val = (e.target as HTMLInputElement).value
currentTag.wiedervorlage_abstand_tage = val === "" ? null : parseInt(val)
guard.markDirty()
}
})
],
m("label", "Farbe"),
m("div", {style: {display: 'flex', flexWrap: 'wrap', gap: '8px', marginBottom: '12px'}},
colorPalette.map(color =>
@@ -279,6 +348,47 @@ export const TagList: m.ClosureComponent = () => {
const tagIcon = tag.is_parent ? "folder" : "label"
const tagTitle = tag.is_parent ? "Parent-Tag (nicht zuordenbar)" : ""
// Workflow-Badges für echte Tags (nicht für Parent-Tags)
let workflowBadges: any[] = []
if (!tag.is_parent) {
workflowBadges = []
if (tag.is_erledigend) {
workflowBadges.push(
m("i.material-icons", {
title: "Erledigend: Kontakt gilt als erledigt",
style: {
verticalAlign: 'middle',
marginRight: '4px',
fontSize: '16px',
color: '#2e7d32'
}
}, "check_circle")
)
}
if (tag.loest_wiedervorlage_aus) {
workflowBadges.push(
m("span", {
title: "Löst Wiedervorlage aus: " + (tag.wiedervorlage_abstand_tage ?? 0) + " Tage",
style: {
verticalAlign: 'middle',
marginRight: '4px',
fontSize: '12px',
color: '#1565c0'
}
},
m("i.material-icons", {
style: {
verticalAlign: 'middle',
marginRight: '2px',
fontSize: '16px'
}
}, "schedule"),
tag.wiedervorlage_abstand_tage != null ? tag.wiedervorlage_abstand_tage + "d" : ""
)
)
}
}
return [
m("span",
m("i.material-icons", {
@@ -291,7 +401,7 @@ export const TagList: m.ClosureComponent = () => {
}, tagIcon),
m("span", {
style: {
backgroundColor: tag.color,
backgroundColor: tag.color || '#9e9e9e',
color: 'white',
padding: '2px 6px',
borderRadius: '3px',
@@ -300,7 +410,8 @@ export const TagList: m.ClosureComponent = () => {
marginRight: '8px'
},
title: tagTitle
}, tag.name)
}, tag.name),
workflowBadges
),
m("span", tag.description),
m("div", {style: {display: 'flex', gap: '0', 'white-space': 'nowrap', 'justify-content': 'flex-end'}},
+6
View File
@@ -12,6 +12,12 @@ export interface iTag {
color: string
parent_id: number | null
is_parent: boolean
/** True = Kontakt gilt als erledigt, wenn dieser Tag zugewiesen wird. */
is_erledigend: boolean
/** True = Tag löst eine Wiedervorlage aus. */
loest_wiedervorlage_aus: boolean
/** Tage bis zur Wiedervorlage (nur relevant, wenn loest_wiedervorlage_aus True). */
wiedervorlage_abstand_tage: number | null
}
/**