Menü Projekt-Berechtigungen, Übersichtssichten und AppAdmin-Erkennung

- Menü: Dropdown "Projekt-Berechtigungen" unter Optionen mit
  Unterpunkten "Projekt-Zugriffe" und "Nutzer-Zugriffe" (nur SysAdmin/AppAdmin)
- AppAdmin-Erkennung: Server liefert isAppAdmin-Flag im Login- und
  getusers-Response (applikationsspezifisch via has_permission)
- Login: CurrentUserIsAppAdmin in sessionStorage speichern/entfernen
- Projekt-Zugriffe: geschachtelter Treetable (Projekt → Nutzer) als
  reine Anzeige, Detail-Sicht aus Projekt-Maske mit Bearbeitung
- Nutzer-Zugriffe: neue Komponente user_projektliste.ts, geschachtelter
  Treetable (Nutzer → Projekte), reine Anzeige
- Server: neuer Endpunkt GET /xhr/projektuser/listall (nur AppAdmin/SysAdmin)
- Server: get_all_projekt_users() mit Batch-Query für Projekte und User
- Treetable-Widget: Platzhalter-Pfeil bei kinderlosen Nodes nur wenn
  Geschwister mit Kindern existieren (hasSiblingsWithChildren)
- Datei projekt_user.ts umbenannt in projekt_userliste.ts (konsistent
  mit user_projektliste.ts)

Generated by Mistral Vibe.
Co-Authored-By: Mistral Vibe <vibe@mistral.ai>
This commit is contained in:
gg committed 2026-09-07 16:04:06 +02:00
1 parent 505177b5e2
commit 9ed5e88e01
10 files changed
+693 -82

No files matched your search

+32 -11
View File
@@ -51,28 +51,35 @@ export const Treetable: m.ClosureComponent<iTreetable> = (vnode) => {
}
return {
view: (vnode) => {
// Prüfen, ob mindestens ein Top-Level-Node Kinder hat.
// Wird benötigt, um bei kinderlosen Top-Level-Nodes den
// Platzhalter-Pfeil nur dann zu rendern, wenn es Geschwister
// mit Kindern (und damit Pfeil) gibt.
const topHasSiblingsWithChildren = vnode.attrs.treenodes.some(
(tn: iTreenode) => tn.hasChildren
)
return m("div.w3-border-theme", {style: {height: '99%', overflow: 'hidden', 'border-style': 'solid', 'border-width': '1px'}},
m("div.w3-bar.w3-theme",
m(TopbarButton, {
onclick: () => {
expandtoggle = true; changeTreenodes(vnode.attrs.treenodes, expandtoggle)
}},
}},
m("i.material-icons", "unfold_more")
),
m(TopbarButton, {
onclick: () => {
expandtoggle = false; changeTreenodes(vnode.attrs.treenodes, expandtoggle)
}},
}},
m("i.material-icons", "unfold_less")
),
vnode.children,
(vnode.attrs.filter != undefined) ? m(SearchField, {setSearchvalue: setSearchvalue} as iSearchfield) : null
(vnode.attrs.filter != undefined) ? m(SearchField, {setSearchvalue: setSearchvalue} as iSearchfield) : null
),
m("div", {style: {height: '100%'}},
m("div", {style: {height: '100%'}},
m("div", {style: {height: 'calc(100% - 32px', overflow: 'auto', width: '100%'}},
m("table.w3-table", {style: {'overflow-y': 'auto'}},
vnode.attrs.treenodes.map((tn:iTreenode, z) => {
if (vnode.attrs.filter == undefined || (vnode.attrs.filter != undefined && vnode.attrs.filter(tn, searchvalue) == true)) {
if (vnode.attrs.filter == undefined || (vnode.attrs.filter != undefined && vnode.attrs.filter(tn, searchvalue) == true)) {
return m(Treerow, {
// ACHTUNG: Änderungen hier müssen unten beim rekursiven Aufruf der Treerow-Funktion ebenfalls eingebaut werden
treenode: tn,
@@ -80,7 +87,8 @@ export const Treetable: m.ClosureComponent<iTreetable> = (vnode) => {
columnmapper1: vnode.attrs.columnmapper,
onRowMarked: onRowMarked,
isSelected: isSelected,
isEven: ((z % 2) == 0)} as iTreerow)
isEven: ((z % 2) == 0),
hasSiblingsWithChildren: topHasSiblingsWithChildren} as iTreerow)
}
else {
return null
@@ -101,6 +109,11 @@ interface iTreerow {
onRowMarked: (node: iTreenode) => void
isSelected?: (node: iTreenode) => boolean
isEven?: boolean
/** True, wenn mindestens ein Geschwister-Node auf demselben Level
* Kinder hat. Wird verwendet, um bei kinderlosen Top-Level-Nodes
* den Platzhalter-Pfeil nur dann zu rendern, wenn es Nodes mit
* Pfeil gibt, an denen der Text ausgerichtet werden kann. */
hasSiblingsWithChildren?: boolean
}
let Treerow: m.ClosureComponent<iTreerow> = (vnode) => {
@@ -110,6 +123,10 @@ let Treerow: m.ClosureComponent<iTreerow> = (vnode) => {
// Zeile selektiert ist; in diesem Fall wird der Hintergrund der
// gesamten <tr>-Zeile eingefärbt.
const rowSelected = vnode.attrs.isSelected ? vnode.attrs.isSelected(vnode.attrs.treenode) : false
// Prüfen, ob mindestens ein Child-Node selbst Kinder hat.
const childHasSiblingsWithChildren = vnode.attrs.treenode.children.some(
(tn: iTreenode) => tn.hasChildren
)
return [
m("tr.w3-border-theme", {onmouseup: () => vnode.attrs.onRowMarked(vnode.attrs.treenode),
onkeyup: () => vnode.attrs.onRowMarked(vnode.attrs.treenode),
@@ -135,10 +152,13 @@ let Treerow: m.ClosureComponent<iTreerow> = (vnode) => {
// angezeigt werden, allerdings soll durch 'margin-left' der entsprechende Platz des
// Buttons eingerückt werden, damit die Einrückungs-Level auch bei "kinderlosen" Treenodes
// denen der Trennodes mit Kindern entsprechen
: vnode.attrs.level == 0
? null // Wenn der kinderlos Treenode allerdings sirekt auf Level 0 ist, soll nocht
// eingerückt werden, da es bei Level 0 keine Eltern-Nodes mit Kindern und
// demzufolge mit "expand_more/expand_less"-Button gibt.
: vnode.attrs.level == 0
? (vnode.attrs.hasSiblingsWithChildren
? m("span", {
style: {'margin-left': '0px',
'margin-right': '32px'}
})
: null)
: m(
"span", {
style: {'margin-left': '24px',
@@ -165,7 +185,8 @@ let Treerow: m.ClosureComponent<iTreerow> = (vnode) => {
columnmapper1: vnode.attrs.columnmapper1,
onRowMarked: vnode.attrs.onRowMarked,
isSelected: vnode.attrs.isSelected,
isEven: ((z % 2) == 0)
isEven: ((z % 2) == 0),
hasSiblingsWithChildren: childHasSiblingsWithChildren
} as iTreerow)
})
: null