feat(nav): Breadcrumb-Navigation für Hierarchie-Aufrufkette

Neue BreadcrumbBar-Widget-Komponente (breadcrumb.ts) zeigt die
Aufrufkette als anklickbare Leiste. Der Route-Param bc (JSON-String)
transportiert die Vorgänger-Ansichten durch alle Listen und Masken.
Jede Ansicht hängt beim Weiternavigieren ihren eigenen Crumb an die
Kette an; Masken reichen bc beim Speichern/Abbrechen zurück.
Erfasst: kunde, projekt, kontaktadresse, call_log, kontakt.

Generated by Mistral Vibe.
Co-Authored-By: Mistral Vibe <vibe@mistral.ai>
This commit is contained in:
gg committed 2026-09-19 16:16:49 +02:00
1 parent f1e5938ae1
commit fd30049a32
6 files changed
+316 -27

No files matched your search

+39 -4
View File
@@ -2,6 +2,7 @@ import m from "mithril"
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 {BreadcrumbBar, iBreadcrumb, iBreadcrumbBar, parseBc, serializeBc} from "./widgets/breadcrumb"
import {iKontakt, fetchKontakte, fetchKontaktById, createKontaktREST, updateKontaktREST} from "../models/kontakt"
import {iKunde, fetchKundeById} from "../models/kunde"
import {showServerError} from "./auth/authmessages"
@@ -13,11 +14,20 @@ export const kontaktliste: m.ClosureComponent = (vnode) => {
// Kundenname für die Überschrift, asynchron per fetchKundeById
// aufgelöst, falls nicht in den Route-Params enthalten.
let kundenName: string = ""
// Breadcrumb-Kette der Vorgänger-Ansichten (aus Route-Param bc).
let breadcrumbs: iBreadcrumb[] = []
const handleRowMarked = (node: iTreenode) => {
selectedNode = node
}
// Eigener Listen-Crumb für die Breadcrumb-Kette.
const ownCrumb = (): iBreadcrumb => ({
route: "/kontaktliste",
params: filterKundenId !== null ? {kunden_id: filterKundenId} : {},
label: filterKundenId !== null ? "Kontakte: " + kundenName : "Kontakte"
})
const addKontaktRoute = () => {
const params: any = {id: -1}
// Nur bei kundengefilterter Liste den Kunden vorbelegen.
@@ -25,12 +35,15 @@ export const kontaktliste: m.ClosureComponent = (vnode) => {
params.kunden_id = filterKundenId
params.kunden_name = kundenName
}
// Breadcrumb-Kette um eigene Position erweitern.
params.bc = serializeBc([...breadcrumbs, ownCrumb()])
m.route.set("/kontakt", params)
}
const editKontakt = (kontakt: iKontakt, e: Event) => {
e.stopPropagation()
m.route.set("/kontakt", {id: kontakt.id})
// Breadcrumb-Kette um eigene Position erweitern.
m.route.set("/kontakt", {id: kontakt.id, bc: serializeBc([...breadcrumbs, ownCrumb()])})
}
const columnmapper: (node: iTreenode) => any[] = (node: iTreenode) => {
@@ -66,6 +79,8 @@ export const kontaktliste: m.ClosureComponent = (vnode) => {
filterKundenId = null
kundenName = ""
}
// Breadcrumb-Kette aus Route-Param parsen.
breadcrumbs = parseBc(routeParams && routeParams.bc)
// Kontakte asynchron vom Server laden. Die Filterung nach
// kunden_id erfolgt serverseitig, sofern ein Filter gesetzt ist.
fetchKontakte(filterKundenId !== null ? filterKundenId : undefined).then((result) => {
@@ -86,6 +101,10 @@ export const kontaktliste: m.ClosureComponent = (vnode) => {
height: 'inherit',
'overflow-y': 'inherit'}},
m("h1", kundenName ? "Kontaktliste für " + kundenName : "Kontaktliste"),
m(BreadcrumbBar, {
breadcrumbs: breadcrumbs,
current: filterKundenId !== null ? "Kontakte: " + kundenName : "Kontakte"
} as iBreadcrumbBar),
m("div", {style: {flex: '1', 'overflow-y': 'inherit'}},
m(Treetable,
{
@@ -129,11 +148,15 @@ export const Kontakt: m.ClosureComponent = (vnode) => {
email: "",
notiz: ""
}
// Breadcrumb-Kette der Vorgänger-Ansichten (aus Route-Param bc).
let breadcrumbs: iBreadcrumb[] = []
return {
oninit: (vnode) => {
const routeParams = m.route.param()
const id = routeParams && routeParams.id ? parseInt(routeParams.id) : -1
// Breadcrumb-Kette aus Route-Param parsen.
breadcrumbs = parseBc(routeParams && routeParams.bc)
if (id !== -1) {
// Kontaktdaten frisch vom Server holen, um zwischenzeitliche
// Änderungen anderer Nutzer nicht zu überschreiben.
@@ -148,6 +171,14 @@ export const Kontakt: m.ClosureComponent = (vnode) => {
}
},
view: (vnode) => {
// Rückkehr zur Kontaktliste; das bc ohne das letzte Glied (die
// Kontaktliste selbst) durchreichen.
const goBack = () => {
const listBc = serializeBc(breadcrumbs.slice(0, -1))
const routeParams: any = {kunden_id: currentKontakt.kunden_id, kunden_name: currentKontakt.kunden_name}
if (listBc) routeParams.bc = listBc
m.route.set("/kontaktliste", routeParams)
}
const submit = (form: HTMLFormElement): boolean => {
const formData = new FormData(form)
const gespeicherterKontakt: iKontakt = {
@@ -162,19 +193,23 @@ 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})).catch((e) => showServerError(e))
createKontaktREST(gespeicherterKontakt).then(() => goBack()).catch((e) => showServerError(e))
} else {
updateKontaktREST(gespeicherterKontakt).then(() => m.route.set("/kontaktliste", {kunden_id: currentKontakt.kunden_id, kunden_name: currentKontakt.kunden_name})).catch((e) => showServerError(e))
updateKontaktREST(gespeicherterKontakt).then(() => goBack()).catch((e) => showServerError(e))
}
return true
}
const cancel = (e: Event) => {
m.route.set("/kontaktliste", {kunden_id: currentKontakt.kunden_id, kunden_name: currentKontakt.kunden_name})
goBack()
}
return m("div.w3-container.w3-auto.form-view-container",
m("h1", currentKontakt.id === -1
? "Neuer Kontakt für " + currentKontakt.kunden_name
: "Kontakt-Stammblatt"),
m(BreadcrumbBar, {
breadcrumbs: breadcrumbs,
current: currentKontakt.id === -1 ? "Neuer Kontakt" : "Kontakt-Stammblatt"
} as iBreadcrumbBar),
m("div", {style: {flex: '1', 'min-height': '0'}},
m(Form, {showButtons: true, onsubmit: submit, oncancel: cancel} as iForm, [
m("input", {type: "hidden", name: "kunden_id", value: currentKontakt.kunden_id}),