Projekt-Sichtbarkeit: Nutzer-Zuordnung, projektbezogene Filterung und Schreibschutz
- Neues Modell ProjektUser (projekt_users) für projektbezogene Sichtbarkeit
- Migration V01_09 legt Tabelle an (keine Initial-Befüllung)
- CRUD-Logik für Projekt-User-Zuordnungen mit Cross-DB-User-Auflösung
- Serverseitige Sichtbarkeits-Filterung: Projekt-Liste/Get filtert nach
projekt_users-Zuordnung für Nicht-SysAdmin/Nicht-AppAdmin
- Auto-Assignment: Ersteller wird bei Projektanlage automatisch zugeordnet
- Sichtbarkeits-Filterung auf CallLog und Kontaktadresse erweitert
(Liste und Einzelabruf)
- Schreibschutz: Alle Schreib-Endpunkte (update/delete) für Projekt,
Kontaktadresse und CallLog prüfen Projekt-Zuordnung vor der Ausführung
- Hilfsfunktionen _get_visible_user_id() und _require_projekt_access()
zur Kapselung der wiederkehrenden Prüfungen
- REST-Endpunkte /xhr/projektuser/{list,add,remove};
Schreib-Endpunkte mit @authorisation_required([]) als Erweiterungspunkt
- Client: Neues Modell projekt_user.ts, Listen-Komponente mit Modal
(Select-Widget), Button in Projekt-Maske, Route registriert
- Build aktualisiert (tsc + rollup)
- AGENTS.md: Vorgaben für projektbezogene Sichtbarkeit dokumentiert
Generated by Mistral Vibe.
Co-Authored-By: Mistral Vibe <vibe@mistral.ai>
This commit is contained in:
1 parent
b2f8684afb
commit
4cb0084066
12 files changed
+758
-25
No files matched your search
@@ -0,0 +1,223 @@
|
||||
import m from "mithril"
|
||||
import {Treenode, iTreenode, Treetable, iTreetable, TopbarButton, iTopbarButton} from "./widgets/Treetable_V2"
|
||||
import {Select, iSelect, iSelectOption} from "./widgets/select"
|
||||
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 {iUser} from "./auth/models/users"
|
||||
import {showServerError} from "./auth/authmessages"
|
||||
|
||||
/**
|
||||
* Liste der einem Projekt zugeordneten Nutzer (Sichtbarkeit).
|
||||
*
|
||||
* 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.
|
||||
*/
|
||||
export const projektuserliste: m.ClosureComponent = () => {
|
||||
let treenodes: iTreenode[] = []
|
||||
let selectedNode: iTreenode | null = null
|
||||
let projektId: number = -1
|
||||
let projektName: string = ""
|
||||
let projektUsers: iProjektUser[] = []
|
||||
// Alle Nutzer aus der Auth-DB, für das Hinzufügen-Modal.
|
||||
let allUsers: iUser[] = []
|
||||
|
||||
const handleRowMarked = (node: iTreenode) => {
|
||||
selectedNode = node
|
||||
}
|
||||
|
||||
/**
|
||||
* Lädt die zugeordneten Nutzer vom Server und aktualisiert die Treenodes.
|
||||
*/
|
||||
const loadProjektUsers = () => {
|
||||
fetchProjektUsers(projektId).then((result) => {
|
||||
projektUsers = result
|
||||
treenodes = result.map((pu: iProjektUser) => new Treenode(pu, true, []))
|
||||
}).catch((e) => showServerError(e))
|
||||
}
|
||||
|
||||
/**
|
||||
* Lädt alle Nutzer aus der Auth-DB (für das Hinzufügen-Modal).
|
||||
*/
|
||||
const loadAllUsers = () => {
|
||||
m.request({
|
||||
method: "GET",
|
||||
url: "/auth/getusers"
|
||||
}).then((result) => {
|
||||
allUsers = result as iUser[]
|
||||
}).catch((e) => showServerError(e))
|
||||
}
|
||||
|
||||
/**
|
||||
* Entfernt einen Nutzer aus dem Projekt. Wird beim Klick auf den
|
||||
* Entfernen-Button in der Zeile aufgerufen.
|
||||
*/
|
||||
const removeUser = (projektUser: iProjektUser, e: Event) => {
|
||||
e.stopPropagation()
|
||||
removeProjektUserREST(projektId, projektUser.user_id).then(() => {
|
||||
loadProjektUsers()
|
||||
}).catch((e) => showServerError(e))
|
||||
}
|
||||
|
||||
const columnmapper: (node: iTreenode) => any[] = (node: iTreenode) => {
|
||||
let pu: iProjektUser = node.nodevalue
|
||||
return [
|
||||
m("div", m("strong", pu.userid)),
|
||||
m("span", pu.username),
|
||||
m("span", pu.email),
|
||||
m("div", {style: {display: 'flex', gap: '0', 'white-space': 'nowrap'}},
|
||||
m("button.w3-button.w3-small.w3-red",
|
||||
{
|
||||
title: "Nutzer entfernen",
|
||||
style: {padding: '0 4px'},
|
||||
onclick: (e: Event) => removeUser(pu, e)
|
||||
},
|
||||
m("i.material-icons", "person_remove"))
|
||||
)
|
||||
]
|
||||
}
|
||||
|
||||
const filterFunction = (node: iTreenode, searchvalue: string): boolean => {
|
||||
let pu: iProjektUser = node.nodevalue
|
||||
let searchLower = searchvalue.toLowerCase()
|
||||
return (pu.userid || "").toLowerCase().includes(searchLower) ||
|
||||
(pu.username || "").toLowerCase().includes(searchLower) ||
|
||||
(pu.email || "").toLowerCase().includes(searchLower)
|
||||
}
|
||||
|
||||
/**
|
||||
* Öffnet das Modal zum Hinzufügen eines Nutzers. Die verfügbaren
|
||||
* Nutzer werden clientseitig gegen die bereits zugeordneten gefiltert.
|
||||
*/
|
||||
const openAddModal = () => {
|
||||
// Alle Nutzer laden, falls noch nicht geschehen.
|
||||
if (allUsers.length === 0) {
|
||||
loadAllUsers()
|
||||
}
|
||||
m.mount(gridMountpoints.modalhook, addUserModal)
|
||||
}
|
||||
|
||||
/**
|
||||
* Modal-Komponente zur Auswahl eines Nutzers, der dem Projekt
|
||||
* zugeordnet werden soll. Verwendet das Select-Widget für die
|
||||
* Suche/Auswahl. Bereits zugeordnete Nutzer werden ausgeblendet.
|
||||
*/
|
||||
const addUserModal: m.ClosureComponent = () => {
|
||||
let selectedUserId: number = -1
|
||||
|
||||
// Verfügbare Nutzer: alle, die noch nicht zugeordnet sind.
|
||||
const getAvailableOptions = (): iSelectOption[] => {
|
||||
const assignedIds = new Set(projektUsers.map((pu) => pu.user_id))
|
||||
return allUsers
|
||||
.filter((u: iUser) => !assignedIds.has(u.id))
|
||||
.map((u: iUser) => ({
|
||||
value: u.id,
|
||||
label: `${u.userid} (${u.username})`
|
||||
} as iSelectOption))
|
||||
}
|
||||
|
||||
const closeModal = () => {
|
||||
m.mount(gridMountpoints.modalhook, null)
|
||||
}
|
||||
|
||||
const confirmAdd = () => {
|
||||
if (selectedUserId > 0) {
|
||||
addProjektUserREST(projektId, selectedUserId).then(() => {
|
||||
closeModal()
|
||||
loadProjektUsers()
|
||||
}).catch((e) => {
|
||||
showServerError(e)
|
||||
closeModal()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
view: (vnode) => {
|
||||
const options = getAvailableOptions()
|
||||
return m(ModalView,
|
||||
{
|
||||
title: "Nutzer zuordnen",
|
||||
type: ModalType.MESSAGE,
|
||||
width: '500px'
|
||||
} as iModalView,
|
||||
m("div.w3-padding-16",
|
||||
m("p", "Wählen Sie einen Nutzer aus, der dem Projekt \"" + projektName + "\" zugeordnet werden soll:"),
|
||||
m("label", "Nutzer"),
|
||||
m(Select, {
|
||||
name: "user_id",
|
||||
value: selectedUserId,
|
||||
options: options,
|
||||
required: true,
|
||||
onchanged: (value: any) => {
|
||||
selectedUserId = value
|
||||
}
|
||||
} as iSelect),
|
||||
m("div.w3-bar.w3-padding-16",
|
||||
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) => {
|
||||
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))
|
||||
}
|
||||
// Alle Nutzer für das Hinzufügen-Modal vorab laden.
|
||||
loadAllUsers()
|
||||
// Zugeordnete Nutzer laden.
|
||||
loadProjektUsers()
|
||||
},
|
||||
view: (vnode) => {
|
||||
return m("div.w3-container.w3-auto",
|
||||
{
|
||||
style: {
|
||||
display: 'flex',
|
||||
'flex-direction': 'column',
|
||||
height: 'inherit',
|
||||
'overflow-y': 'inherit'
|
||||
}
|
||||
},
|
||||
m("h1", projektName
|
||||
? "Nutzerberechtigungen für Projekt: " + projektName
|
||||
: "Nutzerberechtigungen"),
|
||||
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,
|
||||
{
|
||||
onclick: openAddModal,
|
||||
tooltip: "Nutzer hinzufügen"
|
||||
} as iTopbarButton,
|
||||
m("i.material-icons", "person_add")
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user