Benutzerverwaltung, Firmenstamm und Rollenlegende

Die Verwaltung war bisher eine reine Anzeige. Sie gliedert sich jetzt in vier
Reiter und ist handlungsfähig.

Benutzer
- Zugänge anlegen, bearbeiten, sperren und entsperren
- Startkennwort wird erzeugt und genau einmal angezeigt, auf Wunsch zusätzlich
  per E-Mail versandt; es liegt nirgends im Klartext
- Kennwort zurücksetzen beendet zugleich alle laufenden Sitzungen
- Wer sich mit einem Startkennwort anmeldet, wird unmittelbar zur Vergabe eines
  eigenen Kennworts geführt; bis dahin weist ein Streifen im Kopfbereich darauf hin
- Zugänge werden gesperrt, nicht gelöscht: Bautagebucheinträge, Prüfvermerke und
  Bescheinigungen müssen dauerhaft einer Person zuordenbar bleiben

Aussperrschutz
- der eigene Zugang lässt sich weder sperren noch die eigene Administration entziehen
- es muss stets mindestens ein aktiver Administrationszugang bestehen
- Firmen mit bestehenden Verträgen lassen sich nicht stilllegen

Firmen
- Stammdaten mit Art, Anschrift, Kreditorennummer und USt-IdNr., stilllegbar

Rollen und Rechte
- Legende aller Rollen, getrennt nach Auftraggeber- und Auftragnehmerseite
- vollständige Rechtematrix, unmittelbar aus PERMISSIONS erzeugt und damit nicht
  von der Wirklichkeit im Code trennbar
- Übersicht der Projektschalter und der fest verdrahteten Sperren

Protokoll
- Filter nach Aktion und Person, Blätterung, haftungsrelevante Vorgänge hervorgehoben

Nebenbei behoben
- next.config: "output: standalone" entfernt, weil es sich mit "next start" nicht
  verträgt; nicht mehr unterstützten eslint-Schlüssel entfernt
- Unterlagen des Auftraggebers (*.xlsx) gehören nicht ins Repository

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
duffyduck
2026-08-11 13:22:42 +02:00
co-authored by Claude Opus 5
parent 807b7ce541
commit 8af6b384ea
15 changed files with 1495 additions and 139 deletions
+3 -1
View File
@@ -14,6 +14,7 @@ export type SessionUser = {
email: string;
name: string;
isAdmin: boolean;
mustChangePw: boolean;
companyId: string | null;
companyName: string | null;
companyType: string | null;
@@ -62,7 +63,7 @@ export async function login(email: string, password: string) {
path: "/",
expires: expiresAt,
});
return { ok: true as const };
return { ok: true as const, kennwortWechseln: user.mustChangePw };
}
export async function logout() {
@@ -88,6 +89,7 @@ export async function getSessionUser(): Promise<SessionUser | null> {
email: session.user.email,
name: session.user.name,
isAdmin: session.user.isAdmin,
mustChangePw: session.user.mustChangePw,
companyId: session.user.companyId,
companyName: session.user.company?.name ?? null,
companyType: session.user.company?.type ?? null,
+84
View File
@@ -87,6 +87,90 @@ export function assertCan(roles: Role[], permission: Permission) {
}
}
/**
* Klartext für die Rechtelegende. Bewusst als `satisfies Record<Permission, …>`:
* Wer oben ein Recht ergänzt, bekommt hier einen Übersetzungsfehler und kann es
* nicht vergessen.
*/
export const PERMISSION_INFO = {
"projekt.anlegen": { gruppe: "Projekt", label: "Projekt anlegen" },
"projekt.bearbeiten": { gruppe: "Projekt", label: "Projektdaten und Einstellungen ändern" },
"projekt.mitglieder": { gruppe: "Projekt", label: "Beteiligte und Rollen vergeben" },
"dokument.lesen": { gruppe: "Dokumente", label: "Dokumente lesen" },
"dokument.hochladen": { gruppe: "Dokumente", label: "Dokumente einstellen", optional: "anDarfDokumente" },
"dokument.freigeben": { gruppe: "Dokumente", label: "Dokumente prüfen und freigeben" },
"dokument.loeschen": { gruppe: "Dokumente", label: "Dokumente löschen" },
"dokument.intern": { gruppe: "Dokumente", label: "Interne und vertrauliche Unterlagen sehen" },
"ablage.lesen": { gruppe: "Ablagestruktur", label: "Dokumentationsstand einsehen", optional: "anSichtDokumentation" },
"ablage.steuern": { gruppe: "Ablagestruktur", label: "Status und Zuständigkeit setzen" },
"bautagebuch.lesen": { gruppe: "Bautagebuch", label: "Bautagebuch lesen", optional: "anSichtBautagebuch" },
"bautagebuch.schreiben": { gruppe: "Bautagebuch", label: "Bautagebuch führen" },
"bautagebuch.gegenzeichnen": { gruppe: "Bautagebuch", label: "Bautagebuch gegenzeichnen" },
"vorgang.anlegen": { gruppe: "Meldungen", label: "Meldung anlegen" },
"vorgang.bearbeiten": { gruppe: "Meldungen", label: "Meldung bearbeiten" },
"vorgang.schliessen": { gruppe: "Meldungen", label: "Meldung schließen" },
"vertrag.lesen": { gruppe: "Verträge", label: "Verträge einsehen", optional: "anSichtVertrag" },
"vertrag.bearbeiten": { gruppe: "Verträge", label: "Verträge und LV pflegen" },
"nachtrag.pruefen": { gruppe: "Verträge", label: "Nachtrag prüfen und bewerten" },
"stunden.einreichen": { gruppe: "Stunden und Aufmaß", label: "Stundenlohnzettel einreichen" },
"stunden.pruefen": { gruppe: "Stunden und Aufmaß", label: "Stunden zeilenweise anerkennen" },
"aufmass.einreichen": { gruppe: "Stunden und Aufmaß", label: "Aufmaß einreichen" },
"aufmass.pruefen": { gruppe: "Stunden und Aufmaß", label: "Aufmaß anerkennen" },
"rechnung.erfassen": { gruppe: "Rechnungsprüfung", label: "Rechnung erfassen" },
"rechnung.lesen": { gruppe: "Rechnungsprüfung", label: "Rechnungen einsehen", optional: "anSichtRechnungen" },
"rechnung.sachlich": { gruppe: "Rechnungsprüfung", label: "sachlich richtig bescheinigen" },
"rechnung.rechnerisch": { gruppe: "Rechnungsprüfung", label: "rechnerisch richtig bescheinigen" },
"rechnung.freigeben": { gruppe: "Rechnungsprüfung", label: "Zahlung freigeben" },
"chat.schreiben": { gruppe: "Kommunikation", label: "Nachrichten schreiben" },
"chat.kanal.anlegen": { gruppe: "Kommunikation", label: "Kanäle anlegen" },
"admin.benutzer": { gruppe: "Administration", label: "Benutzerverwaltung" },
"admin.firmen": { gruppe: "Administration", label: "Firmenstamm pflegen" },
"admin.audit": { gruppe: "Administration", label: "Protokoll einsehen" },
} as const satisfies Record<Permission, { gruppe: string; label: string; optional?: keyof ProjektOptionen }>;
export const PERMISSION_GRUPPEN = [
"Projekt", "Dokumente", "Ablagestruktur", "Bautagebuch", "Meldungen",
"Verträge", "Stunden und Aufmaß", "Rechnungsprüfung", "Kommunikation", "Administration",
] as const;
export const ROLE_KURZ: Record<Role, string> = {
ADMIN: "ADM",
PROJEKTLEITER: "PL",
BAUHERRENVERTRETER: "BHV",
BAUUEBERWACHER: "BÜB",
FACHBAUUEBERWACHER: "FBÜ",
KAUFMANN: "KFM",
PLANER: "PLA",
AUFTRAGNEHMER: "AN",
PRUEFER: "PRF",
BETREIBER: "BTR",
LESER: "LES",
};
/** Auf welcher Seite des Bauvertrags eine Rolle steht. */
export const ROLE_SEITE: Record<Role, "AG" | "AN" | "NEUTRAL"> = {
ADMIN: "NEUTRAL",
PROJEKTLEITER: "AG",
BAUHERRENVERTRETER: "AG",
BAUUEBERWACHER: "AG",
FACHBAUUEBERWACHER: "AG",
KAUFMANN: "AG",
PLANER: "NEUTRAL",
AUFTRAGNEHMER: "AN",
PRUEFER: "NEUTRAL",
BETREIBER: "NEUTRAL",
LESER: "NEUTRAL",
};
export const ROLE_LABEL: Record<Role, string> = {
ADMIN: "Administration",
PROJEKTLEITER: "Projektleitung (PL)",