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:
1 parent
1d8201f46a
commit
6f86bc9808
5 files changed
+206
-4
No files matched your search
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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'}},
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Reference in new issue
Block a user