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:
1 parent
9d91915916
commit
72055840f7
2 files changed
+881
No files matched your search
@@ -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")
|
||||||
|
])
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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,
|
||||||
|
};
|
||||||
Reference in new issue
Block a user