feat(crm): zentrale TagBadge-Komponente und Ergänzungen zur Workflow-Maske
- Position des Ansprechpartners in der Kopfzeile der Tagesworkflow-Maske anzeigen - Neue TagBadge-Komponente (Farbchip, Name, Workflow-Icons: erledigend/ Wiedervorlage) ersetzt die duplizierte Badge-Darstellung in Notiz-Liste, Workflow-Liste, Historie und Tag-Liste - MultiSelect: optionale content-Option rendert ein fertiges Badge des Aufrufers statt Farbpunkt + Label im Auswahl-Modal; Suchfeld wird beim Öffnen des Modals fokussiert - TagBadge-Attribut heißt tagData statt tag, da Mithril attrs-Objekte mit tag-Property als Vnode-Kinder behandelt (führte zu 'color' undefined) Generated by Mistral Vibe. Co-Authored-By: Mistral Vibe <vibe@mistral.ai>
This commit is contained in:
1 parent
2ff4272ecd
commit
bc9b337696
5 files changed
+146
-96
No files matched your search
@@ -9,6 +9,7 @@ import {iKunde, fetchKunden} from "../models/kunde"
|
||||
import {iProjekt, fetchProjekte, fetchProjektById} from "../models/projekt"
|
||||
import {iKontaktadresse, fetchKontaktadressen} from "../models/kontaktadresse"
|
||||
import {iTag, fetchTags, berechneWiedervorlageDatum} from "../models/tag"
|
||||
import {TagBadge} from "./tagbadge"
|
||||
import {showServerError} from "./auth/authmessages"
|
||||
import {guardedNavigate, DirtyMaskHelper} from "./widgets/dirtyguard"
|
||||
|
||||
@@ -83,18 +84,7 @@ export const call_log_liste: m.ClosureComponent = (vnode) => {
|
||||
|
||||
// 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)
|
||||
m(TagBadge, {tagData: t, compact: true})
|
||||
)
|
||||
|
||||
return [
|
||||
@@ -352,9 +342,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.
|
||||
// Tag-Optionen und Farb-Map für die Mehrfachauswahl. Als content
|
||||
// wird das fertige TagBadge übergeben — das Auswahl-Modal zeigt
|
||||
// damit den Farbchip mit Namen und die Workflow-Icons.
|
||||
const tagOptionen: iSelectOption[] = tagListe.map((t: iTag) => {
|
||||
return {value: t.id, label: t.name}
|
||||
return {value: t.id, label: t.name, content: m(TagBadge, {tagData: t})}
|
||||
})
|
||||
const tagColors: Map<number, string> = new Map(tagListe.map((t: iTag) => [t.id, t.color] as [number, string]))
|
||||
|
||||
|
||||
@@ -0,0 +1,97 @@
|
||||
import m from "mithril"
|
||||
import {iTag} from "../models/tag"
|
||||
|
||||
/**
|
||||
* Der Vnode der Komponente 'TagBadge' akzeptiert die folgenden Parameter:
|
||||
* @property {iTag} tagData - Der darzustellende Tag. Achtung: Das Attribut
|
||||
* darf nicht 'tag' heißen — Mithril behandelt ein attrs-
|
||||
* Objekt mit einer 'tag'-Property als Vnode und verschiebt
|
||||
* es in die children (siehe hyperscriptVnode.js).
|
||||
* @property {boolean} compact (optional) - kompakte Darstellung für dichte
|
||||
* Listen/Tabellen (kleinere Schrift, engeres Padding);
|
||||
* Default ist die Standarddarstellung (fett)
|
||||
* @property {string} title (optional) - Tooltip des Farbchips
|
||||
* (Default: Tag-Name)
|
||||
*/
|
||||
export interface iTagBadge {
|
||||
tagData: iTag
|
||||
compact?: boolean
|
||||
title?: string
|
||||
}
|
||||
|
||||
/**
|
||||
* Farbige Badge-Komponente für einen Tag: rendert den Tag-Namen als Chip in
|
||||
* der Tag-Farbe und dahinter automatisch die Workflow-Zusatzicons —
|
||||
* back_hand (rot) für erledigende Tags, schedule + Tagesabstand (blau) für
|
||||
* Tags, die eine Wiedervorlage auslösen (jeweils mit Tooltip). Parent-Tags
|
||||
* erhalten keine Zusatzicons.
|
||||
*
|
||||
* Die Komponente kapselt die Darstellung zentral, damit alle Stellen
|
||||
* (Notiz-Listen, Workflow-Liste, Historie, Auswahl-Modals, Tag-Liste)
|
||||
* einheitlich aussehen. Aufrufer geben nur den Tag an; Name, Farbe und
|
||||
* Zusatzicons ergeben sich aus den Tag-Daten.
|
||||
*/
|
||||
export const TagBadge: m.ClosureComponent<iTagBadge> = () => {
|
||||
return {
|
||||
view: (vnode) => {
|
||||
const tag = vnode.attrs.tagData
|
||||
const compact = vnode.attrs.compact === true
|
||||
// Farbiger Chip mit dem Tag-Namen. Bei fehlender Farbe grau.
|
||||
const chip = m("span", {
|
||||
style: {
|
||||
backgroundColor: tag.color || '#999',
|
||||
color: 'white',
|
||||
padding: compact ? '1px 5px' : '2px 6px',
|
||||
'border-radius': '3px',
|
||||
'font-size': compact ? '11px' : undefined,
|
||||
fontWeight: compact ? undefined : 'bold',
|
||||
display: 'inline-block',
|
||||
marginRight: '4px'
|
||||
},
|
||||
title: vnode.attrs.title || tag.name
|
||||
}, tag.name)
|
||||
// Workflow-Zusatzicons hinter dem Chip — fest programmiert aus
|
||||
// den Tag-Flags, nicht per Attribut steuerbar. Parent-Tags sind
|
||||
// nicht zuordenbar und erhalten daher keine Icons.
|
||||
const icons: m.Child[] = []
|
||||
if (!tag.is_parent) {
|
||||
if (tag.is_erledigend) {
|
||||
icons.push(m("i.material-icons", {
|
||||
title: "Erledigend: Kontakt gilt als erledigt",
|
||||
style: {
|
||||
verticalAlign: 'middle',
|
||||
fontSize: '16px',
|
||||
color: '#d32f2f'
|
||||
}
|
||||
}, "back_hand"))
|
||||
}
|
||||
if (tag.loest_wiedervorlage_aus) {
|
||||
icons.push(m("span", {
|
||||
title: "Löst Wiedervorlage aus: " + (tag.wiedervorlage_abstand_tage ?? 0) + " Tage",
|
||||
style: {
|
||||
verticalAlign: 'middle',
|
||||
fontSize: '12px',
|
||||
color: '#1565c0'
|
||||
}
|
||||
},
|
||||
m("i.material-icons", {
|
||||
style: {
|
||||
verticalAlign: 'middle',
|
||||
marginRight: '2px',
|
||||
fontSize: '16px'
|
||||
}
|
||||
}, "schedule"),
|
||||
tag.wiedervorlage_abstand_tage != null ? tag.wiedervorlage_abstand_tage + "d" : ""))
|
||||
}
|
||||
}
|
||||
return m("span", {
|
||||
style: {
|
||||
display: 'inline-flex',
|
||||
alignItems: 'center',
|
||||
marginLeft: '4px',
|
||||
whiteSpace: 'nowrap'
|
||||
}
|
||||
}, chip, icons)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -14,6 +14,7 @@ import {iProjekt, fetchProjektById} from "../models/projekt"
|
||||
import {iCallLog, fetchCallLogsByKontaktadresse, createCallLogREST} from "../models/call_log"
|
||||
import {iTag, fetchTags, berechneWiedervorlageDatum} from "../models/tag"
|
||||
import {iTagesWorkflowEntry, iTagesWorkflowFilter, fetchTagesWorkflowList} from "../models/tages_workflow"
|
||||
import {TagBadge} from "./tagbadge"
|
||||
|
||||
// ============================================================================
|
||||
// MODUL-LEVEL WORKFLOW-KONTEXT
|
||||
@@ -148,18 +149,7 @@ export const TagesWorkflowListe: m.ClosureComponent = () => {
|
||||
// Badges der Tags des letzten Gesprächs (gleicher Stil wie in der
|
||||
// Gesprächsnotizen-Liste).
|
||||
const tagBadges = (eintrag.letzte_notiz_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(TagBadge, {tagData: t, compact: true})
|
||||
)
|
||||
return [
|
||||
m("div",
|
||||
@@ -675,9 +665,13 @@ export const TagesWorkflowMaske: m.ClosureComponent = () => {
|
||||
// selbst scrollbar.
|
||||
formAttrs.flexContent = true
|
||||
|
||||
// Tag-Optionen und Farb-Map für die Mehrfachauswahl.
|
||||
// Tag-Optionen und Farb-Map für die Mehrfachauswahl. Als content
|
||||
// wird das fertige TagBadge übergeben — das Auswahl-Modal zeigt
|
||||
// damit den Farbchip mit Namen und automatisch die Workflow-Icons
|
||||
// (back_hand = erledigend, schedule + Tagesabstand = löst
|
||||
// Wiedervorlage aus). label bleibt als Suchbasis gesetzt.
|
||||
const tagOptionen: iSelectOption[] = tagListe.map((t: iTag) => {
|
||||
return {value: t.id, label: t.name}
|
||||
return {value: t.id, label: t.name, content: m(TagBadge, {tagData: t})}
|
||||
})
|
||||
const tagColors: Map<number, string> = new Map(tagListe.map((t: iTag) => [t.id, t.color] as [number, string]))
|
||||
|
||||
@@ -795,6 +789,10 @@ export const TagesWorkflowMaske: m.ClosureComponent = () => {
|
||||
m("p", {style: {margin: '4px 0'}},
|
||||
m("strong", "Kontakt: "),
|
||||
kontaktadresse ? kontaktadresse.name : "",
|
||||
// Position des Ansprechpartners in Klammern
|
||||
// hinter dem Namen (nur wenn gesetzt).
|
||||
kontaktadresse && kontaktadresse.position
|
||||
? " (" + kontaktadresse.position + ")" : "",
|
||||
kontaktadresse && kontaktadresse.telefon ? " | " + kontaktadresse.telefon : "",
|
||||
kontaktadresse && kontaktadresse.email ? " | " + kontaktadresse.email : ""),
|
||||
m("p", {style: {margin: '4px 0'}},
|
||||
@@ -948,18 +946,7 @@ export const TagesWorkflowMaske: m.ClosureComponent = () => {
|
||||
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(TagBadge, {tagData: t, compact: true}))),
|
||||
m("td", {style: {'white-space': 'nowrap'}}, log.wiedervorlage_datum || ""))
|
||||
)
|
||||
)
|
||||
|
||||
@@ -7,6 +7,7 @@ import {gridMountpoints} from "./mainlayout"
|
||||
import {showServerError} from "./auth/authmessages"
|
||||
import {guardedNavigate, DirtyMaskHelper} from "./widgets/dirtyguard"
|
||||
import {iTag, iTagWithChildren, fetchTags, fetchTagHierarchy, fetchTagById, createTagREST, updateTagREST, deleteTagREST} from "../models/tag"
|
||||
import {TagBadge} from "./tagbadge"
|
||||
|
||||
const colorPalette: string[] = [
|
||||
"#ff0000", "#00ff00", "#0000ff", "#ffff00", "#ff00ff", "#00ffff",
|
||||
@@ -348,47 +349,6 @@ export const TagList: m.ClosureComponent = () => {
|
||||
const tagIcon = tag.is_parent ? "folder" : "label"
|
||||
const tagTitle = tag.is_parent ? "Parent-Tag (nicht zuordenbar)" : ""
|
||||
|
||||
// Workflow-Badges für echte Tags (nicht für Parent-Tags)
|
||||
let workflowBadges: any[] = []
|
||||
if (!tag.is_parent) {
|
||||
workflowBadges = []
|
||||
if (tag.is_erledigend) {
|
||||
workflowBadges.push(
|
||||
m("i.material-icons", {
|
||||
title: "Erledigend: Kontakt gilt als erledigt",
|
||||
style: {
|
||||
verticalAlign: 'middle',
|
||||
marginRight: '4px',
|
||||
fontSize: '16px',
|
||||
color: '#d32f2f'
|
||||
}
|
||||
}, "back_hand")
|
||||
)
|
||||
}
|
||||
if (tag.loest_wiedervorlage_aus) {
|
||||
workflowBadges.push(
|
||||
m("span", {
|
||||
title: "Löst Wiedervorlage aus: " + (tag.wiedervorlage_abstand_tage ?? 0) + " Tage",
|
||||
style: {
|
||||
verticalAlign: 'middle',
|
||||
marginRight: '4px',
|
||||
fontSize: '12px',
|
||||
color: '#1565c0'
|
||||
}
|
||||
},
|
||||
m("i.material-icons", {
|
||||
style: {
|
||||
verticalAlign: 'middle',
|
||||
marginRight: '2px',
|
||||
fontSize: '16px'
|
||||
}
|
||||
}, "schedule"),
|
||||
tag.wiedervorlage_abstand_tage != null ? tag.wiedervorlage_abstand_tage + "d" : ""
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
return [
|
||||
m("span",
|
||||
m("i.material-icons", {
|
||||
@@ -399,20 +359,9 @@ export const TagList: m.ClosureComponent = () => {
|
||||
},
|
||||
title: tagTitle
|
||||
}, tagIcon),
|
||||
m("span", {
|
||||
style: {
|
||||
backgroundColor: tag.color || '#9e9e9e',
|
||||
color: 'white',
|
||||
padding: '2px 6px',
|
||||
borderRadius: '3px',
|
||||
fontWeight: 'bold',
|
||||
display: 'inline-block',
|
||||
marginRight: '8px'
|
||||
},
|
||||
title: tagTitle
|
||||
}, tag.name),
|
||||
workflowBadges
|
||||
),
|
||||
// Farbchip mit Name und Workflow-Icons (erledigend /
|
||||
// Wiedervorlage) aus der zentralen TagBadge-Komponente.
|
||||
m(TagBadge, {tagData: tag})),
|
||||
m("span", tag.description),
|
||||
m("div", {style: {display: 'flex', gap: '0', 'white-space': 'nowrap', 'justify-content': 'flex-end'}},
|
||||
m("button.w3-button.w3-small", {title: "Tag bearbeiten", style: {padding: '0 2px'}, onclick: (e: Event) => editTag(tag, e)},
|
||||
|
||||
@@ -8,6 +8,11 @@ export interface iSelectOption {
|
||||
value: any
|
||||
/** Anzeige im Feld bzw. in der Dropdown-Liste (z.B. Kundenname). */
|
||||
label: string
|
||||
/** Optionaler fertiger Darstellungs-Content (z.B. eine Badge-Komponente),
|
||||
* der im Auswahl-Modal des MultiSelect statt Farbpunkt und Label
|
||||
* angezeigt wird. Das label bleibt davon unberührt — es ist weiterhin
|
||||
* die Basis für die Suche im Modal. */
|
||||
content?: m.Child
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -491,12 +496,18 @@ export const MultiSelect: m.ClosureComponent<iMultiSelect> = () => {
|
||||
m("h2", modalTitle)
|
||||
),
|
||||
m("div.w3-container.w3-theme-l1", {style: {padding: '16px'}},
|
||||
// Suchfeld.
|
||||
// Suchfeld. Der oncreate-Hook fokussiert das
|
||||
// Feld direkt beim Öffnen des Modals — das
|
||||
// Input wird ja erst mit dem Modal erzeugt,
|
||||
// sodass der Nutzer sofort tippen kann.
|
||||
m("input.w3-input.w3-border", {
|
||||
type: "text",
|
||||
value: search,
|
||||
placeholder: "Suchbegriff …",
|
||||
style: {'margin-bottom': '10px', width: '100%'},
|
||||
oncreate: (vnode: m.VnodeDOM) => {
|
||||
(vnode.dom as HTMLInputElement).focus()
|
||||
},
|
||||
oninput: (e: Event) => {
|
||||
search = (e.target as HTMLInputElement).value
|
||||
}
|
||||
@@ -526,6 +537,19 @@ export const MultiSelect: m.ClosureComponent<iMultiSelect> = () => {
|
||||
toggleDraft(option)
|
||||
}
|
||||
}),
|
||||
// Ist ein fertiger content gesetzt (z.B. eine
|
||||
// Badge-Komponente des Aufrufers), wird dieser
|
||||
// statt Farbpunkt und Label angezeigt — das
|
||||
// Widget selbst bleibt frei von Tag-Wissen.
|
||||
option.content
|
||||
? m("span", {
|
||||
style: {
|
||||
display: 'inline-flex',
|
||||
alignItems: 'center',
|
||||
flex: '1 1 auto'
|
||||
}
|
||||
}, option.content)
|
||||
: [
|
||||
color
|
||||
? m("span", {
|
||||
style: {
|
||||
@@ -539,6 +563,7 @@ export const MultiSelect: m.ClosureComponent<iMultiSelect> = () => {
|
||||
})
|
||||
: null,
|
||||
m("span", option.label)
|
||||
]
|
||||
)
|
||||
})
|
||||
),
|
||||
|
||||
Reference in new issue
Block a user