Serverseitige Filterung für Listen-Endpunkte und Einzelabrufe in Listen-Komponenten

Server: Die list-Endpunkte für Kontakt, Projekt, Kontaktadresse und CallLog
akzeptieren nun einen optionalen Query-Parameter (kunden_id bzw. projekt_id)
und filtern im ORM-Query. Neue Migration V01.05 legt Indizes auf den
FK-Spalten an.

Client: Die fetch-Funktionen erhalten einen optionalen Parent-ID-Parameter.
Die Listen-Komponenten übergeben die Parent-ID aus den Route-Params an den
Server und laden nur noch benötigte Daten. Statt kompletter Eltern-Listen
wird per fetchXxxById ein einzelner Datensatz für die Namensauflösung geladen.
Die CallLog-Bearbeitungsmaske behält ihre ungesfilterten Dropdown-Abrufe.

Generated by Mistral Vibe.
Co-Authored-By: Mistral Vibe <vibe@mistral.ai>
This commit is contained in:
gg committed 2026-09-04 15:43:43 +02:00
1 parent e9a2f48ede
commit 93827dde0f
15 files changed
+189 -141

No files matched your search

+21 -25
View File
@@ -5,7 +5,7 @@ import {Select, iSelect, iSelectOption} from "./widgets/select"
import {Treenode, iTreenode, Treetable, iTreetable, TopbarButton, iTopbarButton} from "./widgets/Treetable_V2"
import {iCallLog, fetchCallLogs, fetchCallLogById, createCallLogREST, updateCallLogREST} from "../models/call_log"
import {iKunde, fetchKunden} from "../models/kunde"
import {iProjekt, fetchProjekte} from "../models/projekt"
import {iProjekt, fetchProjekte, fetchProjektById} from "../models/projekt"
import {iKontaktadresse, fetchKontaktadressen} from "../models/kontaktadresse"
// Aktualisierungsintervall für die Dauer-Anzeige in Millisekunden.
@@ -15,10 +15,11 @@ export const call_log_liste: m.ClosureComponent = (vnode) => {
let treenodes: iTreenode[] = []
let selectedNode: iTreenode | null = null
let filterProjektId: number | null = null
// Projektname für die Überschrift, asynchron aufgelöst.
// Projektname für die Überschrift, asynchron per fetchProjektById
// aufgelöst. kunden_name wird ebenfalls aus dem Projekt bezogen,
// da es nicht in den Route-Params enthalten ist.
let projektName: string = ""
// Projektliste asynchron laden, für Projektauflösung beim Neuanlegen.
let projektListe: iProjekt[] = []
let kundenName: string = ""
const handleRowMarked = (node: iTreenode) => {
selectedNode = node
@@ -41,12 +42,9 @@ export const call_log_liste: m.ClosureComponent = (vnode) => {
erledigt: false
}
if (filterProjektId !== null) {
const projekt = projektListe.find((p: iProjekt) => p.id === filterProjektId)
if (projekt) {
params.projekt_id = projekt.id
params.projekt_name = projekt.projektname
params.kunden_name = projekt.kunden_name
}
params.projekt_id = filterProjektId
params.projekt_name = projektName
params.kunden_name = kundenName
}
m.route.set("/call_log", params)
}
@@ -94,24 +92,22 @@ export const call_log_liste: m.ClosureComponent = (vnode) => {
filterProjektId = null
projektName = ""
}
// Gesprächsnotizen asynchron vom Server laden.
fetchCallLogs().then((result) => {
let filtered = result
if (filterProjektId !== null) {
filtered = result.filter((l: iCallLog) => l.projekt_id === filterProjektId)
}
treenodes = filtered.map((log: iCallLog) => new Treenode(log, true, []))
// Gesprächsnotizen asynchron vom Server laden. Die Filterung
// nach projekt_id erfolgt serverseitig, sofern ein Filter
// gesetzt ist.
fetchCallLogs(filterProjektId !== null ? filterProjektId : undefined).then((result) => {
treenodes = result.map((log: iCallLog) => new Treenode(log, true, []))
})
// Projektliste asynchron laden, für Projektnamen-Auflösung.
fetchProjekte().then((result) => {
projektListe = result
if (filterProjektId !== null && projektName === "") {
const projekt = projektListe.find((p: iProjekt) => p.id === filterProjektId)
if (projekt) {
// Projektnamen und Kundennamen asynchron auflösen, falls
// nicht in den Route-Params enthalten.
if (filterProjektId !== null) {
fetchProjektById(filterProjektId).then((projekt: iProjekt) => {
if (projektName === "") {
projektName = projekt.projektname
}
}
})
kundenName = projekt.kunden_name
})
}
},
view: (vnode) => {
return m("div.w3-container.w3-auto", {
+15 -31
View File
@@ -3,18 +3,15 @@ import {Form, iForm} from "./widgets/form"
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, fetchKunden} from "../models/kunde"
import {iKunde, fetchKundeById} from "../models/kunde"
export const kontaktliste: m.ClosureComponent = (vnode) => {
let treenodes: iTreenode[] = []
let selectedNode: iTreenode | null = null
let filterKundenId: number | null = null
// Kundenname für die Überschrift, asynchron aus der Kundenliste
// aufgelöst.
// Kundenname für die Überschrift, asynchron per fetchKundeById
// aufgelöst, falls nicht in den Route-Params enthalten.
let kundenName: string = ""
// Kundenliste asynchron vom Server laden, für die Kundenauflösung
// beim Anlegen eines neuen Kontakts.
let kundenListe: iKunde[] = []
const handleRowMarked = (node: iTreenode) => {
selectedNode = node
@@ -24,11 +21,8 @@ export const kontaktliste: m.ClosureComponent = (vnode) => {
const params: any = {id: -1}
// Nur bei kundengefilterter Liste den Kunden vorbelegen.
if (filterKundenId !== null) {
const kunde = kundenListe.find((k: iKunde) => k.id === filterKundenId)
if (kunde) {
params.kunden_id = kunde.id
params.kunden_name = kunde.name
}
params.kunden_id = filterKundenId
params.kunden_name = kundenName
}
m.route.set("/kontakt", params)
}
@@ -71,27 +65,17 @@ export const kontaktliste: m.ClosureComponent = (vnode) => {
filterKundenId = null
kundenName = ""
}
// Kontakte asynchron vom Server laden. m.request löst nach
// Promise-Auflösung automatisch ein Redraw aus.
fetchKontakte().then((result) => {
let filtered = result
if (filterKundenId !== null) {
filtered = result.filter((k: iKontakt) => k.kunden_id === filterKundenId)
}
treenodes = filtered.map((kontakt: iKontakt) => new Treenode(kontakt, true, []))
})
// Kundenliste asynchron vom Server laden.
fetchKunden().then((result) => {
kundenListe = result
// Fallback: Falls kunden_name nicht in den Route-Params
// enthalten war, aus der Kundenliste auflösen.
if (filterKundenId !== null && kundenName === "") {
const kunde = kundenListe.find((k: iKunde) => k.id === filterKundenId)
if (kunde) {
kundenName = kunde.name
}
}
// Kontakte asynchron vom Server laden. Die Filterung nach
// 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, []))
})
// Kundennamen asynchron auflösen, falls nicht in Route-Params.
if (filterKundenId !== null && kundenName === "") {
fetchKundeById(filterKundenId).then((kunde: iKunde) => {
kundenName = kunde.name
})
}
},
view: (vnode) => {
return m("div.w3-container.w3-auto", {
+16 -28
View File
@@ -3,18 +3,15 @@ import {Form, iForm} from "./widgets/form"
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, fetchProjekte} from "../models/projekt"
import {iProjekt, fetchProjektById} from "../models/projekt"
export const kontaktadresseliste: m.ClosureComponent = (vnode) => {
let treenodes: iTreenode[] = []
let selectedNode: iTreenode | null = null
let filterProjektId: number | null = null
// Projektname für die Überschrift, asynchron aus der Projektliste
// aufgelöst.
// Projektname für die Überschrift, asynchron per fetchProjektById
// aufgelöst, falls nicht in den Route-Params enthalten.
let projektName: string = ""
// Projektliste asynchron vom Server laden, für die Projektauflösung
// beim Anlegen einer neuen Kontaktadresse.
let projektListe: iProjekt[] = []
const handleRowMarked = (node: iTreenode) => {
selectedNode = node
@@ -23,11 +20,8 @@ export const kontaktadresseliste: m.ClosureComponent = (vnode) => {
const addKontaktadresseRoute = () => {
const params: any = {id: -1}
if (filterProjektId !== null) {
const projekt = projektListe.find((p: iProjekt) => p.id === filterProjektId)
if (projekt) {
params.projekt_id = projekt.id
params.projekt_name = projekt.projektname
}
params.projekt_id = filterProjektId
params.projekt_name = projektName
}
m.route.set("/kontaktadresse", params)
}
@@ -97,24 +91,18 @@ export const kontaktadresseliste: m.ClosureComponent = (vnode) => {
filterProjektId = null
projektName = ""
}
// Kontaktadressen asynchron vom Server laden.
fetchKontaktadressen().then((result) => {
let filtered = result
if (filterProjektId !== null) {
filtered = result.filter((k: iKontaktadresse) => k.projekt_id === filterProjektId)
}
treenodes = filtered.map((kontaktadresse: iKontaktadresse) => new Treenode(kontaktadresse, true, []))
})
// Projektliste asynchron vom Server laden.
fetchProjekte().then((result) => {
projektListe = result
if (filterProjektId !== null && projektName === "") {
const projekt = projektListe.find((p: iProjekt) => p.id === filterProjektId)
if (projekt) {
projektName = projekt.projektname
}
}
// Kontaktadressen asynchron vom Server laden. Die Filterung
// nach projekt_id erfolgt serverseitig, sofern ein Filter
// gesetzt ist.
fetchKontaktadressen(filterProjektId !== null ? filterProjektId : undefined).then((result) => {
treenodes = result.map((kontaktadresse: iKontaktadresse) => new Treenode(kontaktadresse, true, []))
})
// Projektnamen asynchron auflösen, falls nicht in Route-Params.
if (filterProjektId !== null && projektName === "") {
fetchProjektById(filterProjektId).then((projekt: iProjekt) => {
projektName = projekt.projektname
})
}
},
view: (vnode) => {
return m("div.w3-container.w3-auto", {
+15 -28
View File
@@ -3,18 +3,15 @@ import {Form, iForm} from "./widgets/form"
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, fetchKunden} from "../models/kunde"
import {iKunde, fetchKundeById} from "../models/kunde"
export const projektliste: m.ClosureComponent = (vnode) => {
let treenodes: iTreenode[] = []
let selectedNode: iTreenode | null = null
let filterKundenId: number | null = null
// Kundenname für die Überschrift, asynchron aus der Kundenliste
// aufgelöst.
// Kundenname für die Überschrift, asynchron per fetchKundeById
// aufgelöst, falls nicht in den Route-Params enthalten.
let kundenName: string = ""
// Kundenliste asynchron vom Server laden, für die Kundenauflösung
// beim Anlegen eines neuen Projekts.
let kundenListe: iKunde[] = []
const handleRowMarked = (node: iTreenode) => {
selectedNode = node
@@ -24,11 +21,8 @@ export const projektliste: m.ClosureComponent = (vnode) => {
const params: any = {id: -1}
// Nur bei kundengefilterter Liste den Kunden vorbelegen.
if (filterKundenId !== null) {
const kunde = kundenListe.find((k: iKunde) => k.id === filterKundenId)
if (kunde) {
params.kunden_id = kunde.id
params.kunden_name = kunde.name
}
params.kunden_id = filterKundenId
params.kunden_name = kundenName
}
m.route.set("/projekt", params)
}
@@ -108,24 +102,17 @@ export const projektliste: m.ClosureComponent = (vnode) => {
filterKundenId = null
kundenName = ""
}
// Projekte asynchron vom Server laden.
fetchProjekte().then((result) => {
let filtered = result
if (filterKundenId !== null) {
filtered = result.filter((p: iProjekt) => p.kunden_id === filterKundenId)
}
treenodes = filtered.map((projekt: iProjekt) => new Treenode(projekt, true, []))
})
// Kundenliste asynchron vom Server laden.
fetchKunden().then((result) => {
kundenListe = result
if (filterKundenId !== null && kundenName === "") {
const kunde = kundenListe.find((k: iKunde) => k.id === filterKundenId)
if (kunde) {
kundenName = kunde.name
}
}
// Projekte asynchron vom Server laden. Die Filterung nach
// 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, []))
})
// Kundennamen asynchron auflösen, falls nicht in Route-Params.
if (filterKundenId !== null && kundenName === "") {
fetchKundeById(filterKundenId).then((kunde: iKunde) => {
kundenName = kunde.name
})
}
},
view: (vnode) => {
return m("div.w3-container.w3-auto", {