feat(call-log): Wiedervorlage-Auto-Berechnung und Kunde/Projekt-Select

- Wiedervorlage-Datum wird automatisch aus ausgewählten Tags berechnet
  (Minimum von wiedervorlage_abstand_tage aller Tags mit
  loest_wiedervorlage_aus)
- Kunde/Projekt: Conditional Rendering — vorbelegt als Read-Only,
  nicht vorbelegt als abhängige Select-Widgets
- Live-Binding für datum und wiedervorlage_datum via oninput, um
  Mithril-Redraw-Override durch FormData-Auslesung zu verhindern

Generated by Mistral Vibe.
Co-Authored-By: Mistral Vibe <vibe@mistral.ai>
This commit is contained in:
gg committed 2026-09-22 16:42:36 +02:00
1 parent 5a39c64880
commit 4e721e9a47
1 file changed
+96 -21
+96 -21
View File
@@ -229,6 +229,9 @@ export const Call_Log: m.ClosureComponent = (vnode) => {
let kundenListe: iKunde[] = [] let kundenListe: iKunde[] = []
let projektListe: iProjekt[] = [] let projektListe: iProjekt[] = []
let kontaktadressenListe: iKontaktadresse[] = [] let kontaktadressenListe: iKontaktadresse[] = []
// Im Auswahl-Modus (nicht vorbelegt): gewählte Kunden-ID für die
// abhängige Projekt-Filterung.
let selectedKundenId: number = -1
// Asynchron geladene Tag-Liste (nur zuordenbare, nicht is_parent). // Asynchron geladene Tag-Liste (nur zuordenbare, nicht is_parent).
let tagListe: iTag[] = [] let tagListe: iTag[] = []
// Aktuell ausgewählte Tag-IDs im Formular. // Aktuell ausgewählte Tag-IDs im Formular.
@@ -272,6 +275,27 @@ export const Call_Log: m.ClosureComponent = (vnode) => {
return min.toString().padStart(2, '0') + ':' + sek.toString().padStart(2, '0') return min.toString().padStart(2, '0') + ':' + sek.toString().padStart(2, '0')
} }
/**
* Berechnet das Wiedervorlage-Datum automatisch aus den ausgewählten
* Tags. Basis ist das Datum der Notiz (currentLog.datum) oder heute,
* falls dieses leer ist. Aus allen ausgewählten Tags mit
* loest_wiedervorlage_aus=True wird das Minimum von
* wiedervorlage_abstand_tage ermittelt und zum Basisdatum addiert.
* Liefert einen leeren String, wenn kein Tag mit
* loest_wiedervorlage_aus=True ausgewählt ist.
*/
const berechneWiedervorlageDatum = (tagIds: number[]): string => {
const basis = currentLog.datum || new Date().toISOString().split('T')[0]
const wvTags = tagListe.filter((t: iTag) =>
tagIds.includes(t.id) && t.loest_wiedervorlage_aus && t.wiedervorlage_abstand_tage != null
)
if (wvTags.length === 0) return ""
const minAbstand = Math.min(...wvTags.map(t => t.wiedervorlage_abstand_tage as number))
const basisDate = new Date(basis)
basisDate.setDate(basisDate.getDate() + minAbstand)
return basisDate.toISOString().split('T')[0]
}
return { return {
oninit: (vnode) => { oninit: (vnode) => {
const routeParams = m.route.param() const routeParams = m.route.param()
@@ -332,23 +356,12 @@ export const Call_Log: m.ClosureComponent = (vnode) => {
guard.cleanup() guard.cleanup()
}, },
view: (vnode) => { view: (vnode) => {
// kunden_id aus dem geladenen Projekt ableiten (fuer abhaengiges
// Projekt-Select). Wird nur bei nicht voreingestelltem Projekt
// benoetigt.
const internKundenId = (() => {
if (currentLog.projekt_id !== -1) {
const p = projektListe.find((p: iProjekt) => p.id === currentLog.projekt_id)
return p ? p.kunden_id : -1
}
return -1
})()
const kundenOptionen: iSelectOption[] = kundenListe.map((k: iKunde) => { const kundenOptionen: iSelectOption[] = kundenListe.map((k: iKunde) => {
return {value: k.id, label: k.name} return {value: k.id, label: k.name}
}) })
// Projekt-Optionen sind vom gewählten Kunden abhängig. // Projekt-Optionen sind vom gewählten Kunden abhängig.
const projektOptionen: iSelectOption[] = projektListe const projektOptionen: iSelectOption[] = projektListe
.filter((p: iProjekt) => internKundenId === -1 || p.kunden_id === internKundenId) .filter((p: iProjekt) => selectedKundenId === -1 || p.kunden_id === selectedKundenId)
.map((p: iProjekt) => { .map((p: iProjekt) => {
return {value: p.id, label: p.projektname} return {value: p.id, label: p.projektname}
}) })
@@ -370,17 +383,19 @@ export const Call_Log: m.ClosureComponent = (vnode) => {
const formData = new FormData(form) const formData = new FormData(form)
const gespeichertesLog: iCallLog = { const gespeichertesLog: iCallLog = {
id: currentLog.id, id: currentLog.id,
projekt_id: currentLog.projekt_id, projekt_id: projektVoreingestellt
? currentLog.projekt_id
: parseInt((formData.get("projekt_id_select") as string) || "-1"),
projekt_name: "", projekt_name: "",
kunden_name: "", kunden_name: "",
datum: (formData.get("datum") as string) || "", datum: currentLog.datum || "",
uhrzeit_beginn: (formData.get("uhrzeit_beginn") as string) || "", uhrzeit_beginn: (formData.get("uhrzeit_beginn") as string) || "",
dauer: (formData.get("dauer") as string) || "", dauer: (formData.get("dauer") as string) || "",
betreff: (formData.get("betreff") as string) || "", betreff: (formData.get("betreff") as string) || "",
name_ansprechpartner: (formData.get("name_ansprechpartner") as string) || "", name_ansprechpartner: (formData.get("name_ansprechpartner") as string) || "",
telefon: (formData.get("telefon") as string) || "", telefon: (formData.get("telefon") as string) || "",
notiz: (formData.get("notiz") as string) || "", notiz: (formData.get("notiz") as string) || "",
wiedervorlage_datum: (formData.get("wiedervorlage_datum") as string) || "", wiedervorlage_datum: currentLog.wiedervorlage_datum || "",
erledigt: formData.get("erledigt") === "on", erledigt: formData.get("erledigt") === "on",
tag_ids: formData.getAll("tag_ids").map((v) => parseInt(v as string)).filter((n) => !isNaN(n)) tag_ids: formData.getAll("tag_ids").map((v) => parseInt(v as string)).filter((n) => !isNaN(n))
} }
@@ -400,8 +415,13 @@ export const Call_Log: m.ClosureComponent = (vnode) => {
} as iBreadcrumbBar), } as iBreadcrumbBar),
m("div", {style: {flex: '1', 'min-height': '0'}}, m("div", {style: {flex: '1', 'min-height': '0'}},
m(Form, guard.formAttrs(submit) as iForm, [ m(Form, guard.formAttrs(submit) as iForm, [
m("input", {type: "hidden", name: "projekt_id", value: currentLog.projekt_id}), projektVoreingestellt
// Kunde und Projekt als Read-Only-Text, da voreingestellt. ? m("input", {type: "hidden", name: "projekt_id", value: currentLog.projekt_id})
: null,
// Kunde und Projekt: vorbelegt als Read-Only-Text,
// nicht vorbelegt als auswählbare Select-Widgets.
projektVoreingestellt
? [
m("label", "Kunde", m("label", "Kunde",
m("input.w3-input.w3-border", { m("input.w3-input.w3-border", {
type: "text", type: "text",
@@ -415,11 +435,50 @@ export const Call_Log: m.ClosureComponent = (vnode) => {
value: currentLog.projekt_name, value: currentLog.projekt_name,
readonly: true, readonly: true,
style: {'margin-bottom': '5px'} style: {'margin-bottom': '5px'}
})), }))
]
: [
m("label", "Kunde", m(Select, {
name: "kunden_id",
value: selectedKundenId,
options: kundenOptionen,
required: true,
onchanged: (kundenId: number) => {
selectedKundenId = kundenId
// Projekt zurücksetzen, da es zum
// alten Kunden gehörte.
currentLog.projekt_id = -1
currentLog.projekt_name = ""
currentLog.kunden_name = kundenListe.find(k => k.id === kundenId)?.name || ""
guard.markDirty()
}
} as iSelect)),
m("label", "Projekt", m(Select, {
name: "projekt_id_select",
value: currentLog.projekt_id,
options: projektOptionen,
required: true,
onchanged: (projektId: number) => {
const p = projektListe.find(pr => pr.id === projektId)
currentLog.projekt_id = projektId
currentLog.projekt_name = p?.projektname || ""
currentLog.kunden_name = p?.kunden_name || ""
guard.markDirty()
}
} as iSelect))
],
// Datum, Startzeit und Dauer in einer Zeile. // Datum, Startzeit und Dauer in einer Zeile.
m("div", {style: {display: 'flex', gap: '8px', 'margin-bottom': '5px'}}, m("div", {style: {display: 'flex', gap: '8px', 'margin-bottom': '5px'}},
m("label", {style: {flex: '1'}}, "Datum", m("label", {style: {flex: '1'}}, "Datum",
m(Input, {name: "datum", value: currentLog.datum, type: "date"} as iInput)), m(Input, {
name: "datum",
value: currentLog.datum,
type: "date",
oninput: (e: Event) => {
currentLog.datum = (e.target as HTMLInputElement).value
guard.markDirty()
}
} as iInput)),
m("label", {style: {flex: '1'}}, "Uhrzeit Beginn", m("label", {style: {flex: '1'}}, "Uhrzeit Beginn",
m(Input, {name: "uhrzeit_beginn", value: currentLog.uhrzeit_beginn, type: "time"} as iInput)), m(Input, {name: "uhrzeit_beginn", value: currentLog.uhrzeit_beginn, type: "time"} as iInput)),
m("label", {style: {flex: '1'}}, "Dauer (mm:ss)", m("label", {style: {flex: '1'}}, "Dauer (mm:ss)",
@@ -463,9 +522,25 @@ export const Call_Log: m.ClosureComponent = (vnode) => {
placeholder: "kein Tag", placeholder: "kein Tag",
modalTitle: "Tags auswählen", modalTitle: "Tags auswählen",
editTooltip: "Tags bearbeiten", editTooltip: "Tags bearbeiten",
onchanged: () => { guard.markDirty() } onchanged: (newSelection: number[]) => {
selectedTagIds.splice(0, selectedTagIds.length)
Array.prototype.push.apply(selectedTagIds, newSelection)
const vorschlag = berechneWiedervorlageDatum(newSelection)
if (vorschlag) {
currentLog.wiedervorlage_datum = vorschlag
}
guard.markDirty()
}
} as iMultiSelect)), } as iMultiSelect)),
m("label", "Wiedervorlage Datum", m(Input, {name: "wiedervorlage_datum", value: currentLog.wiedervorlage_datum, type: "date"} as iInput)), m("label", "Wiedervorlage Datum", m(Input, {
name: "wiedervorlage_datum",
value: currentLog.wiedervorlage_datum,
type: "date",
oninput: (e: Event) => {
currentLog.wiedervorlage_datum = (e.target as HTMLInputElement).value
guard.markDirty()
}
} as iInput)),
m("label", m(Input, {name: "erledigt", type: "checkbox", value: currentLog.erledigt} as iInput), "Erledigt") m("label", m(Input, {name: "erledigt", type: "checkbox", value: currentLog.erledigt} as iInput), "Erledigt")
]) ])
) )