diff --git a/AGENTS.md b/AGENTS.md index 549dabe..404ac53 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -30,6 +30,8 @@ Erstelle die Datenmodelle für die Typescript-Entwicklung im Verzeichnis Client/ Beim Aufruf einer Bearbeitungsmaske werden in den Route-Params nur die ID (und ggf. zum Vorbelegen notwendige Eltern-FKs wie `kunden_id`) übergeben — nicht die vollständigen Felder des Objekts. Die Maske lädt die Daten frisch vom Server via `fetchXxxById(id)` im `oninit`. So wird sichergestellt, dass die Daten aktuell sind und zwischenzeitliche Änderungen anderer Nutzer nicht mit veralteten Werten aus der Liste überschrieben werden. Beim Neuanlegen (`id === -1`) entfällt der Fetch; vorbelegte Werte (z.B. `kunden_id`) kommen aus den Route-Params. +Jeder `m.request`-Aufruf (REST-Endpunkt) muss `.catch((e) => showServerError(e))` anhängen, damit Serverfehler (z.B. 401 bei fehlender Autorisierung) dem Nutzer als Modal angezeigt werden. Die Navigation (`m.route.set`) steht im `.then`-Zweig, damit sie bei Fehler nicht ausgeführt wird und der Nutzer auf der Maske bleibt. `showServerError` wird aus `./auth/authmessages` importiert. + Bei `m.ClosureComponent` den Closure-Parameter weglassen (`() =>`) und Attribute ausschließlich über das `vnode` des jeweiligen Lifecycle-Hooks (`view`, `oninit`, `onupdate` etc.) lesen. Das `vnode` der Closure-Funktion ist ein Snapshot zum Erzeugungszeitpunkt und wird bei Redraws nicht aktualisiert — daraus gelesene `attrs` sind veraltet. Initialisierung, die `attrs` braucht, gehört nach `oninit`. Beispiel: ```typescript export const Select: m.ClosureComponent = () => { diff --git a/Client/Components/call_log.ts b/Client/Components/call_log.ts index c6df511..4ad993f 100644 --- a/Client/Components/call_log.ts +++ b/Client/Components/call_log.ts @@ -7,6 +7,7 @@ import {iCallLog, fetchCallLogs, fetchCallLogById, createCallLogREST, updateCall import {iKunde, fetchKunden} from "../models/kunde" import {iProjekt, fetchProjekte, fetchProjektById} from "../models/projekt" import {iKontaktadresse, fetchKontaktadressen} from "../models/kontaktadresse" +import {showServerError} from "./auth/authmessages" // Aktualisierungsintervall für die Dauer-Anzeige in Millisekunden. const DAUER_UPDATE_INTERVALL = 1000 @@ -97,7 +98,7 @@ export const call_log_liste: m.ClosureComponent = (vnode) => { // gesetzt ist. fetchCallLogs(filterProjektId !== null ? filterProjektId : undefined).then((result) => { treenodes = result.map((log: iCallLog) => new Treenode(log, true, [])) - }) + }).catch((e) => showServerError(e)) // Projektnamen und Kundennamen asynchron auflösen, falls // nicht in den Route-Params enthalten. if (filterProjektId !== null) { @@ -106,7 +107,7 @@ export const call_log_liste: m.ClosureComponent = (vnode) => { projektName = projekt.projektname } kundenName = projekt.kunden_name - }) + }).catch((e) => showServerError(e)) } }, view: (vnode) => { @@ -202,7 +203,7 @@ export const Call_Log: m.ClosureComponent = (vnode) => { currentLog = result projektVoreingestellt = true ansprechpartnerVoreingestellt = currentLog.name_ansprechpartner !== "" - }) + }).catch((e) => showServerError(e)) } else { projektVoreingestellt = routeParams && routeParams.projekt_id ? parseInt(routeParams.projekt_id) !== -1 : false currentLog.projekt_id = routeParams && routeParams.projekt_id ? parseInt(routeParams.projekt_id) : -1 @@ -230,9 +231,9 @@ export const Call_Log: m.ClosureComponent = (vnode) => { }, DAUER_UPDATE_INTERVALL) } // Alle Listen asynchron laden, für Select-Optionen. - fetchKunden().then((result) => { kundenListe = result }) - fetchProjekte().then((result) => { projektListe = result }) - fetchKontaktadressen().then((result) => { kontaktadressenListe = result }) + fetchKunden().then((result) => { kundenListe = result }).catch((e) => showServerError(e)) + fetchProjekte().then((result) => { projektListe = result }).catch((e) => showServerError(e)) + fetchKontaktadressen().then((result) => { kontaktadressenListe = result }).catch((e) => showServerError(e)) }, onremove: (vnode) => { if (timerId != null) { window.clearInterval(timerId) } @@ -285,9 +286,9 @@ export const Call_Log: m.ClosureComponent = (vnode) => { erledigt: formData.get("erledigt") === "on" } if (currentLog.id === -1) { - createCallLogREST(gespeichertesLog).then(() => m.route.set("/calllogliste", {projekt_id: currentLog.projekt_id, projekt_name: currentLog.projekt_name})) + createCallLogREST(gespeichertesLog).then(() => m.route.set("/calllogliste", {projekt_id: currentLog.projekt_id, projekt_name: currentLog.projekt_name})).catch((e) => showServerError(e)) } else { - updateCallLogREST(gespeichertesLog).then(() => m.route.set("/calllogliste", {projekt_id: currentLog.projekt_id, projekt_name: currentLog.projekt_name})) + updateCallLogREST(gespeichertesLog).then(() => m.route.set("/calllogliste", {projekt_id: currentLog.projekt_id, projekt_name: currentLog.projekt_name})).catch((e) => showServerError(e)) } return true } diff --git a/Client/Components/kontakt.ts b/Client/Components/kontakt.ts index 5b14d61..0d87c6b 100644 --- a/Client/Components/kontakt.ts +++ b/Client/Components/kontakt.ts @@ -4,6 +4,7 @@ import { Input, iInput, Textarea, iTextarea} from "./widgets/inputs" import {Treenode, iTreenode, Treetable, iTreetable, TopbarButton, iTopbarButton} from "./widgets/Treetable_V2" import {iKontakt, fetchKontakte, fetchKontaktById, createKontaktREST, updateKontaktREST} from "../models/kontakt" import {iKunde, fetchKundeById} from "../models/kunde" +import {showServerError} from "./auth/authmessages" export const kontaktliste: m.ClosureComponent = (vnode) => { let treenodes: iTreenode[] = [] @@ -69,12 +70,12 @@ export const kontaktliste: m.ClosureComponent = (vnode) => { // kunden_id erfolgt serverseitig, sofern ein Filter gesetzt ist. fetchKontakte(filterKundenId !== null ? filterKundenId : undefined).then((result) => { treenodes = result.map((kontakt: iKontakt) => new Treenode(kontakt, true, [])) - }) + }).catch((e) => showServerError(e)) // Kundennamen asynchron auflösen, falls nicht in Route-Params. if (filterKundenId !== null && kundenName === "") { fetchKundeById(filterKundenId).then((kunde: iKunde) => { kundenName = kunde.name - }) + }).catch((e) => showServerError(e)) } }, view: (vnode) => { @@ -138,7 +139,7 @@ export const Kontakt: m.ClosureComponent = (vnode) => { // Änderungen anderer Nutzer nicht zu überschreiben. fetchKontaktById(id).then((result) => { currentKontakt = result - }) + }).catch((e) => showServerError(e)) } else { // Neuanlegen: kunden_id und kunden_name aus Route-Params // (voreingestellt aus der Kontaktliste). @@ -161,9 +162,9 @@ export const Kontakt: m.ClosureComponent = (vnode) => { notiz: (formData.get("notiz") as string) || "" } if (currentKontakt.id === -1) { - createKontaktREST(gespeicherterKontakt).then(() => m.route.set("/kontaktliste", {kunden_id: currentKontakt.kunden_id, kunden_name: currentKontakt.kunden_name})) + createKontaktREST(gespeicherterKontakt).then(() => m.route.set("/kontaktliste", {kunden_id: currentKontakt.kunden_id, kunden_name: currentKontakt.kunden_name})).catch((e) => showServerError(e)) } else { - updateKontaktREST(gespeicherterKontakt).then(() => m.route.set("/kontaktliste", {kunden_id: currentKontakt.kunden_id, kunden_name: currentKontakt.kunden_name})) + updateKontaktREST(gespeicherterKontakt).then(() => m.route.set("/kontaktliste", {kunden_id: currentKontakt.kunden_id, kunden_name: currentKontakt.kunden_name})).catch((e) => showServerError(e)) } return true } diff --git a/Client/Components/kontaktadresse.ts b/Client/Components/kontaktadresse.ts index 25e079b..7946587 100644 --- a/Client/Components/kontaktadresse.ts +++ b/Client/Components/kontaktadresse.ts @@ -4,6 +4,7 @@ import { Input, iInput, Textarea, iTextarea} from "./widgets/inputs" import {Treenode, iTreenode, Treetable, iTreetable, TopbarButton, iTopbarButton} from "./widgets/Treetable_V2" import {iKontaktadresse, fetchKontaktadressen, fetchKontaktadresseById, createKontaktadresseREST, updateKontaktadresseREST} from "../models/kontaktadresse" import {iProjekt, fetchProjektById} from "../models/projekt" +import {showServerError} from "./auth/authmessages" export const kontaktadresseliste: m.ClosureComponent = (vnode) => { let treenodes: iTreenode[] = [] @@ -96,12 +97,12 @@ export const kontaktadresseliste: m.ClosureComponent = (vnode) => { // gesetzt ist. fetchKontaktadressen(filterProjektId !== null ? filterProjektId : undefined).then((result) => { treenodes = result.map((kontaktadresse: iKontaktadresse) => new Treenode(kontaktadresse, true, [])) - }) + }).catch((e) => showServerError(e)) // Projektnamen asynchron auflösen, falls nicht in Route-Params. if (filterProjektId !== null && projektName === "") { fetchProjektById(filterProjektId).then((projekt: iProjekt) => { projektName = projekt.projektname - }) + }).catch((e) => showServerError(e)) } }, view: (vnode) => { @@ -174,7 +175,7 @@ export const Kontaktadresse: m.ClosureComponent = (vnode) => { // Kontaktadresse frisch vom Server holen. fetchKontaktadresseById(id).then((result) => { currentKontaktadresse = result - }) + }).catch((e) => showServerError(e)) } else { // Neuanlegen: projekt_id und projekt_name aus Route-Params. currentKontaktadresse.projekt_id = routeParams && routeParams.projekt_id ? parseInt(routeParams.projekt_id) : -1 @@ -201,9 +202,9 @@ export const Kontaktadresse: m.ClosureComponent = (vnode) => { notiz: (formData.get("notiz") as string) || "" } if (currentKontaktadresse.id === -1) { - createKontaktadresseREST(gespeicherteKontaktadresse).then(() => m.route.set("/kontaktadresseliste", {projekt_id: currentKontaktadresse.projekt_id, projekt_name: currentKontaktadresse.projekt_name})) + createKontaktadresseREST(gespeicherteKontaktadresse).then(() => m.route.set("/kontaktadresseliste", {projekt_id: currentKontaktadresse.projekt_id, projekt_name: currentKontaktadresse.projekt_name})).catch((e) => showServerError(e)) } else { - updateKontaktadresseREST(gespeicherteKontaktadresse).then(() => m.route.set("/kontaktadresseliste", {projekt_id: currentKontaktadresse.projekt_id, projekt_name: currentKontaktadresse.projekt_name})) + updateKontaktadresseREST(gespeicherteKontaktadresse).then(() => m.route.set("/kontaktadresseliste", {projekt_id: currentKontaktadresse.projekt_id, projekt_name: currentKontaktadresse.projekt_name})).catch((e) => showServerError(e)) } return true } diff --git a/Client/Components/kunde.ts b/Client/Components/kunde.ts index 60c98ff..f379632 100644 --- a/Client/Components/kunde.ts +++ b/Client/Components/kunde.ts @@ -3,6 +3,7 @@ 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, fetchKunden, fetchKundeById, createKundeREST, updateKundeREST} from "../models/kunde" +import {showServerError} from "./auth/authmessages" export const kunde: m.ClosureComponent = (vnode) => { let currentKunde: iKunde = { @@ -23,7 +24,7 @@ export const kunde: m.ClosureComponent = (vnode) => { // Änderungen anderer Nutzer nicht zu überschreiben. fetchKundeById(id).then((result) => { currentKunde = result - }) + }).catch((e) => showServerError(e)) } }, view: (vnode) => { @@ -38,9 +39,9 @@ export const kunde: m.ClosureComponent = (vnode) => { land: (formData.get("land") as string) || "" } if (currentKunde.id === -1) { - createKundeREST(gespeicherterKunde).then(() => m.route.set("/kundenliste")) + createKundeREST(gespeicherterKunde).then(() => m.route.set("/kundenliste")).catch((e) => showServerError(e)) } else { - updateKundeREST(gespeicherterKunde).then(() => m.route.set("/kundenliste")) + updateKundeREST(gespeicherterKunde).then(() => m.route.set("/kundenliste")).catch((e) => showServerError(e)) } return true } @@ -130,7 +131,7 @@ export const kundenliste: m.ClosureComponent = (vnode) => { // Promise-Auflösung automatisch ein Redraw aus. fetchKunden().then((kunden: iKunde[]) => { treenodes = kunden.map((kunde: iKunde) => new Treenode(kunde, true, [])) - }) + }).catch((e) => showServerError(e)) }, view: (vnode) => { return m("div.w3-container.w3-auto", { diff --git a/Client/Components/projekt.ts b/Client/Components/projekt.ts index 1d21235..fac0eea 100644 --- a/Client/Components/projekt.ts +++ b/Client/Components/projekt.ts @@ -4,6 +4,7 @@ import { Input, iInput} from "./widgets/inputs" import {Treenode, iTreenode, Treetable, iTreetable, TopbarButton, iTopbarButton} from "./widgets/Treetable_V2" import {iProjekt, fetchProjekte, fetchProjektById, createProjektREST, updateProjektREST} from "../models/projekt" import {iKunde, fetchKundeById} from "../models/kunde" +import {showServerError} from "./auth/authmessages" export const projektliste: m.ClosureComponent = (vnode) => { let treenodes: iTreenode[] = [] @@ -106,12 +107,12 @@ export const projektliste: m.ClosureComponent = (vnode) => { // kunden_id erfolgt serverseitig, sofern ein Filter gesetzt ist. fetchProjekte(filterKundenId !== null ? filterKundenId : undefined).then((result) => { treenodes = result.map((projekt: iProjekt) => new Treenode(projekt, true, [])) - }) + }).catch((e) => showServerError(e)) // Kundennamen asynchron auflösen, falls nicht in Route-Params. if (filterKundenId !== null && kundenName === "") { fetchKundeById(filterKundenId).then((kunde: iKunde) => { kundenName = kunde.name - }) + }).catch((e) => showServerError(e)) } }, view: (vnode) => { @@ -171,7 +172,7 @@ export const Projekt: m.ClosureComponent = (vnode) => { // Änderungen anderer Nutzer nicht zu überschreiben. fetchProjektById(id).then((result) => { currentProjekt = result - }) + }).catch((e) => showServerError(e)) } else { // Neuanlegen: kunden_id und kunden_name aus Route-Params // (voreingestellt aus der Projektliste). @@ -192,9 +193,9 @@ export const Projekt: m.ClosureComponent = (vnode) => { startdatum: (formData.get("startdatum") as string) || "" } if (currentProjekt.id === -1) { - createProjektREST(gespeichertesProjekt).then(() => m.route.set("/projektliste", {kunden_id: currentProjekt.kunden_id, kunden_name: currentProjekt.kunden_name})) + createProjektREST(gespeichertesProjekt).then(() => m.route.set("/projektliste", {kunden_id: currentProjekt.kunden_id, kunden_name: currentProjekt.kunden_name})).catch((e) => showServerError(e)) } else { - updateProjektREST(gespeichertesProjekt).then(() => m.route.set("/projektliste", {kunden_id: currentProjekt.kunden_id, kunden_name: currentProjekt.kunden_name})) + updateProjektREST(gespeichertesProjekt).then(() => m.route.set("/projektliste", {kunden_id: currentProjekt.kunden_id, kunden_name: currentProjekt.kunden_name})).catch((e) => showServerError(e)) } return true }