Compare commits
4
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9ed5e88e01 | ||
|
|
505177b5e2 | ||
|
|
4cb0084066 | ||
|
|
b2f8684afb |
No files matched your search
@@ -30,6 +30,8 @@ Erstelle die Datenmodelle für die Typescript-Entwicklung im Verzeichnis Client/
|
|||||||
|
|
||||||
Beim Aufruf einer Bearbeitungsmaske werden in den Route-Params nur die ID (und ggf. zum Vorbelegen notwendige Eltern-FKs wie `kunden_id`) übergeben — nicht die vollständigen Felder des Objekts. Die Maske lädt die Daten frisch vom Server via `fetchXxxById(id)` im `oninit`. So wird sichergestellt, dass die Daten aktuell sind und zwischenzeitliche Änderungen anderer Nutzer nicht mit veralteten Werten aus der Liste überschrieben werden. Beim Neuanlegen (`id === -1`) entfällt der Fetch; vorbelegte Werte (z.B. `kunden_id`) kommen aus den Route-Params.
|
Beim Aufruf einer Bearbeitungsmaske werden in den Route-Params nur die ID (und ggf. zum Vorbelegen notwendige Eltern-FKs wie `kunden_id`) übergeben — nicht die vollständigen Felder des Objekts. Die Maske lädt die Daten frisch vom Server via `fetchXxxById(id)` im `oninit`. So wird sichergestellt, dass die Daten aktuell sind und zwischenzeitliche Änderungen anderer Nutzer nicht mit veralteten Werten aus der Liste überschrieben werden. Beim Neuanlegen (`id === -1`) entfällt der Fetch; vorbelegte Werte (z.B. `kunden_id`) kommen aus den Route-Params.
|
||||||
|
|
||||||
|
Jeder `m.request`-Aufruf (REST-Endpunkt) muss `.catch((e) => showServerError(e))` anhängen, damit Serverfehler (z.B. 401 bei fehlender Autorisierung) dem Nutzer als Modal angezeigt werden. Die Navigation (`m.route.set`) steht im `.then`-Zweig, damit sie bei Fehler nicht ausgeführt wird und der Nutzer auf der Maske bleibt. `showServerError` wird aus `./auth/authmessages` importiert.
|
||||||
|
|
||||||
Bei `m.ClosureComponent` den Closure-Parameter weglassen (`() =>`) und Attribute ausschließlich über das `vnode` des jeweiligen Lifecycle-Hooks (`view`, `oninit`, `onupdate` etc.) lesen. Das `vnode` der Closure-Funktion ist ein Snapshot zum Erzeugungszeitpunkt und wird bei Redraws nicht aktualisiert — daraus gelesene `attrs` sind veraltet. Initialisierung, die `attrs` braucht, gehört nach `oninit`. Beispiel:
|
Bei `m.ClosureComponent` den Closure-Parameter weglassen (`() =>`) und Attribute ausschließlich über das `vnode` des jeweiligen Lifecycle-Hooks (`view`, `oninit`, `onupdate` etc.) lesen. Das `vnode` der Closure-Funktion ist ein Snapshot zum Erzeugungszeitpunkt und wird bei Redraws nicht aktualisiert — daraus gelesene `attrs` sind veraltet. Initialisierung, die `attrs` braucht, gehört nach `oninit`. Beispiel:
|
||||||
```typescript
|
```typescript
|
||||||
export const Select: m.ClosureComponent<iSelect> = () => {
|
export const Select: m.ClosureComponent<iSelect> = () => {
|
||||||
@@ -53,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
|
## 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
|
## 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.
|
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.
|
||||||
@@ -22,10 +22,17 @@ class AutorisationError(ApplicationError):
|
|||||||
else:
|
else:
|
||||||
userrolestxt = ", ".join([role.name for role in self.user_roles])
|
userrolestxt = ", ".join([role.name for role in self.user_roles])
|
||||||
|
|
||||||
ApplicationError.__init__(self,
|
# Die Meldung über erforderliche Rollen nur aufnehmen, wenn
|
||||||
"Sie haben nicht die passende Rolle für diese Funktion",
|
# welche spezifiziert sind. Bei leerer allowed_roles-Liste
|
||||||
"Ihre Rollen sind: " + userrolestxt,
|
# (z.B. Endpunkte, die nur AppAdmin/SysAdmin erlauben) entfällt
|
||||||
"Erforderliche Rollen sind: " + ", ".join(self.allowed_roles), htmlstatus=HtmlStatusCode.Unauthorized)
|
# dieser Teil der Fehlermeldung komplett.
|
||||||
|
messages = [
|
||||||
|
"Sie haben nicht die passende Rolle für diese Funktion",
|
||||||
|
"Ihre Rollen sind: " + userrolestxt,
|
||||||
|
]
|
||||||
|
if self.allowed_roles:
|
||||||
|
messages.append("Erforderliche Rollen sind: " + ", ".join(self.allowed_roles))
|
||||||
|
ApplicationError.__init__(self, *messages, htmlstatus=HtmlStatusCode.Unauthorized)
|
||||||
|
|
||||||
class HtmlStatusCode():
|
class HtmlStatusCode():
|
||||||
Bad_Request = 400
|
Bad_Request = 400
|
||||||
|
|||||||
@@ -52,6 +52,7 @@ def checklogin():
|
|||||||
login_user(user)
|
login_user(user)
|
||||||
result = user.to_dict()
|
result = user.to_dict()
|
||||||
result["isSysAdmin"] = user.is_SysAdmin()
|
result["isSysAdmin"] = user.is_SysAdmin()
|
||||||
|
result["isAppAdmin"] = user.has_permission(app.config["APPID"], [APP_ADMIN])
|
||||||
return jsonify(result), 200
|
return jsonify(result), 200
|
||||||
|
|
||||||
|
|
||||||
@@ -86,13 +87,18 @@ def logout():
|
|||||||
def getusers():
|
def getusers():
|
||||||
"""
|
"""
|
||||||
Die Rückgabe wird in 'users.get_users()' (s.u.) anhand er App-Berechtigung und der
|
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()
|
userquery = users.get_users()
|
||||||
result = []
|
result = []
|
||||||
user: User
|
user: User
|
||||||
for user in userquery:
|
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
|
return jsonify(result), 200
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ erfolgt ausschließlich über __bind_key__.
|
|||||||
import datetime
|
import datetime
|
||||||
import decimal
|
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 sqlalchemy.orm import relationship
|
||||||
|
|
||||||
from db import dbapp
|
from db import dbapp
|
||||||
@@ -160,6 +160,40 @@ class Projekt(dbapp.Model):
|
|||||||
return result
|
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):
|
class Kontaktadresse(dbapp.Model):
|
||||||
"""
|
"""
|
||||||
Akquise-Ziel innerhalb eines Projekts. Jede Kontaktadresse ist genau
|
Akquise-Ziel innerhalb eines Projekts. Jede Kontaktadresse ist genau
|
||||||
|
|||||||
@@ -10,31 +10,47 @@ dort über den Decorator authorisation_required.
|
|||||||
:version: 1.0
|
:version: 1.0
|
||||||
"""
|
"""
|
||||||
from db import dbapp
|
from db import dbapp
|
||||||
from crm.model import CallLog, Projekt
|
from crm.model import CallLog, Projekt, ProjektUser
|
||||||
|
|
||||||
from auth.model.errors import ApplicationError, HtmlStatusCode
|
from auth.model.errors import ApplicationError, HtmlStatusCode
|
||||||
|
|
||||||
session = dbapp.session
|
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.
|
Liefert Gesprächsnotizen, sortiert nach Datum absteigend.
|
||||||
|
|
||||||
Ist projekt_id gesetzt, werden nur die Notizen dieses Projekts
|
Ist projekt_id gesetzt, werden nur die Notizen dieses Projekts
|
||||||
zurückgegeben (serverseitige Filterung). Ohne projekt_id werden
|
zurückgegeben (serverseitige Filterung). Ohne projekt_id werden
|
||||||
alle Notizen geliefert.
|
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
|
query = CallLog.query
|
||||||
if projekt_id is not None:
|
if projekt_id is not None:
|
||||||
query = query.filter(CallLog.projekt_id == projekt_id)
|
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()
|
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.
|
Liefert eine einzelne Gesprächsnotiz anhand ihrer ID.
|
||||||
Wirft ApplicationError (404), falls die Notiz nicht existiert.
|
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)
|
call_log = CallLog.query.get(call_log_id)
|
||||||
if call_log is None:
|
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),
|
"Gesprächsnotiz mit der ID {} wurde nicht gefunden.".format(call_log_id),
|
||||||
htmlstatus=HtmlStatusCode.Not_Found,
|
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
|
return call_log
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -200,6 +200,18 @@ def _db_V01_08():
|
|||||||
session.commit()
|
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():
|
def checkupdates():
|
||||||
"""
|
"""
|
||||||
Ermittelt die aktuelle DB-Version aus der Systeminfo-Tabelle und führt alle
|
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
|
:version: 1.0
|
||||||
"""
|
"""
|
||||||
from db import dbapp
|
from db import dbapp
|
||||||
from crm.model import Kontaktadresse, Projekt
|
from crm.model import Kontaktadresse, Projekt, ProjektUser
|
||||||
|
|
||||||
from auth.model.errors import ApplicationError, HtmlStatusCode
|
from auth.model.errors import ApplicationError, HtmlStatusCode
|
||||||
|
|
||||||
session = dbapp.session
|
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.
|
Liefert Kontaktadressen, sortiert nach Name.
|
||||||
|
|
||||||
Ist projekt_id gesetzt, werden nur die Kontaktadressen dieses
|
Ist projekt_id gesetzt, werden nur die Kontaktadressen dieses
|
||||||
Projekts zurückgegeben (serverseitige Filterung). Ohne projekt_id
|
Projekts zurückgegeben (serverseitige Filterung). Ohne projekt_id
|
||||||
werden alle Kontaktadressen geliefert.
|
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
|
query = Kontaktadresse.query
|
||||||
if projekt_id is not None:
|
if projekt_id is not None:
|
||||||
query = query.filter(Kontaktadresse.projekt_id == projekt_id)
|
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()
|
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.
|
Liefert eine einzelne Kontaktadresse anhand ihrer ID.
|
||||||
Wirft ApplicationError (404), falls die Kontaktadresse nicht existiert.
|
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)
|
kontaktadresse = Kontaktadresse.query.get(kontaktadresse_id)
|
||||||
if kontaktadresse is None:
|
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),
|
"Kontaktadresse mit der ID {} wurde nicht gefunden.".format(kontaktadresse_id),
|
||||||
htmlstatus=HtmlStatusCode.Not_Found,
|
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
|
return kontaktadresse
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -10,31 +10,54 @@ dort über den Decorator authorisation_required.
|
|||||||
:version: 1.0
|
:version: 1.0
|
||||||
"""
|
"""
|
||||||
from db import dbapp
|
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 auth.model.errors import ApplicationError, HtmlStatusCode
|
||||||
|
from flask_login import current_user
|
||||||
|
current_user: object
|
||||||
|
|
||||||
session = dbapp.session
|
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.
|
Liefert Projekte, sortiert nach projektname.
|
||||||
|
|
||||||
Ist kunden_id gesetzt, werden nur die Projekte dieses Kunden
|
Ist kunden_id gesetzt, werden nur die Projekte dieses Kunden
|
||||||
zurückgegeben (serverseitige Filterung). Ohne kunden_id werden
|
zurückgegeben (serverseitige Filterung). Ohne kunden_id werden
|
||||||
alle Projekte geliefert.
|
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
|
query = Projekt.query
|
||||||
if kunden_id is not None:
|
if kunden_id is not None:
|
||||||
query = query.filter(Projekt.kunden_id == kunden_id)
|
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()
|
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.
|
Liefert ein einzelnes Projekt anhand seiner ID.
|
||||||
Wirft ApplicationError (404), falls das Projekt nicht existiert.
|
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)
|
projekt = Projekt.query.get(projekt_id)
|
||||||
if projekt is None:
|
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),
|
"Projekt mit der ID {} wurde nicht gefunden.".format(projekt_id),
|
||||||
htmlstatus=HtmlStatusCode.Not_Found,
|
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
|
return projekt
|
||||||
|
|
||||||
|
|
||||||
@@ -78,6 +114,14 @@ def create_projekt(data: dict) -> Projekt:
|
|||||||
projekt.startdatum = (data.get("startdatum") or "").strip() or None
|
projekt.startdatum = (data.get("startdatum") or "").strip() or None
|
||||||
session.add(projekt)
|
session.add(projekt)
|
||||||
session.commit()
|
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
|
return projekt
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,209 @@
|
|||||||
|
"""
|
||||||
|
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
|
||||||
|
|
||||||
|
|
||||||
|
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
|
||||||
+136
-12
@@ -15,18 +15,65 @@ KundenManager werden aus crm.model importiert.
|
|||||||
"""
|
"""
|
||||||
from flask import current_app as app
|
from flask import current_app as app
|
||||||
from flask import jsonify, request
|
from flask import jsonify, request
|
||||||
|
from flask_login import current_user
|
||||||
|
|
||||||
from auth.decorators import authorisation_required
|
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 KUNDEN_MANAGER, MITARBEITER
|
||||||
from crm.model import kunde as kunden
|
from crm.model import kunde as kunden
|
||||||
from crm.model import kontakt as kontakte
|
from crm.model import kontakt as kontakte
|
||||||
from crm.model import projekt as projekte
|
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 kontaktadresse as kontaktadressen
|
||||||
from crm.model import call_log as call_logs
|
from crm.model import call_log as call_logs
|
||||||
|
|
||||||
print("crm-routes")
|
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 #
|
# Kunde #
|
||||||
# --------------------------------------------------------------------------- #
|
# --------------------------------------------------------------------------- #
|
||||||
@@ -126,17 +173,23 @@ def kontakt_delete():
|
|||||||
@app.get("/xhr/projekt/list")
|
@app.get("/xhr/projekt/list")
|
||||||
@authorisation_required([MITARBEITER, KUNDEN_MANAGER])
|
@authorisation_required([MITARBEITER, KUNDEN_MANAGER])
|
||||||
def projekt_list():
|
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)
|
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
|
return jsonify(result), 200
|
||||||
|
|
||||||
|
|
||||||
@app.get("/xhr/projekt/get")
|
@app.get("/xhr/projekt/get")
|
||||||
@authorisation_required([MITARBEITER, KUNDEN_MANAGER])
|
@authorisation_required([MITARBEITER, KUNDEN_MANAGER])
|
||||||
def projekt_get():
|
def projekt_get():
|
||||||
"""Liefert ein einzelnes Projekt anhand seiner ID (Query-Param id)."""
|
"""Liefert ein einzelnes Projekt anhand seiner ID (Query-Param id).
|
||||||
projekt = projekte.get_projekt(int(request.args.get("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
|
return jsonify(projekt.to_dict()), 200
|
||||||
|
|
||||||
|
|
||||||
@@ -152,6 +205,7 @@ def projekt_create():
|
|||||||
@authorisation_required([KUNDEN_MANAGER])
|
@authorisation_required([KUNDEN_MANAGER])
|
||||||
def projekt_update():
|
def projekt_update():
|
||||||
"""Aktualisiert ein bestehendes Projekt."""
|
"""Aktualisiert ein bestehendes Projekt."""
|
||||||
|
_require_projekt_access(request.json["id"])
|
||||||
projekt = projekte.update_projekt(request.json)
|
projekt = projekte.update_projekt(request.json)
|
||||||
return jsonify(projekt.to_dict()), 200
|
return jsonify(projekt.to_dict()), 200
|
||||||
|
|
||||||
@@ -160,10 +214,58 @@ def projekt_update():
|
|||||||
@authorisation_required([KUNDEN_MANAGER])
|
@authorisation_required([KUNDEN_MANAGER])
|
||||||
def projekt_delete():
|
def projekt_delete():
|
||||||
"""Löscht ein Projekt anhand seiner ID (Query-Param id)."""
|
"""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")))
|
projekte.delete_projekt(int(request.args.get("id")))
|
||||||
return jsonify({}), 200
|
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.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
|
||||||
|
# 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 #
|
# Kontaktadresse #
|
||||||
# --------------------------------------------------------------------------- #
|
# --------------------------------------------------------------------------- #
|
||||||
@@ -172,17 +274,22 @@ def projekt_delete():
|
|||||||
@app.get("/xhr/kontaktadresse/list")
|
@app.get("/xhr/kontaktadresse/list")
|
||||||
@authorisation_required([MITARBEITER, KUNDEN_MANAGER])
|
@authorisation_required([MITARBEITER, KUNDEN_MANAGER])
|
||||||
def kontaktadresse_list():
|
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)
|
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
|
return jsonify(result), 200
|
||||||
|
|
||||||
|
|
||||||
@app.get("/xhr/kontaktadresse/get")
|
@app.get("/xhr/kontaktadresse/get")
|
||||||
@authorisation_required([MITARBEITER, KUNDEN_MANAGER])
|
@authorisation_required([MITARBEITER, KUNDEN_MANAGER])
|
||||||
def kontaktadresse_get():
|
def kontaktadresse_get():
|
||||||
"""Liefert eine einzelne Kontaktadresse anhand ihrer ID (Query-Param id)."""
|
"""Liefert eine einzelne Kontaktadresse anhand ihrer ID (Query-Param id).
|
||||||
kontaktadresse = kontaktadressen.get_kontaktadresse(int(request.args.get("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
|
return jsonify(kontaktadresse.to_dict()), 200
|
||||||
|
|
||||||
|
|
||||||
@@ -190,6 +297,7 @@ def kontaktadresse_get():
|
|||||||
@authorisation_required([KUNDEN_MANAGER])
|
@authorisation_required([KUNDEN_MANAGER])
|
||||||
def kontaktadresse_create():
|
def kontaktadresse_create():
|
||||||
"""Legt eine neue Kontaktadresse an. ID wird serverseitig vergeben."""
|
"""Legt eine neue Kontaktadresse an. ID wird serverseitig vergeben."""
|
||||||
|
_require_projekt_access(int(request.json["projekt_id"]))
|
||||||
kontaktadresse = kontaktadressen.create_kontaktadresse(request.json)
|
kontaktadresse = kontaktadressen.create_kontaktadresse(request.json)
|
||||||
return jsonify(kontaktadresse.to_dict()), 200
|
return jsonify(kontaktadresse.to_dict()), 200
|
||||||
|
|
||||||
@@ -198,6 +306,7 @@ def kontaktadresse_create():
|
|||||||
@authorisation_required([KUNDEN_MANAGER])
|
@authorisation_required([KUNDEN_MANAGER])
|
||||||
def kontaktadresse_update():
|
def kontaktadresse_update():
|
||||||
"""Aktualisiert eine bestehende Kontaktadresse."""
|
"""Aktualisiert eine bestehende Kontaktadresse."""
|
||||||
|
_require_projekt_access(int(request.json["projekt_id"]))
|
||||||
kontaktadresse = kontaktadressen.update_kontaktadresse(request.json)
|
kontaktadresse = kontaktadressen.update_kontaktadresse(request.json)
|
||||||
return jsonify(kontaktadresse.to_dict()), 200
|
return jsonify(kontaktadresse.to_dict()), 200
|
||||||
|
|
||||||
@@ -206,6 +315,10 @@ def kontaktadresse_update():
|
|||||||
@authorisation_required([KUNDEN_MANAGER])
|
@authorisation_required([KUNDEN_MANAGER])
|
||||||
def kontaktadresse_delete():
|
def kontaktadresse_delete():
|
||||||
"""Löscht eine Kontaktadresse anhand ihrer ID (Query-Param id)."""
|
"""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")))
|
kontaktadressen.delete_kontaktadresse(int(request.args.get("id")))
|
||||||
return jsonify({}), 200
|
return jsonify({}), 200
|
||||||
|
|
||||||
@@ -218,17 +331,22 @@ def kontaktadresse_delete():
|
|||||||
@app.get("/xhr/calllog/list")
|
@app.get("/xhr/calllog/list")
|
||||||
@authorisation_required([MITARBEITER, KUNDEN_MANAGER])
|
@authorisation_required([MITARBEITER, KUNDEN_MANAGER])
|
||||||
def calllog_list():
|
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)
|
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
|
return jsonify(result), 200
|
||||||
|
|
||||||
|
|
||||||
@app.get("/xhr/calllog/get")
|
@app.get("/xhr/calllog/get")
|
||||||
@authorisation_required([MITARBEITER, KUNDEN_MANAGER])
|
@authorisation_required([MITARBEITER, KUNDEN_MANAGER])
|
||||||
def calllog_get():
|
def calllog_get():
|
||||||
"""Liefert eine einzelne Gesprächsnotiz anhand ihrer ID (Query-Param id)."""
|
"""Liefert eine einzelne Gesprächsnotiz anhand ihrer ID (Query-Param id).
|
||||||
call_log = call_logs.get_call_log(int(request.args.get("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
|
return jsonify(call_log.to_dict()), 200
|
||||||
|
|
||||||
|
|
||||||
@@ -236,6 +354,7 @@ def calllog_get():
|
|||||||
@authorisation_required([KUNDEN_MANAGER])
|
@authorisation_required([KUNDEN_MANAGER])
|
||||||
def calllog_create():
|
def calllog_create():
|
||||||
"""Legt eine neue Gesprächsnotiz an. ID wird serverseitig vergeben."""
|
"""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)
|
call_log = call_logs.create_call_log(request.json)
|
||||||
return jsonify(call_log.to_dict()), 200
|
return jsonify(call_log.to_dict()), 200
|
||||||
|
|
||||||
@@ -244,6 +363,7 @@ def calllog_create():
|
|||||||
@authorisation_required([KUNDEN_MANAGER])
|
@authorisation_required([KUNDEN_MANAGER])
|
||||||
def calllog_update():
|
def calllog_update():
|
||||||
"""Aktualisiert eine bestehende Gesprächsnotiz."""
|
"""Aktualisiert eine bestehende Gesprächsnotiz."""
|
||||||
|
_require_projekt_access(int(request.json["projekt_id"]))
|
||||||
call_log = call_logs.update_call_log(request.json)
|
call_log = call_logs.update_call_log(request.json)
|
||||||
return jsonify(call_log.to_dict()), 200
|
return jsonify(call_log.to_dict()), 200
|
||||||
|
|
||||||
@@ -252,6 +372,10 @@ def calllog_update():
|
|||||||
@authorisation_required([KUNDEN_MANAGER])
|
@authorisation_required([KUNDEN_MANAGER])
|
||||||
def calllog_delete():
|
def calllog_delete():
|
||||||
"""Löscht eine Gesprächsnotiz anhand ihrer ID (Query-Param id)."""
|
"""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")))
|
call_logs.delete_call_log(int(request.args.get("id")))
|
||||||
return jsonify({}), 200
|
return jsonify({}), 200
|
||||||
|
|
||||||
|
|||||||
@@ -34,11 +34,18 @@ export const loginscreen: m.ClosureComponent<iLoginscreen> = (vnode) => {
|
|||||||
if (user.isSysAdmin) {
|
if (user.isSysAdmin) {
|
||||||
sessionStorage.setItem("CurrentUserIsSysAdmin", "1");
|
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)})
|
.then(() => {m.route.set(vnode.attrs.setnextpage)})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
sessionStorage.removeItem("CurrentUser");
|
sessionStorage.removeItem("CurrentUser");
|
||||||
sessionStorage.removeItem("CurrentUserIsSysAdmin");
|
sessionStorage.removeItem("CurrentUserIsSysAdmin");
|
||||||
|
sessionStorage.removeItem("CurrentUserIsAppAdmin");
|
||||||
showServerError(error)
|
showServerError(error)
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -52,6 +59,7 @@ export const loginscreen: m.ClosureComponent<iLoginscreen> = (vnode) => {
|
|||||||
.then (() => {
|
.then (() => {
|
||||||
sessionStorage.removeItem("CurrentUser");
|
sessionStorage.removeItem("CurrentUser");
|
||||||
sessionStorage.removeItem("CurrentUserIsSysAdmin");
|
sessionStorage.removeItem("CurrentUserIsSysAdmin");
|
||||||
|
sessionStorage.removeItem("CurrentUserIsAppAdmin");
|
||||||
// global.appglobals.currentuser = "nicht angemeldet"
|
// global.appglobals.currentuser = "nicht angemeldet"
|
||||||
// global.appglobals.currentuserid = ""
|
// global.appglobals.currentuserid = ""
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ export interface iUser {
|
|||||||
email: string
|
email: string
|
||||||
telefon: string
|
telefon: string
|
||||||
isSysAdmin: boolean
|
isSysAdmin: boolean
|
||||||
|
isAppAdmin: boolean
|
||||||
userapps: iUserApp[]
|
userapps: iUserApp[]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import {iCallLog, fetchCallLogs, fetchCallLogById, createCallLogREST, updateCall
|
|||||||
import {iKunde, fetchKunden} from "../models/kunde"
|
import {iKunde, fetchKunden} from "../models/kunde"
|
||||||
import {iProjekt, fetchProjekte, fetchProjektById} from "../models/projekt"
|
import {iProjekt, fetchProjekte, fetchProjektById} from "../models/projekt"
|
||||||
import {iKontaktadresse, fetchKontaktadressen} from "../models/kontaktadresse"
|
import {iKontaktadresse, fetchKontaktadressen} from "../models/kontaktadresse"
|
||||||
|
import {showServerError} from "./auth/authmessages"
|
||||||
|
|
||||||
// Aktualisierungsintervall für die Dauer-Anzeige in Millisekunden.
|
// Aktualisierungsintervall für die Dauer-Anzeige in Millisekunden.
|
||||||
const DAUER_UPDATE_INTERVALL = 1000
|
const DAUER_UPDATE_INTERVALL = 1000
|
||||||
@@ -97,7 +98,7 @@ export const call_log_liste: m.ClosureComponent = (vnode) => {
|
|||||||
// gesetzt ist.
|
// gesetzt ist.
|
||||||
fetchCallLogs(filterProjektId !== null ? filterProjektId : undefined).then((result) => {
|
fetchCallLogs(filterProjektId !== null ? filterProjektId : undefined).then((result) => {
|
||||||
treenodes = result.map((log: iCallLog) => new Treenode(log, true, []))
|
treenodes = result.map((log: iCallLog) => new Treenode(log, true, []))
|
||||||
})
|
}).catch((e) => showServerError(e))
|
||||||
// Projektnamen und Kundennamen asynchron auflösen, falls
|
// Projektnamen und Kundennamen asynchron auflösen, falls
|
||||||
// nicht in den Route-Params enthalten.
|
// nicht in den Route-Params enthalten.
|
||||||
if (filterProjektId !== null) {
|
if (filterProjektId !== null) {
|
||||||
@@ -106,7 +107,7 @@ export const call_log_liste: m.ClosureComponent = (vnode) => {
|
|||||||
projektName = projekt.projektname
|
projektName = projekt.projektname
|
||||||
}
|
}
|
||||||
kundenName = projekt.kunden_name
|
kundenName = projekt.kunden_name
|
||||||
})
|
}).catch((e) => showServerError(e))
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
view: (vnode) => {
|
view: (vnode) => {
|
||||||
@@ -202,7 +203,7 @@ export const Call_Log: m.ClosureComponent = (vnode) => {
|
|||||||
currentLog = result
|
currentLog = result
|
||||||
projektVoreingestellt = true
|
projektVoreingestellt = true
|
||||||
ansprechpartnerVoreingestellt = currentLog.name_ansprechpartner !== ""
|
ansprechpartnerVoreingestellt = currentLog.name_ansprechpartner !== ""
|
||||||
})
|
}).catch((e) => showServerError(e))
|
||||||
} else {
|
} else {
|
||||||
projektVoreingestellt = routeParams && routeParams.projekt_id ? parseInt(routeParams.projekt_id) !== -1 : false
|
projektVoreingestellt = routeParams && routeParams.projekt_id ? parseInt(routeParams.projekt_id) !== -1 : false
|
||||||
currentLog.projekt_id = routeParams && routeParams.projekt_id ? parseInt(routeParams.projekt_id) : -1
|
currentLog.projekt_id = routeParams && routeParams.projekt_id ? parseInt(routeParams.projekt_id) : -1
|
||||||
@@ -230,9 +231,9 @@ export const Call_Log: m.ClosureComponent = (vnode) => {
|
|||||||
}, DAUER_UPDATE_INTERVALL)
|
}, DAUER_UPDATE_INTERVALL)
|
||||||
}
|
}
|
||||||
// Alle Listen asynchron laden, für Select-Optionen.
|
// Alle Listen asynchron laden, für Select-Optionen.
|
||||||
fetchKunden().then((result) => { kundenListe = result })
|
fetchKunden().then((result) => { kundenListe = result }).catch((e) => showServerError(e))
|
||||||
fetchProjekte().then((result) => { projektListe = result })
|
fetchProjekte().then((result) => { projektListe = result }).catch((e) => showServerError(e))
|
||||||
fetchKontaktadressen().then((result) => { kontaktadressenListe = result })
|
fetchKontaktadressen().then((result) => { kontaktadressenListe = result }).catch((e) => showServerError(e))
|
||||||
},
|
},
|
||||||
onremove: (vnode) => {
|
onremove: (vnode) => {
|
||||||
if (timerId != null) { window.clearInterval(timerId) }
|
if (timerId != null) { window.clearInterval(timerId) }
|
||||||
@@ -285,9 +286,9 @@ export const Call_Log: m.ClosureComponent = (vnode) => {
|
|||||||
erledigt: formData.get("erledigt") === "on"
|
erledigt: formData.get("erledigt") === "on"
|
||||||
}
|
}
|
||||||
if (currentLog.id === -1) {
|
if (currentLog.id === -1) {
|
||||||
createCallLogREST(gespeichertesLog).then(() => m.route.set("/calllogliste", {projekt_id: currentLog.projekt_id, projekt_name: currentLog.projekt_name}))
|
createCallLogREST(gespeichertesLog).then(() => m.route.set("/calllogliste", {projekt_id: currentLog.projekt_id, projekt_name: currentLog.projekt_name})).catch((e) => showServerError(e))
|
||||||
} else {
|
} else {
|
||||||
updateCallLogREST(gespeichertesLog).then(() => m.route.set("/calllogliste", {projekt_id: currentLog.projekt_id, projekt_name: currentLog.projekt_name}))
|
updateCallLogREST(gespeichertesLog).then(() => m.route.set("/calllogliste", {projekt_id: currentLog.projekt_id, projekt_name: currentLog.projekt_name})).catch((e) => showServerError(e))
|
||||||
}
|
}
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { Input, iInput, Textarea, iTextarea} from "./widgets/inputs"
|
|||||||
import {Treenode, iTreenode, Treetable, iTreetable, TopbarButton, iTopbarButton} from "./widgets/Treetable_V2"
|
import {Treenode, iTreenode, Treetable, iTreetable, TopbarButton, iTopbarButton} from "./widgets/Treetable_V2"
|
||||||
import {iKontakt, fetchKontakte, fetchKontaktById, createKontaktREST, updateKontaktREST} from "../models/kontakt"
|
import {iKontakt, fetchKontakte, fetchKontaktById, createKontaktREST, updateKontaktREST} from "../models/kontakt"
|
||||||
import {iKunde, fetchKundeById} from "../models/kunde"
|
import {iKunde, fetchKundeById} from "../models/kunde"
|
||||||
|
import {showServerError} from "./auth/authmessages"
|
||||||
|
|
||||||
export const kontaktliste: m.ClosureComponent = (vnode) => {
|
export const kontaktliste: m.ClosureComponent = (vnode) => {
|
||||||
let treenodes: iTreenode[] = []
|
let treenodes: iTreenode[] = []
|
||||||
@@ -69,12 +70,12 @@ export const kontaktliste: m.ClosureComponent = (vnode) => {
|
|||||||
// kunden_id erfolgt serverseitig, sofern ein Filter gesetzt ist.
|
// kunden_id erfolgt serverseitig, sofern ein Filter gesetzt ist.
|
||||||
fetchKontakte(filterKundenId !== null ? filterKundenId : undefined).then((result) => {
|
fetchKontakte(filterKundenId !== null ? filterKundenId : undefined).then((result) => {
|
||||||
treenodes = result.map((kontakt: iKontakt) => new Treenode(kontakt, true, []))
|
treenodes = result.map((kontakt: iKontakt) => new Treenode(kontakt, true, []))
|
||||||
})
|
}).catch((e) => showServerError(e))
|
||||||
// Kundennamen asynchron auflösen, falls nicht in Route-Params.
|
// Kundennamen asynchron auflösen, falls nicht in Route-Params.
|
||||||
if (filterKundenId !== null && kundenName === "") {
|
if (filterKundenId !== null && kundenName === "") {
|
||||||
fetchKundeById(filterKundenId).then((kunde: iKunde) => {
|
fetchKundeById(filterKundenId).then((kunde: iKunde) => {
|
||||||
kundenName = kunde.name
|
kundenName = kunde.name
|
||||||
})
|
}).catch((e) => showServerError(e))
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
view: (vnode) => {
|
view: (vnode) => {
|
||||||
@@ -138,7 +139,7 @@ export const Kontakt: m.ClosureComponent = (vnode) => {
|
|||||||
// Änderungen anderer Nutzer nicht zu überschreiben.
|
// Änderungen anderer Nutzer nicht zu überschreiben.
|
||||||
fetchKontaktById(id).then((result) => {
|
fetchKontaktById(id).then((result) => {
|
||||||
currentKontakt = result
|
currentKontakt = result
|
||||||
})
|
}).catch((e) => showServerError(e))
|
||||||
} else {
|
} else {
|
||||||
// Neuanlegen: kunden_id und kunden_name aus Route-Params
|
// Neuanlegen: kunden_id und kunden_name aus Route-Params
|
||||||
// (voreingestellt aus der Kontaktliste).
|
// (voreingestellt aus der Kontaktliste).
|
||||||
@@ -161,9 +162,9 @@ export const Kontakt: m.ClosureComponent = (vnode) => {
|
|||||||
notiz: (formData.get("notiz") as string) || ""
|
notiz: (formData.get("notiz") as string) || ""
|
||||||
}
|
}
|
||||||
if (currentKontakt.id === -1) {
|
if (currentKontakt.id === -1) {
|
||||||
createKontaktREST(gespeicherterKontakt).then(() => m.route.set("/kontaktliste", {kunden_id: currentKontakt.kunden_id, kunden_name: currentKontakt.kunden_name}))
|
createKontaktREST(gespeicherterKontakt).then(() => m.route.set("/kontaktliste", {kunden_id: currentKontakt.kunden_id, kunden_name: currentKontakt.kunden_name})).catch((e) => showServerError(e))
|
||||||
} else {
|
} else {
|
||||||
updateKontaktREST(gespeicherterKontakt).then(() => m.route.set("/kontaktliste", {kunden_id: currentKontakt.kunden_id, kunden_name: currentKontakt.kunden_name}))
|
updateKontaktREST(gespeicherterKontakt).then(() => m.route.set("/kontaktliste", {kunden_id: currentKontakt.kunden_id, kunden_name: currentKontakt.kunden_name})).catch((e) => showServerError(e))
|
||||||
}
|
}
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { Input, iInput, Textarea, iTextarea} from "./widgets/inputs"
|
|||||||
import {Treenode, iTreenode, Treetable, iTreetable, TopbarButton, iTopbarButton} from "./widgets/Treetable_V2"
|
import {Treenode, iTreenode, Treetable, iTreetable, TopbarButton, iTopbarButton} from "./widgets/Treetable_V2"
|
||||||
import {iKontaktadresse, fetchKontaktadressen, fetchKontaktadresseById, createKontaktadresseREST, updateKontaktadresseREST} from "../models/kontaktadresse"
|
import {iKontaktadresse, fetchKontaktadressen, fetchKontaktadresseById, createKontaktadresseREST, updateKontaktadresseREST} from "../models/kontaktadresse"
|
||||||
import {iProjekt, fetchProjektById} from "../models/projekt"
|
import {iProjekt, fetchProjektById} from "../models/projekt"
|
||||||
|
import {showServerError} from "./auth/authmessages"
|
||||||
|
|
||||||
export const kontaktadresseliste: m.ClosureComponent = (vnode) => {
|
export const kontaktadresseliste: m.ClosureComponent = (vnode) => {
|
||||||
let treenodes: iTreenode[] = []
|
let treenodes: iTreenode[] = []
|
||||||
@@ -96,12 +97,12 @@ export const kontaktadresseliste: m.ClosureComponent = (vnode) => {
|
|||||||
// gesetzt ist.
|
// gesetzt ist.
|
||||||
fetchKontaktadressen(filterProjektId !== null ? filterProjektId : undefined).then((result) => {
|
fetchKontaktadressen(filterProjektId !== null ? filterProjektId : undefined).then((result) => {
|
||||||
treenodes = result.map((kontaktadresse: iKontaktadresse) => new Treenode(kontaktadresse, true, []))
|
treenodes = result.map((kontaktadresse: iKontaktadresse) => new Treenode(kontaktadresse, true, []))
|
||||||
})
|
}).catch((e) => showServerError(e))
|
||||||
// Projektnamen asynchron auflösen, falls nicht in Route-Params.
|
// Projektnamen asynchron auflösen, falls nicht in Route-Params.
|
||||||
if (filterProjektId !== null && projektName === "") {
|
if (filterProjektId !== null && projektName === "") {
|
||||||
fetchProjektById(filterProjektId).then((projekt: iProjekt) => {
|
fetchProjektById(filterProjektId).then((projekt: iProjekt) => {
|
||||||
projektName = projekt.projektname
|
projektName = projekt.projektname
|
||||||
})
|
}).catch((e) => showServerError(e))
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
view: (vnode) => {
|
view: (vnode) => {
|
||||||
@@ -174,7 +175,7 @@ export const Kontaktadresse: m.ClosureComponent = (vnode) => {
|
|||||||
// Kontaktadresse frisch vom Server holen.
|
// Kontaktadresse frisch vom Server holen.
|
||||||
fetchKontaktadresseById(id).then((result) => {
|
fetchKontaktadresseById(id).then((result) => {
|
||||||
currentKontaktadresse = result
|
currentKontaktadresse = result
|
||||||
})
|
}).catch((e) => showServerError(e))
|
||||||
} else {
|
} else {
|
||||||
// Neuanlegen: projekt_id und projekt_name aus Route-Params.
|
// Neuanlegen: projekt_id und projekt_name aus Route-Params.
|
||||||
currentKontaktadresse.projekt_id = routeParams && routeParams.projekt_id ? parseInt(routeParams.projekt_id) : -1
|
currentKontaktadresse.projekt_id = routeParams && routeParams.projekt_id ? parseInt(routeParams.projekt_id) : -1
|
||||||
@@ -201,9 +202,9 @@ export const Kontaktadresse: m.ClosureComponent = (vnode) => {
|
|||||||
notiz: (formData.get("notiz") as string) || ""
|
notiz: (formData.get("notiz") as string) || ""
|
||||||
}
|
}
|
||||||
if (currentKontaktadresse.id === -1) {
|
if (currentKontaktadresse.id === -1) {
|
||||||
createKontaktadresseREST(gespeicherteKontaktadresse).then(() => m.route.set("/kontaktadresseliste", {projekt_id: currentKontaktadresse.projekt_id, projekt_name: currentKontaktadresse.projekt_name}))
|
createKontaktadresseREST(gespeicherteKontaktadresse).then(() => m.route.set("/kontaktadresseliste", {projekt_id: currentKontaktadresse.projekt_id, projekt_name: currentKontaktadresse.projekt_name})).catch((e) => showServerError(e))
|
||||||
} else {
|
} else {
|
||||||
updateKontaktadresseREST(gespeicherteKontaktadresse).then(() => m.route.set("/kontaktadresseliste", {projekt_id: currentKontaktadresse.projekt_id, projekt_name: currentKontaktadresse.projekt_name}))
|
updateKontaktadresseREST(gespeicherteKontaktadresse).then(() => m.route.set("/kontaktadresseliste", {projekt_id: currentKontaktadresse.projekt_id, projekt_name: currentKontaktadresse.projekt_name})).catch((e) => showServerError(e))
|
||||||
}
|
}
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import {Form, iForm} from "./widgets/form"
|
|||||||
import { Input, iInput} from "./widgets/inputs"
|
import { Input, iInput} from "./widgets/inputs"
|
||||||
import {Treenode, iTreenode, Treetable, iTreetable, TopbarButton, iTopbarButton} from "./widgets/Treetable_V2"
|
import {Treenode, iTreenode, Treetable, iTreetable, TopbarButton, iTopbarButton} from "./widgets/Treetable_V2"
|
||||||
import {iKunde, fetchKunden, fetchKundeById, createKundeREST, updateKundeREST} from "../models/kunde"
|
import {iKunde, fetchKunden, fetchKundeById, createKundeREST, updateKundeREST} from "../models/kunde"
|
||||||
|
import {showServerError} from "./auth/authmessages"
|
||||||
|
|
||||||
export const kunde: m.ClosureComponent = (vnode) => {
|
export const kunde: m.ClosureComponent = (vnode) => {
|
||||||
let currentKunde: iKunde = {
|
let currentKunde: iKunde = {
|
||||||
@@ -23,7 +24,7 @@ export const kunde: m.ClosureComponent = (vnode) => {
|
|||||||
// Änderungen anderer Nutzer nicht zu überschreiben.
|
// Änderungen anderer Nutzer nicht zu überschreiben.
|
||||||
fetchKundeById(id).then((result) => {
|
fetchKundeById(id).then((result) => {
|
||||||
currentKunde = result
|
currentKunde = result
|
||||||
})
|
}).catch((e) => showServerError(e))
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
view: (vnode) => {
|
view: (vnode) => {
|
||||||
@@ -38,9 +39,9 @@ export const kunde: m.ClosureComponent = (vnode) => {
|
|||||||
land: (formData.get("land") as string) || ""
|
land: (formData.get("land") as string) || ""
|
||||||
}
|
}
|
||||||
if (currentKunde.id === -1) {
|
if (currentKunde.id === -1) {
|
||||||
createKundeREST(gespeicherterKunde).then(() => m.route.set("/kundenliste"))
|
createKundeREST(gespeicherterKunde).then(() => m.route.set("/kundenliste")).catch((e) => showServerError(e))
|
||||||
} else {
|
} else {
|
||||||
updateKundeREST(gespeicherterKunde).then(() => m.route.set("/kundenliste"))
|
updateKundeREST(gespeicherterKunde).then(() => m.route.set("/kundenliste")).catch((e) => showServerError(e))
|
||||||
}
|
}
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
@@ -130,7 +131,7 @@ export const kundenliste: m.ClosureComponent = (vnode) => {
|
|||||||
// Promise-Auflösung automatisch ein Redraw aus.
|
// Promise-Auflösung automatisch ein Redraw aus.
|
||||||
fetchKunden().then((kunden: iKunde[]) => {
|
fetchKunden().then((kunden: iKunde[]) => {
|
||||||
treenodes = kunden.map((kunde: iKunde) => new Treenode(kunde, true, []))
|
treenodes = kunden.map((kunde: iKunde) => new Treenode(kunde, true, []))
|
||||||
})
|
}).catch((e) => showServerError(e))
|
||||||
},
|
},
|
||||||
view: (vnode) => {
|
view: (vnode) => {
|
||||||
return m("div.w3-container.w3-auto", {
|
return m("div.w3-container.w3-auto", {
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { Input, iInput} from "./widgets/inputs"
|
|||||||
import {Treenode, iTreenode, Treetable, iTreetable, TopbarButton, iTopbarButton} from "./widgets/Treetable_V2"
|
import {Treenode, iTreenode, Treetable, iTreetable, TopbarButton, iTopbarButton} from "./widgets/Treetable_V2"
|
||||||
import {iProjekt, fetchProjekte, fetchProjektById, createProjektREST, updateProjektREST} from "../models/projekt"
|
import {iProjekt, fetchProjekte, fetchProjektById, createProjektREST, updateProjektREST} from "../models/projekt"
|
||||||
import {iKunde, fetchKundeById} from "../models/kunde"
|
import {iKunde, fetchKundeById} from "../models/kunde"
|
||||||
|
import {showServerError} from "./auth/authmessages"
|
||||||
|
|
||||||
export const projektliste: m.ClosureComponent = (vnode) => {
|
export const projektliste: m.ClosureComponent = (vnode) => {
|
||||||
let treenodes: iTreenode[] = []
|
let treenodes: iTreenode[] = []
|
||||||
@@ -106,12 +107,12 @@ export const projektliste: m.ClosureComponent = (vnode) => {
|
|||||||
// kunden_id erfolgt serverseitig, sofern ein Filter gesetzt ist.
|
// kunden_id erfolgt serverseitig, sofern ein Filter gesetzt ist.
|
||||||
fetchProjekte(filterKundenId !== null ? filterKundenId : undefined).then((result) => {
|
fetchProjekte(filterKundenId !== null ? filterKundenId : undefined).then((result) => {
|
||||||
treenodes = result.map((projekt: iProjekt) => new Treenode(projekt, true, []))
|
treenodes = result.map((projekt: iProjekt) => new Treenode(projekt, true, []))
|
||||||
})
|
}).catch((e) => showServerError(e))
|
||||||
// Kundennamen asynchron auflösen, falls nicht in Route-Params.
|
// Kundennamen asynchron auflösen, falls nicht in Route-Params.
|
||||||
if (filterKundenId !== null && kundenName === "") {
|
if (filterKundenId !== null && kundenName === "") {
|
||||||
fetchKundeById(filterKundenId).then((kunde: iKunde) => {
|
fetchKundeById(filterKundenId).then((kunde: iKunde) => {
|
||||||
kundenName = kunde.name
|
kundenName = kunde.name
|
||||||
})
|
}).catch((e) => showServerError(e))
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
view: (vnode) => {
|
view: (vnode) => {
|
||||||
@@ -171,7 +172,7 @@ export const Projekt: m.ClosureComponent = (vnode) => {
|
|||||||
// Änderungen anderer Nutzer nicht zu überschreiben.
|
// Änderungen anderer Nutzer nicht zu überschreiben.
|
||||||
fetchProjektById(id).then((result) => {
|
fetchProjektById(id).then((result) => {
|
||||||
currentProjekt = result
|
currentProjekt = result
|
||||||
})
|
}).catch((e) => showServerError(e))
|
||||||
} else {
|
} else {
|
||||||
// Neuanlegen: kunden_id und kunden_name aus Route-Params
|
// Neuanlegen: kunden_id und kunden_name aus Route-Params
|
||||||
// (voreingestellt aus der Projektliste).
|
// (voreingestellt aus der Projektliste).
|
||||||
@@ -192,9 +193,9 @@ export const Projekt: m.ClosureComponent = (vnode) => {
|
|||||||
startdatum: (formData.get("startdatum") as string) || ""
|
startdatum: (formData.get("startdatum") as string) || ""
|
||||||
}
|
}
|
||||||
if (currentProjekt.id === -1) {
|
if (currentProjekt.id === -1) {
|
||||||
createProjektREST(gespeichertesProjekt).then(() => m.route.set("/projektliste", {kunden_id: currentProjekt.kunden_id, kunden_name: currentProjekt.kunden_name}))
|
createProjektREST(gespeichertesProjekt).then(() => m.route.set("/projektliste", {kunden_id: currentProjekt.kunden_id, kunden_name: currentProjekt.kunden_name})).catch((e) => showServerError(e))
|
||||||
} else {
|
} else {
|
||||||
updateProjektREST(gespeichertesProjekt).then(() => m.route.set("/projektliste", {kunden_id: currentProjekt.kunden_id, kunden_name: currentProjekt.kunden_name}))
|
updateProjektREST(gespeichertesProjekt).then(() => m.route.set("/projektliste", {kunden_id: currentProjekt.kunden_id, kunden_name: currentProjekt.kunden_name})).catch((e) => showServerError(e))
|
||||||
}
|
}
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
@@ -207,9 +208,15 @@ export const Projekt: m.ClosureComponent = (vnode) => {
|
|||||||
? "Neues Projekt für " + currentProjekt.kunden_name
|
? "Neues Projekt für " + currentProjekt.kunden_name
|
||||||
: "Projekt-Stammblatt"),
|
: "Projekt-Stammblatt"),
|
||||||
currentProjekt.id !== -1
|
currentProjekt.id !== -1
|
||||||
? m("button.w3-button.w3-theme",
|
? [
|
||||||
{onclick: () => m.route.set("/kontaktadresseliste", {projekt_id: currentProjekt.id})},
|
m("button.w3-button.w3-theme",
|
||||||
m("i.material-icons", "contact_page"), " Kontaktadressen")
|
{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
|
: null
|
||||||
),
|
),
|
||||||
m("div", {style: {flex: '1', 'min-height': '0'}},
|
m("div", {style: {flex: '1', 'min-height': '0'}},
|
||||||
|
|||||||
@@ -0,0 +1,459 @@
|
|||||||
|
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, 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"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Ü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.
|
||||||
|
*
|
||||||
|
* 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[] = []
|
||||||
|
let selectedNode: iTreenode | null = null
|
||||||
|
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[] = []
|
||||||
|
|
||||||
|
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("span", 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)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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 = () => {
|
||||||
|
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
|
||||||
|
* Checkboxen pro noch nicht zugeordnetem Nutzer. Bereits zugeordnete
|
||||||
|
* Nutzer werden ausgeblendet.
|
||||||
|
*/
|
||||||
|
const addUserModal: m.ClosureComponent = () => {
|
||||||
|
// Menge der ausgewählten user_ids (Checkbox aktiviert).
|
||||||
|
let selectedIds: Set<number> = new Set()
|
||||||
|
// Suchtext zum Filtern der Liste.
|
||||||
|
let search: string = ""
|
||||||
|
|
||||||
|
// Verfügbare Nutzer: alle, die noch nicht zugeordnet sind, nicht
|
||||||
|
// SysAdmin oder AppAdmin sind (diese haben ohnehin immer Zugriff)
|
||||||
|
// und deren Suchtext-Filter entsprechen. Sortiert nach username.
|
||||||
|
const getAvailableUsers = (): iUser[] => {
|
||||||
|
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.isAppAdmin) return false
|
||||||
|
if (assignedIds.has(u.id)) return false
|
||||||
|
if (searchLower === "") return true
|
||||||
|
return (u.userid || "").toLowerCase().includes(searchLower) ||
|
||||||
|
(u.username || "").toLowerCase().includes(searchLower) ||
|
||||||
|
(u.email || "").toLowerCase().includes(searchLower)
|
||||||
|
}).sort((a: iUser, b: iUser) => {
|
||||||
|
return (a.username || "").localeCompare(b.username || "")
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const closeModal = () => {
|
||||||
|
m.mount(gridMountpoints.modalhook, null)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Ordnet alle ausgewählten Nutzer dem Projekt zu. Die Aufrufe
|
||||||
|
* erfolgen parallel; nachdem alle abgeschlossen sind, wird das
|
||||||
|
* Modal geschlossen und die Liste neu geladen.
|
||||||
|
*/
|
||||||
|
const confirmAdd = () => {
|
||||||
|
if (selectedIds.size === 0) {
|
||||||
|
closeModal()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const modalProjektId = getModalProjektId()
|
||||||
|
const promises: Promise<void>[] = []
|
||||||
|
selectedIds.forEach((uid: number) => {
|
||||||
|
promises.push(addProjektUserREST(modalProjektId, uid))
|
||||||
|
})
|
||||||
|
Promise.all(promises).then(() => {
|
||||||
|
closeModal()
|
||||||
|
if (detailMode) {
|
||||||
|
loadProjektUsers()
|
||||||
|
} else {
|
||||||
|
loadOverviewData()
|
||||||
|
}
|
||||||
|
}).catch((e) => {
|
||||||
|
showServerError(e)
|
||||||
|
if (detailMode) {
|
||||||
|
loadProjektUsers()
|
||||||
|
} else {
|
||||||
|
loadOverviewData()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Schaltet die Auswahl für einen Nutzer um.
|
||||||
|
*/
|
||||||
|
const toggleUser = (userId: number) => {
|
||||||
|
if (selectedIds.has(userId)) {
|
||||||
|
selectedIds.delete(userId)
|
||||||
|
} else {
|
||||||
|
selectedIds.add(userId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
view: (vnode) => {
|
||||||
|
const availableUsers = getAvailableUsers()
|
||||||
|
return m(ModalView,
|
||||||
|
{
|
||||||
|
title: "Nutzer zuordnen",
|
||||||
|
type: ModalType.MESSAGE,
|
||||||
|
width: '500px'
|
||||||
|
} as iModalView,
|
||||||
|
m("div.w3-padding-16",
|
||||||
|
m("p", "Wählen Sie die Nutzer aus, die dem Projekt \"" + projektName + "\" zugeordnet werden sollen:"),
|
||||||
|
// Suchfeld zum Filtern der Nutzerliste.
|
||||||
|
m("label", "Suche"),
|
||||||
|
m("input.w3-input.w3-border",
|
||||||
|
{
|
||||||
|
type: "text",
|
||||||
|
value: search,
|
||||||
|
placeholder: "Nach userid, Name oder E-Mail suchen",
|
||||||
|
style: {'margin-bottom': '8px'},
|
||||||
|
oninput: (e: Event) => {
|
||||||
|
search = (e.target as HTMLInputElement).value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
),
|
||||||
|
// Scrollbare Liste mit Checkboxen.
|
||||||
|
m("div",
|
||||||
|
{
|
||||||
|
style: {
|
||||||
|
'max-height': '300px',
|
||||||
|
'overflow-y': 'auto',
|
||||||
|
'margin-top': '8px',
|
||||||
|
border: '1px solid #ccc'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
availableUsers.length === 0
|
||||||
|
? m("div.w3-padding-small", "Keine weiteren Nutzer verfügbar")
|
||||||
|
: availableUsers.map((u: iUser) =>
|
||||||
|
m("label.w3-block.w3-hover-light-grey.w3-text-left",
|
||||||
|
{
|
||||||
|
style: {'border-bottom': '1px solid #eee', padding: '4px 8px'},
|
||||||
|
onclick: () => toggleUser(u.id)
|
||||||
|
},
|
||||||
|
m("input",
|
||||||
|
{
|
||||||
|
type: "checkbox",
|
||||||
|
checked: selectedIds.has(u.id),
|
||||||
|
style: {'margin-right': '8px'},
|
||||||
|
onclick: (e: Event) => e.stopPropagation(),
|
||||||
|
onchange: (e: Event) => toggleUser(u.id)
|
||||||
|
}
|
||||||
|
),
|
||||||
|
m("strong", u.userid),
|
||||||
|
" (" + u.username + ")",
|
||||||
|
u.email ? m("span.w3-small.w3-opacity", " — " + u.email) : null
|
||||||
|
)
|
||||||
|
)
|
||||||
|
),
|
||||||
|
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) => {
|
||||||
|
const routeParams = m.route.param()
|
||||||
|
projektId = routeParams && routeParams.projekt_id ? parseInt(routeParams.projekt_id) : -1
|
||||||
|
projektName = routeParams && routeParams.projekt_name ? routeParams.projekt_name : ""
|
||||||
|
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()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
view: (vnode) => {
|
||||||
|
return m("div.w3-container.w3-auto",
|
||||||
|
{
|
||||||
|
style: {
|
||||||
|
display: 'flex',
|
||||||
|
'flex-direction': 'column',
|
||||||
|
height: 'inherit',
|
||||||
|
'overflow-y': 'inherit'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
m("h1", detailMode
|
||||||
|
? (projektName
|
||||||
|
? "Nutzerberechtigungen für Projekt: " + projektName
|
||||||
|
: "Nutzerberechtigungen")
|
||||||
|
: "Projekt-Zugriffe — Übersicht"),
|
||||||
|
m("div", {style: {flex: '1', 'overflow-y': 'inherit'}},
|
||||||
|
detailMode
|
||||||
|
? 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")
|
||||||
|
)
|
||||||
|
)
|
||||||
|
: 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 {
|
return {
|
||||||
view: (vnode) => {
|
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'}},
|
return m("div.w3-border-theme", {style: {height: '99%', overflow: 'hidden', 'border-style': 'solid', 'border-width': '1px'}},
|
||||||
m("div.w3-bar.w3-theme",
|
m("div.w3-bar.w3-theme",
|
||||||
m(TopbarButton, {
|
m(TopbarButton, {
|
||||||
@@ -80,7 +87,8 @@ export const Treetable: m.ClosureComponent<iTreetable> = (vnode) => {
|
|||||||
columnmapper1: vnode.attrs.columnmapper,
|
columnmapper1: vnode.attrs.columnmapper,
|
||||||
onRowMarked: onRowMarked,
|
onRowMarked: onRowMarked,
|
||||||
isSelected: isSelected,
|
isSelected: isSelected,
|
||||||
isEven: ((z % 2) == 0)} as iTreerow)
|
isEven: ((z % 2) == 0),
|
||||||
|
hasSiblingsWithChildren: topHasSiblingsWithChildren} as iTreerow)
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
return null
|
return null
|
||||||
@@ -101,6 +109,11 @@ interface iTreerow {
|
|||||||
onRowMarked: (node: iTreenode) => void
|
onRowMarked: (node: iTreenode) => void
|
||||||
isSelected?: (node: iTreenode) => boolean
|
isSelected?: (node: iTreenode) => boolean
|
||||||
isEven?: 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) => {
|
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
|
// Zeile selektiert ist; in diesem Fall wird der Hintergrund der
|
||||||
// gesamten <tr>-Zeile eingefärbt.
|
// gesamten <tr>-Zeile eingefärbt.
|
||||||
const rowSelected = vnode.attrs.isSelected ? vnode.attrs.isSelected(vnode.attrs.treenode) : false
|
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 [
|
return [
|
||||||
m("tr.w3-border-theme", {onmouseup: () => vnode.attrs.onRowMarked(vnode.attrs.treenode),
|
m("tr.w3-border-theme", {onmouseup: () => vnode.attrs.onRowMarked(vnode.attrs.treenode),
|
||||||
onkeyup: () => 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
|
// Buttons eingerückt werden, damit die Einrückungs-Level auch bei "kinderlosen" Treenodes
|
||||||
// denen der Trennodes mit Kindern entsprechen
|
// denen der Trennodes mit Kindern entsprechen
|
||||||
: vnode.attrs.level == 0
|
: vnode.attrs.level == 0
|
||||||
? null // Wenn der kinderlos Treenode allerdings sirekt auf Level 0 ist, soll nocht
|
? (vnode.attrs.hasSiblingsWithChildren
|
||||||
// eingerückt werden, da es bei Level 0 keine Eltern-Nodes mit Kindern und
|
? m("span", {
|
||||||
// demzufolge mit "expand_more/expand_less"-Button gibt.
|
style: {'margin-left': '0px',
|
||||||
|
'margin-right': '32px'}
|
||||||
|
})
|
||||||
|
: null)
|
||||||
: m(
|
: m(
|
||||||
"span", {
|
"span", {
|
||||||
style: {'margin-left': '24px',
|
style: {'margin-left': '24px',
|
||||||
@@ -165,7 +185,8 @@ let Treerow: m.ClosureComponent<iTreerow> = (vnode) => {
|
|||||||
columnmapper1: vnode.attrs.columnmapper1,
|
columnmapper1: vnode.attrs.columnmapper1,
|
||||||
onRowMarked: vnode.attrs.onRowMarked,
|
onRowMarked: vnode.attrs.onRowMarked,
|
||||||
isSelected: vnode.attrs.isSelected,
|
isSelected: vnode.attrs.isSelected,
|
||||||
isEven: ((z % 2) == 0)
|
isEven: ((z % 2) == 0),
|
||||||
|
hasSiblingsWithChildren: childHasSiblingsWithChildren
|
||||||
} as iTreerow)
|
} as iTreerow)
|
||||||
})
|
})
|
||||||
: null
|
: null
|
||||||
|
|||||||
+57
-24
@@ -5,6 +5,8 @@ import { appmanagementpage } from "./Components/auth/appmanagementpage"
|
|||||||
import { changepasswordscreen, loginscreen } from "./Components/auth/login"
|
import { changepasswordscreen, loginscreen } from "./Components/auth/login"
|
||||||
import {kunde, kundenliste} from "./Components/kunde"
|
import {kunde, kundenliste} from "./Components/kunde"
|
||||||
import { Projekt, projektliste } from "./Components/projekt"
|
import { Projekt, projektliste } from "./Components/projekt"
|
||||||
|
import { projektuserliste } from "./Components/projekt_userliste"
|
||||||
|
import { userprojektliste } from "./Components/user_projektliste"
|
||||||
import { Call_Log, call_log_liste } from "./Components/call_log"
|
import { Call_Log, call_log_liste } from "./Components/call_log"
|
||||||
import { Kontakt, kontaktliste } from "./Components/kontakt"
|
import { Kontakt, kontaktliste } from "./Components/kontakt"
|
||||||
import { Kontaktadresse, kontaktadresseliste } from "./Components/kontaktadresse"
|
import { Kontaktadresse, kontaktadresseliste } from "./Components/kontaktadresse"
|
||||||
@@ -62,6 +64,7 @@ function logout() {
|
|||||||
console.log(result)
|
console.log(result)
|
||||||
sessionStorage.removeItem("CurrentUser");
|
sessionStorage.removeItem("CurrentUser");
|
||||||
sessionStorage.removeItem("CurrentUserIsSysAdmin");
|
sessionStorage.removeItem("CurrentUserIsSysAdmin");
|
||||||
|
sessionStorage.removeItem("CurrentUserIsAppAdmin");
|
||||||
headerdata.username = "Nicht angemeldet"
|
headerdata.username = "Nicht angemeldet"
|
||||||
if (result['logout'] == true) {m.route.set("/blankpage")}
|
if (result['logout'] == true) {m.route.set("/blankpage")}
|
||||||
})
|
})
|
||||||
@@ -100,31 +103,59 @@ const knopf: m.Component = {
|
|||||||
return m("button", {onclick: healthcheck}, "Aktualisieren")
|
return m("button", {onclick: healthcheck}, "Aktualisieren")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
const menu: m.Component = {
|
const menu: m.ClosureComponent = () => {
|
||||||
view: (vnode) => {
|
let showProjektBerechtigungen = false
|
||||||
return m(menubar, [
|
return {
|
||||||
m("a.w3-button.w3-bar-item", {href: "#!/kundenliste"}, "Kunden"),
|
oninit: (vnode) => {
|
||||||
m("a.w3-button.w3-bar-item", {href: "#!/projektliste"}, "Projekte"),
|
showProjektBerechtigungen = false
|
||||||
m("a.w3-button.w3-bar-item", {href: "#!/calllogliste"}, "Gesprächsnotizen"),
|
},
|
||||||
// m("a.w3-button.w3-bar-item", {onclick: healthcheck}, "Aktualisieren"),
|
view: (vnode) => {
|
||||||
m("div.w3-dropdown-hover",
|
return m(menubar, [
|
||||||
m("button.w3-button", "Optionen"),
|
m("a.w3-button.w3-bar-item", {href: "#!/kundenliste"}, "Kunden"),
|
||||||
m("div.w3-dropdown-content.w3-bar-block.w3-card-4",
|
m("a.w3-button.w3-bar-item", {href: "#!/projektliste"}, "Projekte"),
|
||||||
m("a.w3-button.w3-bar-item", {href: "#!/taglist"}, "Tags"),
|
m("a.w3-button.w3-bar-item", {href: "#!/calllogliste"}, "Gesprächsnotizen"),
|
||||||
m("a.w3-button.w3-bar-item", {href: "#!/default"}, "Datenexport"),
|
m("div.w3-dropdown-hover",
|
||||||
m("a.w3-button.w3-bar-item", {href: "#!/default"}, "Datenimport"),
|
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"),
|
|
||||||
)
|
|
||||||
)
|
|
||||||
])
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -152,6 +183,8 @@ m.route(gridMountpoints.main, "/blankpage", {
|
|||||||
"/kunde": kunde,
|
"/kunde": kunde,
|
||||||
"/projektliste": projektliste,
|
"/projektliste": projektliste,
|
||||||
"/projekt": Projekt,
|
"/projekt": Projekt,
|
||||||
|
"/projektuserliste": projektuserliste,
|
||||||
|
"/userprojektliste": userprojektliste,
|
||||||
"/calllogliste": call_log_liste,
|
"/calllogliste": call_log_liste,
|
||||||
"/call_log": Call_Log,
|
"/call_log": Call_Log,
|
||||||
"/kontaktliste": kontaktliste,
|
"/kontaktliste": kontaktliste,
|
||||||
|
|||||||
@@ -0,0 +1,83 @@
|
|||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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) */
|
||||||
|
/* ------------------------------------------------------------------ */
|
||||||
|
/* 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)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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