Gedankenstriche durch Bindestriche ersetzt
Prüfung / Lint, Typen, Tests (push) Canceled after 0s
Prüfung / Images bauen (pdf) (push) Canceled after 0s
Prüfung / Images bauen (web) (push) Canceled after 0s
Prüfung / Images bauen (worker) (push) Canceled after 0s
Images / bauen (pdf, docker/pdf.Dockerfile) (push) Canceled after 0s
Images / bauen (web, docker/web.Dockerfile) (push) Canceled after 0s
Images / bauen (worker, docker/worker.Dockerfile) (push) Canceled after 0s
Prüfung / Lint, Typen, Tests (push) Canceled after 0s
Prüfung / Images bauen (pdf) (push) Canceled after 0s
Prüfung / Images bauen (web) (push) Canceled after 0s
Prüfung / Images bauen (worker) (push) Canceled after 0s
Images / bauen (pdf, docker/pdf.Dockerfile) (push) Canceled after 0s
Images / bauen (web, docker/web.Dockerfile) (push) Canceled after 0s
Images / bauen (worker, docker/worker.Dockerfile) (push) Canceled after 0s
375 Vorkommen in 210 Dateien. Betrifft Kommentare, Oberflaechentexte und die README; an keiner Stelle Bedeutung, nur Zeichensetzung.
This commit is contained in:
+1
-1
@@ -1,4 +1,4 @@
|
|||||||
# AfiS-Aktensystem — Vorlage für den .env-Reiter in Dockge.
|
# AfiS-Aktensystem - Vorlage für den .env-Reiter in Dockge.
|
||||||
# Diese Datei enthält nur Platzhalter. Die echten Werte gehören nach
|
# Diese Datei enthält nur Platzhalter. Die echten Werte gehören nach
|
||||||
# /opt/stacks/afis/.env und niemals ins Repository. Siehe docs/11-betrieb.md.
|
# /opt/stacks/afis/.env und niemals ins Repository. Siehe docs/11-betrieb.md.
|
||||||
|
|
||||||
|
|||||||
@@ -22,14 +22,14 @@ gehostet. Keine Mandantentrennung, keine öffentliche Registrierung.
|
|||||||
## Zusicherungen
|
## Zusicherungen
|
||||||
|
|
||||||
Acht Regeln, die im Code und wo möglich in der Datenbank durchgesetzt sind. Sie sind der
|
Acht Regeln, die im Code und wo möglich in der Datenbank durchgesetzt sind. Sie sind der
|
||||||
Grund, warum das System so gebaut ist, wie es gebaut ist — Codekommentare verweisen unter
|
Grund, warum das System so gebaut ist, wie es gebaut ist - Codekommentare verweisen unter
|
||||||
ihrer Nummer hierher.
|
ihrer Nummer hierher.
|
||||||
|
|
||||||
1. **Berechtigungsprüfung immer serverseitig und immer in der Datenbankabfrage.** Niemals
|
1. **Berechtigungsprüfung immer serverseitig und immer in der Datenbankabfrage.** Niemals
|
||||||
alle Datensätze laden und danach filtern.
|
alle Datensätze laden und danach filtern.
|
||||||
2. **Verlaufseinträge dürfen nachträglich geändert werden, aber niemals unsichtbar.** Jede
|
2. **Verlaufseinträge dürfen nachträglich geändert werden, aber niemals unsichtbar.** Jede
|
||||||
Änderung erzeugt eine Version, jede Zurücknahme lässt einen Grabstein stehen, beides ist
|
Änderung erzeugt eine Version, jede Zurücknahme lässt einen Grabstein stehen, beides ist
|
||||||
für alle sichtbar, die den Eintrag sehen dürfen. Es gibt kein `DELETE` — das steht als
|
für alle sichtbar, die den Eintrag sehen dürfen. Es gibt kein `DELETE` - das steht als
|
||||||
Regel in der Datenbank, nicht nur in der Anwendung.
|
Regel in der Datenbank, nicht nur in der Anwendung.
|
||||||
3. **Jede schreibende Aktion und jeder Lesezugriff ab Stufe 3 erzeugt einen
|
3. **Jede schreibende Aktion und jeder Lesezugriff ab Stufe 3 erzeugt einen
|
||||||
Protokolleintrag.**
|
Protokolleintrag.**
|
||||||
@@ -64,7 +64,7 @@ Container.
|
|||||||
## Einrichten
|
## Einrichten
|
||||||
|
|
||||||
Discord-Application anlegen, Bot auf den Server einladen und **Server Members Intent
|
Discord-Application anlegen, Bot auf den Server einladen und **Server Members Intent
|
||||||
aktivieren** — ohne das liefert der Bot keine Rollen.
|
aktivieren** - ohne das liefert der Bot keine Rollen.
|
||||||
|
|
||||||
## Lokal entwickeln
|
## Lokal entwickeln
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -1,4 +1,4 @@
|
|||||||
# AfiS-Aktensystem — ein einziger Dockge-Stack.
|
# AfiS-Aktensystem - ein einziger Dockge-Stack.
|
||||||
# Ablage: /opt/stacks/afis/compose.yaml, .env im eigenen Dockge-Reiter.
|
# Ablage: /opt/stacks/afis/compose.yaml, .env im eigenen Dockge-Reiter.
|
||||||
# Begründung der Entscheidungen in docs/11-betrieb.md:
|
# Begründung der Entscheidungen in docs/11-betrieb.md:
|
||||||
# - keine Einmal- und keine Init-Container, Dockge zeigt beendete Container nicht
|
# - keine Einmal- und keine Init-Container, Dockge zeigt beendete Container nicht
|
||||||
|
|||||||
+1
-1
@@ -211,7 +211,7 @@ $$ LANGUAGE sql STABLE;
|
|||||||
|
|
||||||
-- Wie bei case_refresh_search_vector: mit IF verzweigt, nicht mit einem
|
-- Wie bei case_refresh_search_vector: mit IF verzweigt, nicht mit einem
|
||||||
-- CASE-Ausdruck. Der würde auch im nicht genommenen Zweig verlangen, dass
|
-- CASE-Ausdruck. Der würde auch im nicht genommenen Zweig verlangen, dass
|
||||||
-- NEW."documentId" existiert — auf "Document" gibt es das Feld nicht.
|
-- NEW."documentId" existiert - auf "Document" gibt es das Feld nicht.
|
||||||
CREATE OR REPLACE FUNCTION document_refresh_search_vector() RETURNS trigger AS $$
|
CREATE OR REPLACE FUNCTION document_refresh_search_vector() RETURNS trigger AS $$
|
||||||
DECLARE
|
DECLARE
|
||||||
target text;
|
target text;
|
||||||
|
|||||||
@@ -41,7 +41,7 @@ ALTER TABLE "SavedView" ADD CONSTRAINT "SavedView_userId_fkey"
|
|||||||
-- Schwärzungen stufenweise auflösen
|
-- Schwärzungen stufenweise auflösen
|
||||||
--
|
--
|
||||||
-- `strip_redactions` entfernt jede markierte Stelle. Für den Vektor einer
|
-- `strip_redactions` entfernt jede markierte Stelle. Für den Vektor einer
|
||||||
-- Stufe muss aber stehen bleiben, was diese Stufe lesen darf — sonst findet
|
-- Stufe muss aber stehen bleiben, was diese Stufe lesen darf - sonst findet
|
||||||
-- ein Leser der Stufe 4 einen Namen nicht, den er auf der Seite sieht.
|
-- ein Leser der Stufe 4 einen Namen nicht, den er auf der Seite sieht.
|
||||||
-- ===========================================================================
|
-- ===========================================================================
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
-- Phase 9: Ladewerte messen.
|
-- Phase 9: Ladewerte messen.
|
||||||
--
|
--
|
||||||
-- Nur Zahlen und das Routenmuster. Keine Kennungen, keine konkreten Pfade —
|
-- Nur Zahlen und das Routenmuster. Keine Kennungen, keine konkreten Pfade -
|
||||||
-- die Messung soll sagen, ob die Anwendung schnell ist, nicht wer wann welche
|
-- die Messung soll sagen, ob die Anwendung schnell ist, nicht wer wann welche
|
||||||
-- Akte geöffnet hat. Siehe docs/09-performance-und-ux.md.
|
-- Akte geöffnet hat. Siehe docs/09-performance-und-ux.md.
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
--
|
--
|
||||||
-- Eine persönliche Sprungliste, kein Verlauf: je Nutzer und Datensatz genau
|
-- Eine persönliche Sprungliste, kein Verlauf: je Nutzer und Datensatz genau
|
||||||
-- eine Zeile, die beim nächsten Öffnen überschrieben wird. Der Primärschlüssel
|
-- eine Zeile, die beim nächsten Öffnen überschrieben wird. Der Primärschlüssel
|
||||||
-- erzwingt das — es gibt keine Form, in der diese Tabelle sagen könnte, wie oft
|
-- erzwingt das - es gibt keine Form, in der diese Tabelle sagen könnte, wie oft
|
||||||
-- oder in welcher Reihenfolge jemand etwas geöffnet hat.
|
-- oder in welcher Reihenfolge jemand etwas geöffnet hat.
|
||||||
--
|
--
|
||||||
-- Wer wann was gelesen hat, beantwortet weiterhin allein das Zugriffsprotokoll
|
-- Wer wann was gelesen hat, beantwortet weiterhin allein das Zugriffsprotokoll
|
||||||
|
|||||||
@@ -9,7 +9,7 @@
|
|||||||
-- Zeitpunkt dieser Migration stand in keinem Kanal eine Nachricht.
|
-- Zeitpunkt dieser Migration stand in keinem Kanal eine Nachricht.
|
||||||
|
|
||||||
-- Die Nachrichtentabelle ist gegen DELETE gesperrt. Für das einmalige
|
-- Die Nachrichtentabelle ist gegen DELETE gesperrt. Für das einmalige
|
||||||
-- Aufräumen wird die Regel abgenommen und danach wieder gesetzt — sie ist die
|
-- Aufräumen wird die Regel abgenommen und danach wieder gesetzt - sie ist die
|
||||||
-- Zusicherung, dass niemand im Betrieb Nachrichten verschwinden lässt.
|
-- Zusicherung, dass niemand im Betrieb Nachrichten verschwinden lässt.
|
||||||
DROP RULE chat_no_delete ON "ChatMessage";
|
DROP RULE chat_no_delete ON "ChatMessage";
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
--
|
--
|
||||||
-- Damit wird eine Lücke sichtbar, die es schon vorher gab: ein Beteiligter darf
|
-- Damit wird eine Lücke sichtbar, die es schon vorher gab: ein Beteiligter darf
|
||||||
-- die Akte lesen, gleich in welcher Rolle. Bei „Betroffener" fiel das nicht auf,
|
-- die Akte lesen, gleich in welcher Rolle. Bei „Betroffener" fiel das nicht auf,
|
||||||
-- bei „Täter" wäre es offensichtlich falsch — der Beschuldigte läse die
|
-- bei „Täter" wäre es offensichtlich falsch - der Beschuldigte läse die
|
||||||
-- Ermittlung gegen sich selbst mit.
|
-- Ermittlung gegen sich selbst mit.
|
||||||
--
|
--
|
||||||
-- Die Prüfung in lib/access/caseFilter.ts unterscheidet deshalb ab jetzt
|
-- Die Prüfung in lib/access/caseFilter.ts unterscheidet deshalb ab jetzt
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
-- Abteilungen: die Ebene über der Personalakte.
|
-- Abteilungen: die Ebene über der Personalakte.
|
||||||
--
|
--
|
||||||
-- Bisher stand die Abteilung als Freitext in der Personalakte. Damit liess sich
|
-- Bisher stand die Abteilung als Freitext in der Personalakte. Damit liess sich
|
||||||
-- niemand fragen, was es zu einer Abteilung insgesamt gibt — jede Schreibweise
|
-- niemand fragen, was es zu einer Abteilung insgesamt gibt - jede Schreibweise
|
||||||
-- war eine eigene Abteilung. Als eigenes Modell ist sie verwaltbar, und die
|
-- war eine eigene Abteilung. Als eigenes Modell ist sie verwaltbar, und die
|
||||||
-- Zuordnung einer Fallakte ergibt sich aus ihren Beteiligten und deren
|
-- Zuordnung einer Fallakte ergibt sich aus ihren Beteiligten und deren
|
||||||
-- Personalakten. Eine Fallakte trägt deshalb **keine** Abteilung: die
|
-- Personalakten. Eine Fallakte trägt deshalb **keine** Abteilung: die
|
||||||
@@ -28,7 +28,7 @@ CREATE INDEX "Department_isArchived_name_idx" ON "Department"("isArchived", "nam
|
|||||||
ALTER TABLE "PersonnelFile" ADD COLUMN "departmentId" TEXT;
|
ALTER TABLE "PersonnelFile" ADD COLUMN "departmentId" TEXT;
|
||||||
|
|
||||||
-- Was schon als Freitext dastand, wird zur Abteilung. Der Schlüssel ist der
|
-- Was schon als Freitext dastand, wird zur Abteilung. Der Schlüssel ist der
|
||||||
-- getrimmte Text in Grossbuchstaben, der Name der Text selbst — beides lässt
|
-- getrimmte Text in Grossbuchstaben, der Name der Text selbst - beides lässt
|
||||||
-- sich im Adminbereich nachziehen.
|
-- sich im Adminbereich nachziehen.
|
||||||
INSERT INTO "Department" ("id", "key", "name", "updatedAt")
|
INSERT INTO "Department" ("id", "key", "name", "updatedAt")
|
||||||
SELECT gen_random_uuid()::text,
|
SELECT gen_random_uuid()::text,
|
||||||
@@ -125,7 +125,7 @@ CREATE INDEX "PersonnelFile_departmentId_idx" ON "PersonnelFile"("departmentId")
|
|||||||
-- Abteilung als dritter Besitzer wäre eine halbe Sache: die Vorlagen kennen
|
-- Abteilung als dritter Besitzer wäre eine halbe Sache: die Vorlagen kennen
|
||||||
-- über TemplateUse nur die beiden Orte, und ein Feld, das nichts füllen kann,
|
-- über TemplateUse nur die beiden Orte, und ein Feld, das nichts füllen kann,
|
||||||
-- ist schlimmer als keins. Was zu einer Abteilung an Dokumenten gehört, wird
|
-- ist schlimmer als keins. Was zu einer Abteilung an Dokumenten gehört, wird
|
||||||
-- wie alles andere abgeleitet — über ihre Mitglieder und deren Fälle.
|
-- wie alles andere abgeleitet - über ihre Mitglieder und deren Fälle.
|
||||||
|
|
||||||
-- Der Chat bekommt einen dritten Ort. Der CHECK aus der vorigen Migration wird
|
-- Der Chat bekommt einen dritten Ort. Der CHECK aus der vorigen Migration wird
|
||||||
-- entsprechend erweitert: genau einer, weiterhin.
|
-- entsprechend erweitert: genau einer, weiterhin.
|
||||||
@@ -161,7 +161,7 @@ $$ LANGUAGE plpgsql;
|
|||||||
-- Dieselben Zusicherungen wie beim Verlauf einer Akte (harte Regel 2): kein
|
-- Dieselben Zusicherungen wie beim Verlauf einer Akte (harte Regel 2): kein
|
||||||
-- DELETE, jede Änderung erzeugt eine Version, jede Zurücknahme lässt einen
|
-- DELETE, jede Änderung erzeugt eine Version, jede Zurücknahme lässt einen
|
||||||
-- Grabstein stehen. Die Regeln stehen in der Datenbank und nicht nur in der
|
-- Grabstein stehen. Die Regeln stehen in der Datenbank und nicht nur in der
|
||||||
-- Anwendung — die Anwendung kann man umgehen.
|
-- Anwendung - die Anwendung kann man umgehen.
|
||||||
-- ===========================================================================
|
-- ===========================================================================
|
||||||
|
|
||||||
CREATE TABLE "DepartmentEntry" (
|
CREATE TABLE "DepartmentEntry" (
|
||||||
@@ -216,7 +216,7 @@ CREATE RULE dept_entry_version_no_delete
|
|||||||
AS ON DELETE TO "DepartmentEntryVersion" DO INSTEAD NOTHING;
|
AS ON DELETE TO "DepartmentEntryVersion" DO INSTEAD NOTHING;
|
||||||
|
|
||||||
-- Autor, Entstehungszeit und Abteilung sind unantastbar. Wer etwas anderes
|
-- Autor, Entstehungszeit und Abteilung sind unantastbar. Wer etwas anderes
|
||||||
-- sagen will, zieht zurück und schreibt neu — und genau das ist dann sichtbar.
|
-- sagen will, zieht zurück und schreibt neu - und genau das ist dann sichtbar.
|
||||||
CREATE OR REPLACE FUNCTION dept_entry_immutable_guard() RETURNS trigger AS $$
|
CREATE OR REPLACE FUNCTION dept_entry_immutable_guard() RETURNS trigger AS $$
|
||||||
BEGIN
|
BEGIN
|
||||||
IF NEW."authorId" IS DISTINCT FROM OLD."authorId"
|
IF NEW."authorId" IS DISTINCT FROM OLD."authorId"
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
-- Beteiligte in den Suchindex der Fallakte.
|
-- Beteiligte in den Suchindex der Fallakte.
|
||||||
--
|
--
|
||||||
-- Wer nach einem Namen sucht, will die Akten finden, in denen die Person
|
-- Wer nach einem Namen sucht, will die Akten finden, in denen die Person
|
||||||
-- vorkommt — auch dann, wenn sie weder im Titel steht noch im Text erwähnt
|
-- vorkommt - auch dann, wenn sie weder im Titel steht noch im Text erwähnt
|
||||||
-- wird, sondern nur als Zeuge oder Betroffener eingetragen ist.
|
-- wird, sondern nur als Zeuge oder Betroffener eingetragen ist.
|
||||||
--
|
--
|
||||||
-- Das verrät nichts Neues: der Name eines Beteiligten steht ohnehin in der
|
-- Das verrät nichts Neues: der Name eines Beteiligten steht ohnehin in der
|
||||||
@@ -74,7 +74,7 @@ END;
|
|||||||
$$ LANGUAGE plpgsql;
|
$$ LANGUAGE plpgsql;
|
||||||
|
|
||||||
-- Der Verteiler kennt zwei neue Anlässe: eine geänderte Beteiligung, und einen
|
-- Der Verteiler kennt zwei neue Anlässe: eine geänderte Beteiligung, und einen
|
||||||
-- geänderten Namen. Beim Namen sind alle Akten der Person betroffen — sonst
|
-- geänderten Namen. Beim Namen sind alle Akten der Person betroffen - sonst
|
||||||
-- fände die Suche nach einer Umbenennung weiter den alten Namen.
|
-- fände die Suche nach einer Umbenennung weiter den alten Namen.
|
||||||
CREATE OR REPLACE FUNCTION search_index_trigger() RETURNS trigger AS $$
|
CREATE OR REPLACE FUNCTION search_index_trigger() RETURNS trigger AS $$
|
||||||
DECLARE
|
DECLARE
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
// AfiS-Aktensystem — Datenmodell
|
// AfiS-Aktensystem - Datenmodell
|
||||||
// Fachliche Grundlage: docs/04-datenmodell.md
|
// Fachliche Grundlage: docs/04-datenmodell.md
|
||||||
//
|
//
|
||||||
// Konventionen:
|
// Konventionen:
|
||||||
@@ -113,7 +113,7 @@ model RoleMapping {
|
|||||||
}
|
}
|
||||||
|
|
||||||
enum RoleCategory {
|
enum RoleCategory {
|
||||||
/// "AfiS", "Personalabteilung" — regelt, wer sich anmelden darf
|
/// "AfiS", "Personalabteilung" - regelt, wer sich anmelden darf
|
||||||
MEMBERSHIP
|
MEMBERSHIP
|
||||||
/// Dienstgrade, regeln die Rechte
|
/// Dienstgrade, regeln die Rechte
|
||||||
RANK
|
RANK
|
||||||
@@ -202,9 +202,9 @@ enum TemplateUse {
|
|||||||
|
|
||||||
/// Bauform des Dokuments, siehe docs/13-entscheidungen.md.
|
/// Bauform des Dokuments, siehe docs/13-entscheidungen.md.
|
||||||
enum DocumentLayout {
|
enum DocumentLayout {
|
||||||
/// beschriftete Felder, ausgefuellt — der Vorfallsbericht
|
/// beschriftete Felder, ausgefuellt - der Vorfallsbericht
|
||||||
FORM
|
FORM
|
||||||
/// fester Wortlaut mit Platzhaltern, mehrseitig — der Sonderarbeitsvertrag
|
/// fester Wortlaut mit Platzhaltern, mehrseitig - der Sonderarbeitsvertrag
|
||||||
PROSE
|
PROSE
|
||||||
/// ein einzelnes Freitextfeld, dargestellt wie ein Vermerk
|
/// ein einzelnes Freitextfeld, dargestellt wie ein Vermerk
|
||||||
NOTE
|
NOTE
|
||||||
@@ -415,7 +415,7 @@ model CaseParticipant {
|
|||||||
/// Zwei Gruppen, und der Unterschied entscheidet ueber den Zugriff: LEAD,
|
/// Zwei Gruppen, und der Unterschied entscheidet ueber den Zugriff: LEAD,
|
||||||
/// INVESTIGATOR und OBSERVER **arbeiten** an der Akte und duerfen sie deshalb
|
/// INVESTIGATOR und OBSERVER **arbeiten** an der Akte und duerfen sie deshalb
|
||||||
/// lesen. WITNESS, SUBJECT, SUSPECT und ACCOMPLICE sind Angaben **ueber** den
|
/// lesen. WITNESS, SUBJECT, SUSPECT und ACCOMPLICE sind Angaben **ueber** den
|
||||||
/// Fall und verschaffen keinen Zugang — sonst laese der Beschuldigte die
|
/// Fall und verschaffen keinen Zugang - sonst laese der Beschuldigte die
|
||||||
/// Ermittlung gegen sich selbst mit. Siehe lib/cases/terms.ts.
|
/// Ermittlung gegen sich selbst mit. Siehe lib/cases/terms.ts.
|
||||||
enum ParticipantRole {
|
enum ParticipantRole {
|
||||||
LEAD
|
LEAD
|
||||||
@@ -467,8 +467,8 @@ enum LinkKind {
|
|||||||
// Personalakten
|
// Personalakten
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
/// Akte zu einer Person. Ueberwiegend zu **Externen** — Sicherheitsdienst,
|
/// Akte zu einer Person. Ueberwiegend zu **Externen** - Sicherheitsdienst,
|
||||||
/// Zeugen, Vertragspartner — die kein Discord-Konto haben. `userId` bleibt
|
/// Zeugen, Vertragspartner - die kein Discord-Konto haben. `userId` bleibt
|
||||||
/// deshalb der Ausnahmefall, nicht die Regel.
|
/// deshalb der Ausnahmefall, nicht die Regel.
|
||||||
model PersonnelFile {
|
model PersonnelFile {
|
||||||
id String @id @default(uuid(7))
|
id String @id @default(uuid(7))
|
||||||
@@ -512,7 +512,7 @@ model PersonnelFile {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Auswahl der Status-Etiketten, im Adminbereich pflegbar. Die Namen sind
|
/// Auswahl der Status-Etiketten, im Adminbereich pflegbar. Die Namen sind
|
||||||
/// Daten, keine Oberflaechentexte — siehe die Sprachkonvention: Code englisch, Oberflaeche deutsch.
|
/// Daten, keine Oberflaechentexte - siehe die Sprachkonvention: Code englisch, Oberflaeche deutsch.
|
||||||
model PersonnelStatusTag {
|
model PersonnelStatusTag {
|
||||||
key String @id // "tot", "gekuendigt"
|
key String @id // "tot", "gekuendigt"
|
||||||
name String // "Tot", "Gekuendigt"
|
name String // "Tot", "Gekuendigt"
|
||||||
@@ -554,7 +554,7 @@ enum AssignmentRole {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Die Freigabestufen der Foundation, wie sie im Kopf eines Dokuments stehen.
|
/// Die Freigabestufen der Foundation, wie sie im Kopf eines Dokuments stehen.
|
||||||
/// **Sie steuern keinen Zugriff** — das tut allein ClearanceLevelConfig.
|
/// **Sie steuern keinen Zugriff** - das tut allein ClearanceLevelConfig.
|
||||||
/// Siehe docs/13-entscheidungen.md, "Zwei Freigaben im Kopf".
|
/// Siehe docs/13-entscheidungen.md, "Zwei Freigaben im Kopf".
|
||||||
model FoundationClearance {
|
model FoundationClearance {
|
||||||
level Int @id // 1..5
|
level Int @id // 1..5
|
||||||
@@ -849,7 +849,7 @@ model NotificationOutbox {
|
|||||||
///
|
///
|
||||||
/// Fast jede Person gehoert einer an, und die Abteilung ist die Ebene ueber
|
/// Fast jede Person gehoert einer an, und die Abteilung ist die Ebene ueber
|
||||||
/// der Personalakte: was zu ihr aktenkundig ist, ergibt sich aus ihren
|
/// der Personalakte: was zu ihr aktenkundig ist, ergibt sich aus ihren
|
||||||
/// Mitgliedern. Deshalb traegt eine Fallakte **keine** Abteilung — die
|
/// Mitgliedern. Deshalb traegt eine Fallakte **keine** Abteilung - die
|
||||||
/// Zuordnung entsteht ueber die Beteiligten und deren Personalakten und ist
|
/// Zuordnung entsteht ueber die Beteiligten und deren Personalakten und ist
|
||||||
/// damit immer aktuell. Siehe docs/13-entscheidungen.md.
|
/// damit immer aktuell. Siehe docs/13-entscheidungen.md.
|
||||||
model Department {
|
model Department {
|
||||||
@@ -881,7 +881,7 @@ model Department {
|
|||||||
/// Dieselbe Zusicherung wie beim Verlauf einer Akte, in schmalerer Form: es
|
/// Dieselbe Zusicherung wie beim Verlauf einer Akte, in schmalerer Form: es
|
||||||
/// gibt kein DELETE, jede Aenderung erzeugt eine Version, jede Zuruecknahme
|
/// gibt kein DELETE, jede Aenderung erzeugt eine Version, jede Zuruecknahme
|
||||||
/// laesst einen Grabstein stehen. Was fehlt, sind die Teile, die nur eine
|
/// laesst einen Grabstein stehen. Was fehlt, sind die Teile, die nur eine
|
||||||
/// Ermittlung braucht — Korrekturkette, Anhaenge, Karenzzeit. Ein
|
/// Ermittlung braucht - Korrekturkette, Anhaenge, Karenzzeit. Ein
|
||||||
/// Abteilungsvermerk ist ein laufendes Protokoll, kein Beweismittel.
|
/// Abteilungsvermerk ist ein laufendes Protokoll, kein Beweismittel.
|
||||||
model DepartmentEntry {
|
model DepartmentEntry {
|
||||||
id String @id // clientseitig erzeugt, UUIDv7
|
id String @id // clientseitig erzeugt, UUIDv7
|
||||||
@@ -926,7 +926,7 @@ model DepartmentEntryVersion {
|
|||||||
/// Der Vektor zur Stufe N enthaelt alles, was ein Leser dieser Stufe sehen
|
/// Der Vektor zur Stufe N enthaelt alles, was ein Leser dieser Stufe sehen
|
||||||
/// darf: Kopfdaten, Abschnitte und Eintraege bis einschliesslich N, und aus
|
/// darf: Kopfdaten, Abschnitte und Eintraege bis einschliesslich N, und aus
|
||||||
/// dem Fliesstext die geschwaerzten Stellen bis N. Eine Suche nimmt genau
|
/// dem Fliesstext die geschwaerzten Stellen bis N. Eine Suche nimmt genau
|
||||||
/// eine Zeile — die zur Stufe des Suchenden — und kann deshalb nichts finden,
|
/// eine Zeile - die zur Stufe des Suchenden - und kann deshalb nichts finden,
|
||||||
/// was er nicht lesen darf.
|
/// was er nicht lesen darf.
|
||||||
///
|
///
|
||||||
/// Der Vorgaenger war ein einziger Vektor mit nur dem, was jeder sehen darf.
|
/// Der Vorgaenger war ein einziger Vektor mit nur dem, was jeder sehen darf.
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ const CLEARANCE_LEVELS = [
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Startvorlagen nach docs/05-akten-und-vorlagen.md. Die Feldschlüssel sind
|
* Startvorlagen nach docs/05-akten-und-vorlagen.md. Die Feldschlüssel sind
|
||||||
* stabil und werden nie geändert — eine Akte merkt sich ihre Vorlage über
|
* stabil und werden nie geändert - eine Akte merkt sich ihre Vorlage über
|
||||||
* `templateId`, und eine neue Vorlagenversion lässt bestehende Akten unberührt.
|
* `templateId`, und eine neue Vorlagenversion lässt bestehende Akten unberührt.
|
||||||
*
|
*
|
||||||
* Der Zuschnitt der Felder ist bewusst schmal gehalten: den Vorlagen-Editor
|
* Der Zuschnitt der Felder ist bewusst schmal gehalten: den Vorlagen-Editor
|
||||||
@@ -75,7 +75,7 @@ const TEMPLATES: Vorlage[] = [
|
|||||||
|
|
||||||
const TEMPLATE_FIELDS = [
|
const TEMPLATE_FIELDS = [
|
||||||
// Nachgebaut nach dem Vorfallsbericht der Abteilung. Die Foundation-Freigabe
|
// Nachgebaut nach dem Vorfallsbericht der Abteilung. Die Foundation-Freigabe
|
||||||
// steht im Kopf des Dokuments, steuert aber keinen Zugriff — das tut allein
|
// steht im Kopf des Dokuments, steuert aber keinen Zugriff - das tut allein
|
||||||
// die interne Freigabe. Siehe docs/13-entscheidungen.md.
|
// die interne Freigabe. Siehe docs/13-entscheidungen.md.
|
||||||
{
|
{
|
||||||
key: 'foundation_freigabe',
|
key: 'foundation_freigabe',
|
||||||
@@ -200,7 +200,7 @@ const PERSONALAKTE_FIELDS = [
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Dokumentarten. Sie hängen an einer Fallakte, an einer Personalakte oder an
|
* Dokumentarten. Sie hängen an einer Fallakte, an einer Personalakte oder an
|
||||||
* nichts — was davon, steht in `uses`. Eine weitere Art anzulegen ist ein
|
* nichts - was davon, steht in `uses`. Eine weitere Art anzulegen ist ein
|
||||||
* Datensatz, kein Deployment: der Vorlagen-Editor reicht.
|
* Datensatz, kein Deployment: der Vorlagen-Editor reicht.
|
||||||
* Siehe docs/13-entscheidungen.md.
|
* Siehe docs/13-entscheidungen.md.
|
||||||
*/
|
*/
|
||||||
@@ -367,7 +367,7 @@ async function main(): Promise<void> {
|
|||||||
isActive: true,
|
isActive: true,
|
||||||
},
|
},
|
||||||
// Version 1 gehört dem Seed und wird von ihm aktuell gehalten. Ändert
|
// Version 1 gehört dem Seed und wird von ihm aktuell gehalten. Ändert
|
||||||
// der Leiter eine Vorlage im Editor, entsteht Version 2 — die fasst der
|
// der Leiter eine Vorlage im Editor, entsteht Version 2 - die fasst der
|
||||||
// Seed nie an, und bestehende Akten behalten ohnehin ihre Version.
|
// Seed nie an, und bestehende Akten behalten ohnehin ihre Version.
|
||||||
update: {
|
update: {
|
||||||
name: template.name,
|
name: template.name,
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import { resolveTestDatabaseUrl } from '../dist/testDatabase.js'
|
|||||||
* Wird von `pnpm test` vorgeschaltet.
|
* Wird von `pnpm test` vorgeschaltet.
|
||||||
*
|
*
|
||||||
* **Neu aufsetzen, nicht nur anlegen.** Verlaufseinträge, Akten und
|
* **Neu aufsetzen, nicht nur anlegen.** Verlaufseinträge, Akten und
|
||||||
* Protokollzeilen sind nach Zusicherung 2 der README nicht löschbar — was
|
* Protokollzeilen sind nach Zusicherung 2 der README nicht löschbar - was
|
||||||
* ein Testlauf anlegt, bleibt für immer stehen. Nach einigen Dutzend Läufen
|
* ein Testlauf anlegt, bleibt für immer stehen. Nach einigen Dutzend Läufen
|
||||||
* standen 2487 Akten in der Datenbank, und Tests, die eine auf zehn Einträge
|
* standen 2487 Akten in der Datenbank, und Tests, die eine auf zehn Einträge
|
||||||
* begrenzte Liste prüfen, fanden ihre eigene Akte nicht mehr darin. Solche
|
* begrenzte Liste prüfen, fanden ihre eigene Akte nicht mehr darin. Solche
|
||||||
@@ -45,7 +45,7 @@ try {
|
|||||||
const behalten = process.env.KEEP_TEST_DB === '1'
|
const behalten = process.env.KEEP_TEST_DB === '1'
|
||||||
|
|
||||||
if (rowCount > 0 && !behalten) {
|
if (rowCount > 0 && !behalten) {
|
||||||
// Offene Verbindungen verhindern das Verwerfen — etwa ein Prisma Studio
|
// Offene Verbindungen verhindern das Verwerfen - etwa ein Prisma Studio
|
||||||
// oder ein abgebrochener Testlauf.
|
// oder ein abgebrochener Testlauf.
|
||||||
await client.query(
|
await client.query(
|
||||||
`SELECT pg_terminate_backend(pid) FROM pg_stat_activity
|
`SELECT pg_terminate_backend(pid) FROM pg_stat_activity
|
||||||
@@ -87,7 +87,7 @@ execFileSync(process.execPath, [tsxCli, 'prisma/seed.ts'], {
|
|||||||
|
|
||||||
// Die Änderungsrichtlinie ist eine einzige Zeile für die ganze Anwendung. Der
|
// Die Änderungsrichtlinie ist eine einzige Zeile für die ganze Anwendung. Der
|
||||||
// Seed lässt sie bewusst in Ruhe, damit er die Einstellung des Leiters nicht
|
// Seed lässt sie bewusst in Ruhe, damit er die Einstellung des Leiters nicht
|
||||||
// überschreibt — in der Testdatenbank soll sie aber bei jedem Lauf im
|
// überschreibt - in der Testdatenbank soll sie aber bei jedem Lauf im
|
||||||
// Auslieferungszustand starten, sonst hängt das Ergebnis davon ab, was der
|
// Auslieferungszustand starten, sonst hängt das Ergebnis davon ab, was der
|
||||||
// vorige Lauf hinterlassen hat.
|
// vorige Lauf hinterlassen hat.
|
||||||
const reset = new pg.Client({ connectionString: testUrl })
|
const reset = new pg.Client({ connectionString: testUrl })
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ export function resolveTestDatabaseUrl(source = process.env.DATABASE_URL): strin
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Setzt die Umgebung eines Testlaufs auf die Testdatenbank um. Muss laufen,
|
* Setzt die Umgebung eines Testlaufs auf die Testdatenbank um. Muss laufen,
|
||||||
* bevor irgendein Modul einen Prisma-Client baut — deshalb gehört der Aufruf
|
* bevor irgendein Modul einen Prisma-Client baut - deshalb gehört der Aufruf
|
||||||
* in die `setupFiles` von Vitest, nicht in eine einzelne Testdatei.
|
* in die `setupFiles` von Vitest, nicht in eine einzelne Testdatei.
|
||||||
*/
|
*/
|
||||||
export function switchToTestDatabase(): string {
|
export function switchToTestDatabase(): string {
|
||||||
|
|||||||
@@ -48,7 +48,7 @@ suite('Auslieferungszustand', () => {
|
|||||||
// Der Seed vergibt keine Rechte. Rollen kommen ausschliesslich aus dem
|
// Der Seed vergibt keine Rechte. Rollen kommen ausschliesslich aus dem
|
||||||
// Abgleich des Workers und werden im Adminbereich zugeordnet.
|
// Abgleich des Workers und werden im Adminbereich zugeordnet.
|
||||||
// Geprüft wird deshalb, dass der Seed nichts hinterlässt, das Rechte trägt
|
// Geprüft wird deshalb, dass der Seed nichts hinterlässt, das Rechte trägt
|
||||||
// — nicht, dass die Tabelle leer ist: der Worker füllt sie im Betrieb.
|
// - nicht, dass die Tabelle leer ist: der Worker füllt sie im Betrieb.
|
||||||
const mitRechten = await db.roleMapping.findMany({
|
const mitRechten = await db.roleMapping.findMany({
|
||||||
where: { permissions: { isEmpty: false } },
|
where: { permissions: { isEmpty: false } },
|
||||||
select: { label: true, permissions: true },
|
select: { label: true, permissions: true },
|
||||||
|
|||||||
@@ -240,7 +240,7 @@ suite('Datenbankregeln', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
it('löst Schwärzungen im Suchindex stufenweise auf', async () => {
|
it('löst Schwärzungen im Suchindex stufenweise auf', async () => {
|
||||||
// Der Vektor zur Stufe N muss enthalten, was diese Stufe lesen darf — und
|
// Der Vektor zur Stufe N muss enthalten, was diese Stufe lesen darf - und
|
||||||
// nichts darüber. Sonst findet ein Leser der Stufe 4 einen Namen nicht,
|
// nichts darüber. Sonst findet ein Leser der Stufe 4 einen Namen nicht,
|
||||||
// den er auf der Seite sieht, oder ein Leser der Stufe 1 einen, den er
|
// den er auf der Seite sieht, oder ein Leser der Stufe 1 einen, den er
|
||||||
// dort nur als Balken sieht.
|
// dort nur als Balken sieht.
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import path from 'node:path'
|
|||||||
* Der Anlass war handfest: ein von Hand angelegter GIN-Index über den
|
* Der Anlass war handfest: ein von Hand angelegter GIN-Index über den
|
||||||
* Suchvektor stand in der Migration, aber nicht in `schema.prisma`. Prisma
|
* Suchvektor stand in der Migration, aber nicht in `schema.prisma`. Prisma
|
||||||
* hielt das für einen Rückstand und fragte bei jedem `migrate dev` nach einem
|
* hielt das für einen Rückstand und fragte bei jedem `migrate dev` nach einem
|
||||||
* Namen für eine neue Migration — deren Inhalt gewesen wäre, **den Suchindex
|
* Namen für eine neue Migration - deren Inhalt gewesen wäre, **den Suchindex
|
||||||
* zu löschen**. Wer die Frage in Eile beantwortet, verliert die gestufte
|
* zu löschen**. Wer die Frage in Eile beantwortet, verliert die gestufte
|
||||||
* Volltextsuche und merkt es erst, wenn die Suche leer bleibt.
|
* Volltextsuche und merkt es erst, wenn die Suche leer bleibt.
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
# Spielt eine Sicherung in eine Wegwerf-Datenbank ein und prüft, ob alles
|
# Spielt eine Sicherung in eine Wegwerf-Datenbank ein und prüft, ob alles
|
||||||
# angekommen ist. Ein ungetestetes Backup ist keins — siehe docs/11-betrieb.md.
|
# angekommen ist. Ein ungetestetes Backup ist keins - siehe docs/11-betrieb.md.
|
||||||
#
|
#
|
||||||
# scripts/restore-test.sh [container] [quelldatenbank]
|
# scripts/restore-test.sh [container] [quelldatenbank]
|
||||||
#
|
#
|
||||||
|
|||||||
@@ -98,7 +98,7 @@ mkdir -p "${FILES_DIR_LOKAL}/quarantine" "${FILES_DIR_LOKAL}/store"
|
|||||||
info "Dateiablage unter ${FILES_DIR_LOKAL}"
|
info "Dateiablage unter ${FILES_DIR_LOKAL}"
|
||||||
|
|
||||||
# Die Prüfkette braucht beides. Fehlt eines, bleiben die betroffenen Uploads in
|
# Die Prüfkette braucht beides. Fehlt eines, bleiben die betroffenen Uploads in
|
||||||
# der Quarantäne — sie gehen nicht ungeprüft durch. Siehe docs/07.
|
# der Quarantäne - sie gehen nicht ungeprüft durch. Siehe docs/07.
|
||||||
CLAMAV_SOCKET_LOKAL="$(grep -E '^CLAMAV_SOCKET=' .env 2>/dev/null | cut -d= -f2- || true)"
|
CLAMAV_SOCKET_LOKAL="$(grep -E '^CLAMAV_SOCKET=' .env 2>/dev/null | cut -d= -f2- || true)"
|
||||||
[ -S "${CLAMAV_SOCKET_LOKAL:-/var/run/clamav/clamd.sock}" ] \
|
[ -S "${CLAMAV_SOCKET_LOKAL:-/var/run/clamav/clamd.sock}" ] \
|
||||||
|| warn "Kein clamd erreichbar: Uploads landen auf 'Prüfung unvollständig' und brauchen die Freigabe von Hand"
|
|| warn "Kein clamd erreichbar: Uploads landen auf 'Prüfung unvollständig' und brauchen die Freigabe von Hand"
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { Skeleton, SkeletonLine } from '@/components/Skeleton'
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Skelett statt Spinner: die Struktur steht sofort, die Daten fallen hinein.
|
* Skelett statt Spinner: die Struktur steht sofort, die Daten fallen hinein.
|
||||||
* Ohne diese Grenze wartet die Navigation stumm, bis der Server fertig ist —
|
* Ohne diese Grenze wartet die Navigation stumm, bis der Server fertig ist -
|
||||||
* man klickt und es passiert scheinbar nichts. Siehe docs/09-performance-und-ux.md.
|
* man klickt und es passiert scheinbar nichts. Siehe docs/09-performance-und-ux.md.
|
||||||
*/
|
*/
|
||||||
export default function Loading() {
|
export default function Loading() {
|
||||||
|
|||||||
@@ -39,7 +39,7 @@ export async function generateMetadata({
|
|||||||
*
|
*
|
||||||
* Dasselbe Bild wie eine Personalakte, eine Stufe höher: wer gehört dazu, was
|
* Dasselbe Bild wie eine Personalakte, eine Stufe höher: wer gehört dazu, was
|
||||||
* ist dazu aktenkundig, was ist dazu vermerkt, und worüber wird geredet. Der
|
* ist dazu aktenkundig, was ist dazu vermerkt, und worüber wird geredet. Der
|
||||||
* Unterschied ist, dass hier **nichts eingetragen** wird — die Fälle ergeben
|
* Unterschied ist, dass hier **nichts eingetragen** wird - die Fälle ergeben
|
||||||
* sich aus den Beteiligten und deren Personalakten.
|
* sich aus den Beteiligten und deren Personalakten.
|
||||||
*/
|
*/
|
||||||
export default async function DepartmentPage({ params }: { params: Promise<{ key: string }> }) {
|
export default async function DepartmentPage({ params }: { params: Promise<{ key: string }> }) {
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ export const dynamic = 'force-dynamic'
|
|||||||
*
|
*
|
||||||
* Das Diagramm beantwortet die Frage, mit der man auf diese Seite kommt: zu
|
* Das Diagramm beantwortet die Frage, mit der man auf diese Seite kommt: zu
|
||||||
* welcher Abteilung gibt es die meisten Fälle. Gezählt wird nur, was der
|
* welcher Abteilung gibt es die meisten Fälle. Gezählt wird nur, was der
|
||||||
* Fragende auch sehen darf — eine Zahl, die mehr zählt als seine Liste
|
* Fragende auch sehen darf - eine Zahl, die mehr zählt als seine Liste
|
||||||
* hergibt, wäre selbst eine Auskunft.
|
* hergibt, wäre selbst eine Auskunft.
|
||||||
*/
|
*/
|
||||||
export default async function DepartmentsPage() {
|
export default async function DepartmentsPage() {
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ type Existing = {
|
|||||||
*
|
*
|
||||||
* Das Kürzel steht nach dem Anlegen fest: es steckt in der Adresse jeder
|
* Das Kürzel steht nach dem Anlegen fest: es steckt in der Adresse jeder
|
||||||
* Abteilungsseite, und ein wandernder Bezeichner macht geteilte Links wertlos.
|
* Abteilungsseite, und ein wandernder Bezeichner macht geteilte Links wertlos.
|
||||||
* Wer sich vertan hat, legt neu an und archiviert die alte — archivieren
|
* Wer sich vertan hat, legt neu an und archiviert die alte - archivieren
|
||||||
* statt löschen, weil an einer Abteilung Vermerke und Zuordnungen hängen.
|
* statt löschen, weil an einer Abteilung Vermerke und Zuordnungen hängen.
|
||||||
*/
|
*/
|
||||||
export function DepartmentForm({
|
export function DepartmentForm({
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { Skeleton, SkeletonLine } from '@/components/Skeleton'
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Skelett statt Spinner: die Struktur steht sofort, die Daten fallen hinein.
|
* Skelett statt Spinner: die Struktur steht sofort, die Daten fallen hinein.
|
||||||
* Ohne diese Grenze wartet die Navigation stumm, bis der Server fertig ist —
|
* Ohne diese Grenze wartet die Navigation stumm, bis der Server fertig ist -
|
||||||
* man klickt und es passiert scheinbar nichts. Siehe docs/09-performance-und-ux.md.
|
* man klickt und es passiert scheinbar nichts. Siehe docs/09-performance-und-ux.md.
|
||||||
*/
|
*/
|
||||||
export default function Loading() {
|
export default function Loading() {
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ export const dynamic = 'force-dynamic'
|
|||||||
*
|
*
|
||||||
* Wie bei der Fallakte sind die Bereiche **eigene Seiten** unter einem
|
* Wie bei der Fallakte sind die Bereiche **eigene Seiten** unter einem
|
||||||
* gemeinsamen Layout, keine umgeschalteten Blöcke. Wer gar keinen Bereich
|
* gemeinsamen Layout, keine umgeschalteten Blöcke. Wer gar keinen Bereich
|
||||||
* sehen darf, bekommt 404 — die Verwaltung ist für ihn nicht vorhanden.
|
* sehen darf, bekommt 404 - die Verwaltung ist für ihn nicht vorhanden.
|
||||||
*/
|
*/
|
||||||
export default async function AdminLayout({ children }: { children: ReactNode }) {
|
export default async function AdminLayout({ children }: { children: ReactNode }) {
|
||||||
const user = await requireUser()
|
const user = await requireUser()
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { Skeleton, SkeletonLine } from '@/components/Skeleton'
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Skelett statt Spinner: die Struktur steht sofort, die Daten fallen hinein.
|
* Skelett statt Spinner: die Struktur steht sofort, die Daten fallen hinein.
|
||||||
* Ohne diese Grenze wartet die Navigation stumm, bis der Server fertig ist —
|
* Ohne diese Grenze wartet die Navigation stumm, bis der Server fertig ist -
|
||||||
* man klickt und es passiert scheinbar nichts. Siehe docs/09-performance-und-ux.md.
|
* man klickt und es passiert scheinbar nichts. Siehe docs/09-performance-und-ux.md.
|
||||||
*/
|
*/
|
||||||
export default function Loading() {
|
export default function Loading() {
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ export const dynamic = 'force-dynamic'
|
|||||||
* Die Startseite der Verwaltung: was es gibt und wofür es da ist.
|
* Die Startseite der Verwaltung: was es gibt und wofür es da ist.
|
||||||
*
|
*
|
||||||
* Kein Weiterleiten auf den ersten Bereich. Wer die Verwaltung öffnet, weiss
|
* Kein Weiterleiten auf den ersten Bereich. Wer die Verwaltung öffnet, weiss
|
||||||
* nicht immer, wo das Gesuchte liegt — eine Liste mit einem Satz je Bereich
|
* nicht immer, wo das Gesuchte liegt - eine Liste mit einem Satz je Bereich
|
||||||
* beantwortet das schneller als das Durchklicken der Leiste.
|
* beantwortet das schneller als das Durchklicken der Leiste.
|
||||||
*/
|
*/
|
||||||
export default async function AdminIndexPage() {
|
export default async function AdminIndexPage() {
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { Skeleton, SkeletonLine } from '@/components/Skeleton'
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Skelett statt Spinner: die Struktur steht sofort, die Daten fallen hinein.
|
* Skelett statt Spinner: die Struktur steht sofort, die Daten fallen hinein.
|
||||||
* Ohne diese Grenze wartet die Navigation stumm, bis der Server fertig ist —
|
* Ohne diese Grenze wartet die Navigation stumm, bis der Server fertig ist -
|
||||||
* man klickt und es passiert scheinbar nichts. Siehe docs/09-performance-und-ux.md.
|
* man klickt und es passiert scheinbar nichts. Siehe docs/09-performance-und-ux.md.
|
||||||
*/
|
*/
|
||||||
export default function Loading() {
|
export default function Loading() {
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ export type PolicyResult = { ok: true } | { ok: false; error: string }
|
|||||||
/**
|
/**
|
||||||
* Die Änderungsrichtlinie gilt für die ganze Abteilung und wird serverseitig
|
* Die Änderungsrichtlinie gilt für die ganze Abteilung und wird serverseitig
|
||||||
* gelesen, nie aus dem Request. Deshalb ist das hier die einzige Stelle, an
|
* gelesen, nie aus dem Request. Deshalb ist das hier die einzige Stelle, an
|
||||||
* der sie sich ändern lässt — mit Recht `admin` und mit Protokolleintrag.
|
* der sie sich ändern lässt - mit Recht `admin` und mit Protokolleintrag.
|
||||||
*/
|
*/
|
||||||
export async function savePolicy(input: unknown): Promise<PolicyResult> {
|
export async function savePolicy(input: unknown): Promise<PolicyResult> {
|
||||||
const user = await requirePermissionForAction('admin')
|
const user = await requirePermissionForAction('admin')
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { Skeleton, SkeletonLine } from '@/components/Skeleton'
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Skelett statt Spinner: die Struktur steht sofort, die Daten fallen hinein.
|
* Skelett statt Spinner: die Struktur steht sofort, die Daten fallen hinein.
|
||||||
* Ohne diese Grenze wartet die Navigation stumm, bis der Server fertig ist —
|
* Ohne diese Grenze wartet die Navigation stumm, bis der Server fertig ist -
|
||||||
* man klickt und es passiert scheinbar nichts. Siehe docs/09-performance-und-ux.md.
|
* man klickt und es passiert scheinbar nichts. Siehe docs/09-performance-und-ux.md.
|
||||||
*/
|
*/
|
||||||
export default function Loading() {
|
export default function Loading() {
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { Skeleton, SkeletonLine } from '@/components/Skeleton'
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Skelett statt Spinner: die Struktur steht sofort, die Daten fallen hinein.
|
* Skelett statt Spinner: die Struktur steht sofort, die Daten fallen hinein.
|
||||||
* Ohne diese Grenze wartet die Navigation stumm, bis der Server fertig ist —
|
* Ohne diese Grenze wartet die Navigation stumm, bis der Server fertig ist -
|
||||||
* man klickt und es passiert scheinbar nichts. Siehe docs/09-performance-und-ux.md.
|
* man klickt und es passiert scheinbar nichts. Siehe docs/09-performance-und-ux.md.
|
||||||
*/
|
*/
|
||||||
export default function Loading() {
|
export default function Loading() {
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ export const dynamic = 'force-dynamic'
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Reihenfolge der Abschnitte: erst die Ränge, dann die Zugehörigkeit, dann die
|
* Reihenfolge der Abschnitte: erst die Ränge, dann die Zugehörigkeit, dann die
|
||||||
* Zugangsrollen — so, wie die Abteilung sie selbst aufzählt. Innerhalb eines
|
* Zugangsrollen - so, wie die Abteilung sie selbst aufzählt. Innerhalb eines
|
||||||
* Abschnitts entscheidet die Position der Rolle auf dem Discord-Server, oben
|
* Abschnitts entscheidet die Position der Rolle auf dem Discord-Server, oben
|
||||||
* die höchste. Damit steht die Hierarchie an einer Stelle, die ohnehin gepflegt
|
* die höchste. Damit steht die Hierarchie an einer Stelle, die ohnehin gepflegt
|
||||||
* wird, statt in einer zweiten Sortierung in dieser Anwendung.
|
* wird, statt in einer zweiten Sortierung in dieser Anwendung.
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import { de } from '@/lib/i18n/de'
|
|||||||
* Die Bereiche der Verwaltung, an einer Stelle.
|
* Die Bereiche der Verwaltung, an einer Stelle.
|
||||||
*
|
*
|
||||||
* Vorher standen Rollenverwaltung, Vorlagen und Änderungsrichtlinie als drei
|
* Vorher standen Rollenverwaltung, Vorlagen und Änderungsrichtlinie als drei
|
||||||
* eigene Einträge in der Kopfzeile — drei Einträge für etwas, das man selten
|
* eigene Einträge in der Kopfzeile - drei Einträge für etwas, das man selten
|
||||||
* und dann meist zusammenhängend braucht. Jetzt ist es ein Eintrag mit einer
|
* und dann meist zusammenhängend braucht. Jetzt ist es ein Eintrag mit einer
|
||||||
* Leiste darunter.
|
* Leiste darunter.
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { Skeleton, SkeletonLine } from '@/components/Skeleton'
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Skelett statt Spinner: die Struktur steht sofort, die Daten fallen hinein.
|
* Skelett statt Spinner: die Struktur steht sofort, die Daten fallen hinein.
|
||||||
* Ohne diese Grenze wartet die Navigation stumm, bis der Server fertig ist —
|
* Ohne diese Grenze wartet die Navigation stumm, bis der Server fertig ist -
|
||||||
* man klickt und es passiert scheinbar nichts. Siehe docs/09-performance-und-ux.md.
|
* man klickt und es passiert scheinbar nichts. Siehe docs/09-performance-und-ux.md.
|
||||||
*/
|
*/
|
||||||
export default function Loading() {
|
export default function Loading() {
|
||||||
|
|||||||
@@ -97,7 +97,7 @@ export default async function SystemPage() {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Die Ladewerte der letzten sieben Tage. Median und 75. Perzentil, kein
|
* Die Ladewerte der letzten sieben Tage. Median und 75. Perzentil, kein
|
||||||
* Mittelwert — ein Ausreisser auf einem alten Laptop soll die Zahl nicht
|
* Mittelwert - ein Ausreisser auf einem alten Laptop soll die Zahl nicht
|
||||||
* bestimmen.
|
* bestimmen.
|
||||||
*/
|
*/
|
||||||
async function VitalsTable() {
|
async function VitalsTable() {
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { Skeleton, SkeletonLine } from '@/components/Skeleton'
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Skelett statt Spinner: die Struktur steht sofort, die Daten fallen hinein.
|
* Skelett statt Spinner: die Struktur steht sofort, die Daten fallen hinein.
|
||||||
* Ohne diese Grenze wartet die Navigation stumm, bis der Server fertig ist —
|
* Ohne diese Grenze wartet die Navigation stumm, bis der Server fertig ist -
|
||||||
* man klickt und es passiert scheinbar nichts. Siehe docs/09-performance-und-ux.md.
|
* man klickt und es passiert scheinbar nichts. Siehe docs/09-performance-und-ux.md.
|
||||||
*/
|
*/
|
||||||
export default function Loading() {
|
export default function Loading() {
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ const schema = z.object({ id: z.string().min(1) })
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Freigabe einer Datei, deren Prüfung nicht abgeschlossen werden konnte.
|
* Freigabe einer Datei, deren Prüfung nicht abgeschlossen werden konnte.
|
||||||
* Nur mit Recht `admin`, geprüft im Dienst — nicht hier.
|
* Nur mit Recht `admin`, geprüft im Dienst - nicht hier.
|
||||||
*/
|
*/
|
||||||
export async function releaseAttachmentAction(input: unknown): Promise<ActionResult> {
|
export async function releaseAttachmentAction(input: unknown): Promise<ActionResult> {
|
||||||
const user = await requireUser()
|
const user = await requireUser()
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ import {
|
|||||||
* Ausfertigen, Zurückziehen, Umklassifizieren.
|
* Ausfertigen, Zurückziehen, Umklassifizieren.
|
||||||
*
|
*
|
||||||
* Wie beim Fall bewusst **nicht** optimistisch: alle drei sind folgenreich.
|
* Wie beim Fall bewusst **nicht** optimistisch: alle drei sind folgenreich.
|
||||||
* Das Ausfertigen fragt nach, das Zurückziehen verlangt eine Begründung — es
|
* Das Ausfertigen fragt nach, das Zurückziehen verlangt eine Begründung - es
|
||||||
* gibt kein Löschen, der Grabstein bleibt stehen.
|
* gibt kein Löschen, der Grabstein bleibt stehen.
|
||||||
*/
|
*/
|
||||||
export function DocumentActions({
|
export function DocumentActions({
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { Skeleton, SkeletonLine } from '@/components/Skeleton'
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Skelett statt Spinner: die Struktur steht sofort, die Daten fallen hinein.
|
* Skelett statt Spinner: die Struktur steht sofort, die Daten fallen hinein.
|
||||||
* Ohne diese Grenze wartet die Navigation stumm, bis der Server fertig ist —
|
* Ohne diese Grenze wartet die Navigation stumm, bis der Server fertig ist -
|
||||||
* man klickt und es passiert scheinbar nichts. Siehe docs/09-performance-und-ux.md.
|
* man klickt und es passiert scheinbar nichts. Siehe docs/09-performance-und-ux.md.
|
||||||
*/
|
*/
|
||||||
export default function Loading() {
|
export default function Loading() {
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ export type ActionResult = { ok: true } | { ok: false; error: string }
|
|||||||
|
|
||||||
const schema = z.object({ notifyMode: z.enum(['ALL', 'OWN_CASES', 'MENTIONS', 'OFF']) })
|
const schema = z.object({ notifyMode: z.enum(['ALL', 'OWN_CASES', 'MENTIONS', 'OFF']) })
|
||||||
|
|
||||||
/** Jeder stellt nur für sich selbst ein — die Kennung kommt aus der Sitzung. */
|
/** Jeder stellt nur für sich selbst ein - die Kennung kommt aus der Sitzung. */
|
||||||
export async function saveNotifyModeAction(
|
export async function saveNotifyModeAction(
|
||||||
_prev: ActionResult | null,
|
_prev: ActionResult | null,
|
||||||
formData: FormData,
|
formData: FormData,
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { Skeleton } from '@/components/Skeleton'
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Skelett statt Spinner: die Struktur steht sofort, die Daten fallen hinein.
|
* Skelett statt Spinner: die Struktur steht sofort, die Daten fallen hinein.
|
||||||
* Ohne diese Grenze wartet die Navigation stumm, bis der Server fertig ist —
|
* Ohne diese Grenze wartet die Navigation stumm, bis der Server fertig ist -
|
||||||
* man klickt und es passiert scheinbar nichts. Siehe docs/09-performance-und-ux.md.
|
* man klickt und es passiert scheinbar nichts. Siehe docs/09-performance-und-ux.md.
|
||||||
*/
|
*/
|
||||||
export default function Loading() {
|
export default function Loading() {
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ const requestSchema = z.union([
|
|||||||
/**
|
/**
|
||||||
* Export anfordern.
|
* Export anfordern.
|
||||||
*
|
*
|
||||||
* Der Auftrag läuft im Hintergrund weiter, nachdem die Antwort raus ist — das
|
* Der Auftrag läuft im Hintergrund weiter, nachdem die Antwort raus ist - das
|
||||||
* Rendern dauert, und niemand soll auf einen Fortschrittsbalken starren, der
|
* Rendern dauert, und niemand soll auf einen Fortschrittsbalken starren, der
|
||||||
* auf Playwright wartet. Siehe docs/09-performance-und-ux.md.
|
* auf Playwright wartet. Siehe docs/09-performance-und-ux.md.
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -90,7 +90,7 @@ export function CaseActions({
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
{/* Welche Zustände von hier aus erreichbar sind, ergibt sich aus dem
|
{/* Welche Zustände von hier aus erreichbar sind, ergibt sich aus dem
|
||||||
Lebenszyklus — aus einem Entwurf führt nur „Offen" heraus. Ohne
|
Lebenszyklus - aus einem Entwurf führt nur „Offen" heraus. Ohne
|
||||||
diesen Satz sieht die kurze Auswahl aus wie ein Fehler. */}
|
diesen Satz sieht die kurze Auswahl aus wie ein Fehler. */}
|
||||||
<p className="mt-1.5 max-w-sm text-2xs text-text-dim">
|
<p className="mt-1.5 max-w-sm text-2xs text-text-dim">
|
||||||
{caseText.statusHints[nextStatus] ?? caseText.statusChangeHint}
|
{caseText.statusHints[nextStatus] ?? caseText.statusChangeHint}
|
||||||
|
|||||||
@@ -113,7 +113,7 @@ export function Participants({
|
|||||||
))}
|
))}
|
||||||
</PanelSelect>
|
</PanelSelect>
|
||||||
<PanelSelect value={target} onChange={setTarget} label={participantText.person}>
|
<PanelSelect value={target} onChange={setTarget} label={participantText.person}>
|
||||||
<option value="">—</option>
|
<option value="">-</option>
|
||||||
<optgroup label={participantText.pickUser}>
|
<optgroup label={participantText.pickUser}>
|
||||||
{members.map((member) => (
|
{members.map((member) => (
|
||||||
<option key={member.id} value={`user:${member.id}`}>
|
<option key={member.id} value={`user:${member.id}`}>
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import { toggleWatchAction } from '../actions'
|
|||||||
/**
|
/**
|
||||||
* Akte beobachten.
|
* Akte beobachten.
|
||||||
*
|
*
|
||||||
* Sofortiges Umschalten, der Server bestätigt nur — misslingt es, springt der
|
* Sofortiges Umschalten, der Server bestätigt nur - misslingt es, springt der
|
||||||
* Zustand zurück. Beobachten ist umkehrbar und harmlos, dafür braucht es
|
* Zustand zurück. Beobachten ist umkehrbar und harmlos, dafür braucht es
|
||||||
* keinen Wartezustand. Siehe docs/09-performance-und-ux.md.
|
* keinen Wartezustand. Siehe docs/09-performance-und-ux.md.
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { Skeleton, SkeletonLine } from '@/components/Skeleton'
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Skelett statt Spinner: die Struktur steht sofort, die Daten fallen hinein.
|
* Skelett statt Spinner: die Struktur steht sofort, die Daten fallen hinein.
|
||||||
* Ohne diese Grenze wartet die Navigation stumm, bis der Server fertig ist —
|
* Ohne diese Grenze wartet die Navigation stumm, bis der Server fertig ist -
|
||||||
* man klickt und es passiert scheinbar nichts. Siehe docs/09-performance-und-ux.md.
|
* man klickt und es passiert scheinbar nichts. Siehe docs/09-performance-und-ux.md.
|
||||||
*/
|
*/
|
||||||
export default function Loading() {
|
export default function Loading() {
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ export const dynamic = 'force-dynamic'
|
|||||||
*
|
*
|
||||||
* Für die Abteilungsleitung ist das die Ansicht, mit der sie im Zweifel prüft,
|
* Für die Abteilungsleitung ist das die Ansicht, mit der sie im Zweifel prüft,
|
||||||
* ob an einer Akte nachträglich gedreht wurde. Sichtbar für alle, die die Akte
|
* ob an einer Akte nachträglich gedreht wurde. Sichtbar für alle, die die Akte
|
||||||
* lesen dürfen — nicht nur für die Leitung. Das ist der ganze Sinn der Sache.
|
* lesen dürfen - nicht nur für die Leitung. Das ist der ganze Sinn der Sache.
|
||||||
*/
|
*/
|
||||||
export default async function ChangesPage({
|
export default async function ChangesPage({
|
||||||
params,
|
params,
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { Skeleton, SkeletonLine } from '@/components/Skeleton'
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Skelett statt Spinner: die Struktur steht sofort, die Daten fallen hinein.
|
* Skelett statt Spinner: die Struktur steht sofort, die Daten fallen hinein.
|
||||||
* Ohne diese Grenze wartet die Navigation stumm, bis der Server fertig ist —
|
* Ohne diese Grenze wartet die Navigation stumm, bis der Server fertig ist -
|
||||||
* man klickt und es passiert scheinbar nichts. Siehe docs/09-performance-und-ux.md.
|
* man klickt und es passiert scheinbar nichts. Siehe docs/09-performance-und-ux.md.
|
||||||
*/
|
*/
|
||||||
export default function Loading() {
|
export default function Loading() {
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { Skeleton, SkeletonLine } from '@/components/Skeleton'
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Skelett statt Spinner: die Struktur steht sofort, die Daten fallen hinein.
|
* Skelett statt Spinner: die Struktur steht sofort, die Daten fallen hinein.
|
||||||
* Ohne diese Grenze wartet die Navigation stumm, bis der Server fertig ist —
|
* Ohne diese Grenze wartet die Navigation stumm, bis der Server fertig ist -
|
||||||
* man klickt und es passiert scheinbar nichts. Siehe docs/09-performance-und-ux.md.
|
* man klickt und es passiert scheinbar nichts. Siehe docs/09-performance-und-ux.md.
|
||||||
*/
|
*/
|
||||||
export default function Loading() {
|
export default function Loading() {
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { Skeleton, SkeletonLine } from '@/components/Skeleton'
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Skelett statt Spinner: die Struktur steht sofort, die Daten fallen hinein.
|
* Skelett statt Spinner: die Struktur steht sofort, die Daten fallen hinein.
|
||||||
* Ohne diese Grenze wartet die Navigation stumm, bis der Server fertig ist —
|
* Ohne diese Grenze wartet die Navigation stumm, bis der Server fertig ist -
|
||||||
* man klickt und es passiert scheinbar nichts. Siehe docs/09-performance-und-ux.md.
|
* man klickt und es passiert scheinbar nichts. Siehe docs/09-performance-und-ux.md.
|
||||||
*/
|
*/
|
||||||
export default function Loading() {
|
export default function Loading() {
|
||||||
|
|||||||
@@ -20,12 +20,12 @@ export const dynamic = 'force-dynamic'
|
|||||||
* Die Akte ist in Reiter geteilt, und zwar in **eigene Seiten**, nicht in
|
* Die Akte ist in Reiter geteilt, und zwar in **eigene Seiten**, nicht in
|
||||||
* umgeschaltete Blöcke: nur so wird der Code getrennt geladen. Die Fallakte
|
* umgeschaltete Blöcke: nur so wird der Code getrennt geladen. Die Fallakte
|
||||||
* war die dichteste Seite der Anwendung und hatte das Bündelbudget aus
|
* war die dichteste Seite der Anwendung und hatte das Bündelbudget aus
|
||||||
* docs/09-performance-und-ux.md aufgebraucht — Beteiligte, Verknüpfungen,
|
* docs/09-performance-und-ux.md aufgebraucht - Beteiligte, Verknüpfungen,
|
||||||
* Dokumente und Anhänge lagen mit auf der Seite, obwohl man sie selten
|
* Dokumente und Anhänge lagen mit auf der Seite, obwohl man sie selten
|
||||||
* gleichzeitig braucht.
|
* gleichzeitig braucht.
|
||||||
*
|
*
|
||||||
* Fachlich passt der Schnitt ohnehin: die drei Ebenen aus
|
* Fachlich passt der Schnitt ohnehin: die drei Ebenen aus
|
||||||
* docs/05-akten-und-vorlagen.md — Kopfdaten, Abschnitte, Verlauf — bleiben
|
* docs/05-akten-und-vorlagen.md - Kopfdaten, Abschnitte, Verlauf - bleiben
|
||||||
* zusammen, das Umfeld der Akte bekommt seinen eigenen Platz.
|
* zusammen, das Umfeld der Akte bekommt seinen eigenen Platz.
|
||||||
*/
|
*/
|
||||||
export default async function CaseLayout({
|
export default async function CaseLayout({
|
||||||
|
|||||||
@@ -185,7 +185,7 @@ async function ChatBlock({
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Der Reiter „Akte": die drei Ebenen aus docs/05-akten-und-vorlagen.md, die
|
* Der Reiter „Akte": die drei Ebenen aus docs/05-akten-und-vorlagen.md, die
|
||||||
* zusammengehören — Abschnitte als Sachstand, Verlauf als Chronologie, dazu
|
* zusammengehören - Abschnitte als Sachstand, Verlauf als Chronologie, dazu
|
||||||
* der formlose Aktenchat. Kopfdaten und Reiterleiste stehen im Layout.
|
* der formlose Aktenchat. Kopfdaten und Reiterleiste stehen im Layout.
|
||||||
*/
|
*/
|
||||||
export default async function CasePage({
|
export default async function CasePage({
|
||||||
|
|||||||
@@ -51,7 +51,7 @@ export async function saveSectionAction(
|
|||||||
/**
|
/**
|
||||||
* Mehrere Abschnitte in einem Zug.
|
* Mehrere Abschnitte in einem Zug.
|
||||||
*
|
*
|
||||||
* Jedes Feld wird einzeln geschrieben und einzeln beantwortet — Abschnitte
|
* Jedes Feld wird einzeln geschrieben und einzeln beantwortet - Abschnitte
|
||||||
* sind einzeln versioniert, und ein zwischenzeitlich geändertes Feld soll die
|
* sind einzeln versioniert, und ein zwischenzeitlich geändertes Feld soll die
|
||||||
* übrigen nicht aufhalten. Wäre das eine Transaktion, blockierte ein einziger
|
* übrigen nicht aufhalten. Wäre das eine Transaktion, blockierte ein einziger
|
||||||
* fremder Eingriff das ganze Blatt.
|
* fremder Eingriff das ganze Blatt.
|
||||||
|
|||||||
@@ -66,7 +66,7 @@ export async function createCaseAction(
|
|||||||
caseNumber = created.caseNumber
|
caseNumber = created.caseNumber
|
||||||
|
|
||||||
// Die Beteiligten gleich mitverknüpfen, damit sie nicht in einem zweiten
|
// Die Beteiligten gleich mitverknüpfen, damit sie nicht in einem zweiten
|
||||||
// Schritt vergessen werden. Scheitert einer, steht die Akte trotzdem — sie
|
// Schritt vergessen werden. Scheitert einer, steht die Akte trotzdem - sie
|
||||||
// ist das Wichtigere, und nachtragen kann man auf der Aktenseite.
|
// ist das Wichtigere, und nachtragen kann man auf der Aktenseite.
|
||||||
for (const eintrag of beteiligte) {
|
for (const eintrag of beteiligte) {
|
||||||
await addParticipant(user.permissions, {
|
await addParticipant(user.permissions, {
|
||||||
@@ -294,7 +294,7 @@ const watchSchema = z.object({ caseNumber: z.string().min(1) })
|
|||||||
|
|
||||||
export type WatchResult = { ok: true; watching: boolean } | { ok: false; error: string }
|
export type WatchResult = { ok: true; watching: boolean } | { ok: false; error: string }
|
||||||
|
|
||||||
/** Beobachten verschafft keinen Zugriff — nur was sichtbar ist, lässt sich merken. */
|
/** Beobachten verschafft keinen Zugriff - nur was sichtbar ist, lässt sich merken. */
|
||||||
export async function toggleWatchAction(input: unknown): Promise<WatchResult> {
|
export async function toggleWatchAction(input: unknown): Promise<WatchResult> {
|
||||||
const user = await requireUser()
|
const user = await requireUser()
|
||||||
const parsed = watchSchema.safeParse(input)
|
const parsed = watchSchema.safeParse(input)
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ export function NewCaseForm({
|
|||||||
const [state, action, pending] = useActionState<CreateState, FormData>(createCaseAction, {})
|
const [state, action, pending] = useActionState<CreateState, FormData>(createCaseAction, {})
|
||||||
|
|
||||||
// Beliebig viele Beteiligte, jeder mit seiner Rolle. Vorher gab es genau
|
// Beliebig viele Beteiligte, jeder mit seiner Rolle. Vorher gab es genau
|
||||||
// eine „betroffene Person" — wer einen Zeugen und einen Täter eintragen
|
// eine „betroffene Person" - wer einen Zeugen und einen Täter eintragen
|
||||||
// wollte, musste die Akte erst anlegen und dann noch einmal hinein.
|
// wollte, musste die Akte erst anlegen und dann noch einmal hinein.
|
||||||
const [beteiligte, setBeteiligte] = useState<Entwurf[]>([])
|
const [beteiligte, setBeteiligte] = useState<Entwurf[]>([])
|
||||||
const [naechster, setNaechster] = useState(1)
|
const [naechster, setNaechster] = useState(1)
|
||||||
@@ -133,7 +133,7 @@ export function NewCaseForm({
|
|||||||
onChange={(event) => aendern(eintrag.schluessel, { ziel: event.target.value })}
|
onChange={(event) => aendern(eintrag.schluessel, { ziel: event.target.value })}
|
||||||
className="field grow"
|
className="field grow"
|
||||||
>
|
>
|
||||||
<option value="">—</option>
|
<option value="">-</option>
|
||||||
<optgroup label={participantText.pickUser}>
|
<optgroup label={participantText.pickUser}>
|
||||||
{members.map((member) => (
|
{members.map((member) => (
|
||||||
<option key={member.id} value={`user:${member.id}`}>
|
<option key={member.id} value={`user:${member.id}`}>
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { Skeleton } from '@/components/Skeleton'
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Skelett statt Spinner: die Struktur steht sofort, die Daten fallen hinein.
|
* Skelett statt Spinner: die Struktur steht sofort, die Daten fallen hinein.
|
||||||
* Ohne diese Grenze wartet die Navigation stumm, bis der Server fertig ist —
|
* Ohne diese Grenze wartet die Navigation stumm, bis der Server fertig ist -
|
||||||
* man klickt und es passiert scheinbar nichts. Siehe docs/09-performance-und-ux.md.
|
* man klickt und es passiert scheinbar nichts. Siehe docs/09-performance-und-ux.md.
|
||||||
*/
|
*/
|
||||||
export default function Loading() {
|
export default function Loading() {
|
||||||
|
|||||||
@@ -82,7 +82,7 @@ export default async function CaseListPage({
|
|||||||
</Link>
|
</Link>
|
||||||
{/* Alle sieben Zustände, nicht fünf: „Entwurf" und „Versiegelt" fehlten,
|
{/* Alle sieben Zustände, nicht fünf: „Entwurf" und „Versiegelt" fehlten,
|
||||||
und das waren ausgerechnet die beiden, die in der Liste am häufigsten
|
und das waren ausgerechnet die beiden, die in der Liste am häufigsten
|
||||||
standen — man konnte nach dem filtern, was man sah, gerade nicht.
|
standen - man konnte nach dem filtern, was man sah, gerade nicht.
|
||||||
Versiegelt nur für den, der Versiegeltes überhaupt lesen darf. */}
|
Versiegelt nur für den, der Versiegeltes überhaupt lesen darf. */}
|
||||||
{CASE_STATUSES.filter(
|
{CASE_STATUSES.filter(
|
||||||
(value) =>
|
(value) =>
|
||||||
|
|||||||
@@ -13,8 +13,8 @@ import { hasAdminAccess } from './admin/sections'
|
|||||||
/**
|
/**
|
||||||
* Das Rahmenwerk der angemeldeten Anwendung.
|
* Das Rahmenwerk der angemeldeten Anwendung.
|
||||||
*
|
*
|
||||||
* Die Kopfzeile trägt links die Bereiche der Arbeit — Übersicht, Fallakten,
|
* Die Kopfzeile trägt links die Bereiche der Arbeit - Übersicht, Fallakten,
|
||||||
* Personalakten, Verwaltung — und rechts alles, was den Nutzer selbst
|
* Personalakten, Verwaltung - und rechts alles, was den Nutzer selbst
|
||||||
* betrifft: Suchbalken und Profilbild. Thema, Benachrichtigungen und Abmelden
|
* betrifft: Suchbalken und Profilbild. Thema, Benachrichtigungen und Abmelden
|
||||||
* standen früher zwischen den Aktenbereichen und nahmen dort den Platz weg,
|
* standen früher zwischen den Aktenbereichen und nahmen dort den Platz weg,
|
||||||
* den die Arbeit braucht; sie sitzen jetzt hinter dem Profilbild.
|
* den die Arbeit braucht; sie sitzen jetzt hinter dem Profilbild.
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ type Zuordnung = {
|
|||||||
* Mitglieder der Abteilung, die an dieser Personalakte hängen.
|
* Mitglieder der Abteilung, die an dieser Personalakte hängen.
|
||||||
*
|
*
|
||||||
* Wie die Beteiligten einer Fallakte ist das zugleich der Weg zum
|
* Wie die Beteiligten einer Fallakte ist das zugleich der Weg zum
|
||||||
* ausnahmsweisen Zugang — der Hinweis steht deshalb direkt am Formular.
|
* ausnahmsweisen Zugang - der Hinweis steht deshalb direkt am Formular.
|
||||||
*/
|
*/
|
||||||
export function Assignments({
|
export function Assignments({
|
||||||
personnelFileId,
|
personnelFileId,
|
||||||
@@ -94,7 +94,7 @@ export function Assignments({
|
|||||||
))}
|
))}
|
||||||
</PanelSelect>
|
</PanelSelect>
|
||||||
<PanelSelect value={userId} onChange={setUserId} label={participantText.pickUser}>
|
<PanelSelect value={userId} onChange={setUserId} label={participantText.pickUser}>
|
||||||
<option value="">—</option>
|
<option value="">-</option>
|
||||||
{members.map((member) => (
|
{members.map((member) => (
|
||||||
<option key={member.id} value={member.id}>
|
<option key={member.id} value={member.id}>
|
||||||
{member.displayName}
|
{member.displayName}
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ import { linkCaseToPersonAction } from '../actions'
|
|||||||
*
|
*
|
||||||
* Derselbe Vorgang wie „Beteiligte eintragen" auf der Fallakte, nur von der
|
* Derselbe Vorgang wie „Beteiligte eintragen" auf der Fallakte, nur von der
|
||||||
* anderen Seite gedacht: dort sucht man die Person, hier den Fall. Deshalb ein
|
* anderen Seite gedacht: dort sucht man die Person, hier den Fall. Deshalb ein
|
||||||
* Eingabefeld für das Aktenzeichen und keine Auswahlliste — eine Liste aller
|
* Eingabefeld für das Aktenzeichen und keine Auswahlliste - eine Liste aller
|
||||||
* sichtbaren Fälle wäre auf einer Personalakte am falschen Ort.
|
* sichtbaren Fälle wäre auf einer Personalakte am falschen Ort.
|
||||||
*/
|
*/
|
||||||
export function LinkCase({ personnelFileId }: { personnelFileId: string }) {
|
export function LinkCase({ personnelFileId }: { personnelFileId: string }) {
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ type File = {
|
|||||||
* Kopfdaten einer Personalakte: lesen und ändern in derselben Ansicht.
|
* Kopfdaten einer Personalakte: lesen und ändern in derselben Ansicht.
|
||||||
*
|
*
|
||||||
* Die Klassifizierung steht bewusst getrennt. Sie ändert, wer die Akte
|
* Die Klassifizierung steht bewusst getrennt. Sie ändert, wer die Akte
|
||||||
* überhaupt sieht, und eine Herabstufung braucht eine Begründung — das gehört
|
* überhaupt sieht, und eine Herabstufung braucht eine Begründung - das gehört
|
||||||
* nicht in dasselbe Formular wie eine Korrektur der Dienstnummer.
|
* nicht in dasselbe Formular wie eine Korrektur der Dienstnummer.
|
||||||
*/
|
*/
|
||||||
export function PersonnelHead({
|
export function PersonnelHead({
|
||||||
@@ -276,7 +276,7 @@ function Zeile({
|
|||||||
return (
|
return (
|
||||||
<div className="flex gap-3">
|
<div className="flex gap-3">
|
||||||
<dt className="font-condensed text-xs text-text-dim">{label}</dt>
|
<dt className="font-condensed text-xs text-text-dim">{label}</dt>
|
||||||
<dd className={mono ? 'font-mono text-xs' : ''}>{value || '—'}</dd>
|
<dd className={mono ? 'font-mono text-xs' : ''}>{value || '-'}</dd>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -171,7 +171,7 @@ async function AssignmentBlock({
|
|||||||
* dass das Gespräch bei ihrer Akte steht, war genau das, was vorher auf
|
* dass das Gespräch bei ihrer Akte steht, war genau das, was vorher auf
|
||||||
* Discord passiert ist. Siehe docs/13-entscheidungen.md.
|
* Discord passiert ist. Siehe docs/13-entscheidungen.md.
|
||||||
*
|
*
|
||||||
* Übernehmen als Vermerk gibt es hier nicht — dafür braucht es einen Verlauf,
|
* Übernehmen als Vermerk gibt es hier nicht - dafür braucht es einen Verlauf,
|
||||||
* und den führt nur die Fallakte.
|
* und den führt nur die Fallakte.
|
||||||
*/
|
*/
|
||||||
async function ChatBlock({
|
async function ChatBlock({
|
||||||
|
|||||||
@@ -71,7 +71,7 @@ export async function createPersonnelAction(
|
|||||||
id = created.id
|
id = created.id
|
||||||
|
|
||||||
// Das Aktenblatt ist der erste Eintrag zu einer Person und entsteht
|
// Das Aktenblatt ist der erste Eintrag zu einer Person und entsteht
|
||||||
// deshalb gleich mit — als Entwurf, mit dem Namen schon eingetragen.
|
// deshalb gleich mit - als Entwurf, mit dem Namen schon eingetragen.
|
||||||
// Fehlt das Recht dafür, bleibt es bei der Akte allein.
|
// Fehlt das Recht dafür, bleibt es bei der Akte allein.
|
||||||
await erstesAktenblatt(user.permissions, id, parsed.data.displayName)
|
await erstesAktenblatt(user.permissions, id, parsed.data.displayName)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|||||||
@@ -8,8 +8,8 @@ import { createPersonnelAction, type ActionResult } from '../actions'
|
|||||||
/**
|
/**
|
||||||
* Neue Personalakte.
|
* Neue Personalakte.
|
||||||
*
|
*
|
||||||
* Das Discord-Konto ist **optional**. Personen ohne Konto — Zeugen, ehemalige
|
* Das Discord-Konto ist **optional**. Personen ohne Konto - Zeugen, ehemalige
|
||||||
* Mitarbeiter, Externe — brauchen genauso eine Akte, und ihr Name ist dann das
|
* Mitarbeiter, Externe - brauchen genauso eine Akte, und ihr Name ist dann das
|
||||||
* einzige, was sie identifiziert.
|
* einzige, was sie identifiziert.
|
||||||
*/
|
*/
|
||||||
export function NewPersonnelForm({
|
export function NewPersonnelForm({
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { Skeleton } from '@/components/Skeleton'
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Skelett statt Spinner: die Struktur steht sofort, die Daten fallen hinein.
|
* Skelett statt Spinner: die Struktur steht sofort, die Daten fallen hinein.
|
||||||
* Ohne diese Grenze wartet die Navigation stumm, bis der Server fertig ist —
|
* Ohne diese Grenze wartet die Navigation stumm, bis der Server fertig ist -
|
||||||
* man klickt und es passiert scheinbar nichts. Siehe docs/09-performance-und-ux.md.
|
* man klickt und es passiert scheinbar nichts. Siehe docs/09-performance-und-ux.md.
|
||||||
*/
|
*/
|
||||||
export default function Loading() {
|
export default function Loading() {
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ export default async function PersonnelListPage({
|
|||||||
}: {
|
}: {
|
||||||
searchParams: Promise<{ q?: string; status?: string }>
|
searchParams: Promise<{ q?: string; status?: string }>
|
||||||
}) {
|
}) {
|
||||||
// Ohne das Recht gibt es die Seite nicht — 404, nicht 403.
|
// Ohne das Recht gibt es die Seite nicht - 404, nicht 403.
|
||||||
const user = await requirePermission('personnel.read')
|
const user = await requirePermission('personnel.read')
|
||||||
const { q, status } = await searchParams
|
const { q, status } = await searchParams
|
||||||
|
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import { deleteViewAction, saveViewAction } from './actions'
|
|||||||
/**
|
/**
|
||||||
* Ansichten: die mitgelieferten und die eigenen.
|
* Ansichten: die mitgelieferten und die eigenen.
|
||||||
*
|
*
|
||||||
* Beide sind dasselbe — eine Suchadresse. Die mitgelieferten stehen im
|
* Beide sind dasselbe - eine Suchadresse. Die mitgelieferten stehen im
|
||||||
* Programm, weil sie von der Gegenwart abhängen („diese Woche"), die eigenen
|
* Programm, weil sie von der Gegenwart abhängen („diese Woche"), die eigenen
|
||||||
* in der Datenbank.
|
* in der Datenbank.
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ const DEBOUNCE_MS = 250
|
|||||||
/**
|
/**
|
||||||
* Das Suchfeld.
|
* Das Suchfeld.
|
||||||
*
|
*
|
||||||
* Tippen schreibt in die Adresse, mit 250 ms Verzögerung — dadurch ist jeder
|
* Tippen schreibt in die Adresse, mit 250 ms Verzögerung - dadurch ist jeder
|
||||||
* Zustand teilbar und der Zurück-Knopf tut das Richtige. Während die neue
|
* Zustand teilbar und der Zurück-Knopf tut das Richtige. Während die neue
|
||||||
* Seite lädt, bleiben die alten Treffer stehen und werden gedimmt, statt zu
|
* Seite lädt, bleiben die alten Treffer stehen und werden gedimmt, statt zu
|
||||||
* verschwinden. Siehe docs/09-performance-und-ux.md.
|
* verschwinden. Siehe docs/09-performance-und-ux.md.
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { Skeleton, SkeletonLine } from '@/components/Skeleton'
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Skelett statt Spinner: die Struktur steht sofort, die Daten fallen hinein.
|
* Skelett statt Spinner: die Struktur steht sofort, die Daten fallen hinein.
|
||||||
* Ohne diese Grenze wartet die Navigation stumm, bis der Server fertig ist —
|
* Ohne diese Grenze wartet die Navigation stumm, bis der Server fertig ist -
|
||||||
* man klickt und es passiert scheinbar nichts. Siehe docs/09-performance-und-ux.md.
|
* man klickt und es passiert scheinbar nichts. Siehe docs/09-performance-und-ux.md.
|
||||||
*/
|
*/
|
||||||
export default function Loading() {
|
export default function Loading() {
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ export const dynamic = 'force-dynamic'
|
|||||||
* Suchseite.
|
* Suchseite.
|
||||||
*
|
*
|
||||||
* Alles läuft über Adressparameter: das macht jeden Zustand teilbar, den
|
* Alles läuft über Adressparameter: das macht jeden Zustand teilbar, den
|
||||||
* Zurück-Knopf richtig und eine gespeicherte Ansicht zu dem, was sie ist —
|
* Zurück-Knopf richtig und eine gespeicherte Ansicht zu dem, was sie ist -
|
||||||
* ein Lesezeichen. Siehe lib/search/query.ts.
|
* ein Lesezeichen. Siehe lib/search/query.ts.
|
||||||
*/
|
*/
|
||||||
export default async function SearchPage({
|
export default async function SearchPage({
|
||||||
@@ -76,7 +76,7 @@ export default async function SearchPage({
|
|||||||
])
|
])
|
||||||
|
|
||||||
// Bei einer einzelnen Art hängt alles Weitere an ihr: Facetten, Blättern,
|
// Bei einer einzelnen Art hängt alles Weitere an ihr: Facetten, Blättern,
|
||||||
// die Trefferzahl in der Kopfzeile. Bei „Alle" gibt es das nicht — dort ist
|
// die Trefferzahl in der Kopfzeile. Bei „Alle" gibt es das nicht - dort ist
|
||||||
// jede Art ein eigener Block mit eigenem Weg in die Vollansicht.
|
// jede Art ein eigener Block mit eigenem Weg in die Vollansicht.
|
||||||
const ergebnis = alle ? null : (faelle ?? dokumente ?? personen)!
|
const ergebnis = alle ? null : (faelle ?? dokumente ?? personen)!
|
||||||
const gesamt = alle
|
const gesamt = alle
|
||||||
@@ -108,7 +108,7 @@ export default async function SearchPage({
|
|||||||
{SEARCH_SCOPES.map((kind) => {
|
{SEARCH_SCOPES.map((kind) => {
|
||||||
const anzahl = kind === 'all' ? gesamt : zahlen[kind]
|
const anzahl = kind === 'all' ? gesamt : zahlen[kind]
|
||||||
// `null` heisst: diese Art gibt es für diesen Nutzer nicht. Dann
|
// `null` heisst: diese Art gibt es für diesen Nutzer nicht. Dann
|
||||||
// fehlt der Reiter, statt eine Null zu zeigen — auch eine Null wäre
|
// fehlt der Reiter, statt eine Null zu zeigen - auch eine Null wäre
|
||||||
// eine Auskunft.
|
// eine Auskunft.
|
||||||
if (anzahl === null) return null
|
if (anzahl === null) return null
|
||||||
|
|
||||||
@@ -279,7 +279,7 @@ type TrefferView =
|
|||||||
*
|
*
|
||||||
* Welche Art es ist, steht an der Form: ein Treffer mit Aktenzeichen ist eine
|
* Welche Art es ist, steht an der Form: ein Treffer mit Aktenzeichen ist eine
|
||||||
* Fallakte. Das ist kein Trick, sondern der Grund, warum die drei Suchen
|
* Fallakte. Das ist kein Trick, sondern der Grund, warum die drei Suchen
|
||||||
* getrennte Rückgaben haben — ein gemeinsamer Typ mit lauter optionalen
|
* getrennte Rückgaben haben - ein gemeinsamer Typ mit lauter optionalen
|
||||||
* Feldern liesse offen, was wirklich dasteht.
|
* Feldern liesse offen, was wirklich dasteht.
|
||||||
*/
|
*/
|
||||||
function Treffer({
|
function Treffer({
|
||||||
@@ -371,7 +371,7 @@ async function listStatusTags() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Eine Facette. Die Zahlen stammen aus derselben Abfrage wie die Liste — ein
|
* Eine Facette. Die Zahlen stammen aus derselben Abfrage wie die Liste - ein
|
||||||
* Zähler, der mehr zählt als die Liste zeigt, wäre selbst eine Information.
|
* Zähler, der mehr zählt als die Liste zeigt, wäre selbst eine Information.
|
||||||
*/
|
*/
|
||||||
function Facette({
|
function Facette({
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { Skeleton, SkeletonLine } from '@/components/Skeleton'
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Skelett statt Spinner: die Struktur steht sofort, die Daten fallen hinein.
|
* Skelett statt Spinner: die Struktur steht sofort, die Daten fallen hinein.
|
||||||
* Ohne diese Grenze wartet die Navigation stumm, bis der Server fertig ist —
|
* Ohne diese Grenze wartet die Navigation stumm, bis der Server fertig ist -
|
||||||
* man klickt und es passiert scheinbar nichts. Siehe docs/09-performance-und-ux.md.
|
* man klickt und es passiert scheinbar nichts. Siehe docs/09-performance-und-ux.md.
|
||||||
*/
|
*/
|
||||||
export default function Loading() {
|
export default function Loading() {
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ export const dynamic = 'force-dynamic'
|
|||||||
*
|
*
|
||||||
* Woran arbeite ich, was ist fällig, wo hat jemand geschrieben, was hat sich
|
* Woran arbeite ich, was ist fällig, wo hat jemand geschrieben, was hat sich
|
||||||
* getan. Jede Liste läuft durch dieselbe Sichtbarkeitsprüfung wie die
|
* getan. Jede Liste läuft durch dieselbe Sichtbarkeitsprüfung wie die
|
||||||
* Fallliste — das ist der Ort, an dem eine unachtsame Abfrage am ehesten eine
|
* Fallliste - das ist der Ort, an dem eine unachtsame Abfrage am ehesten eine
|
||||||
* Akte verrät, die man nicht sehen darf.
|
* Akte verrät, die man nicht sehen darf.
|
||||||
*/
|
*/
|
||||||
export default async function DashboardPage() {
|
export default async function DashboardPage() {
|
||||||
@@ -206,13 +206,13 @@ function Block({
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Eine Zeile der Sprungliste. Sie kann eine Akte, eine Person oder ein
|
* Eine Zeile der Sprungliste. Sie kann eine Akte, eine Person oder ein
|
||||||
* Dokument sein — deshalb steht die Art nicht dabei, sondern nur das, woran
|
* Dokument sein - deshalb steht die Art nicht dabei, sondern nur das, woran
|
||||||
* man sie erkennt: Aktenzeichen, Name oder Dokumentnummer.
|
* man sie erkennt: Aktenzeichen, Name oder Dokumentnummer.
|
||||||
*
|
*
|
||||||
* Eine Akte und ein Dokument tragen eine **Kennung**, eine Person nicht: ihr
|
* Eine Akte und ein Dokument tragen eine **Kennung**, eine Person nicht: ihr
|
||||||
* Name ist der Titel. Stünde er in der Kennungsspalte, stünde er in Mono, wäre
|
* Name ist der Titel. Stünde er in der Kennungsspalte, stünde er in Mono, wäre
|
||||||
* abgeschnitten, und der Dienstgrad rutschte dorthin, wo sonst der Titel
|
* abgeschnitten, und der Dienstgrad rutschte dorthin, wo sonst der Titel
|
||||||
* steht — untereinander ergäbe das keine Spalte mehr.
|
* steht - untereinander ergäbe das keine Spalte mehr.
|
||||||
*
|
*
|
||||||
* Ohne Vorabruf, anders als die übrigen Listen: die Seiten dahinter merken
|
* Ohne Vorabruf, anders als die übrigen Listen: die Seiten dahinter merken
|
||||||
* sich, dass sie geöffnet wurden. Der Vorabruf wird zwar serverseitig erkannt
|
* sich, dass sie geöffnet wurden. Der Vorabruf wird zwar serverseitig erkannt
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { Skeleton, SkeletonLine } from '@/components/Skeleton'
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Skelett statt Spinner: die Struktur steht sofort, die Daten fallen hinein.
|
* Skelett statt Spinner: die Struktur steht sofort, die Daten fallen hinein.
|
||||||
* Ohne diese Grenze wartet die Navigation stumm, bis der Server fertig ist —
|
* Ohne diese Grenze wartet die Navigation stumm, bis der Server fertig ist -
|
||||||
* man klickt und es passiert scheinbar nichts. Siehe docs/09-performance-und-ux.md.
|
* man klickt und es passiert scheinbar nichts. Siehe docs/09-performance-und-ux.md.
|
||||||
*/
|
*/
|
||||||
export default function Loading() {
|
export default function Loading() {
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import { de } from '@/lib/i18n/de'
|
|||||||
*
|
*
|
||||||
* Bewusst **kein `notFound()`**: die Wurzel hat ein `loading.tsx`, und dessen
|
* Bewusst **kein `notFound()`**: die Wurzel hat ein `loading.tsx`, und dessen
|
||||||
* Skelett bleibt in dieser Gruppe stehen, statt durch die Nicht-gefunden-Seite
|
* Skelett bleibt in dieser Gruppe stehen, statt durch die Nicht-gefunden-Seite
|
||||||
* ersetzt zu werden — die Ansicht bliebe leer. Für ein Blatt, das gedruckt
|
* ersetzt zu werden - die Ansicht bliebe leer. Für ein Blatt, das gedruckt
|
||||||
* wird, ist ein leeres Blatt der schlechteste Ausgang: es sieht aus wie ein
|
* wird, ist ein leeres Blatt der schlechteste Ausgang: es sieht aus wie ein
|
||||||
* Fehler im Papier, nicht wie eine Auskunft.
|
* Fehler im Papier, nicht wie eine Auskunft.
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import { exportText } from '@/lib/i18n/export'
|
|||||||
/**
|
/**
|
||||||
* Die Leiste über einer Druckansicht: zurück zur Akte und drucken.
|
* Die Leiste über einer Druckansicht: zurück zur Akte und drucken.
|
||||||
*
|
*
|
||||||
* Die Druckansichten liegen ausserhalb des angemeldeten Rahmenwerks — der
|
* Die Druckansichten liegen ausserhalb des angemeldeten Rahmenwerks - der
|
||||||
* PDF-Dienst ruft sie mit einem kurzlebigen Zeichen auf und hat keine Sitzung,
|
* PDF-Dienst ruft sie mit einem kurzlebigen Zeichen auf und hat keine Sitzung,
|
||||||
* also darf dort keine Kopfzeile stehen, die `requireUser()` braucht. Genau
|
* also darf dort keine Kopfzeile stehen, die `requireUser()` braucht. Genau
|
||||||
* deshalb gab es aber auch keinen Weg zurück: wer die Ansicht im Browser
|
* deshalb gab es aber auch keinen Weg zurück: wer die Ansicht im Browser
|
||||||
@@ -15,7 +15,7 @@ import { exportText } from '@/lib/i18n/export'
|
|||||||
*
|
*
|
||||||
* Das Ziel wird aus dem Pfad abgeleitet und nicht hereingereicht: `/druck`
|
* Das Ziel wird aus dem Pfad abgeleitet und nicht hereingereicht: `/druck`
|
||||||
* abschneiden ergibt die Akte, aus der man kommt. Das stimmt auch, wenn jemand
|
* abschneiden ergibt die Akte, aus der man kommt. Das stimmt auch, wenn jemand
|
||||||
* die Adresse direkt aufruft — `router.back()` täte das nicht.
|
* die Adresse direkt aufruft - `router.back()` täte das nicht.
|
||||||
*
|
*
|
||||||
* `no-print` blendet die Leiste beim Drucken aus. Auf dem Papier hat ein
|
* `no-print` blendet die Leiste beim Drucken aus. Auf dem Papier hat ein
|
||||||
* Zurück-Knopf nichts verloren.
|
* Zurück-Knopf nichts verloren.
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ export async function generateMetadata({
|
|||||||
/**
|
/**
|
||||||
* Druckansicht eines einzelnen Dokuments.
|
* Druckansicht eines einzelnen Dokuments.
|
||||||
*
|
*
|
||||||
* Kein Verlauf, keine Kopfdatentabelle einer Akte — das Dokument selbst, so
|
* Kein Verlauf, keine Kopfdatentabelle einer Akte - das Dokument selbst, so
|
||||||
* wie seine Vorlage es vorsieht. Ein Aktenblatt kommt als Aktenblatt heraus,
|
* wie seine Vorlage es vorsieht. Ein Aktenblatt kommt als Aktenblatt heraus,
|
||||||
* ein Vertrag als Vertrag. Siehe docs/08-suche-und-export.md.
|
* ein Vertrag als Vertrag. Siehe docs/08-suche-und-export.md.
|
||||||
*/
|
*/
|
||||||
@@ -169,7 +169,7 @@ export default async function PrintDocumentPage({
|
|||||||
</p>
|
</p>
|
||||||
) : (
|
) : (
|
||||||
<p style={{ margin: '1mm 0', whiteSpace: 'pre-wrap' }}>
|
<p style={{ margin: '1mm 0', whiteSpace: 'pre-wrap' }}>
|
||||||
{String(section.value ?? '—')}
|
{String(section.value ?? '-')}
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -99,7 +99,7 @@ export default async function PrintCasePage({
|
|||||||
/>
|
/>
|
||||||
<Zeile
|
<Zeile
|
||||||
label={de.case.dueAt}
|
label={de.case.dueAt}
|
||||||
value={akte.dueAt ? akte.dueAt.toLocaleDateString('de-DE') : '—'}
|
value={akte.dueAt ? akte.dueAt.toLocaleDateString('de-DE') : '-'}
|
||||||
/>
|
/>
|
||||||
<Zeile label={de.export.retrievedBy} value={displayName} />
|
<Zeile label={de.export.retrievedBy} value={displayName} />
|
||||||
<Zeile
|
<Zeile
|
||||||
@@ -164,7 +164,7 @@ export default async function PrintCasePage({
|
|||||||
<td>{anhang.originalName}</td>
|
<td>{anhang.originalName}</td>
|
||||||
<td>{anhang.sizeBytes}</td>
|
<td>{anhang.sizeBytes}</td>
|
||||||
{/* Der Hash macht nachprüfbar, dass die Datei zum Ausdruck
|
{/* Der Hash macht nachprüfbar, dass die Datei zum Ausdruck
|
||||||
gehört — deshalb steht er im Verzeichnis. */}
|
gehört - deshalb steht er im Verzeichnis. */}
|
||||||
<td style={{ fontFamily: 'monospace', fontSize: '7pt', wordBreak: 'break-all' }}>
|
<td style={{ fontFamily: 'monospace', fontSize: '7pt', wordBreak: 'break-all' }}>
|
||||||
{anhang.sha256}
|
{anhang.sha256}
|
||||||
</td>
|
</td>
|
||||||
@@ -188,7 +188,7 @@ function Zeile({ label, value }: { label: string; value: string }) {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Die Abschnitte im Druck. Ein gesperrtes Feld wird als Balken ausgegeben, mit
|
* Die Abschnitte im Druck. Ein gesperrtes Feld wird als Balken ausgegeben, mit
|
||||||
* dem Vermerk, ab welcher Stufe es lesbar wäre — sonst merkt der Empfänger
|
* dem Vermerk, ab welcher Stufe es lesbar wäre - sonst merkt der Empfänger
|
||||||
* nicht, dass er eine gekürzte Fassung in der Hand hält.
|
* nicht, dass er eine gekürzte Fassung in der Hand hält.
|
||||||
*/
|
*/
|
||||||
function PrintSections({
|
function PrintSections({
|
||||||
@@ -256,7 +256,7 @@ function PrintSections({
|
|||||||
</p>
|
</p>
|
||||||
) : (
|
) : (
|
||||||
<p style={{ margin: '1mm 0', whiteSpace: 'pre-wrap' }}>
|
<p style={{ margin: '1mm 0', whiteSpace: 'pre-wrap' }}>
|
||||||
{String(section.value ?? '—')}
|
{String(section.value ?? '-')}
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -10,9 +10,9 @@ export const dynamic = 'force-dynamic'
|
|||||||
* kurzlebigen Zeichen und ohne Sitzung, und eine Weiterleitung zur Anmeldung
|
* kurzlebigen Zeichen und ohne Sitzung, und eine Weiterleitung zur Anmeldung
|
||||||
* ergäbe ein leeres PDF. Die Prüfung steht in den Seiten selbst.
|
* ergäbe ein leeres PDF. Die Prüfung steht in den Seiten selbst.
|
||||||
*
|
*
|
||||||
* Was er tut, ist zweierlei: er legt den weissen Grund unter das Blatt — sonst
|
* Was er tut, ist zweierlei: er legt den weissen Grund unter das Blatt - sonst
|
||||||
* schwebt ein Ausdruck im dunklen Thema der Anwendung, und man hält die
|
* schwebt ein Ausdruck im dunklen Thema der Anwendung, und man hält die
|
||||||
* Ansicht für kaputt — und er gibt einen Weg zurück. Beides verschwindet beim
|
* Ansicht für kaputt - und er gibt einen Weg zurück. Beides verschwindet beim
|
||||||
* Drucken.
|
* Drucken.
|
||||||
*/
|
*/
|
||||||
export default function PrintLayout({ children }: { children: ReactNode }) {
|
export default function PrintLayout({ children }: { children: ReactNode }) {
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ export const dynamic = 'force-dynamic'
|
|||||||
* GET /api/admin/export?kind=bundle ZIP mit den fertigen Exporten
|
* GET /api/admin/export?kind=bundle ZIP mit den fertigen Exporten
|
||||||
*
|
*
|
||||||
* Beide nur mit Recht `admin`, beide protokolliert. Der JSON-Abzug ist die
|
* Beide nur mit Recht `admin`, beide protokolliert. Der JSON-Abzug ist die
|
||||||
* einzige Stelle im System, an der die Klassifizierung nicht greift — deshalb
|
* einzige Stelle im System, an der die Klassifizierung nicht greift - deshalb
|
||||||
* liegt er hinter einem eigenen Endpunkt und nicht hinter einem Knopf an einer
|
* liegt er hinter einem eigenen Endpunkt und nicht hinter einem Knopf an einer
|
||||||
* Akte.
|
* Akte.
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ export const dynamic = 'force-dynamic'
|
|||||||
* Herunterladen eines fertigen Exports.
|
* Herunterladen eines fertigen Exports.
|
||||||
*
|
*
|
||||||
* Nur der Auftraggeber selbst. Ein Export ist auf **seine** Stufe gekürzt und
|
* Nur der Auftraggeber selbst. Ein Export ist auf **seine** Stufe gekürzt und
|
||||||
* trägt **seinen** Namen im Wasserzeichen — ihn an jemand anderen auszuliefern
|
* trägt **seinen** Namen im Wasserzeichen - ihn an jemand anderen auszuliefern
|
||||||
* hiesse, ein Dokument mit fremdem Namen zu verteilen.
|
* hiesse, ein Dokument mit fremdem Namen zu verteilen.
|
||||||
*/
|
*/
|
||||||
export async function GET(
|
export async function GET(
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ export const runtime = 'nodejs'
|
|||||||
export const dynamic = 'force-dynamic'
|
export const dynamic = 'force-dynamic'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Auslieferung. **Niemals als statische Datei** — jeder Abruf läuft durch die
|
* Auslieferung. **Niemals als statische Datei** - jeder Abruf läuft durch die
|
||||||
* Sichtbarkeitsprüfung der zugehörigen Akte und wird ab Stufe 3 protokolliert.
|
* Sichtbarkeitsprüfung der zugehörigen Akte und wird ab Stufe 3 protokolliert.
|
||||||
* Siehe docs/07-dateien-und-sicherheit.md.
|
* Siehe docs/07-dateien-und-sicherheit.md.
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ export const dynamic = 'force-dynamic'
|
|||||||
* Fortschrittsbalken, und 25 MB sind mehr, als eine Server Action annimmt.
|
* Fortschrittsbalken, und 25 MB sind mehr, als eine Server Action annimmt.
|
||||||
*
|
*
|
||||||
* Die Antwort kommt, sobald die Datei in der Quarantäne liegt. Die Prüfkette
|
* Die Antwort kommt, sobald die Datei in der Quarantäne liegt. Die Prüfkette
|
||||||
* läuft danach weiter und meldet sich über `/api/events` — niemand wartet mit
|
* läuft danach weiter und meldet sich über `/api/events` - niemand wartet mit
|
||||||
* laufendem Balken auf einen Virenscanner. Siehe docs/09.
|
* laufendem Balken auf einen Virenscanner. Siehe docs/09.
|
||||||
*/
|
*/
|
||||||
export async function POST(request: Request): Promise<Response> {
|
export async function POST(request: Request): Promise<Response> {
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ const RECENT_IN_PALETTE = 6
|
|||||||
* Schnellsuche für die Kommandopalette.
|
* Schnellsuche für die Kommandopalette.
|
||||||
*
|
*
|
||||||
* Bewusst schmal: Aktenzeichen, Titel und Namen, kein Volltext über Inhalte.
|
* Bewusst schmal: Aktenzeichen, Titel und Namen, kein Volltext über Inhalte.
|
||||||
* Wer die Palette öffnet, will springen, nicht recherchieren — dafür gibt es
|
* Wer die Palette öffnet, will springen, nicht recherchieren - dafür gibt es
|
||||||
* `/suche`.
|
* `/suche`.
|
||||||
*
|
*
|
||||||
* Der Trigram-Index auf `caseNumber` und `displayName` fängt Tippfehler ab,
|
* Der Trigram-Index auf `caseNumber` und `displayName` fängt Tippfehler ab,
|
||||||
@@ -31,7 +31,7 @@ export async function GET(request: Request): Promise<Response> {
|
|||||||
const params = new URL(request.url).searchParams
|
const params = new URL(request.url).searchParams
|
||||||
|
|
||||||
// Leeres Feld: die eigene Sprungliste. Wer die Palette öffnet, will meistens
|
// Leeres Feld: die eigene Sprungliste. Wer die Palette öffnet, will meistens
|
||||||
// dorthin zurück, wo er eben war — und nicht erst tippen.
|
// dorthin zurück, wo er eben war - und nicht erst tippen.
|
||||||
if (params.get('recent') === '1') {
|
if (params.get('recent') === '1') {
|
||||||
const zuletzt = await listRecent(user.permissions, RECENT_IN_PALETTE)
|
const zuletzt = await listRecent(user.permissions, RECENT_IN_PALETTE)
|
||||||
return NextResponse.json({
|
return NextResponse.json({
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ const HEARTBEAT_MS = 25_000
|
|||||||
*
|
*
|
||||||
* Drei Prüfungen, alle nötig:
|
* Drei Prüfungen, alle nötig:
|
||||||
* 1. **Beim Verbinden**: welche der angefragten Themen darf dieser Nutzer
|
* 1. **Beim Verbinden**: welche der angefragten Themen darf dieser Nutzer
|
||||||
* überhaupt abonnieren. Wer ein fremdes Thema nennt, bekommt es nicht —
|
* überhaupt abonnieren. Wer ein fremdes Thema nennt, bekommt es nicht -
|
||||||
* und erfährt auch nicht, ob es existiert.
|
* und erfährt auch nicht, ob es existiert.
|
||||||
* 2. **Je Ereignis**: der Inhalt wird mit den Rechten des Empfängers geladen.
|
* 2. **Je Ereignis**: der Inhalt wird mit den Rechten des Empfängers geladen.
|
||||||
* Eine Umklassifizierung mitten in einer offenen Verbindung wirkt dadurch
|
* Eine Umklassifizierung mitten in einer offenen Verbindung wirkt dadurch
|
||||||
@@ -38,7 +38,7 @@ const HEARTBEAT_MS = 25_000
|
|||||||
* 3. **Laufend**: Sitzung und Rechte werden neu aufgelöst, nicht einmal beim
|
* 3. **Laufend**: Sitzung und Rechte werden neu aufgelöst, nicht einmal beim
|
||||||
* Verbinden eingefroren. Eine offene Verbindung ist sonst der eine Ort im
|
* Verbinden eingefroren. Eine offene Verbindung ist sonst der eine Ort im
|
||||||
* System, an dem ein Entzug nicht ankommt: der Worker löscht die Sitzung,
|
* System, an dem ein Entzug nicht ankommt: der Worker löscht die Sitzung,
|
||||||
* jeder Seitenaufruf ist abgemeldet — und der Strom läuft weiter, weil
|
* jeder Seitenaufruf ist abgemeldet - und der Strom läuft weiter, weil
|
||||||
* niemand mehr fragt. Siehe docs/03-auth-und-rechte.md.
|
* niemand mehr fragt. Siehe docs/03-auth-und-rechte.md.
|
||||||
*/
|
*/
|
||||||
export async function GET(request: Request): Promise<Response> {
|
export async function GET(request: Request): Promise<Response> {
|
||||||
@@ -161,7 +161,7 @@ export async function GET(request: Request): Promise<Response> {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
// Rechte geändert: die erlaubten Themen gelten nicht mehr. Sie neu
|
// Rechte geändert: die erlaubten Themen gelten nicht mehr. Sie neu
|
||||||
// zu bestimmen ist wichtiger als die eine Zustellung — ein Ereignis
|
// zu bestimmen ist wichtiger als die eine Zustellung - ein Ereignis
|
||||||
// trägt bei Verlaufseinträgen nur eine Kennung, das Thema ist dort
|
// trägt bei Verlaufseinträgen nur eine Kennung, das Thema ist dort
|
||||||
// die **einzige** Schranke.
|
// die **einzige** Schranke.
|
||||||
if (abdruck(frisch) !== abdruck(rechte)) {
|
if (abdruck(frisch) !== abdruck(rechte)) {
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ const BEWERTUNGEN = new Set(['good', 'needs-improvement', 'poor'])
|
|||||||
* Zeichenketten aus dem Browser speichert, ist ein Ablagefach für Fremde.
|
* Zeichenketten aus dem Browser speichert, ist ein Ablagefach für Fremde.
|
||||||
*
|
*
|
||||||
* Der Nutzer wird **nicht** mitgeschrieben. Wer wann welche Akte geöffnet hat,
|
* Der Nutzer wird **nicht** mitgeschrieben. Wer wann welche Akte geöffnet hat,
|
||||||
* steht im Zugriffsprotokoll — dort gehört es hin, dort ist es geregelt, und
|
* steht im Zugriffsprotokoll - dort gehört es hin, dort ist es geregelt, und
|
||||||
* eine zweite Spur daneben wäre eine Überwachungsfunktion ohne Auftrag.
|
* eine zweite Spur daneben wäre eine Überwachungsfunktion ohne Auftrag.
|
||||||
*/
|
*/
|
||||||
export async function POST(request: Request): Promise<Response> {
|
export async function POST(request: Request): Promise<Response> {
|
||||||
|
|||||||
+7
-7
@@ -56,7 +56,7 @@
|
|||||||
|
|
||||||
/* Struktur: Flaechen bleiben eckig, Bedienelemente sind rund.
|
/* Struktur: Flaechen bleiben eckig, Bedienelemente sind rund.
|
||||||
Eine Akte ist ein Blatt und hat scharfe Kanten. Was man druecken oder
|
Eine Akte ist ein Blatt und hat scharfe Kanten. Was man druecken oder
|
||||||
ausfuellen kann, ist rund — daran erkennt man es, bevor man es beruehrt. */
|
ausfuellen kann, ist rund - daran erkennt man es, bevor man es beruehrt. */
|
||||||
--radius-none: 0px;
|
--radius-none: 0px;
|
||||||
--radius-sm: 2px;
|
--radius-sm: 2px;
|
||||||
--radius-DEFAULT: 2px;
|
--radius-DEFAULT: 2px;
|
||||||
@@ -87,7 +87,7 @@
|
|||||||
* Klassifizierungsfarben: eine Stufe muss in beiden Themen dieselbe Farbe
|
* Klassifizierungsfarben: eine Stufe muss in beiden Themen dieselbe Farbe
|
||||||
* haben, sonst lernt man sie zweimal. Siehe docs/10-design-system.md.
|
* haben, sonst lernt man sie zweimal. Siehe docs/10-design-system.md.
|
||||||
*
|
*
|
||||||
* Kein warmes Cremeweiss, sondern ein leicht kühles Papierweiss — dieselbe
|
* Kein warmes Cremeweiss, sondern ein leicht kühles Papierweiss - dieselbe
|
||||||
* Anmutung wie das dunkle Thema, nur andersherum. Die Kontraste sind auf
|
* Anmutung wie das dunkle Thema, nur andersherum. Die Kontraste sind auf
|
||||||
* mindestens 4,5:1 für Fliesstext ausgelegt. */
|
* mindestens 4,5:1 für Fliesstext ausgelegt. */
|
||||||
[data-theme='light'] {
|
[data-theme='light'] {
|
||||||
@@ -226,7 +226,7 @@
|
|||||||
|
|
||||||
**Und er muss beim Druecken zurueckmelden, dass er getroffen wurde.**
|
**Und er muss beim Druecken zurueckmelden, dass er getroffen wurde.**
|
||||||
Beim Druecken zieht sich der Knopf leicht zusammen und bekommt einen
|
Beim Druecken zieht sich der Knopf leicht zusammen und bekommt einen
|
||||||
inneren Ring — er wirkt kurz dicker, ohne dass sich die Rahmenbreite
|
inneren Ring - er wirkt kurz dicker, ohne dass sich die Rahmenbreite
|
||||||
wirklich aendert, sonst wuerde das Umfeld springen. Die Rueckmeldung
|
wirklich aendert, sonst wuerde das Umfeld springen. Die Rueckmeldung
|
||||||
setzt ohne Verzoegerung ein und klingt langsam ab: ein kurzer Klick
|
setzt ohne Verzoegerung ein und klingt langsam ab: ein kurzer Klick
|
||||||
bliebe sonst unsichtbar. Siehe docs/10-design-system.md.
|
bliebe sonst unsichtbar. Siehe docs/10-design-system.md.
|
||||||
@@ -318,7 +318,7 @@
|
|||||||
|
|
||||||
Jede Liste in dieser Anwendung zeigt dasselbe Gebilde: eine Kennung, ein
|
Jede Liste in dieser Anwendung zeigt dasselbe Gebilde: eine Kennung, ein
|
||||||
Titel, etwas Beiwerk, eine Stempelmarke. Bisher bestimmte **jede Zeile
|
Titel, etwas Beiwerk, eine Stempelmarke. Bisher bestimmte **jede Zeile
|
||||||
ihre Spaltenbreiten selbst** — die Kennung war so breit wie ihr Text, und
|
ihre Spaltenbreiten selbst** - die Kennung war so breit wie ihr Text, und
|
||||||
damit fing der Titel in jeder Zeile woanders an. Bei „ETHIK" und „WS"
|
damit fing der Titel in jeder Zeile woanders an. Bei „ETHIK" und „WS"
|
||||||
untereinander sieht man das sofort.
|
untereinander sieht man das sofort.
|
||||||
|
|
||||||
@@ -350,7 +350,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
/* Wie die Kennung, aber fuer eine Bezeichnung aus einer festen Liste:
|
/* Wie die Kennung, aber fuer eine Bezeichnung aus einer festen Liste:
|
||||||
die Rolle eines Beteiligten, die Art eines Auftrags. Kein Mono — es ist
|
die Rolle eines Beteiligten, die Art eines Auftrags. Kein Mono - es ist
|
||||||
Sprache, keine Nummer. */
|
Sprache, keine Nummer. */
|
||||||
.record-label {
|
.record-label {
|
||||||
flex: none;
|
flex: none;
|
||||||
@@ -364,7 +364,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
/* Der Dienstgrad vor einem Namen. In derselben Groesse wie der Name, denn
|
/* Der Dienstgrad vor einem Namen. In derselben Groesse wie der Name, denn
|
||||||
er gehoert zu ihm — aber in einer eigenen Spalte, sonst faengt jeder Name
|
er gehoert zu ihm - aber in einer eigenen Spalte, sonst faengt jeder Name
|
||||||
woanders an. */
|
woanders an. */
|
||||||
.record-rank {
|
.record-rank {
|
||||||
flex: none;
|
flex: none;
|
||||||
@@ -827,7 +827,7 @@
|
|||||||
/* Der Grund unter einer Druckansicht.
|
/* Der Grund unter einer Druckansicht.
|
||||||
Ohne ihn liegt ein weisses Blatt auf dem dunklen Thema der Anwendung, und
|
Ohne ihn liegt ein weisses Blatt auf dem dunklen Thema der Anwendung, und
|
||||||
die Ansicht sieht kaputt aus, obwohl sie stimmt. Beim Drucken faellt er
|
die Ansicht sieht kaputt aus, obwohl sie stimmt. Beim Drucken faellt er
|
||||||
weg — Papier bringt seinen eigenen Grund mit. */
|
weg - Papier bringt seinen eigenen Grund mit. */
|
||||||
.print-page {
|
.print-page {
|
||||||
min-height: 100dvh;
|
min-height: 100dvh;
|
||||||
background: #f2f2f0;
|
background: #f2f2f0;
|
||||||
|
|||||||
+2
-2
@@ -21,7 +21,7 @@ export const viewport: Viewport = {
|
|||||||
*
|
*
|
||||||
* Ohne das blitzt beim Laden kurz das falsche Thema auf. Es ist die eine
|
* Ohne das blitzt beim Laden kurz das falsche Thema auf. Es ist die eine
|
||||||
* Stelle mit `dangerouslySetInnerHTML`, und der Inhalt ist eine Konstante aus
|
* Stelle mit `dangerouslySetInnerHTML`, und der Inhalt ist eine Konstante aus
|
||||||
* diesem Modul — kein Nutzertext. Das Verbot aus docs/07 gilt Nutzerinhalten.
|
* diesem Modul - kein Nutzertext. Das Verbot aus docs/07 gilt Nutzerinhalten.
|
||||||
*/
|
*/
|
||||||
const THEME_SCRIPT =
|
const THEME_SCRIPT =
|
||||||
`try{var t=localStorage.getItem(${JSON.stringify(THEME_KEY)});` +
|
`try{var t=localStorage.getItem(${JSON.stringify(THEME_KEY)});` +
|
||||||
@@ -38,7 +38,7 @@ export default async function RootLayout({ children }: { children: React.ReactNo
|
|||||||
{/*
|
{/*
|
||||||
`suppressHydrationWarning` wegen des Nonce, nicht wegen des Inhalts.
|
`suppressHydrationWarning` wegen des Nonce, nicht wegen des Inhalts.
|
||||||
Der Browser **leert das Nonce-Attribut**, sobald er das Element
|
Der Browser **leert das Nonce-Attribut**, sobald er das Element
|
||||||
geparst hat — so schreibt es die CSP-Spezifikation vor, damit kein
|
geparst hat - so schreibt es die CSP-Spezifikation vor, damit kein
|
||||||
Skript den Wert über `getAttribute` auslesen und sich selbst
|
Skript den Wert über `getAttribute` auslesen und sich selbst
|
||||||
nachladen kann. React vergleicht beim Hydrieren aber genau dieses
|
nachladen kann. React vergleicht beim Hydrieren aber genau dieses
|
||||||
Attribut, liest clientseitig den leeren Wert und meldet einen
|
Attribut, liest clientseitig den leeren Wert und meldet einen
|
||||||
|
|||||||
+2
-2
@@ -12,7 +12,7 @@ export { SIGN_IN_ERRORS }
|
|||||||
/**
|
/**
|
||||||
* Auth.js leitet die Callback-URL **nicht** aus dem Host-Header ab, sondern aus
|
* Auth.js leitet die Callback-URL **nicht** aus dem Host-Header ab, sondern aus
|
||||||
* `AUTH_URL`. Fehlt die Variable, bleibt die `redirect_uri` fest bei
|
* `AUTH_URL`. Fehlt die Variable, bleibt die `redirect_uri` fest bei
|
||||||
* `http://localhost:3000` — hinter dem Cloudflare Tunnel wäre die Anmeldung
|
* `http://localhost:3000` - hinter dem Cloudflare Tunnel wäre die Anmeldung
|
||||||
* damit kaputt, und zwar mit einer Fehlermeldung von Discord, die nicht auf die
|
* damit kaputt, und zwar mit einer Fehlermeldung von Discord, die nicht auf die
|
||||||
* Ursache zeigt.
|
* Ursache zeigt.
|
||||||
*
|
*
|
||||||
@@ -38,7 +38,7 @@ type DiscordProfile = {
|
|||||||
* benutzt: die Rollen holt der Bot mit seinem eigenen Token.
|
* benutzt: die Rollen holt der Bot mit seinem eigenen Token.
|
||||||
*
|
*
|
||||||
* Die Sitzung selbst steht in der Datenbank. Im Cookie liegt nur ein
|
* Die Sitzung selbst steht in der Datenbank. Im Cookie liegt nur ein
|
||||||
* signiertes Wertepaar aus `userId` und `sessionId` — ohne die Zeile in
|
* signiertes Wertepaar aus `userId` und `sessionId` - ohne die Zeile in
|
||||||
* `Session` ist es wertlos. Nur so lässt sich eine Sitzung zurückziehen,
|
* `Session` ist es wertlos. Nur so lässt sich eine Sitzung zurückziehen,
|
||||||
* wenn jemand seine Rolle verliert. Siehe docs/03-auth-und-rechte.md.
|
* wenn jemand seine Rolle verliert. Siehe docs/03-auth-und-rechte.md.
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -6,13 +6,13 @@ import { commonText } from '@/lib/i18n/common'
|
|||||||
/**
|
/**
|
||||||
* Ein aufklappbares Formular zum Hinzufügen.
|
* Ein aufklappbares Formular zum Hinzufügen.
|
||||||
*
|
*
|
||||||
* Beteiligte, Verknüpfungen, Dokumente, Zuordnungen — überall derselbe Ablauf:
|
* Beteiligte, Verknüpfungen, Dokumente, Zuordnungen - überall derselbe Ablauf:
|
||||||
* ein Knopf, ein kleines Formular, eine Server Action, eine Fehlermeldung an
|
* ein Knopf, ein kleines Formular, eine Server Action, eine Fehlermeldung an
|
||||||
* Ort und Stelle. Das viermal zu wiederholen kostete nicht nur Zeilen, sondern
|
* Ort und Stelle. Das viermal zu wiederholen kostete nicht nur Zeilen, sondern
|
||||||
* auch Bündelgrösse auf der Fallakte, die ohnehin die dichteste Seite ist.
|
* auch Bündelgrösse auf der Fallakte, die ohnehin die dichteste Seite ist.
|
||||||
*
|
*
|
||||||
* Der Rückruf bekommt `schliessen`, damit das Formular nur nach einem Erfolg
|
* Der Rückruf bekommt `schliessen`, damit das Formular nur nach einem Erfolg
|
||||||
* zugeht — ein Fehler soll die Eingabe nicht wegräumen.
|
* zugeht - ein Fehler soll die Eingabe nicht wegräumen.
|
||||||
*/
|
*/
|
||||||
export function AddPanel({
|
export function AddPanel({
|
||||||
label,
|
label,
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
* im Stack für ein Bild von 64 Pixeln. Dafür steht `cdn.discordapp.com` in der
|
* im Stack für ein Bild von 64 Pixeln. Dafür steht `cdn.discordapp.com` in der
|
||||||
* Inhaltsrichtlinie, siehe middleware.ts.
|
* Inhaltsrichtlinie, siehe middleware.ts.
|
||||||
*
|
*
|
||||||
* Ohne Bild — und das ist der Normalfall bei Konten ohne eigenes Profilbild —
|
* Ohne Bild - und das ist der Normalfall bei Konten ohne eigenes Profilbild -
|
||||||
* stehen die Anfangsbuchstaben da. Kein Platzhaltergesicht: ein leerer Kreis
|
* stehen die Anfangsbuchstaben da. Kein Platzhaltergesicht: ein leerer Kreis
|
||||||
* mit Initialen ist ehrlicher und lädt nichts nach.
|
* mit Initialen ist ehrlicher und lädt nichts nach.
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import { Pending } from '@/components/Pending'
|
|||||||
/**
|
/**
|
||||||
* Die Bereiche der Anwendung in der Kopfzeile.
|
* Die Bereiche der Anwendung in der Kopfzeile.
|
||||||
*
|
*
|
||||||
* Sie waren blosse Textlinks — von einem Fliesstextlink nicht zu unterscheiden
|
* Sie waren blosse Textlinks - von einem Fliesstextlink nicht zu unterscheiden
|
||||||
* und ohne Hinweis darauf, wo man gerade ist. Jetzt tragen sie dieselbe
|
* und ohne Hinweis darauf, wo man gerade ist. Jetzt tragen sie dieselbe
|
||||||
* Knopfform wie alles Bedienbare, und der Bereich, in dem man steht, ist
|
* Knopfform wie alles Bedienbare, und der Bereich, in dem man steht, ist
|
||||||
* gefüllt.
|
* gefüllt.
|
||||||
|
|||||||
@@ -6,12 +6,12 @@ import { useLinkStatus } from 'next/link'
|
|||||||
* Der Ladepunkt in einem Link, solange der Wechsel läuft.
|
* Der Ladepunkt in einem Link, solange der Wechsel läuft.
|
||||||
*
|
*
|
||||||
* Ein Wechsel kann einen Moment dauern, und ohne Rückmeldung sieht es aus, als
|
* Ein Wechsel kann einen Moment dauern, und ohne Rückmeldung sieht es aus, als
|
||||||
* sei der Klick nicht angekommen — man klickt ein zweites Mal. Der Punkt sitzt
|
* sei der Klick nicht angekommen - man klickt ein zweites Mal. Der Punkt sitzt
|
||||||
* **im geklickten Link**, damit sichtbar bleibt, wohin es geht.
|
* **im geklickten Link**, damit sichtbar bleibt, wohin es geht.
|
||||||
*
|
*
|
||||||
* Das ist kein Blockade-Spinner im Sinn von Zusicherung 5 der README: er ersetzt
|
* Das ist kein Blockade-Spinner im Sinn von Zusicherung 5 der README: er ersetzt
|
||||||
* keinen Inhalt und hält nichts auf. Die Zielseite bringt ihr Skelett mit, das
|
* keinen Inhalt und hält nichts auf. Die Zielseite bringt ihr Skelett mit, das
|
||||||
* ist die eigentliche Antwort auf das Warten — hier geht es nur darum, den
|
* ist die eigentliche Antwort auf das Warten - hier geht es nur darum, den
|
||||||
* Klick zu quittieren.
|
* Klick zu quittieren.
|
||||||
*
|
*
|
||||||
* `useLinkStatus` funktioniert nur als Kind eines `next/link`, deshalb die
|
* `useLinkStatus` funktioniert nur als Kind eines `next/link`, deshalb die
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ export function PieChart({
|
|||||||
}: {
|
}: {
|
||||||
slices: Slice[]
|
slices: Slice[]
|
||||||
emptyLabel: string
|
emptyLabel: string
|
||||||
/** „12 Fälle" — steht in der Mitte und sagt, worauf sich die Anteile beziehen. */
|
/** „12 Fälle" - steht in der Mitte und sagt, worauf sich die Anteile beziehen. */
|
||||||
totalLabel: (total: number) => string
|
totalLabel: (total: number) => string
|
||||||
}) {
|
}) {
|
||||||
const gesamt = slices.reduce((summe, stueck) => summe + stueck.value, 0)
|
const gesamt = slices.reduce((summe, stueck) => summe + stueck.value, 0)
|
||||||
@@ -101,7 +101,7 @@ export function PieChart({
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Abstufungen von `--steel`, gleichmässig verteilt. Bei einem einzigen
|
* Abstufungen von `--steel`, gleichmässig verteilt. Bei einem einzigen
|
||||||
* Segment die volle Farbe, sonst von kräftig nach blass — so bleibt die
|
* Segment die volle Farbe, sonst von kräftig nach blass - so bleibt die
|
||||||
* Reihenfolge der Liste auch im Kreis ablesbar.
|
* Reihenfolge der Liste auch im Kreis ablesbar.
|
||||||
*/
|
*/
|
||||||
function farbeFuer(index: number, anzahl: number): string {
|
function farbeFuer(index: number, anzahl: number): string {
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ import { ThemeToggle } from '@/components/ThemeToggle'
|
|||||||
* das Abmelden. In der Kopfzeile standen diese Dinge vorher zwischen den
|
* das Abmelden. In der Kopfzeile standen diese Dinge vorher zwischen den
|
||||||
* Aktenbereichen und nahmen dort Platz weg, den die Arbeit braucht.
|
* Aktenbereichen und nahmen dort Platz weg, den die Arbeit braucht.
|
||||||
*
|
*
|
||||||
* Das Abmelden kommt als Server Action von aussen herein — dieses Fenster darf
|
* Das Abmelden kommt als Server Action von aussen herein - dieses Fenster darf
|
||||||
* die Sitzung nicht selbst beenden können.
|
* die Sitzung nicht selbst beenden können.
|
||||||
*/
|
*/
|
||||||
export function ProfileMenu({
|
export function ProfileMenu({
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { searchBarText } from '@/lib/i18n/shell'
|
|||||||
/**
|
/**
|
||||||
* Der Suchbalken in der Kopfzeile.
|
* Der Suchbalken in der Kopfzeile.
|
||||||
*
|
*
|
||||||
* Ein gewöhnliches Formular, das die Anfrage an `/suche` übergibt — kein
|
* Ein gewöhnliches Formular, das die Anfrage an `/suche` übergibt - kein
|
||||||
* Skript, kein Zustand, keine Bytes im Bündel. Die eigentliche Suche mit
|
* Skript, kein Zustand, keine Bytes im Bündel. Die eigentliche Suche mit
|
||||||
* Filtern, Facetten und Abbruch laufender Anfragen sitzt auf der Suchseite,
|
* Filtern, Facetten und Abbruch laufender Anfragen sitzt auf der Suchseite,
|
||||||
* hier steht nur das Eingangstor.
|
* hier steht nur das Eingangstor.
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ export function stampColor(level: number): string {
|
|||||||
* `ClearanceLevelConfig.colorToken` und `PersonnelStatusTag.colorToken` sind
|
* `ClearanceLevelConfig.colorToken` und `PersonnelStatusTag.colorToken` sind
|
||||||
* Daten und im Adminbereich pflegbar. Sie landen als Name einer CSS-Variablen
|
* Daten und im Adminbereich pflegbar. Sie landen als Name einer CSS-Variablen
|
||||||
* in einem `style`-Attribut, und was aus der Datenbank in ein Stilattribut
|
* in einem `style`-Attribut, und was aus der Datenbank in ein Stilattribut
|
||||||
* geht, wird geprueft — nicht weil es heute jemand setzen koennte, sondern
|
* geht, wird geprueft - nicht weil es heute jemand setzen koennte, sondern
|
||||||
* weil es morgen jemand koennen soll. Ein unbekannter Wert faellt auf die
|
* weil es morgen jemand koennen soll. Ein unbekannter Wert faellt auf die
|
||||||
* neutrale Marke zurueck, statt eine Variable zu erfinden, die es nicht gibt.
|
* neutrale Marke zurueck, statt eine Variable zu erfinden, die es nicht gibt.
|
||||||
*/
|
*/
|
||||||
@@ -45,7 +45,7 @@ export function colorVariable(token: string | null | undefined): string {
|
|||||||
* **Feste Breite.** Die Namen der Stufen sind unterschiedlich lang und im
|
* **Feste Breite.** Die Namen der Stufen sind unterschiedlich lang und im
|
||||||
* Adminbereich aenderbar; unterschiedlich breite Marken untereinander lesen
|
* Adminbereich aenderbar; unterschiedlich breite Marken untereinander lesen
|
||||||
* sich als Unordnung, nicht als Unterschied. Ein zu langer Name wird gekuerzt,
|
* sich als Unordnung, nicht als Unterschied. Ein zu langer Name wird gekuerzt,
|
||||||
* die Stufenziffer davor bleibt immer stehen — sie ist die eigentliche
|
* die Stufenziffer davor bleibt immer stehen - sie ist die eigentliche
|
||||||
* Auskunft.
|
* Auskunft.
|
||||||
*
|
*
|
||||||
* `name` kommt aus ClearanceLevelConfig und ist im Adminbereich aenderbar,
|
* `name` kommt aus ClearanceLevelConfig und ist im Adminbereich aenderbar,
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import { colorVariable } from '@/components/StampMark'
|
|||||||
*
|
*
|
||||||
* Mehrere zugleich: „Gekündigt" und „Verhaftet" schließen einander nicht aus.
|
* Mehrere zugleich: „Gekündigt" und „Verhaftet" schließen einander nicht aus.
|
||||||
* Die Namen und Farben kommen aus `PersonnelStatusTag` und sind im
|
* Die Namen und Farben kommen aus `PersonnelStatusTag` und sind im
|
||||||
* Adminbereich pflegbar — deshalb werden sie hereingereicht und nicht aus
|
* Adminbereich pflegbar - deshalb werden sie hereingereicht und nicht aus
|
||||||
* `lib/i18n/de.ts` gelesen. Siehe die Sprachkonvention: Code englisch, Oberflaeche deutsch.
|
* `lib/i18n/de.ts` gelesen. Siehe die Sprachkonvention: Code englisch, Oberflaeche deutsch.
|
||||||
*/
|
*/
|
||||||
export function StatusTags({
|
export function StatusTags({
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import { Pending } from '@/components/Pending'
|
|||||||
* Eine Reiterleiste.
|
* Eine Reiterleiste.
|
||||||
*
|
*
|
||||||
* Sie hebt hervor, wo man gerade ist. Vorher taten das weder die Reiter der
|
* Sie hebt hervor, wo man gerade ist. Vorher taten das weder die Reiter der
|
||||||
* Fallakte noch die der Verwaltung — fünf gleich aussehende Links, und man
|
* Fallakte noch die der Verwaltung - fünf gleich aussehende Links, und man
|
||||||
* musste an der Seite selbst ablesen, welcher davon offen war.
|
* musste an der Seite selbst ablesen, welcher davon offen war.
|
||||||
*
|
*
|
||||||
* Der Vergleich läuft über den Pfad, nicht über die vollständige Adresse:
|
* Der Vergleich läuft über den Pfad, nicht über die vollständige Adresse:
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ export const THEME_KEY = 'afis:thema'
|
|||||||
*
|
*
|
||||||
* Drei Zustände, nicht zwei: „System" ist der Auslieferungszustand und folgt
|
* Drei Zustände, nicht zwei: „System" ist der Auslieferungszustand und folgt
|
||||||
* `prefers-color-scheme`. Wer ausdrücklich wählt, bekommt seine Wahl auf
|
* `prefers-color-scheme`. Wer ausdrücklich wählt, bekommt seine Wahl auf
|
||||||
* diesem Gerät gemerkt — nicht am Konto, denn es ist eine Eigenschaft des
|
* diesem Gerät gemerkt - nicht am Konto, denn es ist eine Eigenschaft des
|
||||||
* Bildschirms, an dem jemand sitzt, nicht der Person.
|
* Bildschirms, an dem jemand sitzt, nicht der Person.
|
||||||
*
|
*
|
||||||
* Gesetzt wird das Attribut schon vor dem ersten Malen, siehe das Skript im
|
* Gesetzt wird das Attribut schon vor dem ersten Malen, siehe das Skript im
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user