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:
1 parent
505177b5e2
commit
9ed5e88e01
10 files changed
+687
-76
No files matched your search
@@ -52,6 +52,7 @@ def checklogin():
|
||||
login_user(user)
|
||||
result = user.to_dict()
|
||||
result["isSysAdmin"] = user.is_SysAdmin()
|
||||
result["isAppAdmin"] = user.has_permission(app.config["APPID"], [APP_ADMIN])
|
||||
return jsonify(result), 200
|
||||
|
||||
|
||||
@@ -86,13 +87,18 @@ def logout():
|
||||
def getusers():
|
||||
"""
|
||||
Die Rückgabe wird in 'users.get_users()' (s.u.) anhand er App-Berechtigung und der
|
||||
Rollen gefiltert.
|
||||
Rollen gefiltert. Pro User wird zusätzlich isSysAdmin und das app-spezifische
|
||||
isAppAdmin Flag ergänzt, damit der Client keine app-unspezifische Rollenprüfung
|
||||
durchführen muss.
|
||||
"""
|
||||
userquery = users.get_users()
|
||||
result = []
|
||||
user: User
|
||||
for user in userquery:
|
||||
result.append(user.to_relationaldict())
|
||||
eintrag = user.to_relationaldict()
|
||||
eintrag["isSysAdmin"] = user.is_SysAdmin()
|
||||
eintrag["isAppAdmin"] = user.has_permission(app.config["APPID"], [APP_ADMIN])
|
||||
result.append(eintrag)
|
||||
return jsonify(result), 200
|
||||
|
||||
|
||||
|
||||
@@ -165,3 +165,45 @@ def is_user_assigned(projekt_id: int, user_id: int) -> bool:
|
||||
ProjektUser.user_id == user_id,
|
||||
).first()
|
||||
return zuordnung is not None
|
||||
|
||||
|
||||
def get_all_projekt_users():
|
||||
"""
|
||||
Liefert alle Projekt-User-Zuordnungen, angereichert mit projektname
|
||||
(aus Projekt-Relationship) sowie userid, username und email (aus
|
||||
Auth-DB via Batch-Query).
|
||||
|
||||
Wird für die Übersichtssicht "Nutzer-Zugriffe" verwendet, in der alle
|
||||
Zuordnungen in einem Aufruf geladen werden.
|
||||
"""
|
||||
zuordnungen = ProjektUser.query.all()
|
||||
|
||||
if not zuordnungen:
|
||||
return []
|
||||
|
||||
# Projekte und Nutzer in jeweils einer Batch-Query auflösen.
|
||||
projekt_ids = list({z.projekt_id for z in zuordnungen})
|
||||
user_ids = list({z.user_id for z in zuordnungen})
|
||||
|
||||
projekte = Projekt.query.filter(Projekt.id.in_(projekt_ids)).all()
|
||||
projekt_map = {p.id: p for p in projekte}
|
||||
|
||||
users = User.query.filter(User.id.in_(user_ids)).all()
|
||||
user_map = {u.id: u for u in users}
|
||||
|
||||
result = []
|
||||
for z in zuordnungen:
|
||||
eintrag = z.to_dict()
|
||||
projekt = projekt_map.get(z.projekt_id)
|
||||
eintrag['projektname'] = projekt.projektname if projekt else ""
|
||||
user = user_map.get(z.user_id)
|
||||
if user is not None:
|
||||
eintrag['userid'] = user.userid
|
||||
eintrag['username'] = user.username
|
||||
eintrag['email'] = user.email or ""
|
||||
else:
|
||||
eintrag['userid'] = ""
|
||||
eintrag['username'] = ""
|
||||
eintrag['email'] = ""
|
||||
result.append(eintrag)
|
||||
return result
|
||||
@@ -233,6 +233,15 @@ def projektuser_list():
|
||||
return jsonify(result), 200
|
||||
|
||||
|
||||
@app.get("/xhr/projektuser/listall")
|
||||
# Leeres allowed_roles-Array als Erweiterungspunkt (nur AppAdmin/SysAdmin).
|
||||
@authorisation_required([])
|
||||
def projektuser_listall():
|
||||
"""Liefert alle Projekt-User-Zuordnungen inkl. projektname und Nutzerdaten."""
|
||||
result = projekt_users.get_all_projekt_users()
|
||||
return jsonify(result), 200
|
||||
|
||||
|
||||
@app.post("/xhr/projektuser/add")
|
||||
# Leeres allowed_roles-Array als Erweiterungspunkt: Aktuell passieren nur
|
||||
# AppAdmin und SysAdmin (vom Decorator implizit ergänzt). Sobald später eine
|
||||
|
||||
@@ -34,11 +34,18 @@ export const loginscreen: m.ClosureComponent<iLoginscreen> = (vnode) => {
|
||||
if (user.isSysAdmin) {
|
||||
sessionStorage.setItem("CurrentUserIsSysAdmin", "1");
|
||||
}
|
||||
// AppAdmin: der Server prüft applikationsspezifisch via
|
||||
// has_permission(appid, [APP_ADMIN]) und liefert isAppAdmin
|
||||
// im Login-Response. Der Client wertet nur das Flag aus.
|
||||
if (user.isAppAdmin) {
|
||||
sessionStorage.setItem("CurrentUserIsAppAdmin", "1");
|
||||
}
|
||||
})
|
||||
.then(() => {m.route.set(vnode.attrs.setnextpage)})
|
||||
.catch((error) => {
|
||||
sessionStorage.removeItem("CurrentUser");
|
||||
sessionStorage.removeItem("CurrentUserIsSysAdmin");
|
||||
sessionStorage.removeItem("CurrentUserIsAppAdmin");
|
||||
showServerError(error)
|
||||
})
|
||||
|
||||
@@ -52,6 +59,7 @@ export const loginscreen: m.ClosureComponent<iLoginscreen> = (vnode) => {
|
||||
.then (() => {
|
||||
sessionStorage.removeItem("CurrentUser");
|
||||
sessionStorage.removeItem("CurrentUserIsSysAdmin");
|
||||
sessionStorage.removeItem("CurrentUserIsAppAdmin");
|
||||
// global.appglobals.currentuser = "nicht angemeldet"
|
||||
// global.appglobals.currentuserid = ""
|
||||
})
|
||||
|
||||
@@ -9,6 +9,7 @@ export interface iUser {
|
||||
email: string
|
||||
telefon: string
|
||||
isSysAdmin: boolean
|
||||
isAppAdmin: boolean
|
||||
userapps: iUserApp[]
|
||||
}
|
||||
|
||||
|
||||
@@ -2,18 +2,19 @@ import m from "mithril"
|
||||
import {Treenode, iTreenode, Treetable, iTreetable, TopbarButton, iTopbarButton} from "./widgets/Treetable_V2"
|
||||
import {ModalView, ModalType, iModalView} from "./widgets/Modal"
|
||||
import {gridMountpoints} from "./mainlayout"
|
||||
import {iProjektUser, fetchProjektUsers, addProjektUserREST, removeProjektUserREST} from "../models/projekt_user"
|
||||
import {iProjekt, fetchProjektById} from "../models/projekt"
|
||||
import {iProjektUser, iProjektUserWithProjekt, fetchProjektUsers, fetchAllProjektUsers, addProjektUserREST, removeProjektUserREST} from "../models/projekt_user"
|
||||
import {iProjekt, fetchProjektById, fetchProjekte} from "../models/projekt"
|
||||
import {iUser} from "./auth/models/users"
|
||||
import {showServerError} from "./auth/authmessages"
|
||||
|
||||
/**
|
||||
* Liste der einem Projekt zugeordneten Nutzer (Sichtbarkeit).
|
||||
* Übersicht "Projekt-Zugriffe": Zeigt alle Projekte als Top-Level-Knoten
|
||||
* mit ihren jeweils zugeordneten Nutzern als Child-Knoten. Der SysAdmin und der AppAdmin
|
||||
* können Nutzer zuordnen und entfernen.
|
||||
*
|
||||
* Der Liste wird die projekt_id über die Route-Params übergeben. Sie zeigt
|
||||
* alle Nutzer, die das Projekt sehen dürfen. Der AppAdmin (und SysAdmin)
|
||||
* kann Nutzer über ein Modal mit Select-Widget hinzufügen und über einen
|
||||
* Entfernen-Button pro Zeile wieder wegnehmen.
|
||||
* Wird die Komponente mit einer projekt_id in den Route-Params aufgerufen,
|
||||
* zeigt sie nur die Nutzer-Liste für dieses eine Projekt (Detail-Sicht,
|
||||
* aufgerufen aus der Projekt-Maske heraus).
|
||||
*/
|
||||
export const projektuserliste: m.ClosureComponent = () => {
|
||||
let treenodes: iTreenode[] = []
|
||||
@@ -21,6 +22,12 @@ export const projektuserliste: m.ClosureComponent = () => {
|
||||
let projektId: number = -1
|
||||
let projektName: string = ""
|
||||
let projektUsers: iProjektUser[] = []
|
||||
// Alle Projekte für den Übersichtsmodus.
|
||||
let projekte: iProjekt[] = []
|
||||
// Alle Projekt-User-Zuordnungen (inkl. projektname) für den Übersichtsmodus.
|
||||
let allZuordnungen: iProjektUserWithProjekt[] = []
|
||||
// True, wenn die Detail-Sicht (Nutzer-Liste für ein Projekt) aktiv ist.
|
||||
let detailMode: boolean = false
|
||||
// Alle Nutzer aus der Auth-DB, für das Hinzufügen-Modal.
|
||||
let allUsers: iUser[] = []
|
||||
|
||||
@@ -64,7 +71,7 @@ export const projektuserliste: m.ClosureComponent = () => {
|
||||
const columnmapper: (node: iTreenode) => any[] = (node: iTreenode) => {
|
||||
let pu: iProjektUser = node.nodevalue
|
||||
return [
|
||||
m("div", m("strong", pu.userid)),
|
||||
m("span", m("strong", pu.userid)),
|
||||
m("span", pu.username),
|
||||
m("span", pu.email),
|
||||
m("div", {style: {display: 'flex', gap: '0', 'white-space': 'nowrap'}},
|
||||
@@ -88,17 +95,147 @@ export const projektuserliste: m.ClosureComponent = () => {
|
||||
}
|
||||
|
||||
/**
|
||||
* Öffnet das Modal zum Hinzufügen eines Nutzers. Die verfügbaren
|
||||
* Nutzer werden clientseitig gegen die bereits zugeordneten gefiltert.
|
||||
* Entfernt einen Nutzer aus einem Projekt (Übersichtsmodus).
|
||||
*/
|
||||
const removeUserOverview = (zuordnung: iProjektUserWithProjekt, e: Event) => {
|
||||
e.stopPropagation()
|
||||
removeProjektUserREST(zuordnung.projekt_id, zuordnung.user_id).then(() => {
|
||||
loadOverviewData()
|
||||
}).catch((e) => showServerError(e))
|
||||
}
|
||||
|
||||
/**
|
||||
* Columnmapper für Top-Level (Projekt-Zeilen im Übersichtsmodus).
|
||||
*/
|
||||
const overviewProjektColumnmapper: (node: iTreenode) => any[] = (node: iTreenode) => {
|
||||
let projekt: iProjekt = node.nodevalue
|
||||
return [
|
||||
m("span", m("strong", projekt.projektname)),
|
||||
m("span", projekt.kunden_name),
|
||||
m("span.w3-small.w3-opacity", node.children.length + " Nutzer")
|
||||
]
|
||||
}
|
||||
|
||||
/**
|
||||
* Columnmapper für Child-Level (Nutzer-Zeilen im Übersichtsmodus).
|
||||
* Nur Anzeige — keine Buttons.
|
||||
*/
|
||||
const overviewNutzerColumnmapper: (node: iTreenode) => any[] = (node: iTreenode) => {
|
||||
let z: iProjektUserWithProjekt = node.nodevalue
|
||||
return [
|
||||
m("span", m("strong", z.userid)),
|
||||
m("span", z.username + " — " + z.email),
|
||||
m("span", "")
|
||||
]
|
||||
}
|
||||
|
||||
// Der Treetable verwendet denselben columnmapper für alle Levels.
|
||||
// Über die nodevalue-Eigenschaft wird zwischen Projekt- und Nutzer-
|
||||
// Zeilen unterschieden.
|
||||
const overviewColumnmapper: (node: iTreenode) => any[] = (node: iTreenode) => {
|
||||
if (node.nodevalue && node.nodevalue.user_id !== undefined) {
|
||||
return overviewNutzerColumnmapper(node)
|
||||
}
|
||||
return overviewProjektColumnmapper(node)
|
||||
}
|
||||
|
||||
const overviewFilterFunction = (node: iTreenode, searchvalue: string): boolean => {
|
||||
let searchLower = searchvalue.toLowerCase()
|
||||
let nv = node.nodevalue
|
||||
if (nv && nv.user_id !== undefined) {
|
||||
// Nutzer-Zeile: nach userid, username filtern.
|
||||
return (nv.userid || "").toLowerCase().includes(searchLower) ||
|
||||
(nv.username || "").toLowerCase().includes(searchLower)
|
||||
}
|
||||
// Projekt-Zeile: nach projektname, kunden_name filtern.
|
||||
// Bei Treffer in Child-Nutzern das Projekt ebenfalls anzeigen.
|
||||
if ((nv.projektname || "").toLowerCase().includes(searchLower) ||
|
||||
(nv.kunden_name || "").toLowerCase().includes(searchLower)) {
|
||||
return true
|
||||
}
|
||||
return node.children.some((child) => {
|
||||
let z = child.nodevalue
|
||||
return (z.userid || "").toLowerCase().includes(searchLower) ||
|
||||
(z.username || "").toLowerCase().includes(searchLower)
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Lädt alle Projekte und alle Zuordnungen und baut den
|
||||
* Treenode-Baum (Projekt → Nutzer) auf.
|
||||
*/
|
||||
const loadOverviewData = () => {
|
||||
Promise.all([
|
||||
fetchProjekte(),
|
||||
fetchAllProjektUsers(),
|
||||
m.request({method: "GET", url: "/auth/getusers"})
|
||||
]).then(([projekteResult, zuordnungenResult, usersResult]) => {
|
||||
projekte = projekteResult as iProjekt[]
|
||||
allZuordnungen = zuordnungenResult as iProjektUserWithProjekt[]
|
||||
allUsers = usersResult as iUser[]
|
||||
buildOverviewTreenodes()
|
||||
}).catch((e) => showServerError(e))
|
||||
}
|
||||
|
||||
/**
|
||||
* Baut den Treenode-Baum für den Übersichtsmodus:
|
||||
* Top-Level = alle Projekte, Child-Nodes = zugeordnete Nutzer.
|
||||
*/
|
||||
const buildOverviewTreenodes = () => {
|
||||
treenodes = projekte.map((p: iProjekt) => {
|
||||
const nutzerNodes = allZuordnungen
|
||||
.filter((z) => z.projekt_id === p.id)
|
||||
.map((z) => new Treenode(z, true, []))
|
||||
return new Treenode(p, true, nutzerNodes)
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Öffnet das Modal zum Hinzufügen von Nutzern.
|
||||
* Im Detail-Modus für das aktuelle Projekt, im Übersichts-Modus
|
||||
* für das selektierte Projekt.
|
||||
*/
|
||||
const openAddModal = () => {
|
||||
// Alle Nutzer laden, falls noch nicht geschehen.
|
||||
if (allUsers.length === 0) {
|
||||
loadAllUsers()
|
||||
}
|
||||
m.mount(gridMountpoints.modalhook, addUserModal)
|
||||
}
|
||||
|
||||
/**
|
||||
* Liefert die projekt_id, für die das Add-Modal aktuell gilt:
|
||||
* im Detail-Modus die Route-Param projektId, im Übersichts-Modus
|
||||
* die projekt_id der selektierten Projekt-Zeile.
|
||||
*/
|
||||
const getModalProjektId = (): number => {
|
||||
if (detailMode) {
|
||||
return projektId
|
||||
}
|
||||
// Übersichtsmodus: selektierte Projekt-Zeile.
|
||||
if (selectedNode && selectedNode.nodevalue &&
|
||||
selectedNode.nodevalue.user_id === undefined) {
|
||||
return (selectedNode.nodevalue as iProjekt).id
|
||||
}
|
||||
return -1
|
||||
}
|
||||
|
||||
/**
|
||||
* Liefert die bereits zugeordneten Nutzer für das Modal:
|
||||
* im Detail-Modus projektUsers, im Übersichts-Modus aus
|
||||
* allZuordnungen gefiltert.
|
||||
*/
|
||||
const getModalAssignedIds = (): Set<number> => {
|
||||
if (detailMode) {
|
||||
return new Set(projektUsers.map((pu) => pu.user_id))
|
||||
}
|
||||
const pid = getModalProjektId()
|
||||
return new Set(
|
||||
allZuordnungen
|
||||
.filter((z) => z.projekt_id === pid)
|
||||
.map((z) => z.user_id)
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Modal-Komponente zur Auswahl mehrerer Nutzer, die dem Projekt
|
||||
* zugeordnet werden sollen. Zeigt eine durchsuchbare Liste mit
|
||||
@@ -115,15 +252,13 @@ export const projektuserliste: m.ClosureComponent = () => {
|
||||
// SysAdmin oder AppAdmin sind (diese haben ohnehin immer Zugriff)
|
||||
// und deren Suchtext-Filter entsprechen. Sortiert nach username.
|
||||
const getAvailableUsers = (): iUser[] => {
|
||||
const assignedIds = new Set(projektUsers.map((pu) => pu.user_id))
|
||||
const assignedIds = getModalAssignedIds()
|
||||
const searchLower = search.toLowerCase()
|
||||
return allUsers.filter((u: iUser) => {
|
||||
// SysAdmin und AppAdmin haben immer Zugriff,
|
||||
// sie brauchen keine projektbezogene Zuordnung.
|
||||
if (u.isSysAdmin) return false
|
||||
if (u.userapps && u.userapps.some((ua) =>
|
||||
ua.userapp_roles && ua.userapp_roles.some((r) => r.roleid === "AppAdmin")
|
||||
)) return false
|
||||
if (u.isAppAdmin) return false
|
||||
if (assignedIds.has(u.id)) return false
|
||||
if (searchLower === "") return true
|
||||
return (u.userid || "").toLowerCase().includes(searchLower) ||
|
||||
@@ -148,16 +283,25 @@ export const projektuserliste: m.ClosureComponent = () => {
|
||||
closeModal()
|
||||
return
|
||||
}
|
||||
const modalProjektId = getModalProjektId()
|
||||
const promises: Promise<void>[] = []
|
||||
selectedIds.forEach((uid: number) => {
|
||||
promises.push(addProjektUserREST(projektId, uid))
|
||||
promises.push(addProjektUserREST(modalProjektId, uid))
|
||||
})
|
||||
Promise.all(promises).then(() => {
|
||||
closeModal()
|
||||
loadProjektUsers()
|
||||
if (detailMode) {
|
||||
loadProjektUsers()
|
||||
} else {
|
||||
loadOverviewData()
|
||||
}
|
||||
}).catch((e) => {
|
||||
showServerError(e)
|
||||
loadProjektUsers()
|
||||
if (detailMode) {
|
||||
loadProjektUsers()
|
||||
} else {
|
||||
loadOverviewData()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -251,16 +395,20 @@ export const projektuserliste: m.ClosureComponent = () => {
|
||||
const routeParams = m.route.param()
|
||||
projektId = routeParams && routeParams.projekt_id ? parseInt(routeParams.projekt_id) : -1
|
||||
projektName = routeParams && routeParams.projekt_name ? routeParams.projekt_name : ""
|
||||
// Projektname asynchron auflösen, falls nicht in Route-Params.
|
||||
if (projektId !== -1 && projektName === "") {
|
||||
fetchProjektById(projektId).then((projekt: iProjekt) => {
|
||||
projektName = projekt.projektname
|
||||
}).catch((e) => showServerError(e))
|
||||
detailMode = projektId !== -1
|
||||
if (detailMode) {
|
||||
// Detail-Sicht: Nutzer-Liste für ein konkretes Projekt.
|
||||
if (projektName === "") {
|
||||
fetchProjektById(projektId).then((projekt: iProjekt) => {
|
||||
projektName = projekt.projektname
|
||||
}).catch((e) => showServerError(e))
|
||||
}
|
||||
loadAllUsers()
|
||||
loadProjektUsers()
|
||||
} else {
|
||||
// Übersichts-Sicht: alle Projekte + Zuordnungen + Nutzer laden.
|
||||
loadOverviewData()
|
||||
}
|
||||
// Alle Nutzer für das Hinzufügen-Modal vorab laden.
|
||||
loadAllUsers()
|
||||
// Zugeordnete Nutzer laden.
|
||||
loadProjektUsers()
|
||||
},
|
||||
view: (vnode) => {
|
||||
return m("div.w3-container.w3-auto",
|
||||
@@ -272,26 +420,38 @@ export const projektuserliste: m.ClosureComponent = () => {
|
||||
'overflow-y': 'inherit'
|
||||
}
|
||||
},
|
||||
m("h1", projektName
|
||||
? "Nutzerberechtigungen für Projekt: " + projektName
|
||||
: "Nutzerberechtigungen"),
|
||||
m("h1", detailMode
|
||||
? (projektName
|
||||
? "Nutzerberechtigungen für Projekt: " + projektName
|
||||
: "Nutzerberechtigungen")
|
||||
: "Projekt-Zugriffe — Übersicht"),
|
||||
m("div", {style: {flex: '1', 'overflow-y': 'inherit'}},
|
||||
m(Treetable,
|
||||
{
|
||||
treenodes: treenodes,
|
||||
columnmapper: columnmapper,
|
||||
filter: filterFunction,
|
||||
onRowMarked: handleRowMarked,
|
||||
isSelected: (node: iTreenode) => node === selectedNode
|
||||
} as iTreetable,
|
||||
m(TopbarButton,
|
||||
detailMode
|
||||
? m(Treetable,
|
||||
{
|
||||
onclick: openAddModal,
|
||||
tooltip: "Nutzer hinzufügen"
|
||||
} as iTopbarButton,
|
||||
m("i.material-icons", "person_add")
|
||||
treenodes: treenodes,
|
||||
columnmapper: columnmapper,
|
||||
filter: filterFunction,
|
||||
onRowMarked: handleRowMarked,
|
||||
isSelected: (node: iTreenode) => node === selectedNode
|
||||
} as iTreetable,
|
||||
m(TopbarButton,
|
||||
{
|
||||
onclick: openAddModal,
|
||||
tooltip: "Nutzer hinzufügen"
|
||||
} as iTopbarButton,
|
||||
m("i.material-icons", "person_add")
|
||||
)
|
||||
)
|
||||
: m(Treetable,
|
||||
{
|
||||
treenodes: treenodes,
|
||||
columnmapper: overviewColumnmapper,
|
||||
filter: overviewFilterFunction,
|
||||
onRowMarked: handleRowMarked,
|
||||
isSelected: (node: iTreenode) => node === selectedNode
|
||||
} as iTreetable
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,313 @@
|
||||
import m from "mithril"
|
||||
import {Treenode, iTreenode, Treetable, iTreetable, TopbarButton, iTopbarButton} from "./widgets/Treetable_V2"
|
||||
import {ModalView, ModalType, iModalView} from "./widgets/Modal"
|
||||
import {gridMountpoints} from "./mainlayout"
|
||||
import {iProjektUserWithProjekt, fetchAllProjektUsers, addProjektUserREST, removeProjektUserREST} from "../models/projekt_user"
|
||||
import {iProjekt, fetchProjekte} from "../models/projekt"
|
||||
import {iUser} from "./auth/models/users"
|
||||
import {showServerError} from "./auth/authmessages"
|
||||
|
||||
/**
|
||||
* Übersicht "Nutzer-Zugriffe": Zeigt alle Nutzer (ohne SysAdmin/AppAdmin)
|
||||
* als Top-Level-Knoten mit ihren jeweils zugeordneten Projekten als
|
||||
* Child-Knoten. Der AppAdmin kann Projekte zuordnen und entfernen.
|
||||
*/
|
||||
export const userprojektliste: m.ClosureComponent = () => {
|
||||
let treenodes: iTreenode[] = []
|
||||
let selectedNode: iTreenode | null = null
|
||||
// Alle Nutzer aus der Auth-DB.
|
||||
let allUsers: iUser[] = []
|
||||
// Alle Projekt-User-Zuordnungen (inkl. projektname).
|
||||
let allZuordnungen: iProjektUserWithProjekt[] = []
|
||||
// Alle Projekte (für das Hinzufügen-Modal).
|
||||
let allProjekte: iProjekt[] = []
|
||||
|
||||
const handleRowMarked = (node: iTreenode) => {
|
||||
selectedNode = node
|
||||
}
|
||||
|
||||
/**
|
||||
* Lädt alle Nutzer, alle Projekte und alle Zuordnungen und baut
|
||||
* den Treenode-Baum (Nutzer → Projekte) auf.
|
||||
*/
|
||||
const loadData = () => {
|
||||
Promise.all([
|
||||
m.request({method: "GET", url: "/auth/getusers"}),
|
||||
fetchProjekte(),
|
||||
fetchAllProjektUsers()
|
||||
]).then(([usersResult, projekteResult, zuordnungenResult]) => {
|
||||
allUsers = usersResult as iUser[]
|
||||
allProjekte = projekteResult as iProjekt[]
|
||||
allZuordnungen = zuordnungenResult as iProjektUserWithProjekt[]
|
||||
buildTreenodes()
|
||||
}).catch((e) => showServerError(e))
|
||||
}
|
||||
|
||||
/**
|
||||
* Baut den Treenode-Baum: Top-Level = Nutzer (ohne SysAdmin/AppAdmin),
|
||||
* Child-Nodes = zugeordnete Projekte.
|
||||
*/
|
||||
const buildTreenodes = () => {
|
||||
// Nur Nutzer ohne SysAdmin/AppAdmin-Rolle anzeigen.
|
||||
const nutzer = allUsers.filter((u: iUser) => {
|
||||
if (u.isSysAdmin) return false
|
||||
if (u.isAppAdmin) return false
|
||||
return true
|
||||
}).sort((a: iUser, b: iUser) => {
|
||||
return (a.username || "").localeCompare(b.username || "")
|
||||
})
|
||||
|
||||
treenodes = nutzer.map((u: iUser) => {
|
||||
// Zugeordnete Projekte für diesen Nutzer.
|
||||
const projektNodes = allZuordnungen
|
||||
.filter((z) => z.user_id === u.id)
|
||||
.map((z) => new Treenode(z, true, []))
|
||||
return new Treenode(u, true, projektNodes)
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Entfernt ein Projekt von einem Nutzer.
|
||||
*/
|
||||
const removeProjekt = (zuordnung: iProjektUserWithProjekt, e: Event) => {
|
||||
e.stopPropagation()
|
||||
removeProjektUserREST(zuordnung.projekt_id, zuordnung.user_id).then(() => {
|
||||
loadData()
|
||||
}).catch((e) => showServerError(e))
|
||||
}
|
||||
|
||||
/**
|
||||
* Columnmapper für Top-Level (Nutzer-Zeilen).
|
||||
*/
|
||||
const nutzerColumnmapper: (node: iTreenode) => any[] = (node: iTreenode) => {
|
||||
let u: iUser = node.nodevalue
|
||||
return [
|
||||
m("span", m("strong", u.userid)),
|
||||
m("span", u.username),
|
||||
m("span", u.email),
|
||||
m("span.w3-small.w3-opacity", node.children.length + " Projekt(e)")
|
||||
]
|
||||
}
|
||||
|
||||
/**
|
||||
* Columnmapper für Child-Level (Projekt-Zeilen).
|
||||
* Nur Anzeige — keine Buttons.
|
||||
*/
|
||||
const projektColumnmapper: (node: iTreenode) => any[] = (node: iTreenode) => {
|
||||
let z: iProjektUserWithProjekt = node.nodevalue
|
||||
return [
|
||||
m("span", m("strong", z.projektname)),
|
||||
m("span", ""),
|
||||
m("span", ""),
|
||||
m("span", "")
|
||||
]
|
||||
}
|
||||
|
||||
// Der Treetable verwendet pro Level denselben columnmapper. Da Nutzer-
|
||||
// und Projekt-Zeilen unterschiedlich aussehen, wird über die
|
||||
// nodevalue-Eigenschaft zwischen ihnen unterschieden.
|
||||
const columnmapper: (node: iTreenode) => any[] = (node: iTreenode) => {
|
||||
// Wenn die nodevalue eine projekt_id-Eigenschaft hat, ist es
|
||||
// eine Projekt-Zuordnung (Child-Knoten), sonst ein Nutzer (Top-Level).
|
||||
if (node.nodevalue && node.nodevalue.projekt_id !== undefined) {
|
||||
return projektColumnmapper(node)
|
||||
}
|
||||
return nutzerColumnmapper(node)
|
||||
}
|
||||
|
||||
const filterFunction = (node: iTreenode, searchvalue: string): boolean => {
|
||||
let searchLower = searchvalue.toLowerCase()
|
||||
let nv = node.nodevalue
|
||||
if (nv && nv.projekt_id !== undefined) {
|
||||
// Projekt-Zeile: nach projektname filtern.
|
||||
return (nv.projektname || "").toLowerCase().includes(searchLower)
|
||||
}
|
||||
// Nutzer-Zeile: nach userid, username, email filtern.
|
||||
// Bei Suche auch Treffer in den Child-Projekten anzeigen.
|
||||
if ((nv.userid || "").toLowerCase().includes(searchLower) ||
|
||||
(nv.username || "").toLowerCase().includes(searchLower) ||
|
||||
(nv.email || "").toLowerCase().includes(searchLower)) {
|
||||
return true
|
||||
}
|
||||
// Prüfen, ob ein Child-Projekt den Suchtext enthält.
|
||||
return node.children.some((child) => {
|
||||
let z = child.nodevalue
|
||||
return (z.projektname || "").toLowerCase().includes(searchLower)
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Öffnet das Modal zum Hinzufügen von Projekten zu einem Nutzer.
|
||||
*/
|
||||
const openAddModal = (user: iUser) => {
|
||||
m.mount(gridMountpoints.modalhook, {
|
||||
view: () => m(addProjektModal, {user: user})
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Modal zum Hinzufügen mehrerer Projekte zu einem Nutzer.
|
||||
* Zeigt eine durchsuchbare Checkbox-Liste aller Projekte, die dem
|
||||
* Nutzer noch nicht zugeordnet sind.
|
||||
*/
|
||||
const addProjektModal: m.ClosureComponent<{user: iUser}> = () => {
|
||||
let selectedIds: Set<number> = new Set()
|
||||
let search: string = ""
|
||||
|
||||
const getAvailableProjekte = (): iProjekt[] => {
|
||||
const assignedIds = new Set(
|
||||
allZuordnungen
|
||||
.filter((z) => z.user_id === vnode_userId)
|
||||
.map((z) => z.projekt_id)
|
||||
)
|
||||
const searchLower = search.toLowerCase()
|
||||
return allProjekte.filter((p: iProjekt) => {
|
||||
if (assignedIds.has(p.id)) return false
|
||||
if (searchLower === "") return true
|
||||
return (p.projektname || "").toLowerCase().includes(searchLower) ||
|
||||
(p.kunden_name || "").toLowerCase().includes(searchLower)
|
||||
}).sort((a: iProjekt, b: iProjekt) => {
|
||||
return (a.projektname || "").localeCompare(b.projektname || "")
|
||||
})
|
||||
}
|
||||
|
||||
// Die user_id wird in der Closure gehalten, da der Closure-
|
||||
// Parameter vom Typ m.ClosureComponent ist und attrs nur im
|
||||
// view/oninit verfügbar sind.
|
||||
let vnode_userId: number = -1
|
||||
|
||||
const closeModal = () => {
|
||||
m.mount(gridMountpoints.modalhook, null)
|
||||
}
|
||||
|
||||
const confirmAdd = () => {
|
||||
if (selectedIds.size === 0) {
|
||||
closeModal()
|
||||
return
|
||||
}
|
||||
const promises: Promise<void>[] = []
|
||||
selectedIds.forEach((pid: number) => {
|
||||
promises.push(addProjektUserREST(pid, vnode_userId))
|
||||
})
|
||||
Promise.all(promises).then(() => {
|
||||
closeModal()
|
||||
loadData()
|
||||
}).catch((e) => {
|
||||
showServerError(e)
|
||||
loadData()
|
||||
})
|
||||
}
|
||||
|
||||
const toggleProjekt = (projektId: number) => {
|
||||
if (selectedIds.has(projektId)) {
|
||||
selectedIds.delete(projektId)
|
||||
} else {
|
||||
selectedIds.add(projektId)
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
oninit: (vnode) => {
|
||||
vnode_userId = vnode.attrs.user.id
|
||||
},
|
||||
view: (vnode) => {
|
||||
const user = vnode.attrs.user
|
||||
const availableProjekte = getAvailableProjekte()
|
||||
return m(ModalView,
|
||||
{
|
||||
title: "Projekt zuordnen",
|
||||
type: ModalType.MESSAGE,
|
||||
width: '500px'
|
||||
} as iModalView,
|
||||
m("div.w3-padding-16",
|
||||
m("p", "Wählen Sie die Projekte aus, die dem Nutzer \"" + user.username + "\" zugeordnet werden sollen:"),
|
||||
m("label", "Suche"),
|
||||
m("input.w3-input.w3-border",
|
||||
{
|
||||
type: "text",
|
||||
value: search,
|
||||
placeholder: "Nach Projektname oder Kunde suchen",
|
||||
style: {'margin-bottom': '8px'},
|
||||
oninput: (e: Event) => {
|
||||
search = (e.target as HTMLInputElement).value
|
||||
}
|
||||
}
|
||||
),
|
||||
m("div",
|
||||
{
|
||||
style: {
|
||||
'max-height': '300px',
|
||||
'overflow-y': 'auto',
|
||||
'margin-top': '8px',
|
||||
border: '1px solid #ccc'
|
||||
}
|
||||
},
|
||||
availableProjekte.length === 0
|
||||
? m("div.w3-padding-small", "Keine weiteren Projekte verfügbar")
|
||||
: availableProjekte.map((p: iProjekt) =>
|
||||
m("label.w3-block.w3-hover-light-grey.w3-text-left",
|
||||
{
|
||||
style: {'border-bottom': '1px solid #eee', padding: '4px 8px'},
|
||||
onclick: () => toggleProjekt(p.id)
|
||||
},
|
||||
m("input",
|
||||
{
|
||||
type: "checkbox",
|
||||
checked: selectedIds.has(p.id),
|
||||
style: {'margin-right': '8px'},
|
||||
onclick: (e: Event) => e.stopPropagation(),
|
||||
onchange: (e: Event) => toggleProjekt(p.id)
|
||||
}
|
||||
),
|
||||
m("strong", p.projektname),
|
||||
" (" + p.kunden_name + ")"
|
||||
)
|
||||
)
|
||||
),
|
||||
m("div.w3-bar.w3-padding-16",
|
||||
m("span.w3-small.w3-left",
|
||||
{style: {'line-height': '36px', 'margin-right': '8px'}},
|
||||
selectedIds.size + " ausgewählt"),
|
||||
m("button.w3-button.w3-theme.w3-right",
|
||||
{style: {'margin-left': '4px'}, onclick: confirmAdd},
|
||||
"Zuordnen"),
|
||||
m("button.w3-button.w3-theme.w3-right",
|
||||
{style: {'margin-left': '4px'}, onclick: closeModal},
|
||||
"Abbrechen")
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
oninit: (vnode) => {
|
||||
loadData()
|
||||
},
|
||||
view: (vnode) => {
|
||||
return m("div.w3-container.w3-auto",
|
||||
{
|
||||
style: {
|
||||
display: 'flex',
|
||||
'flex-direction': 'column',
|
||||
height: 'inherit',
|
||||
'overflow-y': 'inherit'
|
||||
}
|
||||
},
|
||||
m("h1", "Nutzer-Zugriffe — Übersicht"),
|
||||
m("div", {style: {flex: '1', 'overflow-y': 'inherit'}},
|
||||
m(Treetable,
|
||||
{
|
||||
treenodes: treenodes,
|
||||
columnmapper: columnmapper,
|
||||
filter: filterFunction,
|
||||
onRowMarked: handleRowMarked,
|
||||
isSelected: (node: iTreenode) => node === selectedNode
|
||||
} as iTreetable
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -51,6 +51,13 @@ 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, {
|
||||
@@ -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),
|
||||
@@ -136,9 +153,12 @@ let Treerow: m.ClosureComponent<iTreerow> = (vnode) => {
|
||||
// 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.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
|
||||
|
||||
+56
-25
@@ -5,7 +5,8 @@ import { appmanagementpage } from "./Components/auth/appmanagementpage"
|
||||
import { changepasswordscreen, loginscreen } from "./Components/auth/login"
|
||||
import {kunde, kundenliste} from "./Components/kunde"
|
||||
import { Projekt, projektliste } from "./Components/projekt"
|
||||
import { projektuserliste } from "./Components/projekt_user"
|
||||
import { projektuserliste } from "./Components/projekt_userliste"
|
||||
import { userprojektliste } from "./Components/user_projektliste"
|
||||
import { Call_Log, call_log_liste } from "./Components/call_log"
|
||||
import { Kontakt, kontaktliste } from "./Components/kontakt"
|
||||
import { Kontaktadresse, kontaktadresseliste } from "./Components/kontaktadresse"
|
||||
@@ -63,6 +64,7 @@ function logout() {
|
||||
console.log(result)
|
||||
sessionStorage.removeItem("CurrentUser");
|
||||
sessionStorage.removeItem("CurrentUserIsSysAdmin");
|
||||
sessionStorage.removeItem("CurrentUserIsAppAdmin");
|
||||
headerdata.username = "Nicht angemeldet"
|
||||
if (result['logout'] == true) {m.route.set("/blankpage")}
|
||||
})
|
||||
@@ -101,31 +103,59 @@ const knopf: m.Component = {
|
||||
return m("button", {onclick: healthcheck}, "Aktualisieren")
|
||||
}
|
||||
}
|
||||
const menu: m.Component = {
|
||||
view: (vnode) => {
|
||||
return m(menubar, [
|
||||
m("a.w3-button.w3-bar-item", {href: "#!/kundenliste"}, "Kunden"),
|
||||
m("a.w3-button.w3-bar-item", {href: "#!/projektliste"}, "Projekte"),
|
||||
m("a.w3-button.w3-bar-item", {href: "#!/calllogliste"}, "Gesprächsnotizen"),
|
||||
// m("a.w3-button.w3-bar-item", {onclick: healthcheck}, "Aktualisieren"),
|
||||
m("div.w3-dropdown-hover",
|
||||
m("button.w3-button", "Optionen"),
|
||||
m("div.w3-dropdown-content.w3-bar-block.w3-card-4",
|
||||
m("a.w3-button.w3-bar-item", {href: "#!/taglist"}, "Tags"),
|
||||
m("a.w3-button.w3-bar-item", {href: "#!/default"}, "Datenexport"),
|
||||
m("a.w3-button.w3-bar-item", {href: "#!/default"}, "Datenimport"),
|
||||
const menu: m.ClosureComponent = () => {
|
||||
let showProjektBerechtigungen = false
|
||||
return {
|
||||
oninit: (vnode) => {
|
||||
showProjektBerechtigungen = false
|
||||
},
|
||||
view: (vnode) => {
|
||||
return m(menubar, [
|
||||
m("a.w3-button.w3-bar-item", {href: "#!/kundenliste"}, "Kunden"),
|
||||
m("a.w3-button.w3-bar-item", {href: "#!/projektliste"}, "Projekte"),
|
||||
m("a.w3-button.w3-bar-item", {href: "#!/calllogliste"}, "Gesprächsnotizen"),
|
||||
m("div.w3-dropdown-hover",
|
||||
m("button.w3-button", "Optionen"),
|
||||
m("div.w3-dropdown-content.w3-bar-block.w3-card-4",
|
||||
m("a.w3-button.w3-bar-item", {href: "#!/taglist"}, "Tags"),
|
||||
m("a.w3-button.w3-bar-item", {href: "#!/default"}, "Datenexport"),
|
||||
m("a.w3-button.w3-bar-item", {href: "#!/default"}, "Datenimport"),
|
||||
// Projekt-Berechtigungen nur für SysAdmin/AppAdmin sichtbar.
|
||||
// Als Toggle-Button realisiert, da verschachtelte w3-dropdown-hover
|
||||
// innerhalb eines offenen Dropdowns nicht zuverlässig funktionieren.
|
||||
(sessionStorage.getItem("CurrentUserIsSysAdmin") || sessionStorage.getItem("CurrentUserIsAppAdmin"))
|
||||
? [
|
||||
m("button.w3-button.w3-bar-item",
|
||||
{
|
||||
onclick: () => {showProjektBerechtigungen = !showProjektBerechtigungen}
|
||||
},
|
||||
m("span", showProjektBerechtigungen ? "Projekt-Berechtigungen \u25B2" : "Projekt-Berechtigungen \u25BC")
|
||||
),
|
||||
showProjektBerechtigungen
|
||||
? m("div.w3-bar-block",
|
||||
m("a.w3-button.w3-bar-item",
|
||||
{href: "#!/projektuserliste", style: {'padding-left': '32px'}},
|
||||
"Projekt-Zugriffe"),
|
||||
m("a.w3-button.w3-bar-item",
|
||||
{href: "#!/userprojektliste", style: {'padding-left': '32px'}},
|
||||
"Nutzer-Zugriffe")
|
||||
)
|
||||
: null
|
||||
]
|
||||
: null,
|
||||
)
|
||||
),
|
||||
m("div.w3-button.w3-right", {onclick: logout}, "Logout"),
|
||||
m("div.w3-dropdown-hover.w3-right",
|
||||
m("button.w3-button", "System"),
|
||||
m("div.w3-dropdown-content.w3-bar-block.w3-card-4",
|
||||
m("a.w3-button.w3-bar-item", {href: "#!/login"}, "Login"),
|
||||
m("a.w3-button.w3-bar-item", {href: "#!/changepassword"}, "Kennwort ändern"),
|
||||
m("a.w3-button.w3-bar-item", {href: "#!/usermanagement"}, "Verwalten"),
|
||||
)
|
||||
)
|
||||
),
|
||||
m("div.w3-button.w3-right", {onclick: logout}, "Logout"),
|
||||
m("div.w3-dropdown-hover.w3-right",
|
||||
m("button.w3-button", "System"),
|
||||
m("div.w3-dropdown-content.w3-bar-block.w3-card-4",
|
||||
m("a.w3-button.w3-bar-item", {href: "#!/login"}, "Login"),
|
||||
m("a.w3-button.w3-bar-item", {href: "#!/changepassword"}, "Kennwort ändern"),
|
||||
m("a.w3-button.w3-bar-item", {href: "#!/usermanagement"}, "Verwalten"),
|
||||
)
|
||||
)
|
||||
])
|
||||
])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -154,6 +184,7 @@ m.route(gridMountpoints.main, "/blankpage", {
|
||||
"/projektliste": projektliste,
|
||||
"/projekt": Projekt,
|
||||
"/projektuserliste": projektuserliste,
|
||||
"/userprojektliste": userprojektliste,
|
||||
"/calllogliste": call_log_liste,
|
||||
"/call_log": Call_Log,
|
||||
"/kontaktliste": kontaktliste,
|
||||
|
||||
@@ -17,6 +17,15 @@ export interface iProjektUser {
|
||||
email: string
|
||||
}
|
||||
|
||||
/**
|
||||
* Erweitertes Interface für die Übersichtssicht "Nutzer-Zugriffe".
|
||||
* Enthält zusätzlich den Projektnamen, der serverseitig aus der
|
||||
* Projekt-Relationship aufgelöst wird.
|
||||
*/
|
||||
export interface iProjektUserWithProjekt extends iProjektUser {
|
||||
projektname: string
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* REST-API-Aufrufe (Client-Seite) */
|
||||
/* ------------------------------------------------------------------ */
|
||||
@@ -61,3 +70,14 @@ export function removeProjektUserREST(projektId: number, userId: number): Promis
|
||||
params: {projekt_id: projektId, user_id: userId}
|
||||
}).then(() => undefined)
|
||||
}
|
||||
|
||||
/**
|
||||
* Lädt alle Projekt-User-Zuordnungen inkl. projektname und Nutzerdaten.
|
||||
* Nur für AppAdmin/SysAdmin.
|
||||
*/
|
||||
export function fetchAllProjektUsers(): Promise<iProjektUserWithProjekt[]> {
|
||||
return m.request({
|
||||
method: "GET",
|
||||
url: PROJEKTUSER_REST_BASE + "/listall"
|
||||
}).then((result) => result as iProjektUserWithProjekt[])
|
||||
}
|
||||
Reference in new issue
Block a user