From 15d4e2ac48f9ea09d300547a1fee07c9b7b569bd Mon Sep 17 00:00:00 2001 From: Gerd Date: Thu, 3 Sep 2026 16:43:50 +0200 Subject: [PATCH] Client: Kunden-Datenhaltung auf Server-REST-Endpunkte umgestellt In-Memory-Datenhaltung (kundenDaten, getNewKundeId, addKunde, updateKunde, getKunden) aus kunde.ts entfernt. Die Kundenliste und das Formular nutzen jetzt fetchKunden/createKundeREST/updateKundeREST asynchron vom Server. projekt.ts, kontakt.ts, call_log.ts: getKunden() durch asynchrones fetchKunden() im oninit ersetzt, Kundenliste im Closure-State gehalten. Generated by Mistral Vibe. Co-Authored-By: Mistral Vibe --- Client/Components/call_log.ts | 10 +++++-- Client/Components/kontakt.ts | 21 ++++++++++++--- Client/Components/kunde.ts | 31 +++++++++------------ Client/Components/projekt.ts | 23 ++++++++++++---- Client/models/kunde.ts | 51 +++-------------------------------- 5 files changed, 58 insertions(+), 78 deletions(-) diff --git a/Client/Components/call_log.ts b/Client/Components/call_log.ts index 0ccbd39..d311d95 100644 --- a/Client/Components/call_log.ts +++ b/Client/Components/call_log.ts @@ -4,7 +4,7 @@ import { Input, iInput, Textarea, iTextarea} from "./widgets/inputs" import {Select, iSelect, iSelectOption} from "./widgets/select" import {Treenode, iTreenode, Treetable, iTreetable, TopbarButton, iTopbarButton} from "./widgets/Treetable_V2" import {iCallLog, getCallLogsByProjektId, getNewCallLogId, addCallLogData, updateCallLogData} from "../models/call_log" -import {iKunde, getKunden} from "../models/kunde" +import {iKunde, fetchKunden} from "../models/kunde" import {getProjekteByKundenId, getProjektById} from "../models/projekt" import {getKontaktadressenByProjektId, getKontaktadresseById} from "../models/kontaktadresse" @@ -190,6 +190,8 @@ export const Call_Log: m.ClosureComponent = (vnode) => { // ohne Redraw aktualisieren kann (ein Redraw würde die übrigen Felder // mit den veralteten currentLog-Werten überschreiben). let dauerInput: HTMLInputElement | null = null + // Kundenliste asynchron vom Server laden, für Select-Optionen. + let kundenListe: iKunde[] = [] /** * Formatiert verstrichene Sekunden als mm:ss (Minuten:Sekunden). @@ -246,6 +248,10 @@ export const Call_Log: m.ClosureComponent = (vnode) => { }, DAUER_UPDATE_INTERVALL) } } + // Kundenliste asynchron vom Server laden. + fetchKunden().then((result) => { + kundenListe = result + }) }, onremove: (vnode) => { // Timer stoppen, damit er nach Verlassen des Views nicht weiterläuft. @@ -253,7 +259,7 @@ export const Call_Log: m.ClosureComponent = (vnode) => { }, view: (vnode) => { // Select-Optionen aus Kunden- und Projektliste aufbauen. - const kundenOptionen: iSelectOption[] = getKunden().map((k: iKunde) => { + const kundenOptionen: iSelectOption[] = kundenListe.map((k: iKunde) => { return {value: k.id, label: k.name} }) // Projekt-Optionen sind vom gewählten Kunden abhängig. diff --git a/Client/Components/kontakt.ts b/Client/Components/kontakt.ts index 3e2717b..dd6ab9e 100644 --- a/Client/Components/kontakt.ts +++ b/Client/Components/kontakt.ts @@ -4,7 +4,7 @@ import { Input, iInput, Textarea, iTextarea} from "./widgets/inputs" import {Select, iSelect, iSelectOption} from "./widgets/select" import {Treenode, iTreenode, Treetable, iTreetable, TopbarButton, iTopbarButton} from "./widgets/Treetable_V2" import {iKontakt, getKontakteByKundenId, getNewKontaktId, addKontakt, updateKontakt} from "../models/kontakt" -import {iKunde, getKunden} from "../models/kunde" +import {iKunde, fetchKunden} from "../models/kunde" function createKontaktTreenodes(kundenId: number | null = null): iTreenode[] { let filteredKontakte = getKontakteByKundenId(kundenId) @@ -18,6 +18,9 @@ export const kontaktliste: m.ClosureComponent = (vnode) => { let treenodes: iTreenode[] = createKontaktTreenodes() let selectedNode: iTreenode | null = null let filterKundenId: number | null = null + // Kundenliste asynchron vom Server laden, für die Kundenauflösung + // beim Anlegen eines neuen Kontakts. + let kundenListe: iKunde[] = [] const handleRowMarked = (node: iTreenode) => { selectedNode = node @@ -35,7 +38,7 @@ export const kontaktliste: m.ClosureComponent = (vnode) => { } // Nur bei kundengefilterter Liste den Kunden vorbelegen. if (filterKundenId !== null) { - const kunde = getKunden().find((k: iKunde) => k.id === filterKundenId) + const kunde = kundenListe.find((k: iKunde) => k.id === filterKundenId) if (kunde) { params.kundenId = kunde.id params.kundenname = kunde.name @@ -91,6 +94,10 @@ export const kontaktliste: m.ClosureComponent = (vnode) => { filterKundenId = null } treenodes = createKontaktTreenodes(filterKundenId) + // Kundenliste asynchron vom Server laden. + fetchKunden().then((result) => { + kundenListe = result + }) }, view: (vnode) => { return m("div.w3-container.w3-auto", { @@ -145,6 +152,9 @@ export const Kontakt: m.ClosureComponent = (vnode) => { } // Ist der Kunde voreingestellt (gefilterte Kontaktliste)? let kundeVoreingestellt: boolean = false + // Kundenliste asynchron vom Server laden, für Select-Optionen und + // Namensauflösung im submit-Handler. + let kundenListe: iKunde[] = [] return { oninit: (vnode) => { @@ -164,13 +174,16 @@ export const Kontakt: m.ClosureComponent = (vnode) => { notiz: routeParams.notiz || "" } } + fetchKunden().then((result) => { + kundenListe = result + }) }, view: (vnode) => { const submit = (form: HTMLFormElement): boolean => { const formData = new FormData(form) const kundenId = parseInt(formData.get("kundenId") as string) // Kundennamen zur gewählten kundenId auflösen. - const kunde = getKunden().find((k: iKunde) => k.id === kundenId) + const kunde = kundenListe.find((k: iKunde) => k.id === kundenId) const gespeicherterKontakt: iKontakt = { id: currentKontakt.id, kundenId: kundenId, @@ -195,7 +208,7 @@ export const Kontakt: m.ClosureComponent = (vnode) => { m.route.set("/kontaktliste") } // Select-Optionen aus der Kundenliste aufbauen. - const kundenOptionen: iSelectOption[] = getKunden().map((k: iKunde) => { + const kundenOptionen: iSelectOption[] = kundenListe.map((k: iKunde) => { return {value: k.id, label: k.name} }) return m("div.w3-container.w3-auto.form-view-container", diff --git a/Client/Components/kunde.ts b/Client/Components/kunde.ts index 96ac10e..1babe51 100644 --- a/Client/Components/kunde.ts +++ b/Client/Components/kunde.ts @@ -2,7 +2,7 @@ import m from "mithril" import {Form, iForm} from "./widgets/form" import { Input, iInput} from "./widgets/inputs" import {Treenode, iTreenode, Treetable, iTreetable, TopbarButton, iTopbarButton} from "./widgets/Treetable_V2" -import {iKunde, kundenDaten, getNewKundeId, addKunde, updateKunde} from "../models/kunde" +import {iKunde, fetchKunden, createKundeREST, updateKundeREST} from "../models/kunde" export const kunde: m.ClosureComponent = (vnode) => { let currentKunde: iKunde = { @@ -40,12 +40,10 @@ export const kunde: m.ClosureComponent = (vnode) => { land: (formData.get("land") as string) || "" } if (currentKunde.id === -1) { - gespeicherterKunde.id = getNewKundeId() - addKunde(gespeicherterKunde) + createKundeREST(gespeicherterKunde).then(() => m.route.set("/kundenliste")) } else { - updateKunde(gespeicherterKunde) + updateKundeREST(gespeicherterKunde).then(() => m.route.set("/kundenliste")) } - m.route.set("/kundenliste") return true } const cancel = (e: Event) => { @@ -74,22 +72,10 @@ export const kunde: m.ClosureComponent = (vnode) => { } } -/** - * Erzeugt aus der Kundendatenliste die Treenode-Struktur für die Treetable der - * Kundenliste. Jeder Kunde wird zu einem expandierten Treenode ohne Kinder - * (expand=true, children=[]) - d.h. ein Blattknoten, der sofort in der - * Tabelle sichtbar ist und sich nicht weiter aufklappen lässt. - */ -function createKundenTreenodes(): iTreenode[] { - return kundenDaten.map((kunde: iKunde) => { - return new Treenode(kunde, true, []) - }) -} - export const kundenliste: m.ClosureComponent = (vnode) => { - let treenodes: iTreenode[] = createKundenTreenodes() + let treenodes: iTreenode[] = [] let selectedNode: iTreenode | null = null - + const handleRowMarked = (node: iTreenode) => { selectedNode = node } @@ -141,6 +127,13 @@ export const kundenliste: m.ClosureComponent = (vnode) => { } return { + oninit: (vnode) => { + // Kundenliste asynchron vom Server laden. m.request löst nach + // Promise-Auflösung automatisch ein Redraw aus. + fetchKunden().then((kunden: iKunde[]) => { + treenodes = kunden.map((kunde: iKunde) => new Treenode(kunde, true, [])) + }) + }, view: (vnode) => { return m("div.w3-container.w3-auto", { style: { diff --git a/Client/Components/projekt.ts b/Client/Components/projekt.ts index 1ad2b60..93aaf84 100644 --- a/Client/Components/projekt.ts +++ b/Client/Components/projekt.ts @@ -4,7 +4,7 @@ import { Input, iInput} from "./widgets/inputs" import {Select, iSelect, iSelectOption} from "./widgets/select" import {Treenode, iTreenode, Treetable, iTreetable, TopbarButton, iTopbarButton} from "./widgets/Treetable_V2" import {iProjekt, getProjekteByKundenId, getNewProjektId, addProjekt, updateProjekt} from "../models/projekt" -import {iKunde, getKunden} from "../models/kunde" +import {iKunde, fetchKunden} from "../models/kunde" function createProjektTreenodes(kundenId: number | null = null): iTreenode[] { let filteredProjekte = getProjekteByKundenId(kundenId) @@ -18,7 +18,10 @@ export const projektliste: m.ClosureComponent = (vnode) => { let treenodes: iTreenode[] = createProjektTreenodes() let selectedNode: iTreenode | null = null let filterKundenId: number | null = null - + // Kundenliste asynchron vom Server laden, für die Kundenauflösung + // beim Anlegen eines neuen Projekts. + let kundenListe: iKunde[] = [] + const handleRowMarked = (node: iTreenode) => { selectedNode = node } @@ -34,7 +37,7 @@ export const projektliste: m.ClosureComponent = (vnode) => { } // Nur bei kundengefilterter Liste den Kunden vorbelegen. if (filterKundenId !== null) { - const kunde = getKunden().find((k: iKunde) => k.id === filterKundenId) + const kunde = kundenListe.find((k: iKunde) => k.id === filterKundenId) if (kunde) { params.kundenId = kunde.id params.kundenname = kunde.name @@ -125,6 +128,10 @@ export const projektliste: m.ClosureComponent = (vnode) => { filterKundenId = null } treenodes = createProjektTreenodes(filterKundenId) + // Kundenliste asynchron vom Server laden. + fetchKunden().then((result) => { + kundenListe = result + }) }, view: (vnode) => { return m("div.w3-container.w3-auto", { @@ -175,6 +182,9 @@ export const Projekt: m.ClosureComponent = (vnode) => { } // Ist der Kunde voreingestellt (gefilterte Projektliste)? let kundeVoreingestellt: boolean = false + // Kundenliste asynchron vom Server laden, für Select-Optionen und + // Namensauflösung im submit-Handler. + let kundenListe: iKunde[] = [] return { oninit: (vnode) => { @@ -192,13 +202,16 @@ export const Projekt: m.ClosureComponent = (vnode) => { startdatum: routeParams.startdatum || "" } } + fetchKunden().then((result) => { + kundenListe = result + }) }, view: (vnode) => { const submit = (form: HTMLFormElement): boolean => { const formData = new FormData(form) const kundenId = parseInt(formData.get("kundenId") as string) // Kundennamen zur gewählten kundenId auflösen. - const kunde = getKunden().find((k: iKunde) => k.id === kundenId) + const kunde = kundenListe.find((k: iKunde) => k.id === kundenId) const gespeichertesProjekt: iProjekt = { id: currentProjekt.id, kundenId: kundenId, @@ -221,7 +234,7 @@ export const Projekt: m.ClosureComponent = (vnode) => { m.route.set("/projektliste") } // Select-Optionen aus der Kundenliste aufbauen. - const kundenOptionen: iSelectOption[] = getKunden().map((k: iKunde) => { + const kundenOptionen: iSelectOption[] = kundenListe.map((k: iKunde) => { return {value: k.id, label: k.name} }) return m("div.w3-container.w3-auto.form-view-container", diff --git a/Client/models/kunde.ts b/Client/models/kunde.ts index 5362b1f..7c2845a 100644 --- a/Client/models/kunde.ts +++ b/Client/models/kunde.ts @@ -12,57 +12,11 @@ export interface iKunde { land: string } -/** - * In-Memory-Datenspeicher für Kunden. Wird als Fallback solange verwendet, - * bis die serverseitigen REST-Endpunkte verfügbar sind. - */ -export const kundenDaten: iKunde[] = [ - {id: 1, name: "Musterfirma GmbH", strasse: "Musterstraße 1", plz: "12345", ort: "Musterstadt", land: "Deutschland"}, - {id: 2, name: "Beispiel AG", strasse: "Beispielweg 2", plz: "54321", ort: "Beispielstadt", land: "Deutschland"}, - {id: 3, name: "Test Unternehmen", strasse: "Testallee 3", plz: "67890", ort: "Testdorf", land: "Österreich"} -] - -/** - * Liefert die nächste freie Kunden-ID auf Basis des In-Memory-Speichers. - * Sobald die serverseitige ID-Vergabe verfügbar ist, entfällt diese Funktion. - */ -export function getNewKundeId(): number { - if (kundenDaten.length === 0) { - return 1 - } - return Math.max(...kundenDaten.map((kunde: iKunde) => kunde.id)) + 1 -} - -/** - * Fügt einen neuen Kunden dem In-Memory-Speicher hinzu. - */ -export function addKunde(kunde: iKunde): void { - kundenDaten.push(kunde) -} - -/** - * Aktualisiert einen bestehenden Kunden im In-Memory-Speicher. - */ -export function updateKunde(kunde: iKunde): void { - const index = kundenDaten.findIndex((k: iKunde) => k.id === kunde.id) - if (index !== -1) { - kundenDaten[index] = kunde - } -} - -/** - * Liefert eine Kopie aller Kunden aus dem In-Memory-Speicher. - */ -export function getKunden(): iKunde[] { - return [...kundenDaten] -} - /* ------------------------------------------------------------------ */ /* REST-API-Aufrufe (Client-Seite) */ /* ------------------------------------------------------------------ */ /* Die folgenden Funktionen kapseln die Kommunikation mit den */ -/* serverseitigen REST-Endpunkten. Die Endpunkte sind noch nicht */ -/* implementiert und werden später nachgereicht. */ +/* serverseitigen REST-Endpunkten (/xhr/kunde/*). */ /* ------------------------------------------------------------------ */ /** @@ -92,7 +46,8 @@ export function fetchKundeById(id: number): Promise { } /** - * Legt einen neuen Kunden serverseitig an. + * Legt einen neuen Kunden serverseitig an. Die ID wird vom Server + * vergeben und im zurückgegebenen Kundenobjekt gesetzt. */ export function createKundeREST(kunde: iKunde): Promise { return m.request({