feat(workflow): Erledigt-Feld entfernen und Tages-Workflow-Maske überarbeiten
Das redundante 'erledigt'-Feld wird aus CallLog entfernt (Modell, API, Client, Migration _db_V01_15 DROP COLUMN). Der Erledigt-Status ergibt sich dynamisch aus den zugeordneten Tags (is_erledigend). Die Tages-Workflow-Maske zeigt Datum, Uhrzeit Beginn und Dauer der neuen Notiz als Read-Only-Panel; die Dauer läuft als Timer (mm:ss) mit. Ein Betreff-Feld ergänzt die neue Notiz. Mit flexContent wird der Formular-Inhalt als Flex-Spalte gerendert, das Historie-Panel füllt den verbleibenden Platz und scrollt in sich. Generated by Mistral Vibe. Co-Authored-By: Mistral Vibe <vibe@mistral.ai>
This commit is contained in:
1 parent
929664453e
commit
6e3cc7375d
9 files changed
+224
-59
No files matched your search
@@ -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()
|
||||
|
||||
|
||||
@@ -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')
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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))
|
||||
])
|
||||
)
|
||||
)
|
||||
|
||||
@@ -73,7 +73,6 @@ export const kontaktadresseliste: m.ClosureComponent = (vnode) => {
|
||||
telefon: kontaktadresse.telefon,
|
||||
notiz: "",
|
||||
wiedervorlage_datum: "",
|
||||
erledigt: false,
|
||||
bc: serializeBc(chain)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -85,7 +85,6 @@ export const projektliste: m.ClosureComponent = (vnode) => {
|
||||
telefon: "",
|
||||
notiz: "",
|
||||
wiedervorlage_datum: "",
|
||||
erledigt: false,
|
||||
bc: serializeBc(chain)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -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,6 +710,11 @@ export const TagesWorkflowMaske: m.ClosureComponent = () => {
|
||||
: null,
|
||||
m("div", {style: {flex: '1', 'min-height': '0'}},
|
||||
m(Form, formAttrs, [
|
||||
// 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",
|
||||
@@ -679,43 +743,39 @@ export const TagesWorkflowMaske: m.ClosureComponent = () => {
|
||||
"Kontakt bearbeiten")))
|
||||
)
|
||||
),
|
||||
// ---- 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 || ""))
|
||||
)
|
||||
)
|
||||
),
|
||||
// ---- 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",
|
||||
@@ -727,6 +787,11 @@ export const TagesWorkflowMaske: m.ClosureComponent = () => {
|
||||
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,
|
||||
@@ -746,7 +811,8 @@ export const TagesWorkflowMaske: m.ClosureComponent = () => {
|
||||
wiedervorlageDatum = vorschlag
|
||||
}
|
||||
}
|
||||
} as iMultiSelect)),
|
||||
} as iMultiSelect))),
|
||||
m("div.w3-third",
|
||||
m("label", "Wiedervorlage Datum", m(Input, {
|
||||
name: "wiedervorlage_datum",
|
||||
value: wiedervorlageDatum,
|
||||
@@ -754,7 +820,83 @@ export const TagesWorkflowMaske: m.ClosureComponent = () => {
|
||||
oninput: (e: Event) => {
|
||||
wiedervorlageDatum = (e.target as HTMLInputElement).value
|
||||
}
|
||||
} as iInput))
|
||||
} as iInput)))
|
||||
),
|
||||
),
|
||||
// ---- Historie der bisherigen Gesprächsnotizen ----
|
||||
// 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 || ""))
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
])
|
||||
)
|
||||
)
|
||||
|
||||
@@ -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). */
|
||||
|
||||
Reference in new issue
Block a user