From 3accb8f69079b7016387380907bfb4a375c89cd1 Mon Sep 17 00:00:00 2001 From: Gerd Date: Mon, 31 Aug 2026 18:54:54 +0200 Subject: [PATCH] Call_Log: Notiz als Textarea, Dauer-Timer mit mm:ss und DOM-Direktaktualisierung MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- Client/Components/call_log.ts | 60 +++++++++++++++++++++++++++++++++-- 1 file changed, 57 insertions(+), 3 deletions(-) diff --git a/Client/Components/call_log.ts b/Client/Components/call_log.ts index 0c82a61..070ffb2 100644 --- a/Client/Components/call_log.ts +++ b/Client/Components/call_log.ts @@ -1,12 +1,16 @@ import m from "mithril" 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 {Treenode, iTreenode, Treetable, iTreetable, TopbarButton, iTopbarButton} from "./widgets/Treetable_V2" import {iCallLog, getCallLogsByProjektId, getNewCallLogId, addCallLogData, updateCallLogData} from "../models/call_log" import {iKunde, getKunden} from "../models/kunde" 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[] { let filteredLogs = getCallLogsByProjektId(projektId) @@ -174,6 +178,23 @@ export const Call_Log: m.ClosureComponent = (vnode) => { // Sind Kunde und Projekt voreingestellt (Liste wurde projektgefiltert // oder aus der Projektliste aufgerufen)? 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 { oninit: (vnode) => { @@ -203,8 +224,28 @@ export const Call_Log: m.ClosureComponent = (vnode) => { wiedervorlageDatum: routeParams.wiedervorlageDatum || "", 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) => { // Select-Optionen aus Kunden- und Projektliste aufbauen. const kundenOptionen: iSelectOption[] = getKunden().map((k: iKunde) => { @@ -214,6 +255,7 @@ export const Call_Log: m.ClosureComponent = (vnode) => { const projektOptionen: iSelectOption[] = getProjekteByKundenId(currentLog.kundenId).map((p) => { return {value: p.id, label: p.projektname} }) + // Einheit der Dauer-Anzeige im Format mm:ss. return m("div.w3-container.w3-auto", m("h1", currentLog.id === -1 ? "Neue Gesprächsnotiz anlegen" : "Gesprächsnotiz"), m(Form, {showButtons: true, onsubmit: (null), oncancel: (null)} as iForm, [ @@ -242,11 +284,23 @@ export const Call_Log: m.ClosureComponent = (vnode) => { } as iSelect)), 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", "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", "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", "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", m(Input, {name: "erledigt", type: "checkbox", value: currentLog.erledigt} as iInput), "Erledigt") ])