import m from "mithril" import {Form, iForm} from "./widgets/form" 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, fetchCallLogs, fetchCallLogById, createCallLogREST, updateCallLogREST} from "../models/call_log" import {iKunde, fetchKunden} from "../models/kunde" import {iProjekt, fetchProjekte, fetchProjektById} from "../models/projekt" import {iKontaktadresse, fetchKontaktadressen} from "../models/kontaktadresse" // Aktualisierungsintervall für die Dauer-Anzeige in Millisekunden. const DAUER_UPDATE_INTERVALL = 1000 export const call_log_liste: m.ClosureComponent = (vnode) => { let treenodes: iTreenode[] = [] let selectedNode: iTreenode | null = null let filterProjektId: number | null = null // Projektname für die Überschrift, asynchron per fetchProjektById // aufgelöst. kunden_name wird ebenfalls aus dem Projekt bezogen, // da es nicht in den Route-Params enthalten ist. let projektName: string = "" let kundenName: string = "" const handleRowMarked = (node: iTreenode) => { selectedNode = node } const addCallLogRoute = () => { const now = new Date() const datum = now.toISOString().split('T')[0] const uhrzeit = now.toTimeString().split(':')[0] + ':' + now.toTimeString().split(':')[1] const params: any = { id: -1, datum: datum, uhrzeit_beginn: uhrzeit, dauer: "", betreff: "", name_ansprechpartner: "", telefon: "", notiz: "", wiedervorlage_datum: "", erledigt: false } if (filterProjektId !== null) { params.projekt_id = filterProjektId params.projekt_name = projektName params.kunden_name = kundenName } m.route.set("/call_log", params) } const editCallLog = (log: iCallLog, e: Event) => { e.stopPropagation() m.route.set("/call_log", {id: log.id}) } const columnmapper: (node: iTreenode) => any[] = (node: iTreenode) => { let log: iCallLog = node.nodevalue return [ m("div", m("strong", log.kunden_name + ": " + log.projekt_name)), m("span", log.datum), m("span", log.uhrzeit_beginn), m("span", log.dauer + " min"), m("span", log.betreff), m("span", log.wiedervorlage_datum), m("span", log.erledigt ? "Erledigt" : "Offen"), m("button.w3-button.w3-small", {title: "Gesprächsnotiz bearbeiten", style: {padding: '0 2px'}, onclick: (e: Event) => editCallLog(log, e)}, m("i.material-icons", "edit")) ] } const filterFunction = (node: iTreenode, searchvalue: string): boolean => { let log: iCallLog = node.nodevalue let searchLower = searchvalue.toLowerCase() return (log.kunden_name || "").toLowerCase().includes(searchLower) || (log.projekt_name || "").toLowerCase().includes(searchLower) || (log.datum || "").includes(searchLower) || (log.uhrzeit_beginn || "").toLowerCase().includes(searchLower) || (log.betreff || "").toLowerCase().includes(searchLower) || (log.name_ansprechpartner || "").toLowerCase().includes(searchLower) || (log.notiz || "").toLowerCase().includes(searchLower) || (log.wiedervorlage_datum || "").includes(searchLower) } return { oninit: (vnode) => { const routeParams = m.route.param() if (routeParams && routeParams.projekt_id) { filterProjektId = parseInt(routeParams.projekt_id) projektName = routeParams.projekt_name || "" } else { filterProjektId = null projektName = "" } // Gesprächsnotizen asynchron vom Server laden. Die Filterung // nach projekt_id erfolgt serverseitig, sofern ein Filter // gesetzt ist. fetchCallLogs(filterProjektId !== null ? filterProjektId : undefined).then((result) => { treenodes = result.map((log: iCallLog) => new Treenode(log, true, [])) }) // Projektnamen und Kundennamen asynchron auflösen, falls // nicht in den Route-Params enthalten. if (filterProjektId !== null) { fetchProjektById(filterProjektId).then((projekt: iProjekt) => { if (projektName === "") { projektName = projekt.projektname } kundenName = projekt.kunden_name }) } }, view: (vnode) => { return m("div.w3-container.w3-auto", { style: { display: 'flex', 'flex-direction': 'column', height: 'inherit', 'overflow-y': 'inherit'}}, m("h1", projektName ? "Gesprächsnotizen für " + projektName : "Gesprächsnotizen"), m("div", {style: {flex: '1', 'overflow-y': 'inherit'}}, m(Treetable, { treenodes: treenodes, columnmapper: columnmapper, filter: filterFunction, onRowMarked: handleRowMarked, isSelected: (node: iTreenode) => node === selectedNode } as iTreetable, m(TopbarButton, {onclick: addCallLogRoute, tooltip: "Neue Gesprächsnotiz anlegen"} as iTopbarButton, m("i.material-icons", "add_call") ) ) ) ) } } } export const Call_Log: m.ClosureComponent = (vnode) => { // currentLog hält den Formularzustand. projekt_id referenziert das // Projekt, kunden_name und projekt_name dienen der Anzeige. let currentLog: { id: number projekt_id: number projekt_name: string kunden_name: string datum: string uhrzeit_beginn: string dauer: string betreff: string name_ansprechpartner: string telefon: string notiz: string wiedervorlage_datum: string erledigt: boolean } = { id: -1, projekt_id: -1, projekt_name: "", kunden_name: "", datum: "", uhrzeit_beginn: "", dauer: "", betreff: "", name_ansprechpartner: "", telefon: "", notiz: "", wiedervorlage_datum: "", erledigt: false } // Ist das Projekt voreingestellt (Liste wurde projektgefiltert)? let projektVoreingestellt: boolean = false // Ist der Ansprechpartner voreingestellt (aus Kontaktadressenliste)? let ansprechpartnerVoreingestellt: 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. let dauerInput: HTMLInputElement | null = null // Asynchron geladene Listen für Select-Optionen. let kundenListe: iKunde[] = [] let projektListe: iProjekt[] = [] let kontaktadressenListe: iKontaktadresse[] = [] /** * Formatiert verstrichene Sekunden als mm:ss. */ const formatDauer = (sekunden: number): string => { const min = Math.floor(sekunden / 60) const sek = sekunden % 60 return min.toString().padStart(2, '0') + ':' + sek.toString().padStart(2, '0') } return { oninit: (vnode) => { const routeParams = m.route.param() const id = routeParams && routeParams.id ? parseInt(routeParams.id) : -1 if (id !== -1) { // Gesprächsnotiz frisch vom Server holen. fetchCallLogById(id).then((result) => { currentLog = result projektVoreingestellt = true ansprechpartnerVoreingestellt = currentLog.name_ansprechpartner !== "" }) } else { projektVoreingestellt = routeParams && routeParams.projekt_id ? parseInt(routeParams.projekt_id) !== -1 : false currentLog.projekt_id = routeParams && routeParams.projekt_id ? parseInt(routeParams.projekt_id) : -1 currentLog.projekt_name = routeParams && routeParams.projekt_name ? routeParams.projekt_name : "" currentLog.kunden_name = routeParams && routeParams.kunden_name ? routeParams.kunden_name : "" currentLog.datum = routeParams && routeParams.datum ? routeParams.datum : "" currentLog.uhrzeit_beginn = routeParams && routeParams.uhrzeit_beginn ? routeParams.uhrzeit_beginn : "" currentLog.dauer = routeParams && routeParams.dauer ? routeParams.dauer : "" currentLog.betreff = routeParams && routeParams.betreff ? routeParams.betreff : "" currentLog.name_ansprechpartner = routeParams && routeParams.name_ansprechpartner ? routeParams.name_ansprechpartner : "" currentLog.telefon = routeParams && routeParams.telefon ? routeParams.telefon : "" currentLog.notiz = routeParams && routeParams.notiz ? routeParams.notiz : "" currentLog.wiedervorlage_datum = routeParams && routeParams.wiedervorlage_datum ? routeParams.wiedervorlage_datum : "" currentLog.erledigt = routeParams && (routeParams.erledigt === true || routeParams.erledigt === "true") ansprechpartnerVoreingestellt = currentLog.name_ansprechpartner !== "" // Bei Neuanlage Startzeit merken und Dauer-Timer starten. startZeit = new Date() timerId = window.setInterval(() => { if (startZeit) { const sekunden = Math.floor((Date.now() - startZeit.getTime()) / 1000) const dauer = formatDauer(sekunden) currentLog.dauer = dauer if (dauerInput) { dauerInput.value = dauer } } }, DAUER_UPDATE_INTERVALL) } // Alle Listen asynchron laden, für Select-Optionen. fetchKunden().then((result) => { kundenListe = result }) fetchProjekte().then((result) => { projektListe = result }) fetchKontaktadressen().then((result) => { kontaktadressenListe = result }) }, onremove: (vnode) => { if (timerId != null) { window.clearInterval(timerId) } }, 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) => { return {value: k.id, label: k.name} }) // Projekt-Optionen sind vom gewählten Kunden abhängig. const projektOptionen: iSelectOption[] = projektListe .filter((p: iProjekt) => internKundenId === -1 || p.kunden_id === internKundenId) .map((p: iProjekt) => { return {value: p.id, label: p.projektname} }) // Kontaktadressen-Optionen sind vom gewählten Projekt abhängig. const kontaktadressenOptionen: iSelectOption[] = !ansprechpartnerVoreingestellt ? kontaktadressenListe .filter((ka: iKontaktadresse) => ka.projekt_id === currentLog.projekt_id) .map((ka: iKontaktadresse) => { return {value: ka.id, label: ka.name + (ka.firma ? " (" + ka.firma + ")" : "")} }) : [] const submit = (form: HTMLFormElement): boolean => { const formData = new FormData(form) const gespeichertesLog: iCallLog = { id: currentLog.id, projekt_id: currentLog.projekt_id, projekt_name: "", kunden_name: "", datum: (formData.get("datum") as string) || "", uhrzeit_beginn: (formData.get("uhrzeit_beginn") as string) || "", dauer: (formData.get("dauer") as string) || "", betreff: (formData.get("betreff") as string) || "", name_ansprechpartner: (formData.get("name_ansprechpartner") as string) || "", telefon: (formData.get("telefon") as string) || "", notiz: (formData.get("notiz") as string) || "", wiedervorlage_datum: (formData.get("wiedervorlage_datum") as string) || "", erledigt: formData.get("erledigt") === "on" } if (currentLog.id === -1) { createCallLogREST(gespeichertesLog).then(() => m.route.set("/calllogliste", {projekt_id: currentLog.projekt_id, projekt_name: currentLog.projekt_name})) } else { updateCallLogREST(gespeichertesLog).then(() => m.route.set("/calllogliste", {projekt_id: currentLog.projekt_id, projekt_name: currentLog.projekt_name})) } return true } const cancel = (e: Event) => { m.route.set("/calllogliste", {projekt_id: currentLog.projekt_id, projekt_name: currentLog.projekt_name}) } return m("div.w3-container.w3-auto.form-view-container", m("h1", currentLog.id === -1 ? "Neue Gesprächsnotiz anlegen" : "Gesprächsnotiz"), m("div", {style: {flex: '1', 'min-height': '0'}}, m(Form, {showButtons: true, onsubmit: submit, oncancel: cancel} as iForm, [ m("input", {type: "hidden", name: "projekt_id", value: currentLog.projekt_id}), // Kunde und Projekt als Read-Only-Text, da voreingestellt. m("label", "Kunde", m("input.w3-input.w3-border", { type: "text", value: currentLog.kunden_name, readonly: true, style: {'margin-bottom': '5px'} })), m("label", "Projekt", m("input.w3-input.w3-border", { type: "text", value: currentLog.projekt_name, readonly: true, style: {'margin-bottom': '5px'} })), // Datum, Startzeit und Dauer in einer Zeile. m("div", {style: {display: 'flex', gap: '8px', 'margin-bottom': '5px'}}, m("label", {style: {flex: '1'}}, "Datum", m(Input, {name: "datum", value: currentLog.datum, type: "date"} as iInput)), m("label", {style: {flex: '1'}}, "Uhrzeit Beginn", m(Input, {name: "uhrzeit_beginn", value: currentLog.uhrzeit_beginn, type: "time"} as iInput)), m("label", {style: {flex: '1'}}, "Dauer (mm:ss)", m("input.w3-input.w3-border", { name: "dauer", type: "text", value: currentLog.dauer, oncreate: (vnode: m.VnodeDOM) => { dauerInput = vnode.dom as HTMLInputElement } })) ), m("label", "Betreff", m(Input, {name: "betreff", value: currentLog.betreff, type: "text"} as iInput)), // Ansprechpartner und Telefon in einer Zeile. m("div", {style: {display: 'flex', gap: '8px', 'margin-bottom': '5px'}}, m("label", {style: {flex: '1'}}, "Name Ansprechpartner", ansprechpartnerVoreingestellt ? m(Input, {name: "name_ansprechpartner", value: currentLog.name_ansprechpartner, type: "text", readonly: true} as iInput) : m(Select, { name: "kontaktadresse_id", value: -1, options: kontaktadressenOptionen, onchanged: (kontaktadresseId: number) => { const ka = kontaktadressenListe.find((k: iKontaktadresse) => k.id === kontaktadresseId) if (ka) { currentLog.name_ansprechpartner = ka.name currentLog.telefon = ka.telefon } } } as iSelect)), m("label", {style: {flex: '1'}}, "Telefon", m(Input, {name: "telefon", value: currentLog.telefon, type: "tel"} as iInput)) ), m("label", "Notiz", m(Textarea, {name: "notiz", value: currentLog.notiz, rows: 4} as iTextarea)), m("label", "Wiedervorlage Datum", m(Input, {name: "wiedervorlage_datum", value: currentLog.wiedervorlage_datum, type: "date"} as iInput)), m("label", m(Input, {name: "erledigt", type: "checkbox", value: currentLog.erledigt} as iInput), "Erledigt") ]) ) ) } } }