Call_Log: Notiz als Textarea, Dauer-Timer mit mm:ss und DOM-Direktaktualisierung

Notiz-Feld auf Textarea-Widget umgestellt (mehrzeilige Eingabe). Dauer-Feld
bei Neuanlage mit Timer automatisch aktualisiert: verstrichene Sekunden als
mm:ss formatiert, direkt im DOM gesetzt (ohne redraw, damit andere Felder
nicht überschrieben werden). Timer in onremove gestoppt. Konstante
DAUER_UPDATE_INTERVALL steuert die Update-Frequenz (Sekunde/Minute).

Generated by Mistral Vibe.
Co-Authored-By: Mistral Vibe <vibe@mistral.ai>
This commit is contained in:
gg committed 2026-08-31 18:54:54 +02:00
1 parent 16eaab8b64
commit 3accb8f690
1 file changed
+57 -3
+57 -3
View File
@@ -1,12 +1,16 @@
import m from "mithril" import m from "mithril"
import {Form, iForm} from "./widgets/form" import {Form, iForm} from "./widgets/form"
import { Input, iInput} from "./widgets/inputs" import { Input, iInput, Textarea, iTextarea} from "./widgets/inputs"
import {Select, iSelect, iSelectOption} from "./widgets/select" import {Select, iSelect, iSelectOption} from "./widgets/select"
import {Treenode, iTreenode, Treetable, iTreetable, TopbarButton, iTopbarButton} from "./widgets/Treetable_V2" import {Treenode, iTreenode, Treetable, iTreetable, TopbarButton, iTopbarButton} from "./widgets/Treetable_V2"
import {iCallLog, getCallLogsByProjektId, getNewCallLogId, addCallLogData, updateCallLogData} from "../models/call_log" import {iCallLog, getCallLogsByProjektId, getNewCallLogId, addCallLogData, updateCallLogData} from "../models/call_log"
import {iKunde, getKunden} from "../models/kunde" import {iKunde, getKunden} from "../models/kunde"
import {getProjekteByKundenId, getProjektById} from "../models/projekt" import {getProjekteByKundenId, getProjektById} from "../models/projekt"
// Aktualisierungsintervall für die Dauer-Anzeige in Millisekunden.
// 1000 = jede Sekunde, 60000 = jede Minute.
const DAUER_UPDATE_INTERVALL = 1000
function createCallLogTreenodes(projektId: number | null = null): iTreenode[] { function createCallLogTreenodes(projektId: number | null = null): iTreenode[] {
let filteredLogs = getCallLogsByProjektId(projektId) let filteredLogs = getCallLogsByProjektId(projektId)
@@ -174,6 +178,23 @@ export const Call_Log: m.ClosureComponent = (vnode) => {
// Sind Kunde und Projekt voreingestellt (Liste wurde projektgefiltert // Sind Kunde und Projekt voreingestellt (Liste wurde projektgefiltert
// oder aus der Projektliste aufgerufen)? // oder aus der Projektliste aufgerufen)?
let kundeVoreingestellt: boolean = false let kundeVoreingestellt: boolean = false
// Startzeit für die Dauer-Messung (nur bei Neuanlage gesetzt).
let startZeit: Date | null = null
// Timer-Handle für die Dauer-Aktualisierung.
let timerId: number | null = null
// Direkte DOM-Referenz auf das Dauer-Feld, damit der Timer den Wert
// ohne Redraw aktualisieren kann (ein Redraw würde die übrigen Felder
// mit den veralteten currentLog-Werten überschreiben).
let dauerInput: HTMLInputElement | null = null
/**
* Formatiert verstrichene Sekunden als mm:ss (Minuten:Sekunden).
*/
const formatDauer = (sekunden: number): string => {
const m = Math.floor(sekunden / 60)
const s = sekunden % 60
return m.toString().padStart(2, '0') + ':' + s.toString().padStart(2, '0')
}
return { return {
oninit: (vnode) => { oninit: (vnode) => {
@@ -203,7 +224,27 @@ export const Call_Log: m.ClosureComponent = (vnode) => {
wiedervorlageDatum: routeParams.wiedervorlageDatum || "", wiedervorlageDatum: routeParams.wiedervorlageDatum || "",
erledigt: routeParams.erledigt === true || routeParams.erledigt === "true" erledigt: routeParams.erledigt === true || routeParams.erledigt === "true"
} }
// Bei Neuanlage Startzeit merken und Dauer-Timer starten.
if (currentLog.id === -1) {
startZeit = new Date()
timerId = window.setInterval(() => {
if (startZeit) {
// Verstrichene Sekunden berechnen; das Intervall
// steuert nur die Update-Frequenz.
const sekunden = Math.floor((Date.now() - startZeit.getTime()) / 1000)
const dauer = formatDauer(sekunden)
currentLog.dauer = dauer
// Nur das Dauer-Feld direkt im DOM aktualisieren,
// ohne redraw (sonst werden andere Felder zurückgesetzt).
if (dauerInput) { dauerInput.value = dauer }
} }
}, DAUER_UPDATE_INTERVALL)
}
}
},
onremove: (vnode) => {
// Timer stoppen, damit er nach Verlassen des Views nicht weiterläuft.
if (timerId != null) { window.clearInterval(timerId) }
}, },
view: (vnode) => { view: (vnode) => {
// Select-Optionen aus Kunden- und Projektliste aufbauen. // Select-Optionen aus Kunden- und Projektliste aufbauen.
@@ -214,6 +255,7 @@ export const Call_Log: m.ClosureComponent = (vnode) => {
const projektOptionen: iSelectOption[] = getProjekteByKundenId(currentLog.kundenId).map((p) => { const projektOptionen: iSelectOption[] = getProjekteByKundenId(currentLog.kundenId).map((p) => {
return {value: p.id, label: p.projektname} return {value: p.id, label: p.projektname}
}) })
// Einheit der Dauer-Anzeige im Format mm:ss.
return m("div.w3-container.w3-auto", return m("div.w3-container.w3-auto",
m("h1", currentLog.id === -1 ? "Neue Gesprächsnotiz anlegen" : "Gesprächsnotiz"), m("h1", currentLog.id === -1 ? "Neue Gesprächsnotiz anlegen" : "Gesprächsnotiz"),
m(Form, {showButtons: true, onsubmit: (null), oncancel: (null)} as iForm, [ m(Form, {showButtons: true, onsubmit: (null), oncancel: (null)} as iForm, [
@@ -242,11 +284,23 @@ export const Call_Log: m.ClosureComponent = (vnode) => {
} as iSelect)), } as iSelect)),
m("label", "Datum", m(Input, {name: "datum", value: currentLog.datum, type: "date"} as iInput)), m("label", "Datum", m(Input, {name: "datum", value: currentLog.datum, type: "date"} as iInput)),
m("label", "Uhrzeit Beginn", m(Input, {name: "uhrzeitBeginn", value: currentLog.uhrzeitBeginn, type: "time"} as iInput)), m("label", "Uhrzeit Beginn", m(Input, {name: "uhrzeitBeginn", value: currentLog.uhrzeitBeginn, type: "time"} as iInput)),
m("label", "Dauer (Minuten)", m(Input, {name: "dauer", value: currentLog.dauer, type: "number"} as iInput)), m("label", "Dauer (mm:ss)",
// Raw input statt Input-Widget, damit oncreate die
// DOM-Referenz für den Timer greifen kann. Text-Typ,
// da das Format mm:ss keine Zahl ist.
m("input.w3-input.w3-border", {
name: "dauer",
type: "text",
value: currentLog.dauer,
style: {'margin-bottom': '5px', width: '100%'},
oncreate: (vnode: m.VnodeDOM) => {
dauerInput = vnode.dom as HTMLInputElement
}
})),
m("label", "Betreff", m(Input, {name: "betreff", value: currentLog.betreff, type: "text"} as iInput)), m("label", "Betreff", m(Input, {name: "betreff", value: currentLog.betreff, type: "text"} as iInput)),
m("label", "Name Ansprechpartner", m(Input, {name: "nameAnsprechpartner", value: currentLog.nameAnsprechpartner, type: "text"} as iInput)), m("label", "Name Ansprechpartner", m(Input, {name: "nameAnsprechpartner", value: currentLog.nameAnsprechpartner, type: "text"} as iInput)),
m("label", "Telefon", m(Input, {name: "telefon", value: currentLog.telefon, type: "tel"} as iInput)), m("label", "Telefon", m(Input, {name: "telefon", value: currentLog.telefon, type: "tel"} as iInput)),
m("label", "Notiz", m(Input, {name: "notiz", value: currentLog.notiz, type: "text"} as iInput)), m("label", "Notiz", m(Textarea, {name: "notiz", value: currentLog.notiz, rows: 4} as iTextarea)),
m("label", "Wiedervorlage Datum", m(Input, {name: "wiedervorlageDatum", value: currentLog.wiedervorlageDatum, type: "date"} as iInput)), m("label", "Wiedervorlage Datum", m(Input, {name: "wiedervorlageDatum", value: currentLog.wiedervorlageDatum, type: "date"} 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")
]) ])