Neue Frontend-Komponenten für Gesprächsnotizen und Tags

- call_log.ts: Komponente zur Verwaltung von Gesprächsnotizen mit Liste und Einzelansicht
- tags.ts: Komponente zur hierarchischen Tag-Verwaltung mit Baumstruktur
This commit is contained in:
gg committed 2026-08-30 15:24:19 +02:00
1 parent 9d91915916
commit 72055840f7
2 files changed
+881

No files matched your search

+204
View File
@@ -0,0 +1,204 @@
import m from "mithril"
import {Form, iForm} from "./widgets/form"
import { Input, iInput} from "./widgets/inputs"
import {Treenode, iTreenode, Treetable, iTreetable} from "./widgets/Treetable_V2"
interface iCallLog {
id: number
kundenName: string
projektId: number
projektName: string
datum: string
uhrzeitBeginn: string
dauer: string
betreff: string
nameAnsprechpartner: string
telefon: string
notiz: string
wiedervorlageDatum: string
erledigt: boolean
}
const callLogDaten: iCallLog[] = [
{id: 1, kundenName: "Musterfirma GmbH", projektId: 1, projektName: "Website-Relaunch", datum: "2024-01-20", uhrzeitBeginn: "09:30", dauer: "45", betreff: "Anforderungen klären", nameAnsprechpartner: "Herr Schmidt", telefon: "+49 123 45678", notiz: "Kunde möchte responsive Design und neue Farbgestaltung. Budget noch offen.", wiedervorlageDatum: "2024-01-27", erledigt: false},
{id: 2, kundenName: "Musterfirma GmbH", projektId: 1, projektName: "Website-Relaunch", datum: "2024-01-25", uhrzeitBeginn: "14:00", dauer: "30", betreff: "Feedback zum Design", nameAnsprechpartner: "Frau Müller", telefon: "+49 123 45679", notiz: "Design-Entwürfe wurden positiv aufgenommen. Kleine Änderungen an der Navigation nötig.", wiedervorlageDatum: "2024-02-01", erledigt: false},
{id: 3, kundenName: "Beispiel AG", projektId: 2, projektName: "E-Commerce Plattform", datum: "2024-02-10", uhrzeitBeginn: "10:00", dauer: "60", betreff: "Technische Anforderungen", nameAnsprechpartner: "Herr Weber", telefon: "+49 123 45680", notiz: "Payment-Gateway und Versand-Schnittstellen müssen integriert werden. Zeitplan besprochen.", wiedervorlageDatum: "2024-02-15", erledigt: true},
{id: 4, kundenName: "Beispiel AG", projektId: 3, projektName: "CRM-System", datum: "2024-02-18", uhrzeitBeginn: "11:15", dauer: "20", betreff: "Schulungstermin", nameAnsprechpartner: "Frau Bauer", telefon: "+49 123 45681", notiz: "Schulung für 10 Mitarbeiter geplant. Raum noch zu buchen.", wiedervorlageDatum: "2024-02-20", erledigt: false},
{id: 5, kundenName: "Test Unternehmen", projektId: 4, projektName: "Mobile App", datum: "2024-03-05", uhrzeitBeginn: "15:45", dauer: "35", betreff: "Feature-Request", nameAnsprechpartner: "Herr König", telefon: "+49 123 45682", notiz: "Kunde wünscht Push-Benachrichtigungen und Offline-Modus.", wiedervorlageDatum: "2024-03-12", erledigt: false}
]
function createCallLogTreenodes(projektId: number | null = null): iTreenode[] {
let filteredLogs = projektId
? callLogDaten.filter(log => log.projektId === projektId)
: callLogDaten
return filteredLogs.map((log: iCallLog) => {
return new Treenode(log, true, [])
})
}
export const call_log_liste: m.ClosureComponent = (vnode) => {
let treenodes: iTreenode[] = createCallLogTreenodes()
let selectedNode: iTreenode | null = null
let filterProjektId: number | null = null
const handleRowMarked = (node: iTreenode) => {
selectedNode = node
}
const editCallLog = (log: iCallLog, e: Event) => {
e.stopPropagation()
m.route.set("/call_log", {
id: log.id,
kundenName: log.kundenName,
projektId: log.projektId,
projektName: log.projektName,
datum: log.datum,
uhrzeitBeginn: log.uhrzeitBeginn,
dauer: log.dauer,
betreff: log.betreff,
nameAnsprechpartner: log.nameAnsprechpartner,
telefon: log.telefon,
notiz: log.notiz,
wiedervorlageDatum: log.wiedervorlageDatum,
erledigt: log.erledigt
})
}
const columnmapper: (node: iTreenode) => any[] = (node: iTreenode) => {
let log: iCallLog = node.nodevalue
const isSelected = selectedNode === node
const cellStyle = isSelected ? {backgroundColor: '#e6f7ff'} : {}
return [
m("div", {style: cellStyle}, m("strong", log.kundenName + ": " + log.projektName)),
m("span", {style: cellStyle}, log.datum),
m("span", {style: cellStyle}, log.uhrzeitBeginn),
m("span", {style: cellStyle}, log.dauer + " min"),
m("span", {style: cellStyle}, log.betreff),
m("span", {style: cellStyle}, log.wiedervorlageDatum),
m("span", {style: cellStyle}, log.erledigt ? "Erledigt" : "Offen"),
m("button.w3-button.w3-small", {style: cellStyle, onclick: (e: Event) => editCallLog(log, e)}, m("i.material-icons", "edit"))
]
}
const filterFunction = (node: iTreenode, searchvalue: string): boolean => {
let log: iCallLog = node.nodevalue
let searchLower = searchvalue.toLowerCase()
return log.kundenName.toLowerCase().includes(searchLower) ||
log.projektName.toLowerCase().includes(searchLower) ||
log.datum.includes(searchLower) ||
log.uhrzeitBeginn.toLowerCase().includes(searchLower) ||
log.betreff.toLowerCase().includes(searchLower) ||
log.nameAnsprechpartner.toLowerCase().includes(searchLower) ||
log.notiz.toLowerCase().includes(searchLower) ||
log.wiedervorlageDatum.includes(searchLower)
}
return {
oninit: (vnode) => {
const routeParams = m.route.param()
if (routeParams && routeParams.projektId) {
filterProjektId = parseInt(routeParams.projektId)
} else {
filterProjektId = null
}
treenodes = createCallLogTreenodes(filterProjektId)
},
view: (vnode) => {
return m("div.w3-container", {
style: {
'max-width': '80%',
'margin-left': 'auto',
'margin-right': 'auto',
display: 'flex',
'flex-direction': 'column',
height: 'inherit',
'overflow-y': 'inherit'}},
m("h1", "Gesprächsnotizen"),
m("div", {style: {flex: '1', 'overflow-y': 'inherit'}},
m(Treetable,
{
treenodes: treenodes,
columnmapper: columnmapper,
filter: filterFunction,
onRowMarked: handleRowMarked
} as iTreetable
)
)
)
}
}
}
export const Call_Log: m.ClosureComponent = (vnode) => {
let currentLog: {
id: number
kundenName: string
projektId: number
projektName: string
datum: string
uhrzeitBeginn: string
dauer: string
betreff: string
nameAnsprechpartner: string
telefon: string
notiz: string
wiedervorlageDatum: string
erledigt: boolean
} = {
id: -1,
kundenName: "",
projektId: -1,
projektName: "",
datum: "",
uhrzeitBeginn: "",
dauer: "",
betreff: "",
nameAnsprechpartner: "",
telefon: "",
notiz: "",
wiedervorlageDatum: "",
erledigt: false
}
return {
oninit: (vnode) => {
const routeParams = m.route.param()
if (routeParams) {
currentLog = {
id: routeParams.id ? parseInt(routeParams.id) : -1,
kundenName: routeParams.kundenName || "",
projektId: routeParams.projektId ? parseInt(routeParams.projektId) : -1,
projektName: routeParams.projektName || "",
datum: routeParams.datum || "",
uhrzeitBeginn: routeParams.uhrzeitBeginn || "",
dauer: routeParams.dauer || "",
betreff: routeParams.betreff || "",
nameAnsprechpartner: routeParams.nameAnsprechpartner || "",
telefon: routeParams.telefon || "",
notiz: routeParams.notiz || "",
wiedervorlageDatum: routeParams.wiedervorlageDatum || "",
erledigt: routeParams.erledigt === true || routeParams.erledigt === "true"
}
}
},
view: (vnode) => {
return m("div.w3-container.w3-twothird",
m("h1", "Gesprächsnotiz"),
m(Form, {showButtons: true, onsubmit: (null), oncancel: (null)} as iForm, [
m("h4.w3-padding-small", currentLog.kundenName + " / " + currentLog.projektName),
m("label", "Datum", m(Input, {name: "datum", value: currentLog.datum, type: "date"} as iInput)),
m("label", "Uhrzeit Beginn", m(Input, {name: "uhrzeitBeginn", value: currentLog.uhrzeitBeginn, type: "time"} as iInput)),
m("label", "Dauer (Minuten)", m(Input, {name: "dauer", value: currentLog.dauer, type: "number"} as iInput)),
m("label", "Betreff", m(Input, {name: "betreff", value: currentLog.betreff, type: "text"} as iInput)),
m("label", "Name Ansprechpartner", m(Input, {name: "nameAnsprechpartner", value: currentLog.nameAnsprechpartner, type: "text"} as iInput)),
m("label", "Telefon", m(Input, {name: "telefon", value: currentLog.telefon, type: "tel"} as iInput)),
m("label", "Notiz", m(Input, {name: "notiz", value: currentLog.notiz, type: "text"} as iInput)),
m("label", "Wiedervorlage Datum", m(Input, {name: "wiedervorlageDatum", value: currentLog.wiedervorlageDatum, type: "date"} as iInput)),
m("label", m(Input, {name: "erledigt", type: "checkbox", value: currentLog.erledigt} as iInput), "Erledigt")
])
)
}
}
}
+677
View File
@@ -0,0 +1,677 @@
import m from "mithril"
import {Form, iForm} from "./widgets/form"
import { Input, iInput} from "./widgets/inputs"
import {Treenode, iTreenode, Treetable, iTreetable} from "./widgets/Treetable_V2"
const colorPalette: string[] = [
"#ff0000", "#00ff00", "#0000ff", "#ffff00", "#ff00ff", "#00ffff",
"#ff8000", "#80ff00", "#8000ff", "#ff8080", "#80ff80", "#8080ff",
"#ffa500", "#008000", "#000080", "#800000", "#808000", "#008080"
]
// ============================================================================
// INTERFACES FÜR HIERARCHISCHE TAGS
// ============================================================================
/**
* Basis-Tag-Interface für die hierarchische Struktur.
*/
export interface iTag {
id: number; // Numerische ID (für Kompatibilität mit bestehendem Code)
name: string; // Anzeigename des Tags
description: string; // Beschreibung
color: string; // Farbcode (z. B. "#ff0000")
parentId: number | null; // Referenz auf den Eltern-Tag (null = Wurzelebene)
icon?: string; // Optional: Icon-Name oder URL
order?: number; // Optional: Sortierreihenfolge innerhalb der Ebene
}
/**
* Tag mit verschachtelten Kindern für die Hierarchie-Darstellung.
*/
interface iTagWithChildren extends iTag {
children: iTagWithChildren[];
}
// ============================================================================
// BEISPIELDATEN (HIERARCHISCH)
// ============================================================================
/**
* Flache Liste aller Tags (für Datenbank-ähnliche Speicherung).
* Hierarchie wird über parentId abgebildet.
*/
const tagDaten: iTag[] = [
// ========== WURZEL-TAGS (Top-Level) ==========
{
id: 1,
name: "Wichtig",
description: "Hochpriorisierte Aufgaben",
color: "#ff0000",
parentId: null,
icon: "priority_high",
order: 1,
},
{
id: 2,
name: "Kunde",
description: "Kundenbezogene Tags",
color: "#00ff00",
parentId: null,
icon: "person",
order: 2,
},
{
id: 3,
name: "Projekt",
description: "Projektbezogene Tags",
color: "#0000ff",
parentId: null,
icon: "folder",
order: 3,
},
{
id: 4,
name: "Follow-up",
description: "Nachverfolgung erforderlich",
color: "#ffff00",
parentId: null,
icon: "schedule",
order: 4,
},
{
id: 5,
name: "Archiv",
description: "Archivierte Einträge",
color: "#808080",
parentId: null,
icon: "archive",
order: 5,
},
// ========== TECHNISCHE KATEGORIEN (neu) ==========
{
id: 100,
name: "Backend",
description: "Server-seitige Technologien und Frameworks",
color: "#4285F4",
parentId: null,
icon: "server",
order: 6,
},
{
id: 101,
name: "Frontend",
description: "Client-seitige Technologien und Frameworks",
color: "#34A853",
parentId: null,
icon: "browser",
order: 7,
},
{
id: 102,
name: "DevOps",
description: "Infrastruktur, Deployment und CI/CD",
color: "#EA4335",
parentId: null,
icon: "cog",
order: 8,
},
{
id: 103,
name: "Daten",
description: "Datenbanken, Analyse und KI",
color: "#FBBC04",
parentId: null,
icon: "database",
order: 9,
},
// Backend -> Programmiersprachen
{
id: 200,
name: "Python",
description: "Allgemeine Python-Entwicklung",
color: "#3776AB",
parentId: 100,
icon: "python",
order: 1,
},
{
id: 201,
name: "Java",
description: "Java und JVM-Ökosystem",
color: "#007396",
parentId: 100,
icon: "java",
order: 2,
},
{
id: 202,
name: "Node.js",
description: "JavaScript auf dem Server",
color: "#339933",
parentId: 100,
icon: "nodejs",
order: 3,
},
// Backend -> Python -> Frameworks
{
id: 300,
name: "Flask",
description: "Mikro-Webframework für Python",
color: "#000000",
parentId: 200,
icon: "flask",
order: 1,
},
{
id: 301,
name: "Django",
description: "Full-Stack Webframework für Python",
color: "#092E20",
parentId: 200,
icon: "django",
order: 2,
},
{
id: 302,
name: "FastAPI",
description: "Moderne API-Entwicklung mit Python",
color: "#005577",
parentId: 200,
icon: "fastapi",
order: 3,
},
// Backend -> Node.js -> Frameworks
{
id: 310,
name: "Express",
description: "Minimalistisches Webframework für Node.js",
color: "#000000",
parentId: 202,
icon: "express",
order: 1,
},
{
id: 311,
name: "NestJS",
description: "Progressives Node.js-Framework für Enterprise-Anwendungen",
color: "#E0234E",
parentId: 202,
icon: "nestjs",
order: 2,
},
// Backend -> Datenbanken
{
id: 210,
name: "Datenbanken",
description: "Backend-Datenbank-Systeme",
color: "#336791",
parentId: 100,
icon: "database",
order: 4,
},
{
id: 211,
name: "PostgreSQL",
description: "Relationale Datenbank mit erweiterten Features",
color: "#336791",
parentId: 210,
icon: "postgres",
order: 1,
},
{
id: 212,
name: "MongoDB",
description: "Dokumentenorientierte NoSQL-Datenbank",
color: "#47A248",
parentId: 210,
icon: "mongodb",
order: 2,
},
// Frontend -> JavaScript
{
id: 220,
name: "JavaScript",
description: "Die Hauptsprache des Web",
color: "#F7DF1E",
parentId: 101,
icon: "js",
order: 1,
},
{
id: 221,
name: "TypeScript",
description: "Typsicheres JavaScript",
color: "#3178C6",
parentId: 101,
icon: "ts",
order: 2,
},
// Frontend -> JavaScript -> Frameworks
{
id: 320,
name: "React",
description: "Komponentenbasiertes UI-Framework",
color: "#61DAFB",
parentId: 220,
icon: "react",
order: 1,
},
{
id: 321,
name: "Vue.js",
description: "Progressives JavaScript-Framework",
color: "#4FC08D",
parentId: 220,
icon: "vue",
order: 2,
},
{
id: 322,
name: "Angular",
description: "Full-Stack Framework von Google",
color: "#DD1B16",
parentId: 220,
icon: "angular",
order: 3,
},
// Frontend -> CSS
{
id: 230,
name: "CSS",
description: "Stildefinition für Webseiten",
color: "#1572B6",
parentId: 101,
icon: "css",
order: 3,
},
{
id: 330,
name: "Tailwind CSS",
description: "Utility-first CSS-Framework",
color: "#06B6D4",
parentId: 230,
icon: "tailwind",
order: 1,
},
// DevOps
{
id: 240,
name: "Docker",
description: "Containerisierung von Anwendungen",
color: "#2496ED",
parentId: 102,
icon: "docker",
order: 1,
},
{
id: 241,
name: "Kubernetes",
description: "Orchestrierung von Containern",
color: "#326CE5",
parentId: 102,
icon: "kubernetes",
order: 2,
},
{
id: 242,
name: "CI/CD",
description: "Continuous Integration und Deployment",
color: "#FFA500",
parentId: 102,
icon: "cicd",
order: 3,
},
// Daten
{
id: 250,
name: "Künstliche Intelligenz",
description: "Machine Learning und KI-Technologien",
color: "#8B00FF",
parentId: 103,
icon: "ai",
order: 1,
},
{
id: 251,
name: "Big Data",
description: "Verarbeitung großer Datenmengen",
color: "#FF6B6B",
parentId: 103,
icon: "bigdata",
order: 2,
},
];
// ============================================================================
// HILFSFUNKTIONEN FÜR HIERARCHISCHE TAGS
// ============================================================================
/**
* Gibt alle Wurzel-Tags (ohne Eltern) zurück.
*/
function getRootTags(): iTag[] {
return tagDaten.filter((tag) => tag.parentId === null);
}
/**
* Baut aus der flachen Liste eine hierarchische Struktur mit Kindern.
* @param parentId - ID des Eltern-Tags (null für Wurzelebene)
* @returns Array von Tags mit verschachtelten Kindern
*/
function buildHierarchy(parentId: number | null = null): iTagWithChildren[] {
const children = tagDaten.filter((tag) => tag.parentId === parentId);
// Sortieren nach order-Feld (falls vorhanden)
children.sort((a, b) => (a.order || 0) - (b.order || 0));
return children.map((tag) => ({
...tag,
children: buildHierarchy(tag.id),
}));
}
/**
* Gibt die vollständige Hierarchie als verschachteltes Objekt zurück.
*/
function getFullHierarchy(): iTagWithChildren[] {
return buildHierarchy(null);
}
/**
* Sucht einen Tag nach ID.
*/
function findTagById(id: number): iTag | undefined {
return tagDaten.find((tag) => tag.id === id);
}
/**
* Gibt den vollständigen Pfad eines Tags als String zurück (z. B. "/Backend/Python/Flask").
*/
function getTagPath(tagId: number): string {
const tag = findTagById(tagId);
if (!tag) return "";
if (tag.parentId === null) return `/${tag.name}`;
const parentPath = getTagPath(tag.parentId);
return `${parentPath}/${tag.name}`;
}
/**
* Gibt alle Nachfahren (Kinder, Enkel, ...) eines Tags zurück (flache Liste).
*/
function getDescendants(tagId: number): iTag[] {
const descendants: iTag[] = [];
const children = tagDaten.filter((tag) => tag.parentId === tagId);
for (const child of children) {
descendants.push(child);
descendants.push(...getDescendants(child.id));
}
return descendants;
}
/**
* Gibt alle Vorfahren (Eltern, Großeltern, ...) eines Tags zurück.
*/
function getAncestors(tagId: number): iTag[] {
const ancestors: iTag[] = [];
const tag = findTagById(tagId);
if (!tag || tag.parentId === null) return ancestors;
const parent = findTagById(tag.parentId);
if (parent) {
ancestors.push(parent);
ancestors.push(...getAncestors(parent.id));
}
return ancestors;
}
/**
* Sucht Tags nach Namen (case-insensitive, partiell).
*/
function searchTagsByName(query: string): iTag[] {
const lowerQuery = query.toLowerCase();
return tagDaten.filter((tag) => tag.name.toLowerCase().includes(lowerQuery));
}
/**
* Fügt einen neuen Tag hinzu (mit automatischer ID-Vergabe).
*/
function addTag(name: string, parentId: number | null = null, description: string = "", color: string = "#808080", icon?: string, order?: number): iTag {
const newId = tagDaten.length > 0 ? Math.max(...tagDaten.map(t => t.id)) + 1 : 1;
const tag: iTag = {
id: newId,
name,
description,
color,
parentId,
icon,
order,
};
tagDaten.push(tag);
return tag;
}
/**
* Ersetzt einen bestehenden Tag (z. B. nach Bearbeitung).
*/
function updateTag(updatedTag: iTag): void {
const index = tagDaten.findIndex(t => t.id === updatedTag.id);
if (index !== -1) {
tagDaten[index] = updatedTag;
}
}
// ============================================================================
// TREENODE-ERZEUGUNG (FÜR DIE TREETABLE)
// ============================================================================
/**
* Erzeugt Treenodes aus der hierarchischen Tag-Struktur.
* Berücksichtigt die Verschachtelung der Tags.
*/
function createTagTreenodes(): iTreenode[] {
const hierarchy = getFullHierarchy();
return hierarchy.map((tag) => createTreenodeFromTag(tag));
}
/**
* Rekursive Funktion zum Erstellen eines Treenode aus einem Tag mit Kindern.
*/
function createTreenodeFromTag(tag: iTagWithChildren): iTreenode {
const children = tag.children.map(child => createTreenodeFromTag(child));
return new Treenode(tag, true, children);
}
// ============================================================================
// KOMPONENTEN
// ============================================================================
export const Tag: m.ClosureComponent = (vnode) => {
let currentTag: iTag = {
id: -1,
name: "",
description: "",
color: "",
parentId: null,
};
const setColor = (color: string) => {
currentTag.color = color;
};
// Liste aller Tags für die Eltern-Auswahl
const allTags = tagDaten.filter(t => t.id !== currentTag.id); // Ausschließen des aktuellen Tags
return {
oninit: (vnode) => {
const routeParams = m.route.param();
if (routeParams) {
currentTag = {
id: routeParams.id ? parseInt(routeParams.id) : -1,
name: routeParams.name || "",
description: routeParams.description || "",
color: routeParams.color || "",
parentId: routeParams.parentId ? parseInt(routeParams.parentId) : null,
icon: routeParams.icon || undefined,
order: routeParams.order ? parseInt(routeParams.order) : undefined,
};
}
},
view: (vnode) => {
return m("div.w3-container.w3-twothird",
m("h1", currentTag.id === -1 ? "Neuen Tag erstellen" : "Tag bearbeiten"),
m(Form, {showButtons: true, onsubmit: (null), oncancel: (null)} as iForm, [
m("label", "Tag-Name", m(Input, {name: "name", value: currentTag.name, type: "text"} as iInput)),
m("label", "Beschreibung", m(Input, {name: "description", value: currentTag.description, type: "text"} as iInput)),
// Eltern-Tag-Auswahl
m("label", "Übergeordneter Tag (optional)",
m("select.w3-select", {name: "parentId", value: currentTag.parentId || ""},
m("option", {value: ""}, "-- Kein übergeordneter Tag --"),
allTags.map(tag =>
m("option", {value: tag.id, selected: currentTag.parentId === tag.id}, tag.name)
)
)
),
m("label", "Farbe"),
m("div", {style: {display: 'flex', flexWrap: 'wrap', gap: '8px', marginBottom: '12px'}},
colorPalette.map(color =>
m("button", {
style: {
width: '32px',
height: '32px',
backgroundColor: color,
border: currentTag.color === color ? '2px solid black' : '1px solid #ccc',
borderRadius: '4px',
cursor: 'pointer'
},
onclick: () => setColor(color)
})
)
),
m(Input, {name: "color", value: currentTag.color, type: "text"} as iInput),
m("label", "Icon (optional)", m(Input, {name: "icon", value: currentTag.icon || "", type: "text"} as iInput)),
m("label", "Sortierreihenfolge (optional)", m(Input, {name: "order", value: currentTag.order?.toString() || "", type: "number"} as iInput)),
])
);
}
};
};
export const TagList: m.ClosureComponent = (vnode) => {
let treenodes: iTreenode[] = createTagTreenodes();
let selectedNode: iTreenode | null = null;
// Aktualisiere treenodes, wenn sich die Daten ändern
const updateTreenodes = () => {
treenodes = createTagTreenodes();
selectedNode = null;
};
const handleRowMarked = (node: iTreenode) => {
selectedNode = node;
};
const editTag = (tag: iTag, e: Event) => {
e.stopPropagation();
m.route.set("/tag", {
id: tag.id,
name: tag.name,
description: tag.description,
color: tag.color,
parentId: tag.parentId,
icon: tag.icon,
order: tag.order
});
};
const columnmapper: (node: iTreenode) => any[] = (node: iTreenode) => {
let tag: iTag = node.nodevalue;
const isSelected = selectedNode === node;
const cellStyle = isSelected ? {backgroundColor: 'lightgray'} : {};
return [
m("span", {style: cellStyle},
m("span", {
style: {
backgroundColor: tag.color,
color: 'white',
padding: '2px 6px',
borderRadius: '3px',
fontWeight: 'bold',
display: 'inline-block',
marginRight: '8px'
}
}, tag.name)
),
m("span", {style: cellStyle}, tag.description),
m("div", {style: cellStyle},
m("button.w3-button.w3-small", {onclick: (e: Event) => editTag(tag, e)},
m("i.material-icons", "edit")
)
)
];
};
const filterFunction = (node: iTreenode, searchvalue: string): boolean => {
let tag: iTag = node.nodevalue;
let searchLower = searchvalue.toLowerCase();
return tag.name.toLowerCase().includes(searchLower) ||
tag.description.toLowerCase().includes(searchLower) ||
tag.color.toLowerCase().includes(searchLower) ||
(tag.icon && tag.icon.toLowerCase().includes(searchLower));
};
return {
view: (vnode) => {
return m("div.w3-container", {
style: {
'max-width': '80%',
'margin-left': 'auto',
'margin-right': 'auto',
display: 'flex',
'flex-direction': 'column',
height: 'inherit',
'overflow-y': 'inherit'}},
m("h1", "Tags"),
m("p.w3-text-grey", "Hierarchie: Tags können beliebig verschachtelt werden (z. B. Backend → Python → Flask)."),
m("div", {style: {flex: '1', 'overflow-y': 'inherit'}},
m(Treetable,
{
treenodes: treenodes,
columnmapper: columnmapper,
filter: filterFunction,
onRowMarked: handleRowMarked
} as iTreetable
)
)
);
}
};
};
// ============================================================================
// EXPORTE FÜR EXTERNE NUTZUNG
// ============================================================================
export {
tagDaten as tags, // Flache Liste aller Tags
getFullHierarchy as hierarchy, // Vollständige hierarchische Struktur
findTagById,
getTagPath,
getDescendants,
getAncestors,
searchTagsByName,
addTag,
updateTag,
getRootTags,
};