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
@@ -55,5 +55,20 @@ Nach jeder Änderung an `.ts`-Dateien unter `Client/` ist der Build-Prozess (tsc
|
||||
|
||||
## Vorgaben für die Server-seitige Entwicklung unter Python
|
||||
|
||||
### Projektbezogene Sichtbarkeit und Schreibschutz
|
||||
Projekte sind nutzerbezogen: Ein Nutzer sieht nur Projekte, denen er über die Tabelle `projekt_users` zugeordnet ist. Diese Sichtbarkeits-Filterung muss für **jede** projektbezogene Entität (Projekt selbst und alle davon abhängigen Entitäten wie Kontaktadresse, CallLog) in den Lese- und Schreib-Endpunkten umgesetzt werden.
|
||||
|
||||
Die Prüfung erfolgt rollenbasiert, nicht flagbasiert: SysAdmin und AppAdmin (geprüft via `current_user.is_SysAdmin()` bzw. `current_user.has_permission(app.config["APPID"], [APP_ADMIN])`) sehen alle Projekte. Alle anderen Nutzer werden über ihre `current_user.id` gefiltert.
|
||||
|
||||
Dafür werden in `crm/routes.py` zwei Hilfsfunktionen bereitgestellt:
|
||||
- `_get_visible_user_id()`: Liefert `None` für SysAdmin/AppAdmin (kein Filter), sonst `current_user.id`. Wird an die Modell-Funktionen der Lese-Endpunkte (list/get) übergeben.
|
||||
- `_require_projekt_access(projekt_id)`: Prüft vor Schreiboperationen (create/update/delete), ob der Nutzer dem Projekt zugeordnet ist. Bei fehlender Zuordnung wird 404 (nicht 403) geworfen, um keine Rückschlüsse auf die Existenz des Projekts zu ermöglichen.
|
||||
|
||||
Bei neuen projektbezogenen Entitäten muss:
|
||||
1. Die Modell-Funktion `get_xxx(...)` einen optionalen `user_id`-Parameter erhalten. Ist dieser gesetzt, wird per Subquery auf `projekt_users` gefiltert.
|
||||
2. Die Modell-Funktion `get_xxx_by_id(id, user_id)` bei gesetztem `user_id` die Projekt-Zuordnung prüfen und bei Fehlen 404 werfen.
|
||||
3. Die Lese-Endpunkte in `crm/routes.py` `_get_visible_user_id()` aufrufen und das Ergebnis an die Modell-Funktion übergeben.
|
||||
4. Die Schreib-Endpunkte `_require_projekt_access(projekt_id)` aufrufen, bevor die Änderung durchgeführt wird. Bei create/update steht `projekt_id` im JSON-Body (`request.json["projekt_id"]` bzw. `request.json["id"]`); bei delete muss die Entität ggf. erst mit Sichtbarkeits-Check geladen werden, um die `projekt_id` zu ermitteln.
|
||||
|
||||
## Generierte Dateien nicht beachten
|
||||
Das Verzeichnis `Client/tsout/` enthält die JavaScript-Ausgabe der TypeScript-Kompilation und wird durch den Build-Prozess (tsc + rollup) erzeugt. Diese Dateien sind nicht manuell zu bearbeiten und brauchen bei der Codeanalyse nicht berücksichtigt zu werden. Ebenso sind die JavaScript-Dateien in `NGINX_Server/html/static/` (z.B. `kbp_crm_00.00.01.js`) die Ausgabe des Rollup-Builds und nicht zu untersuchen.
|
||||
@@ -12,7 +12,7 @@ erfolgt ausschließlich über __bind_key__.
|
||||
import datetime
|
||||
import decimal
|
||||
|
||||
from sqlalchemy import Boolean, Column, Date, DateTime, ForeignKey, Index, Integer, String
|
||||
from sqlalchemy import Boolean, Column, Date, DateTime, ForeignKey, Index, Integer, String, UniqueConstraint
|
||||
from sqlalchemy.orm import relationship
|
||||
|
||||
from db import dbapp
|
||||
@@ -160,6 +160,40 @@ class Projekt(dbapp.Model):
|
||||
return result
|
||||
|
||||
|
||||
class ProjektUser(dbapp.Model):
|
||||
"""
|
||||
Verknüpfungstabelle zwischen Projekt und User.
|
||||
|
||||
Steuert die Sichtbarkeit von Projekten: Ein Nutzer sieht nur Projekte,
|
||||
denen er über diese Tabelle zugeordnet ist. Die Zuordnung erfolgt durch
|
||||
den AppAdmin (oder später andere berechtigte Rollen).
|
||||
|
||||
Die Tabelle liegt in der CRM-Datenbank (appdb). Die Spalte 'user_id'
|
||||
referenziert logisch die 'id' in der Auth-Datenbanktabelle 'users', ein
|
||||
physischer Fremdschlüssel ist aufgrund der zwei getrennten SQLite-Datenbanken
|
||||
nicht möglich. Die Auflösung von user_id auf username etc. erfolgt zur
|
||||
Laufzeit durch eine separate Query gegen die Auth-Datenbank.
|
||||
|
||||
Das UniqueConstraint stellt sicher, dass jeder Nutzer nur einmal pro
|
||||
Projekt zugeordnet werden kann.
|
||||
"""
|
||||
__tablename__ = 'projekt_users'
|
||||
__bind_key__ = BINDKEY
|
||||
__table_args__ = (
|
||||
Index('ix_projekt_users_projekt_id', 'projekt_id'),
|
||||
UniqueConstraint('projekt_id', 'user_id', name='uq_projekt_user'),
|
||||
)
|
||||
id = Column(Integer, primary_key=True)
|
||||
projekt_id = Column(Integer, ForeignKey('projekte.id'), nullable=False)
|
||||
user_id = Column(Integer, nullable=False)
|
||||
|
||||
projekt = relationship('Projekt')
|
||||
|
||||
def to_dict(self):
|
||||
"""Liefert die Projekt-User-Zuordnung als Dictionary."""
|
||||
return columns2dict(self)
|
||||
|
||||
|
||||
class Kontaktadresse(dbapp.Model):
|
||||
"""
|
||||
Akquise-Ziel innerhalb eines Projekts. Jede Kontaktadresse ist genau
|
||||
|
||||
@@ -10,31 +10,47 @@ dort über den Decorator authorisation_required.
|
||||
:version: 1.0
|
||||
"""
|
||||
from db import dbapp
|
||||
from crm.model import CallLog, Projekt
|
||||
from crm.model import CallLog, Projekt, ProjektUser
|
||||
|
||||
from auth.model.errors import ApplicationError, HtmlStatusCode
|
||||
|
||||
session = dbapp.session
|
||||
|
||||
|
||||
def get_call_logs(projekt_id: int | None = None):
|
||||
def get_call_logs(projekt_id: int | None = None, user_id: int | None = None):
|
||||
"""
|
||||
Liefert Gesprächsnotizen, sortiert nach Datum absteigend.
|
||||
|
||||
Ist projekt_id gesetzt, werden nur die Notizen dieses Projekts
|
||||
zurückgegeben (serverseitige Filterung). Ohne projekt_id werden
|
||||
alle Notizen geliefert.
|
||||
|
||||
Ist user_id gesetzt, werden nur Notizen von Projekten geliefert, denen
|
||||
der Nutzer über projekt_users zugeordnet ist (Sichtbarkeits-Filterung).
|
||||
SysAdmin und AppAdmin übergeben user_id=None und sehen alle Notizen.
|
||||
"""
|
||||
query = CallLog.query
|
||||
if projekt_id is not None:
|
||||
query = query.filter(CallLog.projekt_id == projekt_id)
|
||||
if user_id is not None:
|
||||
# Nur Notizen von Projekten, denen der Nutzer zugeordnet ist.
|
||||
query = query.filter(
|
||||
CallLog.projekt_id.in_(
|
||||
dbapp.session.query(ProjektUser.projekt_id).filter(
|
||||
ProjektUser.user_id == user_id
|
||||
)
|
||||
)
|
||||
)
|
||||
return query.order_by(CallLog.datum.desc()).all()
|
||||
|
||||
|
||||
def get_call_log(call_log_id: int) -> CallLog:
|
||||
def get_call_log(call_log_id: int, user_id: int | None = None) -> CallLog:
|
||||
"""
|
||||
Liefert eine einzelne Gesprächsnotiz anhand ihrer ID.
|
||||
Wirft ApplicationError (404), falls die Notiz nicht existiert.
|
||||
|
||||
Ist user_id gesetzt, wird zusätzlich geprüft, ob der Nutzer dem
|
||||
Projekt der Notiz zugeordnet ist; sonst wird 404 geliefert.
|
||||
"""
|
||||
call_log = CallLog.query.get(call_log_id)
|
||||
if call_log is None:
|
||||
@@ -42,6 +58,16 @@ def get_call_log(call_log_id: int) -> CallLog:
|
||||
"Gesprächsnotiz mit der ID {} wurde nicht gefunden.".format(call_log_id),
|
||||
htmlstatus=HtmlStatusCode.Not_Found,
|
||||
)
|
||||
if user_id is not None:
|
||||
zuordnung = ProjektUser.query.filter(
|
||||
ProjektUser.projekt_id == call_log.projekt_id,
|
||||
ProjektUser.user_id == user_id,
|
||||
).first()
|
||||
if zuordnung is None:
|
||||
raise ApplicationError(
|
||||
"Gesprächsnotiz mit der ID {} wurde nicht gefunden.".format(call_log_id),
|
||||
htmlstatus=HtmlStatusCode.Not_Found,
|
||||
)
|
||||
return call_log
|
||||
|
||||
|
||||
|
||||
@@ -200,6 +200,18 @@ def _db_V01_08():
|
||||
session.commit()
|
||||
|
||||
|
||||
def _db_V01_09():
|
||||
"""
|
||||
Legt die Tabelle 'projekt_users' an. Keine Initial-Befüllung —
|
||||
bestehende Projekte bleiben ohne Zuordnungen, bis der AppAdmin
|
||||
diese manuell vergibt. SysAdmin/AppAdmin sehen alle Projekte ohnehin.
|
||||
create_all erzeugt nur fehlende Tabellen, bestehende bleiben unangetastet.
|
||||
"""
|
||||
dbapp.create_all(bind_key=BINDKEY)
|
||||
_updatesysteminfo('01.09')
|
||||
session.commit()
|
||||
|
||||
|
||||
def checkupdates():
|
||||
"""
|
||||
Ermittelt die aktuelle DB-Version aus der Systeminfo-Tabelle und führt alle
|
||||
|
||||
@@ -10,31 +10,47 @@ dort über den Decorator authorisation_required.
|
||||
:version: 1.0
|
||||
"""
|
||||
from db import dbapp
|
||||
from crm.model import Kontaktadresse, Projekt
|
||||
from crm.model import Kontaktadresse, Projekt, ProjektUser
|
||||
|
||||
from auth.model.errors import ApplicationError, HtmlStatusCode
|
||||
|
||||
session = dbapp.session
|
||||
|
||||
|
||||
def get_kontaktadressen(projekt_id: int | None = None):
|
||||
def get_kontaktadressen(projekt_id: int | None = None, user_id: int | None = None):
|
||||
"""
|
||||
Liefert Kontaktadressen, sortiert nach Name.
|
||||
|
||||
Ist projekt_id gesetzt, werden nur die Kontaktadressen dieses
|
||||
Projekts zurückgegeben (serverseitige Filterung). Ohne projekt_id
|
||||
werden alle Kontaktadressen geliefert.
|
||||
|
||||
Ist user_id gesetzt, werden nur Kontaktadressen von Projekten geliefert,
|
||||
denen der Nutzer über projekt_users zugeordnet ist (Sichtbarkeits-
|
||||
Filterung). SysAdmin und AppAdmin übergeben user_id=None und sehen alle.
|
||||
"""
|
||||
query = Kontaktadresse.query
|
||||
if projekt_id is not None:
|
||||
query = query.filter(Kontaktadresse.projekt_id == projekt_id)
|
||||
if user_id is not None:
|
||||
# Nur Kontaktadressen von Projekten, denen der Nutzer zugeordnet ist.
|
||||
query = query.filter(
|
||||
Kontaktadresse.projekt_id.in_(
|
||||
dbapp.session.query(ProjektUser.projekt_id).filter(
|
||||
ProjektUser.user_id == user_id
|
||||
)
|
||||
)
|
||||
)
|
||||
return query.order_by(Kontaktadresse.name).all()
|
||||
|
||||
|
||||
def get_kontaktadresse(kontaktadresse_id: int) -> Kontaktadresse:
|
||||
def get_kontaktadresse(kontaktadresse_id: int, user_id: int | None = None) -> Kontaktadresse:
|
||||
"""
|
||||
Liefert eine einzelne Kontaktadresse anhand ihrer ID.
|
||||
Wirft ApplicationError (404), falls die Kontaktadresse nicht existiert.
|
||||
|
||||
Ist user_id gesetzt, wird zusätzlich geprüft, ob der Nutzer dem
|
||||
Projekt der Kontaktadresse zugeordnet ist; sonst wird 404 geliefert.
|
||||
"""
|
||||
kontaktadresse = Kontaktadresse.query.get(kontaktadresse_id)
|
||||
if kontaktadresse is None:
|
||||
@@ -42,6 +58,16 @@ def get_kontaktadresse(kontaktadresse_id: int) -> Kontaktadresse:
|
||||
"Kontaktadresse mit der ID {} wurde nicht gefunden.".format(kontaktadresse_id),
|
||||
htmlstatus=HtmlStatusCode.Not_Found,
|
||||
)
|
||||
if user_id is not None:
|
||||
zuordnung = ProjektUser.query.filter(
|
||||
ProjektUser.projekt_id == kontaktadresse.projekt_id,
|
||||
ProjektUser.user_id == user_id,
|
||||
).first()
|
||||
if zuordnung is None:
|
||||
raise ApplicationError(
|
||||
"Kontaktadresse mit der ID {} wurde nicht gefunden.".format(kontaktadresse_id),
|
||||
htmlstatus=HtmlStatusCode.Not_Found,
|
||||
)
|
||||
return kontaktadresse
|
||||
|
||||
|
||||
|
||||
@@ -10,31 +10,54 @@ dort über den Decorator authorisation_required.
|
||||
:version: 1.0
|
||||
"""
|
||||
from db import dbapp
|
||||
from crm.model import Projekt, Kunde
|
||||
from crm.model import Projekt, Kunde, ProjektUser
|
||||
|
||||
from auth.model.errors import ApplicationError, HtmlStatusCode
|
||||
from flask_login import current_user
|
||||
current_user: object
|
||||
|
||||
session = dbapp.session
|
||||
|
||||
|
||||
def get_projekte(kunden_id: int | None = None):
|
||||
def get_projekte(kunden_id: int | None = None, user_id: int | None = None):
|
||||
"""
|
||||
Liefert Projekte, sortiert nach projektname.
|
||||
|
||||
Ist kunden_id gesetzt, werden nur die Projekte dieses Kunden
|
||||
zurückgegeben (serverseitige Filterung). Ohne kunden_id werden
|
||||
alle Projekte geliefert.
|
||||
|
||||
Ist user_id gesetzt, werden nur Projekte geliefert, denen der
|
||||
Nutzer über die Tabelle 'projekt_users' zugeordnet ist. Dies
|
||||
steuert die Sichtbarkeit: Nicht zugeordnete Nutzer sehen diese
|
||||
Projekte nicht. SysAdmin und AppAdmin übergeben user_id=None
|
||||
und sehen alle Projekte.
|
||||
"""
|
||||
query = Projekt.query
|
||||
if kunden_id is not None:
|
||||
query = query.filter(Projekt.kunden_id == kunden_id)
|
||||
if user_id is not None:
|
||||
# Nur Projekte, denen der Nutzer zugeordnet ist. Der Subquery
|
||||
# auf projekt_users filtert die sichtbaren Projekt-IDs.
|
||||
query = query.filter(
|
||||
Projekt.id.in_(
|
||||
dbapp.session.query(ProjektUser.projekt_id).filter(
|
||||
ProjektUser.user_id == user_id
|
||||
)
|
||||
)
|
||||
)
|
||||
return query.order_by(Projekt.projektname).all()
|
||||
|
||||
|
||||
def get_projekt(projekt_id: int) -> Projekt:
|
||||
def get_projekt(projekt_id: int, user_id: int | None = None) -> Projekt:
|
||||
"""
|
||||
Liefert ein einzelnes Projekt anhand seiner ID.
|
||||
Wirft ApplicationError (404), falls das Projekt nicht existiert.
|
||||
|
||||
Ist user_id gesetzt, wird zusätzlich geprüft, ob der Nutzer dem
|
||||
Projekt zugeordnet ist. Ist er nicht zugeordnet, wird ein 404
|
||||
geliefert, damit nicht zugeordnete Nutzer keine Rückschlüsse auf
|
||||
die Existenz des Projekts ziehen können.
|
||||
"""
|
||||
projekt = Projekt.query.get(projekt_id)
|
||||
if projekt is None:
|
||||
@@ -42,6 +65,19 @@ def get_projekt(projekt_id: int) -> Projekt:
|
||||
"Projekt mit der ID {} wurde nicht gefunden.".format(projekt_id),
|
||||
htmlstatus=HtmlStatusCode.Not_Found,
|
||||
)
|
||||
if user_id is not None:
|
||||
# Sichtbarkeitsprüfung: Ist der Nutzer dem Projekt zugeordnet?
|
||||
zuordnung = ProjektUser.query.filter(
|
||||
ProjektUser.projekt_id == projekt_id,
|
||||
ProjektUser.user_id == user_id,
|
||||
).first()
|
||||
if zuordnung is None:
|
||||
# 404 statt 403, damit nicht zugeordnete Nutzer keine Info über
|
||||
# die Existenz des Projekts erhalten.
|
||||
raise ApplicationError(
|
||||
"Projekt mit der ID {} wurde nicht gefunden.".format(projekt_id),
|
||||
htmlstatus=HtmlStatusCode.Not_Found,
|
||||
)
|
||||
return projekt
|
||||
|
||||
|
||||
@@ -78,6 +114,14 @@ def create_projekt(data: dict) -> Projekt:
|
||||
projekt.startdatum = (data.get("startdatum") or "").strip() or None
|
||||
session.add(projekt)
|
||||
session.commit()
|
||||
# Auto-Assignment: Den Ersteller dem neuen Projekt zuordnen, damit
|
||||
# er es sehen kann. Nur für authentifizierte Nutzer mit einer id.
|
||||
if hasattr(current_user, "id") and current_user.id is not None:
|
||||
zuordnung = ProjektUser()
|
||||
zuordnung.projekt_id = projekt.id
|
||||
zuordnung.user_id = current_user.id
|
||||
session.add(zuordnung)
|
||||
session.commit()
|
||||
return projekt
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,167 @@
|
||||
"""
|
||||
Geschäftslogik für die Projekt-User-Zuordnung (Sichtbarkeit von Projekten).
|
||||
|
||||
Diese Funktionen kapseln den DB-Zugriff auf die Tabelle 'projekt_users' und
|
||||
die Auflösung der user_id gegen die Auth-Datenbank (username, email etc.).
|
||||
Da CRM- und Auth-Datenbank getrennte SQLite-Dateien sind, erfolgt die
|
||||
Nutzer-Auflösung durch eine separate Query, nicht durch einen SQL-Join.
|
||||
|
||||
Aufgerufen werden die Funktionen aus crm/routes.py; die Autorisierungsprüfung
|
||||
erfolgt dort über den Decorator authorisation_required.
|
||||
|
||||
:author: Mistral Vibe <vibe@mistral.ai>
|
||||
:date: 2026-09-05
|
||||
:version: 1.0
|
||||
"""
|
||||
from db import dbapp
|
||||
from crm.model import ProjektUser, Projekt
|
||||
from auth.model import User
|
||||
|
||||
from auth.model.errors import ApplicationError, HtmlStatusCode
|
||||
|
||||
session = dbapp.session
|
||||
|
||||
|
||||
def get_projekt_users(projekt_id: int):
|
||||
"""
|
||||
Liefert alle einem Projekt zugeordneten Nutzer als Liste von Dictionaries.
|
||||
|
||||
Jedes Dictionary enthält die Felder der ProjektUser-Zuordnung (id,
|
||||
projekt_id, user_id) sowie die aus der Auth-Datenbank aufgelösten
|
||||
Nutzerdaten (userid, username, email).
|
||||
|
||||
- projekt_id: int
|
||||
- Die ID des Projekts, dessen Zuordnungen geliefert werden sollen.
|
||||
"""
|
||||
zuordnungen = ProjektUser.query.filter(
|
||||
ProjektUser.projekt_id == projekt_id
|
||||
).all()
|
||||
|
||||
if not zuordnungen:
|
||||
return []
|
||||
|
||||
# Alle user_ids sammeln und in einer einzigen Query gegen die Auth-DB
|
||||
# auflösen (Batch-Query statt N+1).
|
||||
user_ids = [z.user_id for z in zuordnungen]
|
||||
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()
|
||||
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:
|
||||
# Nutzer wurde aus der Auth-DB gelöscht, die Zuordnung existiert
|
||||
# noch. Die Felder werden leer geliefert.
|
||||
eintrag['userid'] = ""
|
||||
eintrag['username'] = ""
|
||||
eintrag['email'] = ""
|
||||
result.append(eintrag)
|
||||
return result
|
||||
|
||||
|
||||
def add_projekt_user(projekt_id: int, user_id: int) -> ProjektUser:
|
||||
"""
|
||||
Ordnet einen Nutzer einem Projekt zu. Das UniqueConstraint verhindert
|
||||
Duplikate. Wirft ApplicationError (404), falls das Projekt nicht
|
||||
existiert, oder (400), falls die Zuordnung bereits besteht oder die
|
||||
Nutzer-ID ungültig ist.
|
||||
|
||||
- projekt_id: int
|
||||
- Die ID des Projekts.
|
||||
- user_id: int
|
||||
- Die ID (nicht userid) des Nutzers aus der Auth-Datenbank.
|
||||
"""
|
||||
projekt = Projekt.query.get(projekt_id)
|
||||
if projekt is None:
|
||||
raise ApplicationError(
|
||||
"Das Projekt mit der ID {} wurde nicht gefunden.".format(projekt_id),
|
||||
htmlstatus=HtmlStatusCode.Not_Found,
|
||||
)
|
||||
if user_id is None or int(user_id) <= 0:
|
||||
raise ApplicationError(
|
||||
"Es muss ein gültiger Nutzer ausgewählt werden.",
|
||||
htmlstatus=HtmlStatusCode.Bad_Request,
|
||||
)
|
||||
# Prüfen, ob der Nutzer in der Auth-Datenbank existiert.
|
||||
user = User.query.get(int(user_id))
|
||||
if user is None:
|
||||
raise ApplicationError(
|
||||
"Der ausgewählte Nutzer existiert nicht.",
|
||||
htmlstatus=HtmlStatusCode.Bad_Request,
|
||||
)
|
||||
# Duplikat-Prüfung (zusätzlich zum UniqueConstraint, um eine
|
||||
# aussagekräftige Fehlermeldung zu liefern).
|
||||
existing = ProjektUser.query.filter(
|
||||
ProjektUser.projekt_id == projekt_id,
|
||||
ProjektUser.user_id == int(user_id),
|
||||
).first()
|
||||
if existing is not None:
|
||||
raise ApplicationError(
|
||||
"Der Nutzer ist diesem Projekt bereits zugeordnet.",
|
||||
htmlstatus=HtmlStatusCode.Bad_Request,
|
||||
)
|
||||
zuordnung = ProjektUser()
|
||||
zuordnung.projekt_id = projekt_id
|
||||
zuordnung.user_id = int(user_id)
|
||||
session.add(zuordnung)
|
||||
session.commit()
|
||||
return zuordnung
|
||||
|
||||
|
||||
def remove_projekt_user(projekt_id: int, user_id: int):
|
||||
"""
|
||||
Entfernt die Zuordnung eines Nutzers zu einem Projekt.
|
||||
Wirft ApplicationError (404), falls die Zuordnung nicht existiert.
|
||||
|
||||
- projekt_id: int
|
||||
- Die ID des Projekts.
|
||||
- user_id: int
|
||||
- Die ID (nicht userid) des Nutzers aus der Auth-Datenbank.
|
||||
"""
|
||||
zuordnung = ProjektUser.query.filter(
|
||||
ProjektUser.projekt_id == projekt_id,
|
||||
ProjektUser.user_id == int(user_id),
|
||||
).first()
|
||||
if zuordnung is None:
|
||||
raise ApplicationError(
|
||||
"Die Zuordnung existiert nicht.",
|
||||
htmlstatus=HtmlStatusCode.Not_Found,
|
||||
)
|
||||
session.delete(zuordnung)
|
||||
session.commit()
|
||||
|
||||
|
||||
def get_projekt_ids_for_user(user_id: int):
|
||||
"""
|
||||
Liefert alle Projekt-IDs, denen der Nutzer zugeordnet ist.
|
||||
Wird für die serverseitige Sichtbarkeits-Filterung in get_projekte
|
||||
und get_projekt verwendet.
|
||||
|
||||
- user_id: int
|
||||
- Die ID (nicht userid) des Nutzers aus der Auth-Datenbank.
|
||||
"""
|
||||
zuordnungen = ProjektUser.query.filter(
|
||||
ProjektUser.user_id == user_id
|
||||
).all()
|
||||
return [z.projekt_id for z in zuordnungen]
|
||||
|
||||
|
||||
def is_user_assigned(projekt_id: int, user_id: int) -> bool:
|
||||
"""
|
||||
Prüft, ob ein Nutzer einem Projekt zugeordnet ist.
|
||||
|
||||
- projekt_id: int
|
||||
- Die ID des Projekts.
|
||||
- user_id: int
|
||||
- Die ID (nicht userid) des Nutzers aus der Auth-Datenbank.
|
||||
"""
|
||||
zuordnung = ProjektUser.query.filter(
|
||||
ProjektUser.projekt_id == projekt_id,
|
||||
ProjektUser.user_id == user_id,
|
||||
).first()
|
||||
return zuordnung is not None
|
||||
+127
-12
@@ -15,18 +15,65 @@ KundenManager werden aus crm.model importiert.
|
||||
"""
|
||||
from flask import current_app as app
|
||||
from flask import jsonify, request
|
||||
from flask_login import current_user
|
||||
|
||||
from auth.decorators import authorisation_required
|
||||
from auth.model import APP_ADMIN
|
||||
from auth.model.errors import ApplicationError, HtmlStatusCode
|
||||
from crm.model import KUNDEN_MANAGER, MITARBEITER
|
||||
from crm.model import kunde as kunden
|
||||
from crm.model import kontakt as kontakte
|
||||
from crm.model import projekt as projekte
|
||||
from crm.model import projekt_user as projekt_users
|
||||
from crm.model import kontaktadresse as kontaktadressen
|
||||
from crm.model import call_log as call_logs
|
||||
|
||||
print("crm-routes")
|
||||
|
||||
|
||||
def _get_visible_user_id():
|
||||
"""
|
||||
Liefert die user_id für die Sichtbarkeits-Filterung.
|
||||
|
||||
SysAdmin und AppAdmin sehen alle Projekte und abhängige Entitäten —
|
||||
für sie wird None zurückgegeben, sodass die Modell-Funktionen keinen
|
||||
Filter anwenden. Alle anderen Nutzer (Mitarbeiter, KundenManager)
|
||||
erhalten ihre current_user.id, sodass nur die ihnen zugeordneten
|
||||
Projekte bzw. abhängige Einträge geliefert werden.
|
||||
"""
|
||||
if current_user.is_SysAdmin() or current_user.has_permission(
|
||||
app.config["APPID"], [APP_ADMIN]
|
||||
):
|
||||
return None
|
||||
return current_user.id
|
||||
|
||||
|
||||
def _require_projekt_access(projekt_id: int):
|
||||
"""
|
||||
Prüft, ob der aktuelle Nutzer für das angegebene Projekt schreibberechtigt ist.
|
||||
|
||||
SysAdmin und AppAdmin haben immer Zugriff. Alle anderen Nutzer müssen
|
||||
dem Projekt über projekt_users zugeordnet sein. Ist der Nutzer nicht
|
||||
zugeordnet, wird ein ApplicationError (404) geworfen — 404 statt 403,
|
||||
damit nicht zugeordnete Nutzer keine Rückschlüsse auf die Existenz
|
||||
des Projekts ziehen können.
|
||||
|
||||
Muss in allen Schreib-Endpunkten (create/update/delete) aufgerufen
|
||||
werden, die ein konkretes Projekt betreffen, bevor die Änderung
|
||||
durchgeführt wird.
|
||||
|
||||
- projekt_id: int
|
||||
- Die ID des Projekts, auf das zugegriffen werden soll.
|
||||
"""
|
||||
user_id = _get_visible_user_id()
|
||||
if user_id is not None:
|
||||
if not projekt_users.is_user_assigned(projekt_id, user_id):
|
||||
raise ApplicationError(
|
||||
"Projekt mit der ID {} wurde nicht gefunden.".format(projekt_id),
|
||||
htmlstatus=HtmlStatusCode.Not_Found,
|
||||
)
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------- #
|
||||
# Kunde #
|
||||
# --------------------------------------------------------------------------- #
|
||||
@@ -126,17 +173,23 @@ def kontakt_delete():
|
||||
@app.get("/xhr/projekt/list")
|
||||
@authorisation_required([MITARBEITER, KUNDEN_MANAGER])
|
||||
def projekt_list():
|
||||
"""Liefert Projekte als Liste. Optional nach kunden_id gefiltert."""
|
||||
"""Liefert Projekte als Liste. Optional nach kunden_id gefiltert.
|
||||
Für Nicht-SysAdmin/Nicht-AppAdmin werden nur die Projekte geliefert,
|
||||
denen der Nutzer über projekt_users zugeordnet ist."""
|
||||
kunden_id = request.args.get("kunden_id", type=int)
|
||||
result = [p.to_dict() for p in projekte.get_projekte(kunden_id)]
|
||||
user_id = _get_visible_user_id()
|
||||
result = [p.to_dict() for p in projekte.get_projekte(kunden_id, user_id)]
|
||||
return jsonify(result), 200
|
||||
|
||||
|
||||
@app.get("/xhr/projekt/get")
|
||||
@authorisation_required([MITARBEITER, KUNDEN_MANAGER])
|
||||
def projekt_get():
|
||||
"""Liefert ein einzelnes Projekt anhand seiner ID (Query-Param id)."""
|
||||
projekt = projekte.get_projekt(int(request.args.get("id")))
|
||||
"""Liefert ein einzelnes Projekt anhand seiner ID (Query-Param id).
|
||||
Für Nicht-SysAdmin/Nicht-AppAdmin wird geprüft, ob der Nutzer dem
|
||||
Projekt zugeordnet ist; sonst wird 404 geliefert."""
|
||||
user_id = _get_visible_user_id()
|
||||
projekt = projekte.get_projekt(int(request.args.get("id")), user_id)
|
||||
return jsonify(projekt.to_dict()), 200
|
||||
|
||||
|
||||
@@ -152,6 +205,7 @@ def projekt_create():
|
||||
@authorisation_required([KUNDEN_MANAGER])
|
||||
def projekt_update():
|
||||
"""Aktualisiert ein bestehendes Projekt."""
|
||||
_require_projekt_access(request.json["id"])
|
||||
projekt = projekte.update_projekt(request.json)
|
||||
return jsonify(projekt.to_dict()), 200
|
||||
|
||||
@@ -160,10 +214,49 @@ def projekt_update():
|
||||
@authorisation_required([KUNDEN_MANAGER])
|
||||
def projekt_delete():
|
||||
"""Löscht ein Projekt anhand seiner ID (Query-Param id)."""
|
||||
_require_projekt_access(int(request.args.get("id")))
|
||||
projekte.delete_projekt(int(request.args.get("id")))
|
||||
return jsonify({}), 200
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------- #
|
||||
# ProjektUser (Nutzer-Zuordnung / Sichtbarkeit) #
|
||||
# --------------------------------------------------------------------------- #
|
||||
|
||||
|
||||
@app.get("/xhr/projektuser/list")
|
||||
@authorisation_required([MITARBEITER, KUNDEN_MANAGER])
|
||||
def projektuser_list():
|
||||
"""Liefert alle einem Projekt zugeordneten Nutzer (Query-Param projekt_id)."""
|
||||
projekt_id = request.args.get("projekt_id", type=int)
|
||||
result = projekt_users.get_projekt_users(projekt_id)
|
||||
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
|
||||
# weitere Rolle diese Endpunkte nutzen soll, wird sie einfach hier eingetragen,
|
||||
# z.B. @authorisation_required([KUNDEN_MANAGER]).
|
||||
@authorisation_required([])
|
||||
def projektuser_add():
|
||||
"""Ordnet einen Nutzer einem Projekt zu (body: {projekt_id, user_id})."""
|
||||
data = request.json
|
||||
projekt_users.add_projekt_user(data["projekt_id"], data["user_id"])
|
||||
return jsonify({}), 200
|
||||
|
||||
|
||||
@app.delete("/xhr/projektuser/remove")
|
||||
# Leeres allowed_roles-Array als Erweiterungspunkt (siehe projektuser_add).
|
||||
@authorisation_required([])
|
||||
def projektuser_remove():
|
||||
"""Entfernt die Zuordnung eines Nutzers zu einem Projekt (params: projekt_id, user_id)."""
|
||||
projekt_id = request.args.get("projekt_id", type=int)
|
||||
user_id = request.args.get("user_id", type=int)
|
||||
projekt_users.remove_projekt_user(projekt_id, user_id)
|
||||
return jsonify({}), 200
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------- #
|
||||
# Kontaktadresse #
|
||||
# --------------------------------------------------------------------------- #
|
||||
@@ -172,17 +265,22 @@ def projekt_delete():
|
||||
@app.get("/xhr/kontaktadresse/list")
|
||||
@authorisation_required([MITARBEITER, KUNDEN_MANAGER])
|
||||
def kontaktadresse_list():
|
||||
"""Liefert Kontaktadressen als Liste. Optional nach projekt_id gefiltert."""
|
||||
"""Liefert Kontaktadressen als Liste. Optional nach projekt_id gefiltert.
|
||||
Für Nicht-SysAdmin/Nicht-AppAdmin nur Kontaktadressen sichtbarer Projekte."""
|
||||
projekt_id = request.args.get("projekt_id", type=int)
|
||||
result = [k.to_dict() for k in kontaktadressen.get_kontaktadressen(projekt_id)]
|
||||
user_id = _get_visible_user_id()
|
||||
result = [k.to_dict() for k in kontaktadressen.get_kontaktadressen(projekt_id, user_id)]
|
||||
return jsonify(result), 200
|
||||
|
||||
|
||||
@app.get("/xhr/kontaktadresse/get")
|
||||
@authorisation_required([MITARBEITER, KUNDEN_MANAGER])
|
||||
def kontaktadresse_get():
|
||||
"""Liefert eine einzelne Kontaktadresse anhand ihrer ID (Query-Param id)."""
|
||||
kontaktadresse = kontaktadressen.get_kontaktadresse(int(request.args.get("id")))
|
||||
"""Liefert eine einzelne Kontaktadresse anhand ihrer ID (Query-Param id).
|
||||
Für Nicht-SysAdmin/Nicht-AppAdmin wird geprüft, ob der Nutzer dem
|
||||
Projekt der Kontaktadresse zugeordnet ist; sonst 404."""
|
||||
user_id = _get_visible_user_id()
|
||||
kontaktadresse = kontaktadressen.get_kontaktadresse(int(request.args.get("id")), user_id)
|
||||
return jsonify(kontaktadresse.to_dict()), 200
|
||||
|
||||
|
||||
@@ -190,6 +288,7 @@ def kontaktadresse_get():
|
||||
@authorisation_required([KUNDEN_MANAGER])
|
||||
def kontaktadresse_create():
|
||||
"""Legt eine neue Kontaktadresse an. ID wird serverseitig vergeben."""
|
||||
_require_projekt_access(int(request.json["projekt_id"]))
|
||||
kontaktadresse = kontaktadressen.create_kontaktadresse(request.json)
|
||||
return jsonify(kontaktadresse.to_dict()), 200
|
||||
|
||||
@@ -198,6 +297,7 @@ def kontaktadresse_create():
|
||||
@authorisation_required([KUNDEN_MANAGER])
|
||||
def kontaktadresse_update():
|
||||
"""Aktualisiert eine bestehende Kontaktadresse."""
|
||||
_require_projekt_access(int(request.json["projekt_id"]))
|
||||
kontaktadresse = kontaktadressen.update_kontaktadresse(request.json)
|
||||
return jsonify(kontaktadresse.to_dict()), 200
|
||||
|
||||
@@ -206,6 +306,10 @@ def kontaktadresse_update():
|
||||
@authorisation_required([KUNDEN_MANAGER])
|
||||
def kontaktadresse_delete():
|
||||
"""Löscht eine Kontaktadresse anhand ihrer ID (Query-Param id)."""
|
||||
# projekt_id der Kontaktadresse ermitteln, um die Projekt-Zuordnung
|
||||
# des Nutzers zu prüfen, bevor gelöscht wird.
|
||||
user_id = _get_visible_user_id()
|
||||
kontaktadresse = kontaktadressen.get_kontaktadresse(int(request.args.get("id")), user_id)
|
||||
kontaktadressen.delete_kontaktadresse(int(request.args.get("id")))
|
||||
return jsonify({}), 200
|
||||
|
||||
@@ -218,17 +322,22 @@ def kontaktadresse_delete():
|
||||
@app.get("/xhr/calllog/list")
|
||||
@authorisation_required([MITARBEITER, KUNDEN_MANAGER])
|
||||
def calllog_list():
|
||||
"""Liefert Gesprächsnotizen als Liste. Optional nach projekt_id gefiltert."""
|
||||
"""Liefert Gesprächsnotizen als Liste. Optional nach projekt_id gefiltert.
|
||||
Für Nicht-SysAdmin/Nicht-AppAdmin nur Notizen sichtbarer Projekte."""
|
||||
projekt_id = request.args.get("projekt_id", type=int)
|
||||
result = [l.to_dict() for l in call_logs.get_call_logs(projekt_id)]
|
||||
user_id = _get_visible_user_id()
|
||||
result = [l.to_dict() for l in call_logs.get_call_logs(projekt_id, user_id)]
|
||||
return jsonify(result), 200
|
||||
|
||||
|
||||
@app.get("/xhr/calllog/get")
|
||||
@authorisation_required([MITARBEITER, KUNDEN_MANAGER])
|
||||
def calllog_get():
|
||||
"""Liefert eine einzelne Gesprächsnotiz anhand ihrer ID (Query-Param id)."""
|
||||
call_log = call_logs.get_call_log(int(request.args.get("id")))
|
||||
"""Liefert eine einzelne Gesprächsnotiz anhand ihrer ID (Query-Param id).
|
||||
Für Nicht-SysAdmin/Nicht-AppAdmin wird geprüft, ob der Nutzer dem
|
||||
Projekt der Notiz zugeordnet ist; sonst 404."""
|
||||
user_id = _get_visible_user_id()
|
||||
call_log = call_logs.get_call_log(int(request.args.get("id")), user_id)
|
||||
return jsonify(call_log.to_dict()), 200
|
||||
|
||||
|
||||
@@ -236,6 +345,7 @@ def calllog_get():
|
||||
@authorisation_required([KUNDEN_MANAGER])
|
||||
def calllog_create():
|
||||
"""Legt eine neue Gesprächsnotiz an. ID wird serverseitig vergeben."""
|
||||
_require_projekt_access(int(request.json["projekt_id"]))
|
||||
call_log = call_logs.create_call_log(request.json)
|
||||
return jsonify(call_log.to_dict()), 200
|
||||
|
||||
@@ -244,6 +354,7 @@ def calllog_create():
|
||||
@authorisation_required([KUNDEN_MANAGER])
|
||||
def calllog_update():
|
||||
"""Aktualisiert eine bestehende Gesprächsnotiz."""
|
||||
_require_projekt_access(int(request.json["projekt_id"]))
|
||||
call_log = call_logs.update_call_log(request.json)
|
||||
return jsonify(call_log.to_dict()), 200
|
||||
|
||||
@@ -252,6 +363,10 @@ def calllog_update():
|
||||
@authorisation_required([KUNDEN_MANAGER])
|
||||
def calllog_delete():
|
||||
"""Löscht eine Gesprächsnotiz anhand ihrer ID (Query-Param id)."""
|
||||
# projekt_id der Notiz ermitteln, um die Projekt-Zuordnung
|
||||
# des Nutzers zu prüfen, bevor gelöscht wird.
|
||||
user_id = _get_visible_user_id()
|
||||
call_log = call_logs.get_call_log(int(request.args.get("id")), user_id)
|
||||
call_logs.delete_call_log(int(request.args.get("id")))
|
||||
return jsonify({}), 200
|
||||
|
||||
|
||||
@@ -208,9 +208,15 @@ export const Projekt: m.ClosureComponent = (vnode) => {
|
||||
? "Neues Projekt für " + currentProjekt.kunden_name
|
||||
: "Projekt-Stammblatt"),
|
||||
currentProjekt.id !== -1
|
||||
? m("button.w3-button.w3-theme",
|
||||
{onclick: () => m.route.set("/kontaktadresseliste", {projekt_id: currentProjekt.id})},
|
||||
m("i.material-icons", "contact_page"), " Kontaktadressen")
|
||||
? [
|
||||
m("button.w3-button.w3-theme",
|
||||
{onclick: () => m.route.set("/kontaktadresseliste", {projekt_id: currentProjekt.id})},
|
||||
m("i.material-icons", "contact_page"), " Kontaktadressen"),
|
||||
m("button.w3-button.w3-theme",
|
||||
{style: {'margin-left': '4px'},
|
||||
onclick: () => m.route.set("/projektuserliste", {projekt_id: currentProjekt.id})},
|
||||
m("i.material-icons", "group"), " Nutzerberechtigungen")
|
||||
]
|
||||
: null
|
||||
),
|
||||
m("div", {style: {flex: '1', 'min-height': '0'}},
|
||||
|
||||
@@ -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")
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -5,6 +5,7 @@ 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 { Call_Log, call_log_liste } from "./Components/call_log"
|
||||
import { Kontakt, kontaktliste } from "./Components/kontakt"
|
||||
import { Kontaktadresse, kontaktadresseliste } from "./Components/kontaktadresse"
|
||||
@@ -152,6 +153,7 @@ m.route(gridMountpoints.main, "/blankpage", {
|
||||
"/kunde": kunde,
|
||||
"/projektliste": projektliste,
|
||||
"/projekt": Projekt,
|
||||
"/projektuserliste": projektuserliste,
|
||||
"/calllogliste": call_log_liste,
|
||||
"/call_log": Call_Log,
|
||||
"/kontaktliste": kontaktliste,
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
import m from "mithril"
|
||||
|
||||
/**
|
||||
* Datenmodell für eine Projekt-User-Zuordnung (Sichtbarkeit).
|
||||
*
|
||||
* Die Felder id, projekt_id und user_id sind die ORM-Spalten der
|
||||
* Verknüpfungstabelle 'projekt_users'. Die Felder userid, username und
|
||||
* email werden serverseitig aus der Auth-Datenbank aufgelöst und im JSON
|
||||
* ergänzt.
|
||||
*/
|
||||
export interface iProjektUser {
|
||||
id: number
|
||||
projekt_id: number
|
||||
user_id: number
|
||||
userid: string
|
||||
username: string
|
||||
email: string
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* REST-API-Aufrufe (Client-Seite) */
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* Die folgenden Funktionen kapseln die Kommunikation mit den */
|
||||
/* serverseitigen REST-Endpunkten (/xhr/projektuser/*). */
|
||||
/* ------------------------------------------------------------------ */
|
||||
|
||||
/**
|
||||
* Basis-Pfad der ProjektUser-REST-Endpunkte.
|
||||
*/
|
||||
const PROJEKTUSER_REST_BASE = "/xhr/projektuser"
|
||||
|
||||
/**
|
||||
* Lädt die Liste der einem Projekt zugeordneten Nutzer vom Server.
|
||||
*/
|
||||
export function fetchProjektUsers(projektId: number): Promise<iProjektUser[]> {
|
||||
return m.request({
|
||||
method: "GET",
|
||||
url: PROJEKTUSER_REST_BASE + "/list",
|
||||
params: {projekt_id: projektId}
|
||||
}).then((result) => result as iProjektUser[])
|
||||
}
|
||||
|
||||
/**
|
||||
* Ordnet einen Nutzer einem Projekt zu.
|
||||
*/
|
||||
export function addProjektUserREST(projektId: number, userId: number): Promise<void> {
|
||||
return m.request({
|
||||
method: "POST",
|
||||
url: PROJEKTUSER_REST_BASE + "/add",
|
||||
body: {projekt_id: projektId, user_id: userId}
|
||||
}).then(() => undefined)
|
||||
}
|
||||
|
||||
/**
|
||||
* Entfernt die Zuordnung eines Nutzers zu einem Projekt.
|
||||
*/
|
||||
export function removeProjektUserREST(projektId: number, userId: number): Promise<void> {
|
||||
return m.request({
|
||||
method: "DELETE",
|
||||
url: PROJEKTUSER_REST_BASE + "/remove",
|
||||
params: {projekt_id: projektId, user_id: userId}
|
||||
}).then(() => undefined)
|
||||
}
|
||||
Reference in new issue
Block a user