Compare commits
1
Commits
2ff4272ecd
..
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
bc9b337696 |
No files matched your search
@@ -9,6 +9,7 @@ import {iKunde, fetchKunden} from "../models/kunde"
|
|||||||
import {iProjekt, fetchProjekte, fetchProjektById} from "../models/projekt"
|
import {iProjekt, fetchProjekte, fetchProjektById} from "../models/projekt"
|
||||||
import {iKontaktadresse, fetchKontaktadressen} from "../models/kontaktadresse"
|
import {iKontaktadresse, fetchKontaktadressen} from "../models/kontaktadresse"
|
||||||
import {iTag, fetchTags, berechneWiedervorlageDatum} from "../models/tag"
|
import {iTag, fetchTags, berechneWiedervorlageDatum} from "../models/tag"
|
||||||
|
import {TagBadge} from "./tagbadge"
|
||||||
import {showServerError} from "./auth/authmessages"
|
import {showServerError} from "./auth/authmessages"
|
||||||
import {guardedNavigate, DirtyMaskHelper} from "./widgets/dirtyguard"
|
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.
|
// Zugewiesene Tags als farbige Badges hinter dem Betreff darstellen.
|
||||||
const tagBadges = (log.tags || []).map((t: iTag) =>
|
const tagBadges = (log.tags || []).map((t: iTag) =>
|
||||||
m("span", {
|
m(TagBadge, {tagData: t, compact: true})
|
||||||
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 [
|
return [
|
||||||
@@ -352,9 +342,11 @@ export const Call_Log: m.ClosureComponent = (vnode) => {
|
|||||||
return {value: ka.id, label: ka.name + (ka.firma ? " (" + ka.firma + ")" : "")}
|
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) => {
|
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]))
|
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 {iCallLog, fetchCallLogsByKontaktadresse, createCallLogREST} from "../models/call_log"
|
||||||
import {iTag, fetchTags, berechneWiedervorlageDatum} from "../models/tag"
|
import {iTag, fetchTags, berechneWiedervorlageDatum} from "../models/tag"
|
||||||
import {iTagesWorkflowEntry, iTagesWorkflowFilter, fetchTagesWorkflowList} from "../models/tages_workflow"
|
import {iTagesWorkflowEntry, iTagesWorkflowFilter, fetchTagesWorkflowList} from "../models/tages_workflow"
|
||||||
|
import {TagBadge} from "./tagbadge"
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
// MODUL-LEVEL WORKFLOW-KONTEXT
|
// 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
|
// Badges der Tags des letzten Gesprächs (gleicher Stil wie in der
|
||||||
// Gesprächsnotizen-Liste).
|
// Gesprächsnotizen-Liste).
|
||||||
const tagBadges = (eintrag.letzte_notiz_tags || []).map((t: iTag) =>
|
const tagBadges = (eintrag.letzte_notiz_tags || []).map((t: iTag) =>
|
||||||
m("span", {
|
m(TagBadge, {tagData: t, compact: true})
|
||||||
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 [
|
return [
|
||||||
m("div",
|
m("div",
|
||||||
@@ -675,9 +665,13 @@ export const TagesWorkflowMaske: m.ClosureComponent = () => {
|
|||||||
// selbst scrollbar.
|
// selbst scrollbar.
|
||||||
formAttrs.flexContent = true
|
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) => {
|
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]))
|
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("p", {style: {margin: '4px 0'}},
|
||||||
m("strong", "Kontakt: "),
|
m("strong", "Kontakt: "),
|
||||||
kontaktadresse ? kontaktadresse.name : "",
|
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.telefon ? " | " + kontaktadresse.telefon : "",
|
||||||
kontaktadresse && kontaktadresse.email ? " | " + kontaktadresse.email : ""),
|
kontaktadresse && kontaktadresse.email ? " | " + kontaktadresse.email : ""),
|
||||||
m("p", {style: {margin: '4px 0'}},
|
m("p", {style: {margin: '4px 0'}},
|
||||||
@@ -948,18 +946,7 @@ export const TagesWorkflowMaske: m.ClosureComponent = () => {
|
|||||||
m("td", log.betreff || ""),
|
m("td", log.betreff || ""),
|
||||||
m("td", log.notiz || ""),
|
m("td", log.notiz || ""),
|
||||||
m("td", (log.tags || []).map((t: iTag) =>
|
m("td", (log.tags || []).map((t: iTag) =>
|
||||||
m("span", {
|
m(TagBadge, {tagData: t, compact: true}))),
|
||||||
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 || ""))
|
m("td", {style: {'white-space': 'nowrap'}}, log.wiedervorlage_datum || ""))
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import {gridMountpoints} from "./mainlayout"
|
|||||||
import {showServerError} from "./auth/authmessages"
|
import {showServerError} from "./auth/authmessages"
|
||||||
import {guardedNavigate, DirtyMaskHelper} from "./widgets/dirtyguard"
|
import {guardedNavigate, DirtyMaskHelper} from "./widgets/dirtyguard"
|
||||||
import {iTag, iTagWithChildren, fetchTags, fetchTagHierarchy, fetchTagById, createTagREST, updateTagREST, deleteTagREST} from "../models/tag"
|
import {iTag, iTagWithChildren, fetchTags, fetchTagHierarchy, fetchTagById, createTagREST, updateTagREST, deleteTagREST} from "../models/tag"
|
||||||
|
import {TagBadge} from "./tagbadge"
|
||||||
|
|
||||||
const colorPalette: string[] = [
|
const colorPalette: string[] = [
|
||||||
"#ff0000", "#00ff00", "#0000ff", "#ffff00", "#ff00ff", "#00ffff",
|
"#ff0000", "#00ff00", "#0000ff", "#ffff00", "#ff00ff", "#00ffff",
|
||||||
@@ -348,47 +349,6 @@ export const TagList: m.ClosureComponent = () => {
|
|||||||
const tagIcon = tag.is_parent ? "folder" : "label"
|
const tagIcon = tag.is_parent ? "folder" : "label"
|
||||||
const tagTitle = tag.is_parent ? "Parent-Tag (nicht zuordenbar)" : ""
|
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 [
|
return [
|
||||||
m("span",
|
m("span",
|
||||||
m("i.material-icons", {
|
m("i.material-icons", {
|
||||||
@@ -399,20 +359,9 @@ export const TagList: m.ClosureComponent = () => {
|
|||||||
},
|
},
|
||||||
title: tagTitle
|
title: tagTitle
|
||||||
}, tagIcon),
|
}, tagIcon),
|
||||||
m("span", {
|
// Farbchip mit Name und Workflow-Icons (erledigend /
|
||||||
style: {
|
// Wiedervorlage) aus der zentralen TagBadge-Komponente.
|
||||||
backgroundColor: tag.color || '#9e9e9e',
|
m(TagBadge, {tagData: tag})),
|
||||||
color: 'white',
|
|
||||||
padding: '2px 6px',
|
|
||||||
borderRadius: '3px',
|
|
||||||
fontWeight: 'bold',
|
|
||||||
display: 'inline-block',
|
|
||||||
marginRight: '8px'
|
|
||||||
},
|
|
||||||
title: tagTitle
|
|
||||||
}, tag.name),
|
|
||||||
workflowBadges
|
|
||||||
),
|
|
||||||
m("span", tag.description),
|
m("span", tag.description),
|
||||||
m("div", {style: {display: 'flex', gap: '0', 'white-space': 'nowrap', 'justify-content': 'flex-end'}},
|
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)},
|
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
|
value: any
|
||||||
/** Anzeige im Feld bzw. in der Dropdown-Liste (z.B. Kundenname). */
|
/** Anzeige im Feld bzw. in der Dropdown-Liste (z.B. Kundenname). */
|
||||||
label: string
|
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("h2", modalTitle)
|
||||||
),
|
),
|
||||||
m("div.w3-container.w3-theme-l1", {style: {padding: '16px'}},
|
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", {
|
m("input.w3-input.w3-border", {
|
||||||
type: "text",
|
type: "text",
|
||||||
value: search,
|
value: search,
|
||||||
placeholder: "Suchbegriff …",
|
placeholder: "Suchbegriff …",
|
||||||
style: {'margin-bottom': '10px', width: '100%'},
|
style: {'margin-bottom': '10px', width: '100%'},
|
||||||
|
oncreate: (vnode: m.VnodeDOM) => {
|
||||||
|
(vnode.dom as HTMLInputElement).focus()
|
||||||
|
},
|
||||||
oninput: (e: Event) => {
|
oninput: (e: Event) => {
|
||||||
search = (e.target as HTMLInputElement).value
|
search = (e.target as HTMLInputElement).value
|
||||||
}
|
}
|
||||||
@@ -526,6 +537,19 @@ export const MultiSelect: m.ClosureComponent<iMultiSelect> = () => {
|
|||||||
toggleDraft(option)
|
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
|
color
|
||||||
? m("span", {
|
? m("span", {
|
||||||
style: {
|
style: {
|
||||||
@@ -539,6 +563,7 @@ export const MultiSelect: m.ClosureComponent<iMultiSelect> = () => {
|
|||||||
})
|
})
|
||||||
: null,
|
: null,
|
||||||
m("span", option.label)
|
m("span", option.label)
|
||||||
|
]
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
),
|
),
|
||||||
|
|||||||
Reference in new issue
Block a user