feat(calllog): Tags einer Gesprächsnotiz zuordnen (n:m)
Backend: - Neues CallLogTag-Model (Tabelle call_logs_tags) mit Relationship auf CallLog (cascade delete-orphan); to_dict liefert 'tags'. - Migration _db_V01_11 (idempotent via create_all). - _set_call_log_tags in create/update_call_log: ersetzt Tag-Zuordnungen, filtert is_parent-Tags, eliminiert Duplikate. - _parse_date wandelt ISO-Strings in date-Objekte (fixt autoflush- Crash bei Date-Spalten). Frontend: - iCallLog um tags (Lesen) und tag_ids (Schreiben) ergänzt. - Neues generisches MultiSelect-Widget (widgets/select.ts): Badges + Edit-Button, inline Modal mit Suchfeld, Checkbox-Liste, OK-Button; konfigurierbare Texte (placeholder, modalTitle, editTooltip). - Call_Log-Formular: Tag-Auswahl via MultiSelect, Submit via FormData.getAll; Tag-Badges in der Listenansicht hinter dem Betreff. Generated by Mistral Vibe. Co-Authored-By: Mistral Vibe <vibe@mistral.ai>
This commit is contained in:
1 parent
5abcb2d96c
commit
d30adf1579
6 files changed
+421
-9
No files matched your search
@@ -1,12 +1,13 @@
|
||||
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 {Select, iSelect, MultiSelect, iMultiSelect, 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"
|
||||
import {iTag, fetchTags} from "../models/tag"
|
||||
import {showServerError} from "./auth/authmessages"
|
||||
|
||||
// Aktualisierungsintervall für die Dauer-Anzeige in Millisekunden.
|
||||
@@ -58,12 +59,28 @@ export const call_log_liste: m.ClosureComponent = (vnode) => {
|
||||
const columnmapper: (node: iTreenode) => any[] = (node: iTreenode) => {
|
||||
let log: iCallLog = node.nodevalue
|
||||
|
||||
// Zugewiesene Tags als farbige Badges hinter dem Betreff darstellen.
|
||||
const tagBadges = (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)
|
||||
)
|
||||
|
||||
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.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"))
|
||||
@@ -183,6 +200,10 @@ export const Call_Log: m.ClosureComponent = (vnode) => {
|
||||
let kundenListe: iKunde[] = []
|
||||
let projektListe: iProjekt[] = []
|
||||
let kontaktadressenListe: iKontaktadresse[] = []
|
||||
// Asynchron geladene Tag-Liste (nur zuordenbare, nicht is_parent).
|
||||
let tagListe: iTag[] = []
|
||||
// Aktuell ausgewählte Tag-IDs im Formular.
|
||||
let selectedTagIds: number[] = []
|
||||
|
||||
/**
|
||||
* Formatiert verstrichene Sekunden als mm:ss.
|
||||
@@ -203,6 +224,7 @@ export const Call_Log: m.ClosureComponent = (vnode) => {
|
||||
currentLog = result
|
||||
projektVoreingestellt = true
|
||||
ansprechpartnerVoreingestellt = currentLog.name_ansprechpartner !== ""
|
||||
selectedTagIds = (result.tags || []).map((t: iTag) => t.id)
|
||||
}).catch((e) => showServerError(e))
|
||||
} else {
|
||||
projektVoreingestellt = routeParams && routeParams.projekt_id ? parseInt(routeParams.projekt_id) !== -1 : false
|
||||
@@ -234,6 +256,8 @@ export const Call_Log: m.ClosureComponent = (vnode) => {
|
||||
fetchKunden().then((result) => { kundenListe = result }).catch((e) => showServerError(e))
|
||||
fetchProjekte().then((result) => { projektListe = result }).catch((e) => showServerError(e))
|
||||
fetchKontaktadressen().then((result) => { kontaktadressenListe = result }).catch((e) => showServerError(e))
|
||||
// Tags laden — nur zuordenbare (is_parent=false) Tags.
|
||||
fetchTags().then((result) => { tagListe = result.filter((t: iTag) => !t.is_parent) }).catch((e) => showServerError(e))
|
||||
},
|
||||
onremove: (vnode) => {
|
||||
if (timerId != null) { window.clearInterval(timerId) }
|
||||
@@ -267,6 +291,11 @@ export const Call_Log: m.ClosureComponent = (vnode) => {
|
||||
return {value: ka.id, label: ka.name + (ka.firma ? " (" + ka.firma + ")" : "")}
|
||||
})
|
||||
: []
|
||||
// Tag-Optionen und Farb-Map für die Mehrfachauswahl.
|
||||
const tagOptionen: iSelectOption[] = tagListe.map((t: iTag) => {
|
||||
return {value: t.id, label: t.name}
|
||||
})
|
||||
const tagColors: Map<number, string> = new Map(tagListe.map((t: iTag) => [t.id, t.color] as [number, string]))
|
||||
|
||||
const submit = (form: HTMLFormElement): boolean => {
|
||||
const formData = new FormData(form)
|
||||
@@ -283,7 +312,8 @@ export const Call_Log: m.ClosureComponent = (vnode) => {
|
||||
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"
|
||||
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) {
|
||||
createCallLogREST(gespeichertesLog).then(() => m.route.set("/calllogliste", {projekt_id: currentLog.projekt_id, projekt_name: currentLog.projekt_name})).catch((e) => showServerError(e))
|
||||
@@ -353,6 +383,15 @@ export const Call_Log: m.ClosureComponent = (vnode) => {
|
||||
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", "Tags", m(MultiSelect, {
|
||||
name: "tag_ids",
|
||||
selected: selectedTagIds,
|
||||
options: tagOptionen,
|
||||
colors: tagColors,
|
||||
placeholder: "kein Tag",
|
||||
modalTitle: "Tags auswählen",
|
||||
editTooltip: "Tags bearbeiten"
|
||||
} as iMultiSelect)),
|
||||
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")
|
||||
])
|
||||
|
||||
Reference in new issue
Block a user