diff --git a/App_Server/root/cli.py b/App_Server/root/cli.py index 0b31cb5..46b1675 100644 --- a/App_Server/root/cli.py +++ b/App_Server/root/cli.py @@ -257,7 +257,6 @@ def _seed_muster_calllogs(): log.notiz = rng.choice(notizvorlagen) wiedervorlage_monat = monat + 1 if monat < 12 else 1 log.wiedervorlage_datum = "2024-0{m}-{t}".format(m=wiedervorlage_monat, t=tag) if wiedervorlage_monat < 10 else "2024-{m}-{t}".format(m=wiedervorlage_monat, t=tag) - log.erledigt = rng.choice([True, False]) session.add(log) session.commit() diff --git a/App_Server/root/crm/model/__init__.py b/App_Server/root/crm/model/__init__.py index f1aff32..8ae348c 100644 --- a/App_Server/root/crm/model/__init__.py +++ b/App_Server/root/crm/model/__init__.py @@ -314,7 +314,6 @@ class CallLog(dbapp.Model): telefon = Column(String(100), nullable=True) notiz = Column(String(2000), nullable=True) wiedervorlage_datum = Column(Date, nullable=True) - erledigt = Column(Boolean, nullable=False, default=False) projekt = relationship('Projekt') kontaktadresse = relationship('Kontaktadresse', back_populates='call_logs') diff --git a/App_Server/root/crm/model/call_log.py b/App_Server/root/crm/model/call_log.py index 57cc3d2..6eb516e 100644 --- a/App_Server/root/crm/model/call_log.py +++ b/App_Server/root/crm/model/call_log.py @@ -161,7 +161,7 @@ def create_call_log(data: dict, user_id: int | None = None) -> CallLog: - data: dict mit den Feldern projekt_id, kontaktadresse_id, datum, uhrzeit_beginn, dauer, betreff, name_ansprechpartner, telefon, - notiz, wiedervorlage_datum, erledigt, tag_ids + notiz, wiedervorlage_datum, tag_ids - user_id: ID des angemeldeten Mitarbeiters, der die Notiz erfasst (Ersteller). Wird von der Route übergeben (current_user.id), damit diese Funktion frei vom Flask-Request-Kontext bleibt. @@ -198,7 +198,6 @@ def create_call_log(data: dict, user_id: int | None = None) -> CallLog: call_log.telefon = (data.get("telefon") or "").strip() or None call_log.notiz = (data.get("notiz") or "").strip() or None call_log.wiedervorlage_datum = _parse_date(data.get("wiedervorlage_datum")) - call_log.erledigt = bool(data.get("erledigt", False)) _set_call_log_tags(call_log, data.get("tag_ids")) session.add(call_log) session.commit() @@ -234,7 +233,6 @@ def update_call_log(data: dict) -> CallLog: call_log.telefon = (data.get("telefon") or "").strip() or None call_log.notiz = (data.get("notiz") or "").strip() or None call_log.wiedervorlage_datum = _parse_date(data.get("wiedervorlage_datum")) - call_log.erledigt = bool(data.get("erledigt", False)) _set_call_log_tags(call_log, data.get("tag_ids")) session.commit() return call_log diff --git a/App_Server/root/crm/model/checkdb.py b/App_Server/root/crm/model/checkdb.py index 0273987..4842655 100644 --- a/App_Server/root/crm/model/checkdb.py +++ b/App_Server/root/crm/model/checkdb.py @@ -366,6 +366,40 @@ def _db_V01_14(): session.commit() +def _db_V01_15(): + """ + Entfernt die Spalte 'erledigt' aus der Tabelle 'call_logs'. Der + Erledigt-Status einer Gesprächsnotiz ist redundante Information: + Er ergibt sich dynamisch daraus, ob einer der zugeordneten Tags + is_erledigend=True hat (Kontaktadresse.is_erledigt()). Das Feld + wurde aus Modell und Client entfernt. + + SQLite unterstützt DROP COLUMN ab Version 3.35; die Laufzeitumgebung + liefert 3.40.1. + + Idempotent: Bei Neuinstallation existiert die Spalte nicht mehr + (create_all legt sie nicht mehr an) — der PRAGMA-Check überspringt + das DROP COLUMN in diesem Fall. + """ + from sqlalchemy import text + + result = session.execute(text("PRAGMA table_info(call_logs)")) + spalten = {row[1] for row in result} + if 'erledigt' in spalten: + session.execute(text( + "ALTER TABLE call_logs DROP COLUMN erledigt" + )) + # Roh-SQL-Änderungen committen, BEVOR die Systeminfo aktualisiert wird + # (dasselbe Muster wie _db_V01_08 bis _db_V01_14): Die DDL-Anweisung + # läuft über die Default-Bind-Verbindung — SQLALCHEMY_DATABASE_URI + # zeigt auf dieselbe Datei wie der appdb-Bind. Das ORM-Update der + # Systeminfo läuft über die appdb-Bind-Verbindung; ohne dieses Commit + # blockiert die offene Transaktion das UPDATE ('database is locked'). + session.commit() + _updatesysteminfo('01.15') + session.commit() + + def checkupdates(): """ Ermittelt die aktuelle DB-Version aus der Systeminfo-Tabelle und führt alle diff --git a/Client/Components/call_log.ts b/Client/Components/call_log.ts index 2d5d13c..668bea4 100644 --- a/Client/Components/call_log.ts +++ b/Client/Components/call_log.ts @@ -52,7 +52,6 @@ export const call_log_liste: m.ClosureComponent = (vnode) => { telefon: "", notiz: "", wiedervorlage_datum: "", - erledigt: false, from_liste: "1" } if (filterProjektId !== null) { @@ -105,7 +104,6 @@ export const call_log_liste: m.ClosureComponent = (vnode) => { m("span", log.dauer + " min"), m("span", log.betreff, ...tagBadges), 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")) ] } @@ -200,7 +198,6 @@ export const Call_Log: m.ClosureComponent = (vnode) => { telefon: string notiz: string wiedervorlage_datum: string - erledigt: boolean } = { id: -1, projekt_id: -1, @@ -214,8 +211,7 @@ export const Call_Log: m.ClosureComponent = (vnode) => { name_ansprechpartner: "", telefon: "", notiz: "", - wiedervorlage_datum: "", - erledigt: false + wiedervorlage_datum: "" } // Ist das Projekt voreingestellt (Liste wurde projektgefiltert)? let projektVoreingestellt: boolean = false @@ -312,7 +308,6 @@ export const Call_Log: m.ClosureComponent = (vnode) => { 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") // Kontaktadresse-ID aus den Route-Params übernehmen, wenn die // Maske aus der Kontaktadressenliste geöffnet wurde. currentLog.kontaktadresse_id = routeParams && routeParams.kontaktadresse_id ? parseInt(routeParams.kontaktadresse_id) : -1 @@ -391,7 +386,6 @@ export const Call_Log: m.ClosureComponent = (vnode) => { telefon: (formData.get("telefon") as string) || "", notiz: (formData.get("notiz") as string) || "", wiedervorlage_datum: currentLog.wiedervorlage_datum || "", - erledigt: formData.get("erledigt") === "on", tag_ids: formData.getAll("tag_ids").map((v) => parseInt(v as string)).filter((n) => !isNaN(n)) } if (currentLog.id === -1) { @@ -540,8 +534,7 @@ export const Call_Log: m.ClosureComponent = (vnode) => { 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") + } as iInput)) ]) ) ) diff --git a/Client/Components/kontaktadresse.ts b/Client/Components/kontaktadresse.ts index 6848570..b89775f 100644 --- a/Client/Components/kontaktadresse.ts +++ b/Client/Components/kontaktadresse.ts @@ -73,7 +73,6 @@ export const kontaktadresseliste: m.ClosureComponent = (vnode) => { telefon: kontaktadresse.telefon, notiz: "", wiedervorlage_datum: "", - erledigt: false, bc: serializeBc(chain) }) } diff --git a/Client/Components/projekt.ts b/Client/Components/projekt.ts index 59cd78c..6c29c5a 100644 --- a/Client/Components/projekt.ts +++ b/Client/Components/projekt.ts @@ -85,7 +85,6 @@ export const projektliste: m.ClosureComponent = (vnode) => { telefon: "", notiz: "", wiedervorlage_datum: "", - erledigt: false, bc: serializeBc(chain) }) } diff --git a/Client/Components/tages_workflow.ts b/Client/Components/tages_workflow.ts index 8a80e97..7614f47 100644 --- a/Client/Components/tages_workflow.ts +++ b/Client/Components/tages_workflow.ts @@ -53,6 +53,21 @@ function jetztUhrzeit(): string { return now.toTimeString().split(':')[0] + ':' + now.toTimeString().split(':')[1] } +/** + * Aktualisierungsintervall für die Dauer-Anzeige in der Workflow-Maske + * (Millisekunden), analog zur CallLog-Maske. + */ +const DAUER_UPDATE_INTERVALL = 1000 + +/** + * Formatiert verstrichene Sekunden als mm:ss (wie die CallLog-Maske). + */ +function 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') +} + // ============================================================================ // TAGES-WORKFLOW-LISTE // ============================================================================ @@ -395,6 +410,26 @@ export const TagesWorkflowMaske: m.ClosureComponent = () => { let notizText: string = "" let wiedervorlageDatum: string = "" let selectedTagIds: number[] = [] + // Datum, Uhrzeit Beginn, Dauer und Betreff der neuen Notiz + // (analog zur CallLog-Maske). Datum, Uhrzeit und Dauer werden beim + // Öffnen der Maske gesetzt und als Read-Only-Anzeige dargestellt — + // die Dauer läuft als Timer mit (mm:ss). Ein Erledigt-Feld gibt es + // nicht: Der Status ergibt sich aus einem ggf. zugeordneten + // erledigend-Tag. + let notizDatum: string = "" + let notizUhrzeit: string = "" + let notizDauer: string = "" + let notizBetreff: string = "" + // Startzeit für die Dauer-Messung: wird bei jedem Laden (Maskenaufruf + // und Kontaktwechsel) neu gesetzt; der Timer läuft durch und aktualisiert + // die Dauer-Anzeige laufend. + let startZeit: Date | null = null + // Timer-Handle für die Dauer-Aktualisierung. + let timerId: number | null = null + // Direkte DOM-Referenz auf die Dauer-Anzeige: Der Timer ist kein + // Mithril-Event und löst keinen Redraw aus — die Anzeige wird daher + // direkt im DOM aktualisiert (wie in der CallLog-Maske). + let dauerAnzeige: HTMLElement | null = null // Asynchron geladene Tag-Liste (nur zuordenbare, nicht is_parent). let tagListe: iTag[] = [] // Hinweis aus der Navigation (Kontakt übersprungen), siehe workflowHinweis. @@ -424,7 +459,13 @@ export const TagesWorkflowMaske: m.ClosureComponent = () => { notizText = "" wiedervorlageDatum = "" selectedTagIds = [] + notizDatum = heuteIso() + notizUhrzeit = jetztUhrzeit() + notizDauer = "00:00" + notizBetreff = "" formDirty = false + // Dauer-Messung für die neue Notiz starten. + startZeit = new Date() // Hinweis der vorherigen Navigation übernehmen und konsumieren. hinweis = workflowHinweis workflowHinweis = null @@ -508,6 +549,17 @@ export const TagesWorkflowMaske: m.ClosureComponent = () => { ? parseInt(routeParams.kontaktadresse_id) : -1 guard.registerGuard() laden(kontaktadresseId) + // Dauer-Timer starten (läuft über Kontaktwechsel hinweg; die + // Startzeit setzt laden() bei jedem Kontakt neu). + if (timerId == null) { + timerId = window.setInterval(() => { + if (startZeit) { + const sekunden = Math.floor((Date.now() - startZeit.getTime()) / 1000) + notizDauer = formatDauer(sekunden) + if (dauerAnzeige) { dauerAnzeige.textContent = notizDauer } + } + }, DAUER_UPDATE_INTERVALL) + } // Tags laden — nur zuordenbare (is_parent=false) Tags. fetchTags().then((result) => { tagListe = result.filter((t: iTag) => !t.is_parent) @@ -526,6 +578,7 @@ export const TagesWorkflowMaske: m.ClosureComponent = () => { return true }, onremove: (vnode) => { + if (timerId != null) { window.clearInterval(timerId) } guard.cleanup() }, view: (vnode) => { @@ -541,15 +594,16 @@ export const TagesWorkflowMaske: m.ClosureComponent = () => { projekt_name: "", kunden_name: "", mitarbeiter_name: null, - datum: heuteIso(), - uhrzeit_beginn: jetztUhrzeit(), - dauer: "", - betreff: "", + // Datum, Uhrzeit und Dauer kommen aus den Read-Only- + // Anzeigen (Status), nicht aus dem Formular. + datum: notizDatum || heuteIso(), + uhrzeit_beginn: notizUhrzeit || jetztUhrzeit(), + dauer: notizDauer, + betreff: ((formData.get("betreff") as string) || "").trim(), name_ansprechpartner: kontaktadresse.name, telefon: kontaktadresse.telefon || "", notiz: ((formData.get("notiz") as string) || "").trim(), wiedervorlage_datum: wiedervorlageDatum || "", - erledigt: false, tag_ids: selectedTagIds.slice() } createCallLogREST(log).then(() => { @@ -614,6 +668,11 @@ export const TagesWorkflowMaske: m.ClosureComponent = () => { m("i.material-icons", {style: {verticalAlign: 'middle', marginRight: '4px'}}, "arrow_forward"), "Nächster Kontakt") ] + // Inhaltsbereich der Form als Flex-Spalte rendern (flexContent): + // Das Historie-Panel unten füllt damit den verbleibenden Platz + // und scrollt in sich — lange Historien machen die Maske nicht + // selbst scrollbar. + formAttrs.flexContent = true // Tag-Optionen und Farb-Map für die Mehrfachauswahl. const tagOptionen: iSelectOption[] = tagListe.map((t: iTag) => { @@ -651,110 +710,193 @@ export const TagesWorkflowMaske: m.ClosureComponent = () => { : null, m("div", {style: {flex: '1', 'min-height': '0'}}, m(Form, formAttrs, [ - // ---- Kopfzeile: Projekt- und Kontaktinformationen ---- - m("div.w3-panel.w3-theme-l4.w3-round-large", {style: {'margin-bottom': '12px'}}, - m("div.w3-row", - m("div.w3-half", - m("p", {style: {margin: '4px 0'}}, - m("strong", "Kunde: "), kontaktadresse ? kontaktadresse.kunden_name : ""), - m("p", {style: {margin: '4px 0'}}, - m("strong", "Projekt: "), kontaktadresse ? kontaktadresse.projekt_name : "")), - m("div.w3-half", - m("p", {style: {margin: '4px 0'}}, - m("strong", "Kontakt: "), - kontaktadresse ? kontaktadresse.name : "", - kontaktadresse && kontaktadresse.telefon ? " | " + kontaktadresse.telefon : "", - kontaktadresse && kontaktadresse.email ? " | " + kontaktadresse.email : ""), - m("p", {style: {margin: '4px 0'}}, - m("strong", "Status: "), - m("span.w3-tag" + (kontaktadresse && kontaktadresse.is_erledigt ? ".w3-green" : ".w3-grey"), - kontaktadresse && kontaktadresse.is_erledigt ? "Erledigt" : "Offen"), - m("button.w3-button.w3-theme.w3-right", - { - type: "button", - style: {'margin-top': '-4px'}, - onclick: kontaktBearbeiten - }, - m("i.material-icons", {style: {verticalAlign: 'middle', marginRight: '4px'}}, "edit"), - "Kontakt bearbeiten"))) - ) + // Fester Teil der Maske (Kopfzeile, Neue Gesprächsnotiz): + // flex '0 0 auto' verhindert, dass der Flex-Inhaltsbereich + // der Form (flexContent) diese Blöcke staucht — bei zu + // wenig Platz scrollt die Maske stattdessen als Ganzes. + m("div", {style: {flex: '0 0 auto'}}, + // ---- Kopfzeile: Projekt- und Kontaktinformationen ---- + m("div.w3-panel.w3-theme-l4.w3-round-large", {style: {'margin-bottom': '12px'}}, + m("div.w3-row", + m("div.w3-half", + m("p", {style: {margin: '4px 0'}}, + m("strong", "Kunde: "), kontaktadresse ? kontaktadresse.kunden_name : ""), + m("p", {style: {margin: '4px 0'}}, + m("strong", "Projekt: "), kontaktadresse ? kontaktadresse.projekt_name : "")), + m("div.w3-half", + m("p", {style: {margin: '4px 0'}}, + m("strong", "Kontakt: "), + kontaktadresse ? kontaktadresse.name : "", + kontaktadresse && kontaktadresse.telefon ? " | " + kontaktadresse.telefon : "", + kontaktadresse && kontaktadresse.email ? " | " + kontaktadresse.email : ""), + m("p", {style: {margin: '4px 0'}}, + m("strong", "Status: "), + m("span.w3-tag" + (kontaktadresse && kontaktadresse.is_erledigt ? ".w3-green" : ".w3-grey"), + kontaktadresse && kontaktadresse.is_erledigt ? "Erledigt" : "Offen"), + m("button.w3-button.w3-theme.w3-right", + { + type: "button", + style: {'margin-top': '-4px'}, + onclick: kontaktBearbeiten + }, + m("i.material-icons", {style: {verticalAlign: 'middle', marginRight: '4px'}}, "edit"), + "Kontakt bearbeiten"))) + ) + ), + // ---- Formular für die neue Gesprächsnotiz ---- + m("h2", {style: {fontSize: '20px'}}, "Neue Gesprächsnotiz"), + // Datum, Uhrzeit Beginn und Dauer als Read-Only-Info im + // Stil der Kopfzeile (gleiches Panel-Layout): Die Werte + // werden beim Öffnen der Maske gesetzt, die Dauer läuft + // als Timer mit (mm:ss) und wird direkt im DOM + // aktualisiert. w3-third bricht die Zeile auf kleinen + // Bildschirmen um. + m("div.w3-panel.w3-theme-l4.w3-round-large", {style: {'margin-bottom': '12px'}}, + m("div.w3-row", + m("div.w3-third", + m("p", {style: {margin: '4px 0'}}, + m("strong", "Datum: "), notizDatum || "—")), + m("div.w3-third", + m("p", {style: {margin: '4px 0'}}, + m("strong", "Uhrzeit Beginn: "), notizUhrzeit || "—")), + m("div.w3-third", + m("p", {style: {margin: '4px 0'}}, + m("strong", "Dauer: "), + m("span", { + oncreate: (vnode: m.VnodeDOM) => { + dauerAnzeige = vnode.dom as HTMLElement + } + }, notizDauer)))) + ), + m("label", "Betreff", m(Input, { + name: "betreff", + value: notizBetreff, + type: "text", + oninput: (e: Event) => { + notizBetreff = (e.target as HTMLInputElement).value + } + } as iInput)), + m("label", "Notiz", + m("textarea.w3-input.w3-border", { + name: "notiz", + value: notizText, + rows: 4, + required: true, + style: {'margin-bottom': '8px'}, + oninput: (e: Event) => { + notizText = (e.target as HTMLTextAreaElement).value + } + })), + // Tags (links) und Wiedervorlage-Datum (rechts) in + // einer Zeile, um Platz zu sparen. w3-twothird/w3-third + // brechen die Zeile auf kleinen Bildschirmen um. + m("div.w3-row-padding", + m("div.w3-twothird", + m("label", "Tags", m(MultiSelect, { + name: "tag_ids", + selected: selectedTagIds, + options: tagOptionen, + colors: tagColors, + placeholder: "kein Tag", + modalTitle: "Tags auswählen", + editTooltip: "Tags bearbeiten", + onchanged: (newSelection: number[]) => { + selectedTagIds = newSelection.slice() + // Wiedervorlage-Datum automatisch aus den Tags + // berechnen (Basis heute), falls ein Tag mit + // loest_wiedervorlage_aus gewählt wurde. + const vorschlag = berechneWiedervorlageDatum( + newSelection, tagListe, heuteIso()) + if (vorschlag) { + wiedervorlageDatum = vorschlag + } + } + } as iMultiSelect))), + m("div.w3-third", + m("label", "Wiedervorlage Datum", m(Input, { + name: "wiedervorlage_datum", + value: wiedervorlageDatum, + type: "date", + oninput: (e: Event) => { + wiedervorlageDatum = (e.target as HTMLInputElement).value + } + } as iInput))) + ), ), // ---- Historie der bisherigen Gesprächsnotizen ---- - m("h2", {style: {fontSize: '20px'}}, "Bisherige Gesprächsnotizen"), - m("div", {style: {'overflow-x': 'auto', 'margin-bottom': '16px'}}, - m("table.w3-table.w3-bordered.w3-striped", - m("thead", - m("tr.w3-theme", - m("th", "Datum"), - m("th", "Mitarbeiter"), - m("th", "Notiz"), - m("th", "Tags"), - m("th", "Wiedervorlage"))), - historieSortiert.map((log: iCallLog) => - m("tr", - m("td", {style: {'white-space': 'nowrap'}}, - log.datum + (log.uhrzeit_beginn ? ", " + log.uhrzeit_beginn : "")), - m("td", {style: {'white-space': 'nowrap'}}, - log.mitarbeiter_name || "unbekannt"), - m("td", log.notiz || ""), - m("td", (log.tags || []).map((t: iTag) => - m("span", { - style: { - backgroundColor: t.color || '#999', - color: 'white', - padding: '1px 5px', - 'border-radius': '3px', - 'font-size': '11px', - 'margin-left': '4px', - display: 'inline-block' - }, - title: t.name - }, t.name))), - m("td", {style: {'white-space': 'nowrap'}}, log.wiedervorlage_datum || "")) + // Steht bewusst unterhalb des Formulars: Der Nutzer + // sieht beim Öffnen der Maske direkt die Eingabefelder + // für die neue Notiz, die Historie dient als Nachschlag. + // Das Panel füllt den verbleibenden Platz des Inhalts- + // bereichs (flex '1 0 200px': füllt auf, schrumpft aber + // nie unter 200px — reicht der Platz nicht, scrollt die + // Maske als Ganzes). Die Tabelle scrollt darin in sich. + // Abgerundeter grauer Panel-Hintergrund (analog der + // Kopfzeile); die Schriften im Block sind eine Stufe + // kleiner gehalten (Überschrift 18px, Tabelle 13px), + // damit die Historie optisch hinter dem Formular + // zurücktritt. + m("div.w3-panel.w3-light-grey.w3-round-large", { + style: { + flex: '1 0 200px', + 'min-height': '0', + display: 'flex', + 'flex-direction': 'column', + 'margin-bottom': '0' + } + }, + m("h2", {style: {fontSize: '18px', flex: '0 0 auto'}}, "Bisherige Gesprächsnotizen"), + // Eigener Scroll-Container: Bei langer Historie + // scrollt nur die Tabelle, nicht die gesamte Maske. + // Wichtig: Der div muss selbst der Scroll-Container + // sein (nicht die Tabelle), damit sich position:sticky + // der th-Zellen auf ihn bezieht (Muster wie im + // Treetable-Widget). + m("div", {style: {flex: '1', 'min-height': '0', 'overflow-x': 'auto', 'overflow-y': 'auto'}}, + m("table.w3-table.w3-bordered.w3-striped", {style: {fontSize: '13px'}}, + m("thead", + m("tr.w3-theme", + ["Datum", "Mitarbeiter", "Dauer", "Betreff", "Notiz", "Tags", "Wiedervorlage"].map((titel: string) => + m("th.w3-theme", { + // Sticky-Header mit eigenem Hintergrund: + // Der tr-Hintergrund wandert beim + // Scrollen mit den Daten weg, ohne + // eigene Fläche scheinen Zeilen + // durch (Muster wie Treetable). + style: { + position: 'sticky', + top: '0', + 'z-index': '1', + 'text-align': 'left' + } + }, titel)))), + historieSortiert.map((log: iCallLog) => + m("tr", + m("td", {style: {'white-space': 'nowrap'}}, + log.datum + (log.uhrzeit_beginn ? ", " + log.uhrzeit_beginn : "")), + m("td", {style: {'white-space': 'nowrap'}}, + log.mitarbeiter_name || "unbekannt"), + m("td", {style: {'white-space': 'nowrap'}}, log.dauer || ""), + m("td", log.betreff || ""), + m("td", log.notiz || ""), + m("td", (log.tags || []).map((t: iTag) => + m("span", { + style: { + backgroundColor: t.color || '#999', + color: 'white', + padding: '1px 5px', + 'border-radius': '3px', + 'font-size': '11px', + 'margin-left': '4px', + display: 'inline-block' + }, + title: t.name + }, t.name))), + m("td", {style: {'white-space': 'nowrap'}}, log.wiedervorlage_datum || "")) + ) ) ) - ), - // ---- Formular für die neue Gesprächsnotiz ---- - m("h2", {style: {fontSize: '20px'}}, "Neue Gesprächsnotiz"), - m("label", "Notiz", - m("textarea.w3-input.w3-border", { - name: "notiz", - value: notizText, - rows: 4, - required: true, - style: {'margin-bottom': '8px'}, - oninput: (e: Event) => { - notizText = (e.target as HTMLTextAreaElement).value - } - })), - m("label", "Tags", m(MultiSelect, { - name: "tag_ids", - selected: selectedTagIds, - options: tagOptionen, - colors: tagColors, - placeholder: "kein Tag", - modalTitle: "Tags auswählen", - editTooltip: "Tags bearbeiten", - onchanged: (newSelection: number[]) => { - selectedTagIds = newSelection.slice() - // Wiedervorlage-Datum automatisch aus den Tags - // berechnen (Basis heute), falls ein Tag mit - // loest_wiedervorlage_aus gewählt wurde. - const vorschlag = berechneWiedervorlageDatum( - newSelection, tagListe, heuteIso()) - if (vorschlag) { - wiedervorlageDatum = vorschlag - } - } - } as iMultiSelect)), - m("label", "Wiedervorlage Datum", m(Input, { - name: "wiedervorlage_datum", - value: wiedervorlageDatum, - type: "date", - oninput: (e: Event) => { - wiedervorlageDatum = (e.target as HTMLInputElement).value - } - } as iInput)) + ) ]) ) ) diff --git a/Client/models/call_log.ts b/Client/models/call_log.ts index 92a03d5..adcdf75 100644 --- a/Client/models/call_log.ts +++ b/Client/models/call_log.ts @@ -13,6 +13,9 @@ import {iTag} from "./tag" * Alt-Notizen ohne erfassten Ersteller haben null. * mitarbeiter_name ist keine Spalte, sondern wird serverseitig aus der * user_id gegen die Auth-Datenbank aufgelöst (null ohne user_id). + * Ein Erledigt-Status ist bewusst kein Feld der Notiz: Er ergibt sich + * daraus, ob einer der zugeordneten Tags is_erledigend=True hat (siehe + * Kontaktadresse.is_erledigt). * tags (vom Server beim Lesen) und tag_ids (vom Client beim Schreiben) * steuern die n:m-Beziehung zu den Tags. */ @@ -34,7 +37,6 @@ export interface iCallLog { telefon: string notiz: string wiedervorlage_datum: string - erledigt: boolean /** Vom Server gelieferte zugeordnete Tags (nur beim Lesen). */ tags?: iTag[] /** Vom Client zu übermittelnde Tag-IDs (nur beim Schreiben). */