From 72055840f71f949973ba7c0c64a10385fca1a2bc Mon Sep 17 00:00:00 2001 From: Gerd Date: Sun, 30 Aug 2026 15:24:19 +0200 Subject: [PATCH] =?UTF-8?q?Neue=20Frontend-Komponenten=20f=C3=BCr=20Gespr?= =?UTF-8?q?=C3=A4chsnotizen=20und=20Tags=20-=20call=5Flog.ts:=20Komponente?= =?UTF-8?q?=20zur=20Verwaltung=20von=20Gespr=C3=A4chsnotizen=20mit=20Liste?= =?UTF-8?q?=20und=20Einzelansicht=20-=20tags.ts:=20Komponente=20zur=20hier?= =?UTF-8?q?archischen=20Tag-Verwaltung=20mit=20Baumstruktur?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Client/Components/call_log.ts | 204 ++++++++++ Client/Components/tags.ts | 677 ++++++++++++++++++++++++++++++++++ 2 files changed, 881 insertions(+) create mode 100644 Client/Components/call_log.ts create mode 100644 Client/Components/tags.ts diff --git a/Client/Components/call_log.ts b/Client/Components/call_log.ts new file mode 100644 index 0000000..b2e52c2 --- /dev/null +++ b/Client/Components/call_log.ts @@ -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") + ]) + ) + } + } +} diff --git a/Client/Components/tags.ts b/Client/Components/tags.ts new file mode 100644 index 0000000..37f114d --- /dev/null +++ b/Client/Components/tags.ts @@ -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, +};