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

375 Vorkommen in 210 Dateien. Betrifft Kommentare, Oberflaechentexte und die
README; an keiner Stelle Bedeutung, nur Zeichensetzung.
This commit is contained in:
Harlekin
2026-09-04 08:54:52 +02:00
parent 38be773636
commit e0b03ea336
210 changed files with 368 additions and 368 deletions
+1 -1
View File
@@ -2,7 +2,7 @@ import { Skeleton, SkeletonLine } from '@/components/Skeleton'
/**
* 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.
*/
export default function Loading() {
+1 -1
View File
@@ -39,7 +39,7 @@ export async function generateMetadata({
*
* 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
* 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.
*/
export default async function DepartmentPage({ params }: { params: Promise<{ key: string }> }) {
+1 -1
View File
@@ -14,7 +14,7 @@ export const dynamic = 'force-dynamic'
*
* 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
* 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.
*/
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
* 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.
*/
export function DepartmentForm({
+1 -1
View File
@@ -2,7 +2,7 @@ import { Skeleton, SkeletonLine } from '@/components/Skeleton'
/**
* 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.
*/
export default function Loading() {
+1 -1
View File
@@ -12,7 +12,7 @@ export const dynamic = 'force-dynamic'
*
* Wie bei der Fallakte sind die Bereiche **eigene Seiten** unter einem
* 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 }) {
const user = await requireUser()
+1 -1
View File
@@ -2,7 +2,7 @@ import { Skeleton, SkeletonLine } from '@/components/Skeleton'
/**
* 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.
*/
export default function Loading() {
+1 -1
View File
@@ -11,7 +11,7 @@ export const dynamic = 'force-dynamic'
* Die Startseite der Verwaltung: was es gibt und wofür es da ist.
*
* 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.
*/
export default async function AdminIndexPage() {
+1 -1
View File
@@ -2,7 +2,7 @@ import { Skeleton, SkeletonLine } from '@/components/Skeleton'
/**
* 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.
*/
export default function Loading() {
+1 -1
View File
@@ -18,7 +18,7 @@ export type PolicyResult = { ok: true } | { ok: false; error: string }
/**
* Die Änderungsrichtlinie gilt für die ganze Abteilung und wird serverseitig
* 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> {
const user = await requirePermissionForAction('admin')
+1 -1
View File
@@ -2,7 +2,7 @@ import { Skeleton, SkeletonLine } from '@/components/Skeleton'
/**
* 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.
*/
export default function Loading() {
+1 -1
View File
@@ -2,7 +2,7 @@ import { Skeleton, SkeletonLine } from '@/components/Skeleton'
/**
* 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.
*/
export default function Loading() {
+1 -1
View File
@@ -10,7 +10,7 @@ export const dynamic = 'force-dynamic'
/**
* 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
* die höchste. Damit steht die Hierarchie an einer Stelle, die ohnehin gepflegt
* wird, statt in einer zweiten Sortierung in dieser Anwendung.
+1 -1
View File
@@ -5,7 +5,7 @@ import { de } from '@/lib/i18n/de'
* Die Bereiche der Verwaltung, an einer Stelle.
*
* 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
* Leiste darunter.
*
+1 -1
View File
@@ -2,7 +2,7 @@ import { Skeleton, SkeletonLine } from '@/components/Skeleton'
/**
* 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.
*/
export default function Loading() {
+1 -1
View File
@@ -97,7 +97,7 @@ export default async function SystemPage() {
/**
* 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.
*/
async function VitalsTable() {
+1 -1
View File
@@ -2,7 +2,7 @@ import { Skeleton, SkeletonLine } from '@/components/Skeleton'
/**
* 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.
*/
export default function Loading() {
+1 -1
View File
@@ -13,7 +13,7 @@ const schema = z.object({ id: z.string().min(1) })
/**
* 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> {
const user = await requireUser()
@@ -14,7 +14,7 @@ import {
* Ausfertigen, Zurückziehen, Umklassifizieren.
*
* 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.
*/
export function DocumentActions({
@@ -2,7 +2,7 @@ import { Skeleton, SkeletonLine } from '@/components/Skeleton'
/**
* 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.
*/
export default function Loading() {
+1 -1
View File
@@ -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']) })
/** 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(
_prev: ActionResult | null,
formData: FormData,
+1 -1
View File
@@ -2,7 +2,7 @@ import { Skeleton } from '@/components/Skeleton'
/**
* 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.
*/
export default function Loading() {
+1 -1
View File
@@ -28,7 +28,7 @@ const requestSchema = z.union([
/**
* 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
* auf Playwright wartet. Siehe docs/09-performance-und-ux.md.
*/
@@ -90,7 +90,7 @@ export function CaseActions({
</button>
</div>
{/* 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. */}
<p className="mt-1.5 max-w-sm text-2xs text-text-dim">
{caseText.statusHints[nextStatus] ?? caseText.statusChangeHint}
@@ -113,7 +113,7 @@ export function Participants({
))}
</PanelSelect>
<PanelSelect value={target} onChange={setTarget} label={participantText.person}>
<option value="">—</option>
<option value="">-</option>
<optgroup label={participantText.pickUser}>
{members.map((member) => (
<option key={member.id} value={`user:${member.id}`}>
@@ -7,7 +7,7 @@ import { toggleWatchAction } from '../actions'
/**
* 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
* 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.
* 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.
*/
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,
* 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({
params,
@@ -2,7 +2,7 @@ import { Skeleton, SkeletonLine } from '@/components/Skeleton'
/**
* 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.
*/
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.
* 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.
*/
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.
* 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.
*/
export default function Loading() {
+2 -2
View File
@@ -20,12 +20,12 @@ export const dynamic = 'force-dynamic'
* 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
* 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
* gleichzeitig braucht.
*
* 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.
*/
export default async function CaseLayout({
+1 -1
View File
@@ -185,7 +185,7 @@ async function ChatBlock({
/**
* 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.
*/
export default async function CasePage({
@@ -51,7 +51,7 @@ export async function saveSectionAction(
/**
* 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
* übrigen nicht aufhalten. Wäre das eine Transaktion, blockierte ein einziger
* fremder Eingriff das ganze Blatt.
+2 -2
View File
@@ -66,7 +66,7 @@ export async function createCaseAction(
caseNumber = created.caseNumber
// 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.
for (const eintrag of beteiligte) {
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 }
/** 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> {
const user = await requireUser()
const parsed = watchSchema.safeParse(input)
+2 -2
View File
@@ -25,7 +25,7 @@ export function NewCaseForm({
const [state, action, pending] = useActionState<CreateState, FormData>(createCaseAction, {})
// 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.
const [beteiligte, setBeteiligte] = useState<Entwurf[]>([])
const [naechster, setNaechster] = useState(1)
@@ -133,7 +133,7 @@ export function NewCaseForm({
onChange={(event) => aendern(eintrag.schluessel, { ziel: event.target.value })}
className="field grow"
>
<option value="">—</option>
<option value="">-</option>
<optgroup label={participantText.pickUser}>
{members.map((member) => (
<option key={member.id} value={`user:${member.id}`}>
+1 -1
View File
@@ -2,7 +2,7 @@ import { Skeleton } from '@/components/Skeleton'
/**
* 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.
*/
export default function Loading() {
+1 -1
View File
@@ -82,7 +82,7 @@ export default async function CaseListPage({
</Link>
{/* Alle sieben Zustände, nicht fünf: „Entwurf" und „Versiegelt" fehlten,
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. */}
{CASE_STATUSES.filter(
(value) =>
+2 -2
View File
@@ -13,8 +13,8 @@ import { hasAdminAccess } from './admin/sections'
/**
* Das Rahmenwerk der angemeldeten Anwendung.
*
* Die Kopfzeile trägt links die Bereiche der Arbeit — Übersicht, Fallakten,
* Personalakten, Verwaltung — und rechts alles, was den Nutzer selbst
* Die Kopfzeile trägt links die Bereiche der Arbeit - Übersicht, Fallakten,
* Personalakten, Verwaltung - und rechts alles, was den Nutzer selbst
* betrifft: Suchbalken und Profilbild. Thema, Benachrichtigungen und Abmelden
* standen früher zwischen den Aktenbereichen und nahmen dort den Platz weg,
* 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.
*
* 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({
personnelFileId,
@@ -94,7 +94,7 @@ export function Assignments({
))}
</PanelSelect>
<PanelSelect value={userId} onChange={setUserId} label={participantText.pickUser}>
<option value="">—</option>
<option value="">-</option>
{members.map((member) => (
<option key={member.id} value={member.id}>
{member.displayName}
@@ -12,7 +12,7 @@ import { linkCaseToPersonAction } from '../actions'
*
* Derselbe Vorgang wie „Beteiligte eintragen" auf der Fallakte, nur von der
* 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.
*/
export function LinkCase({ personnelFileId }: { personnelFileId: string }) {
@@ -27,7 +27,7 @@ type File = {
* Kopfdaten einer Personalakte: lesen und ändern in derselben Ansicht.
*
* 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.
*/
export function PersonnelHead({
@@ -276,7 +276,7 @@ function Zeile({
return (
<div className="flex gap-3">
<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>
)
}
+1 -1
View File
@@ -171,7 +171,7 @@ async function AssignmentBlock({
* dass das Gespräch bei ihrer Akte steht, war genau das, was vorher auf
* 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.
*/
async function ChatBlock({
+1 -1
View File
@@ -71,7 +71,7 @@ export async function createPersonnelAction(
id = created.id
// 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.
await erstesAktenblatt(user.permissions, id, parsed.data.displayName)
} catch (error) {
@@ -8,8 +8,8 @@ import { createPersonnelAction, type ActionResult } from '../actions'
/**
* Neue Personalakte.
*
* Das Discord-Konto ist **optional**. Personen ohne Konto — Zeugen, ehemalige
* Mitarbeiter, Externe — brauchen genauso eine Akte, und ihr Name ist dann das
* Das Discord-Konto ist **optional**. Personen ohne Konto - Zeugen, ehemalige
* Mitarbeiter, Externe - brauchen genauso eine Akte, und ihr Name ist dann das
* einzige, was sie identifiziert.
*/
export function NewPersonnelForm({
+1 -1
View File
@@ -2,7 +2,7 @@ import { Skeleton } from '@/components/Skeleton'
/**
* 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.
*/
export default function Loading() {
+1 -1
View File
@@ -22,7 +22,7 @@ export default async function PersonnelListPage({
}: {
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 { q, status } = await searchParams
+1 -1
View File
@@ -9,7 +9,7 @@ import { deleteViewAction, saveViewAction } from './actions'
/**
* 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
* in der Datenbank.
*/
+1 -1
View File
@@ -10,7 +10,7 @@ const DEBOUNCE_MS = 250
/**
* 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
* Seite lädt, bleiben die alten Treffer stehen und werden gedimmt, statt zu
* verschwinden. Siehe docs/09-performance-und-ux.md.
+1 -1
View File
@@ -2,7 +2,7 @@ import { Skeleton, SkeletonLine } from '@/components/Skeleton'
/**
* 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.
*/
export default function Loading() {
+5 -5
View File
@@ -30,7 +30,7 @@ export const dynamic = 'force-dynamic'
* Suchseite.
*
* 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.
*/
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,
// 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.
const ergebnis = alle ? null : (faelle ?? dokumente ?? personen)!
const gesamt = alle
@@ -108,7 +108,7 @@ export default async function SearchPage({
{SEARCH_SCOPES.map((kind) => {
const anzahl = kind === 'all' ? gesamt : zahlen[kind]
// `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.
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
* 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.
*/
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.
*/
function Facette({
+1 -1
View File
@@ -2,7 +2,7 @@ import { Skeleton, SkeletonLine } from '@/components/Skeleton'
/**
* 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.
*/
export default function Loading() {
+3 -3
View File
@@ -16,7 +16,7 @@ export const dynamic = 'force-dynamic'
*
* 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
* 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.
*/
export default async function DashboardPage() {
@@ -206,13 +206,13 @@ function Block({
/**
* 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.
*
* 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
* 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
* sich, dass sie geöffnet wurden. Der Vorabruf wird zwar serverseitig erkannt
+1 -1
View File
@@ -2,7 +2,7 @@ import { Skeleton, SkeletonLine } from '@/components/Skeleton'
/**
* 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.
*/
export default function Loading() {
+1 -1
View File
@@ -5,7 +5,7 @@ import { de } from '@/lib/i18n/de'
*
* Bewusst **kein `notFound()`**: die Wurzel hat ein `loading.tsx`, und dessen
* 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
* Fehler im Papier, nicht wie eine Auskunft.
*
+2 -2
View File
@@ -7,7 +7,7 @@ import { exportText } from '@/lib/i18n/export'
/**
* 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,
* also darf dort keine Kopfzeile stehen, die `requireUser()` braucht. Genau
* 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`
* 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
* Zurück-Knopf nichts verloren.
@@ -23,7 +23,7 @@ export async function generateMetadata({
/**
* 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,
* ein Vertrag als Vertrag. Siehe docs/08-suche-und-export.md.
*/
@@ -169,7 +169,7 @@ export default async function PrintDocumentPage({
</p>
) : (
<p style={{ margin: '1mm 0', whiteSpace: 'pre-wrap' }}>
{String(section.value ?? '—')}
{String(section.value ?? '-')}
</p>
)}
</div>
@@ -99,7 +99,7 @@ export default async function PrintCasePage({
/>
<Zeile
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
@@ -164,7 +164,7 @@ export default async function PrintCasePage({
<td>{anhang.originalName}</td>
<td>{anhang.sizeBytes}</td>
{/* 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' }}>
{anhang.sha256}
</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
* 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.
*/
function PrintSections({
@@ -256,7 +256,7 @@ function PrintSections({
</p>
) : (
<p style={{ margin: '1mm 0', whiteSpace: 'pre-wrap' }}>
{String(section.value ?? '—')}
{String(section.value ?? '-')}
</p>
)}
</div>
+2 -2
View File
@@ -10,9 +10,9 @@ export const dynamic = 'force-dynamic'
* kurzlebigen Zeichen und ohne Sitzung, und eine Weiterleitung zur Anmeldung
* 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
* 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.
*/
export default function PrintLayout({ children }: { children: ReactNode }) {
+1 -1
View File
@@ -18,7 +18,7 @@ export const dynamic = 'force-dynamic'
* GET /api/admin/export?kind=bundle ZIP mit den fertigen Exporten
*
* 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
* Akte.
*/
+1 -1
View File
@@ -14,7 +14,7 @@ export const dynamic = 'force-dynamic'
* Herunterladen eines fertigen Exports.
*
* 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.
*/
export async function GET(
+1 -1
View File
@@ -13,7 +13,7 @@ export const runtime = 'nodejs'
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.
* Siehe docs/07-dateien-und-sicherheit.md.
*
+1 -1
View File
@@ -16,7 +16,7 @@ export const dynamic = 'force-dynamic'
* 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
* 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.
*/
export async function POST(request: Request): Promise<Response> {
+2 -2
View File
@@ -18,7 +18,7 @@ const RECENT_IN_PALETTE = 6
* Schnellsuche für die Kommandopalette.
*
* 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`.
*
* 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
// 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') {
const zuletzt = await listRecent(user.permissions, RECENT_IN_PALETTE)
return NextResponse.json({
+3 -3
View File
@@ -30,7 +30,7 @@ const HEARTBEAT_MS = 25_000
*
* Drei Prüfungen, alle nötig:
* 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.
* 2. **Je Ereignis**: der Inhalt wird mit den Rechten des Empfängers geladen.
* 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
* Verbinden eingefroren. Eine offene Verbindung ist sonst der eine Ort im
* 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.
*/
export async function GET(request: Request): Promise<Response> {
@@ -161,7 +161,7 @@ export async function GET(request: Request): Promise<Response> {
return
}
// 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
// die **einzige** Schranke.
if (abdruck(frisch) !== abdruck(rechte)) {
+1 -1
View File
@@ -17,7 +17,7 @@ const BEWERTUNGEN = new Set(['good', 'needs-improvement', 'poor'])
* Zeichenketten aus dem Browser speichert, ist ein Ablagefach für Fremde.
*
* 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.
*/
export async function POST(request: Request): Promise<Response> {
+7 -7
View File
@@ -56,7 +56,7 @@
/* Struktur: Flaechen bleiben eckig, Bedienelemente sind rund.
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-sm: 2px;
--radius-DEFAULT: 2px;
@@ -87,7 +87,7 @@
* Klassifizierungsfarben: eine Stufe muss in beiden Themen dieselbe Farbe
* 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
* mindestens 4,5:1 für Fliesstext ausgelegt. */
[data-theme='light'] {
@@ -226,7 +226,7 @@
**Und er muss beim Druecken zurueckmelden, dass er getroffen wurde.**
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
setzt ohne Verzoegerung ein und klingt langsam ab: ein kurzer Klick
bliebe sonst unsichtbar. Siehe docs/10-design-system.md.
@@ -318,7 +318,7 @@
Jede Liste in dieser Anwendung zeigt dasselbe Gebilde: eine Kennung, ein
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"
untereinander sieht man das sofort.
@@ -350,7 +350,7 @@
}
/* 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. */
.record-label {
flex: none;
@@ -364,7 +364,7 @@
}
/* 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. */
.record-rank {
flex: none;
@@ -827,7 +827,7 @@
/* Der Grund unter einer Druckansicht.
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
weg — Papier bringt seinen eigenen Grund mit. */
weg - Papier bringt seinen eigenen Grund mit. */
.print-page {
min-height: 100dvh;
background: #f2f2f0;
+2 -2
View File
@@ -21,7 +21,7 @@ export const viewport: Viewport = {
*
* Ohne das blitzt beim Laden kurz das falsche Thema auf. Es ist die eine
* 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 =
`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.
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
nachladen kann. React vergleicht beim Hydrieren aber genau dieses
Attribut, liest clientseitig den leeren Wert und meldet einen
+2 -2
View File
@@ -12,7 +12,7 @@ export { SIGN_IN_ERRORS }
/**
* 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
* `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
* Ursache zeigt.
*
@@ -38,7 +38,7 @@ type DiscordProfile = {
* benutzt: die Rollen holt der Bot mit seinem eigenen Token.
*
* 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,
* wenn jemand seine Rolle verliert. Siehe docs/03-auth-und-rechte.md.
*/
+2 -2
View File
@@ -6,13 +6,13 @@ import { commonText } from '@/lib/i18n/common'
/**
* 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
* 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.
*
* 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({
label,
+1 -1
View File
@@ -6,7 +6,7 @@
* im Stack für ein Bild von 64 Pixeln. Dafür steht `cdn.discordapp.com` in der
* 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
* mit Initialen ist ehrlicher und lädt nichts nach.
*/
+1 -1
View File
@@ -7,7 +7,7 @@ import { Pending } from '@/components/Pending'
/**
* 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
* Knopfform wie alles Bedienbare, und der Bereich, in dem man steht, ist
* gefüllt.
+2 -2
View File
@@ -6,12 +6,12 @@ import { useLinkStatus } from 'next/link'
* Der Ladepunkt in einem Link, solange der Wechsel läuft.
*
* 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.
*
* 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
* 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.
*
* `useLinkStatus` funktioniert nur als Kind eines `next/link`, deshalb die
+2 -2
View File
@@ -27,7 +27,7 @@ export function PieChart({
}: {
slices: Slice[]
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
}) {
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
* 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.
*/
function farbeFuer(index: number, anzahl: number): string {
+1 -1
View File
@@ -19,7 +19,7 @@ import { ThemeToggle } from '@/components/ThemeToggle'
* das Abmelden. In der Kopfzeile standen diese Dinge vorher zwischen den
* 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.
*/
export function ProfileMenu({
+1 -1
View File
@@ -3,7 +3,7 @@ import { searchBarText } from '@/lib/i18n/shell'
/**
* 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
* Filtern, Facetten und Abbruch laufender Anfragen sitzt auf der Suchseite,
* hier steht nur das Eingangstor.
+2 -2
View File
@@ -17,7 +17,7 @@ export function stampColor(level: number): string {
* `ClearanceLevelConfig.colorToken` und `PersonnelStatusTag.colorToken` sind
* Daten und im Adminbereich pflegbar. Sie landen als Name einer CSS-Variablen
* 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
* 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
* Adminbereich aenderbar; unterschiedlich breite Marken untereinander lesen
* 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.
*
* `name` kommt aus ClearanceLevelConfig und ist im Adminbereich aenderbar,
+1 -1
View File
@@ -5,7 +5,7 @@ import { colorVariable } from '@/components/StampMark'
*
* Mehrere zugleich: „Gekündigt" und „Verhaftet" schließen einander nicht aus.
* 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.
*/
export function StatusTags({
+1 -1
View File
@@ -8,7 +8,7 @@ import { Pending } from '@/components/Pending'
* Eine Reiterleiste.
*
* 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.
*
* Der Vergleich läuft über den Pfad, nicht über die vollständige Adresse:
+1 -1
View File
@@ -12,7 +12,7 @@ export const THEME_KEY = 'afis:thema'
*
* Drei Zustände, nicht zwei: „System" ist der Auslieferungszustand und folgt
* `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.
*
* Gesetzt wird das Attribut schon vor dem ersten Malen, siehe das Skript im
+1 -1
View File
@@ -8,7 +8,7 @@ import { usePathname } from 'next/navigation'
*
* Gesendet wird mit `sendBeacon`: das überlebt den Seitenwechsel, blockiert
* nichts und braucht keine offene Verbindung. Die Bibliothek wird erst
* geladen, wenn der Browser Zeit hat — die Messung darf nicht selbst zur
* geladen, wenn der Browser Zeit hat - die Messung darf nicht selbst zur
* Ladezeit beitragen.
*
* Übertragen wird das **Routenmuster**, nicht der konkrete Pfad: eine
+5 -5
View File
@@ -25,16 +25,16 @@ export type ChatScopeView =
type Reaktion = { messageId: string; userId: string; key: string }
/**
* Der Chat einer Akte — Fallakte oder Personalakte.
* Der Chat einer Akte - Fallakte oder Personalakte.
*
* Optisch klar vom Verlauf abgesetzt, weil er inhaltlich etwas anderes ist:
* formlos, kein Aktenbestandteil. Die Zeitachse des Verlaufs hat eine
* durchgehende Linie, der Chat nicht — der Unterschied muss ohne Beschriftung
* durchgehende Linie, der Chat nicht - der Unterschied muss ohne Beschriftung
* erkennbar sein. Siehe docs/10-design-system.md.
*
* Gesendet wird optimistisch: die Nachricht steht sofort da, mit einer
* clientseitig erzeugten UUIDv7 als Kennung. Kommt dieselbe Kennung über den
* Strom zurück, wird der optimistische Eintrag ersetzt statt verdoppelt — und
* Strom zurück, wird der optimistische Eintrag ersetzt statt verdoppelt - und
* ein wiederholtes Absenden nach einem Netzfehler erzeugt keine zweite
* Nachricht. Siehe docs/09-performance-und-ux.md.
*/
@@ -194,7 +194,7 @@ export function Chat({
{nachricht.replyToId ? (
<p className="mb-1 border-l-(length:--spacing-edge) border-line pl-2 text-2xs text-text-dim">
{chatText.replyingTo} {nachId.get(nachricht.replyToId)?.authorName ?? '—'}:{' '}
{chatText.replyingTo} {nachId.get(nachricht.replyToId)?.authorName ?? '-'}:{' '}
{(nachId.get(nachricht.replyToId)?.body ?? '').slice(0, 80)}
</p>
) : null}
@@ -357,7 +357,7 @@ export function Chat({
<div className="border-t border-line px-4 py-3">
{antwortAuf ? (
<p className="mb-2 flex items-center gap-3 text-2xs text-text-dim">
{chatText.replyingTo} {nachId.get(antwortAuf)?.authorName ?? '—'}
{chatText.replyingTo} {nachId.get(antwortAuf)?.authorName ?? '-'}
<button
type="button"
onClick={() => setAntwortAuf(null)}
+1 -1
View File
@@ -18,7 +18,7 @@ const ChatInner = dynamic(() => import('./Chat').then((modul) => modul.Chat), {
* Lädt den Chat erst, wenn er in Sichtweite kommt.
*
* Zwei Gründe, beide handfest: die Fallakte ist das, weswegen die Seite
* geöffnet wird — ihr Bündelbudget soll nicht für einen Chat draufgehen, den
* geöffnet wird - ihr Bündelbudget soll nicht für einen Chat draufgehen, den
* viele nie aufklappen. Und der Chat hält eine offene Verbindung zum
* Ereignisstrom; wer nur die Akte liest, soll sie nicht aufbauen.
*
+1 -1
View File
@@ -14,7 +14,7 @@ type Eintrag = {
}
/**
* Dokumente eines Besitzers. Die Liste ist bereits gefiltert — was hier
* Dokumente eines Besitzers. Die Liste ist bereits gefiltert - was hier
* ankommt, darf der Leser sehen. Kein Ausblenden im Client.
*/
export function DocumentList({
+1 -1
View File
@@ -9,7 +9,7 @@ import { createDocumentAction, type ActionResult } from '@/app/(app)/dokumente/a
* Dokument anlegen, an einer Fallakte oder an einer Personalakte.
*
* Die Vorlagenliste kommt vom Server und enthält nur, was an dieser Stelle
* erlaubt ist. Geprüft wird das trotzdem noch einmal beim Anlegen — die
* erlaubt ist. Geprüft wird das trotzdem noch einmal beim Anlegen - die
* Auswahl im Formular ist eine Bequemlichkeit, keine Sicherung.
*/
export function NewDocument({
+3 -3
View File
@@ -14,7 +14,7 @@ import type { RenderedSection } from '@/lib/sections/render'
* Das Aktenblatt: ein Formular, kein Bildschirmelement.
*
* Nachgebaut nach dem Blatt, das die Personalabteilung bislang von Hand
* ausgefüllt hat — Raster, Rahmenstärken und Beschriftungen entsprechen ihm.
* ausgefüllt hat - Raster, Rahmenstärken und Beschriftungen entsprechen ihm.
* Es steht bewusst außerhalb des Design-Systems der Oberfläche, weil es
* gedruckt weitergereicht wird und auf Papier gleich aussehen muss.
*
@@ -81,7 +81,7 @@ export function Sheet({
style={{ backgroundColor: balken?.colorHex ?? '#333333' }}
>
<span>
{subtitle} — {balken ? `${balken.name} [Level ${balken.level}]` : de.document.unclassified}
{subtitle} - {balken ? `${balken.name} [Level ${balken.level}]` : de.document.unclassified}
</span>
<span>
{de.document.issuedAt}: {formatiereDatum(issuedAt)}
@@ -213,7 +213,7 @@ function Cell({
<div className="sheet-signature">{asText(section?.value)}</div>
{field.dateKey ? (
<div className="sheet-label" style={{ marginTop: 5 }}>
{de.document.signedOn}: {asText(dateSection?.value) || '—'}
{de.document.signedOn}: {asText(dateSection?.value) || '-'}
</div>
) : null}
</td>
+1 -1
View File
@@ -7,7 +7,7 @@ import { documentText } from '@/lib/i18n/documents'
* Umschalter zwischen Aktenblatt und Bearbeitung.
*
* Beide Bäume kommen fertig gerendert vom Server herein; hier wird nur
* umgeschaltet. Das Blatt bleibt damit das, was es ist — ein Formular —, ohne
* umgeschaltet. Das Blatt bleibt damit das, was es ist - ein Formular -, ohne
* dass die Bearbeitung es mit Eingabefeldern zerlegt.
*/
export function SheetSwitch({
+1 -1
View File
@@ -10,7 +10,7 @@ import { requestExportAction, type ExportState } from '@/app/(app)/export/action
*
* Kein Fortschrittsbalken, der eine Zahl erfindet: der Auftrag hat drei
* Zustände, und die stehen da. Fertig meldet der Ereignisstrom, dann steht
* der Knopf zum Herunterladen bereit — automatisch losladen wäre unhöflich,
* der Knopf zum Herunterladen bereit - automatisch losladen wäre unhöflich,
* wenn jemand die Seite inzwischen wieder verlassen hat und zurückkommt.
*/
export function ExportPanel({
+1 -1
View File
@@ -18,7 +18,7 @@ const SEITEN: Treffer[] = paletteText.pages.map((seite) => ({
* Aktenzeichen eintippen und hinspringen, mehr will sie nicht. Die Treffer
* kommen serverseitig gefiltert; was hier ankommt, darf der Nutzer sehen.
*
* Eine laufende Anfrage wird bei neuer Eingabe abgebrochen — sonst überholt
* Eine laufende Anfrage wird bei neuer Eingabe abgebrochen - sonst überholt
* eine langsame Antwort eine schnellere und zeigt Treffer zu einem Begriff,
* der nicht mehr im Feld steht. Siehe docs/09-performance-und-ux.md.
*/
+1 -1
View File
@@ -13,7 +13,7 @@ const CommandPalette = dynamic(
*
* Absichtlich winzig: dieser Teil steckt in jeder Seite, und die Fallakte hat
* kein Bündel übrig. Die Palette selbst wird erst geholt, wenn jemand sie
* öffnet — vorher ist sie ein Tastaturereignis und sonst nichts.
* öffnet - vorher ist sie ein Tastaturereignis und sonst nichts.
*/
export function PaletteTrigger() {
const [offen, setOffen] = useState(false)
+2 -2
View File
@@ -11,7 +11,7 @@ import { useEffect, useRef } from 'react'
* Verkehr kostet nichts.
*
* Die Rückrufe stehen in einem Ref, damit ein neu erzeugter Handler nicht die
* ganze Verbindung neu aufbaut — sonst reisst der Strom bei jedem Tastendruck
* ganze Verbindung neu aufbaut - sonst reisst der Strom bei jedem Tastendruck
* im Eingabefeld ab.
*/
export type StreamHandlers = Record<string, (data: unknown) => void>
@@ -46,7 +46,7 @@ export function useStream(topics: string[], handlers: StreamHandlers): void {
// Der Server meldet einen Entzug ausdrücklich, statt die Verbindung nur
// fallen zu lassen: `EventSource` baut sonst sofort wieder auf, und man
// arbeitet auf einer Seite weiter, für die man keine Rechte mehr hat.
// Neu laden führt in die Anmeldung — dorthin, wo man jetzt hingehört.
// Neu laden führt in die Anmeldung - dorthin, wo man jetzt hingehört.
const entzogen: EventListener = () => {
quelle.close()
window.location.reload()
+4 -4
View File
@@ -6,7 +6,7 @@ import { useSectionsForm } from './SectionsForm'
import type { SaveSection } from './types'
type Props = {
/** Wohin gespeichert wird — gebundene Server Action der Akte oder des Dokuments. */
/** Wohin gespeichert wird - gebundene Server Action der Akte oder des Dokuments. */
save: SaveSection
/** Eindeutig je Besitzer, damit Entwürfe zweier Akten sich nicht mischen. */
scope: string
@@ -25,7 +25,7 @@ const AUTOSAVE_MS = 2000
/**
* Ein Abschnitt zum Bearbeiten. Trägt einen Fallabschnitt genauso wie ein
* Feld eines Dokuments — wohin gespeichert wird, kommt von außen.
* Feld eines Dokuments - wohin gespeichert wird, kommt von außen.
*
* Entwürfe werden alle zwei Sekunden **lokal** gesichert, damit ein
* versehentlicher Tab-Wechsel keinen Text kostet. Gespeichert wird erst auf
@@ -153,7 +153,7 @@ export function SectionEditor(props: Props) {
if (props.readOnly) {
return (
<p className="mt-1 max-w-(--container-measure) text-sm whitespace-pre-wrap">
{String(value || '—')}
{String(value || '-')}
</p>
)
}
@@ -166,7 +166,7 @@ export function SectionEditor(props: Props) {
onChange={(event) => change(event.target.value)}
className="field"
>
<option value="">—</option>
<option value="">-</option>
{props.options?.map((option) => (
<option key={option} value={option}>
{option}
+3 -3
View File
@@ -11,12 +11,12 @@ import type { RenderedSection } from '@/lib/sections/render'
* Ausgefüllte Vorlage: der Sachstand einer Fallakte oder der Inhalt eines
* Dokuments. Beide sind dasselbe Gebilde, deshalb dieselbe Darstellung.
*
* Feste Textbausteine der Vorlage werden gerendert, nicht bearbeitet — ihre
* Feste Textbausteine der Vorlage werden gerendert, nicht bearbeitet - ihre
* Platzhalter ziehen die Werte der Eingabefelder. Damit trägt dieselbe
* Darstellung ein Formular und einen Vertragstext.
*
* Gespeichert wird das Blatt als Ganzes: ein Knopf unten, nicht einer je Feld.
* Lichtbilder sind die Ausnahme — ein Bild wird hochgeladen und geprüft, das
* Lichtbilder sind die Ausnahme - ein Bild wird hochgeladen und geprüft, das
* ist ein eigener Vorgang und keine Feldänderung.
*/
export function Sections({
@@ -73,7 +73,7 @@ export function Sections({
}
if (field.type === 'photo') {
// Ohne Dokument gibt es kein Ziel für den Upload — Fallabschnitte
// Ohne Dokument gibt es kein Ziel für den Upload - Fallabschnitte
// tragen keine Bilder, ihre Dateien hängen an der Akte.
if (!documentNumber) {
return (
+1 -1
View File
@@ -46,7 +46,7 @@ export function SectionsForm({
children,
}: {
save: SaveSections
/** „Dokument speichern" oder „Abschnitte speichern" — je nachdem, was es ist. */
/** „Dokument speichern" oder „Abschnitte speichern" - je nachdem, was es ist. */
label: string
children: ReactNode
}) {
+1 -1
View File
@@ -24,7 +24,7 @@ export type SaveSection = (input: {
*
* Zurück kommt ein Ergebnis **je Feld**, nicht eines für alles. Abschnitte
* sind einzeln versioniert, und wenn ein Feld zwischenzeitlich von jemand
* anderem geändert wurde, soll das nicht die übrigen elf aufhalten — es soll
* anderem geändert wurde, soll das nicht die übrigen elf aufhalten - es soll
* genau an diesem Feld stehen.
*/
export type SaveSectionsInput = {
+2 -2
View File
@@ -59,7 +59,7 @@ test.describe('Geschuetzte Bereiche', () => {
test.describe('Druckansichten', () => {
// Sie liegen ausserhalb des angemeldeten Bereichs, weil der PDF-Dienst sie
// mit einem kurzlebigen Zeichen aufruft und keine Sitzung hat. Ohne
// gueltiges Zeichen gibt es sie nicht — 404 statt Weiterleitung, denn die
// gueltiges Zeichen gibt es sie nicht - 404 statt Weiterleitung, denn die
// Existenz ist selbst eine Information.
// Geprueft wird der Inhalt, nicht der Statuscode: Next streamt die Antwort,
// der Kopf ist also schon raus, wenn notFound() greift. Entscheidend ist,
@@ -81,7 +81,7 @@ test.describe('Druckansichten', () => {
test.describe('Schmale Schirme', () => {
// Die Seite darf auf keinem Gerät waagerecht scrollen. Ein Formular, das
// seitlich hinausragt, ist auf dem Telefon unbedienbar — und genau dort
// seitlich hinausragt, ist auf dem Telefon unbedienbar - und genau dort
// wird eine Akte unterwegs gelesen. Siehe docs/09-performance-und-ux.md.
for (const pfad of ['/anmelden', '/anmelden/fehler?grund=keine-afis-rolle']) {
test(`${pfad} laeuft nicht seitlich ueber`, async ({ page }) => {
+1 -1
View File
@@ -6,7 +6,7 @@ import { visibleDocumentWhere } from '@/lib/access/personnelFilter'
/**
* Anhänge, die ein Nutzer sehen darf.
*
* Ein Anhang hat keine eigene Freigabestufe — er erbt die seines Besitzers.
* Ein Anhang hat keine eigene Freigabestufe - er erbt die seines Besitzers.
* Das ist die richtige Bauart: eine Datei an einem Verlaufseintrag der Stufe 4
* ist so geheim wie der Eintrag, und läge die Stufe am Anhang, müsste jemand
* beide gleichzeitig richtig setzen.
+2 -2
View File
@@ -16,13 +16,13 @@ import { WORKING_ROLES } from '@/lib/cases/terms'
* 4. Stufe reicht → Zugriff
* 5. sonst kein Zugriff
*
* Zu 3.: Die Beteiligtenliste beantwortet zwei Fragen — wer bearbeitet den
* Zu 3.: Die Beteiligtenliste beantwortet zwei Fragen - wer bearbeitet den
* Fall, und um wen geht es darin. Nur die erste verschafft Zugang. Ein
* Betroffener, ein Zeuge oder ein Täter mit Discord-Konto läse sonst die
* Ermittlung gegen sich selbst mit. Siehe lib/cases/terms.ts.
*
* Der Ausschluss steht auch über dem Admin. Das Codebeispiel im Dokument gibt
* für Admins `{}` zurück und übergeht ihn damit — das widerspricht der
* für Admins `{}` zurück und übergeht ihn damit - das widerspricht der
* Reihenfolge eine Zeile darüber. Hier gilt die Reihenfolge: der Ausschluss
* deckt gerade den Fall ab, dass gegen einen hochrangigen Mitarbeiter
* ermittelt wird, und der Administrator ist einer.
+2 -2
View File
@@ -27,7 +27,7 @@ function requirePermission(p: EffectivePermissions, permission: Permission): Wri
/**
* Darf dieser Nutzer eine Akte mit dieser Klassifizierung anlegen?
*
* Niemand legt eine Akte über der eigenen Stufe an — auch kein Admin, denn er
* Niemand legt eine Akte über der eigenen Stufe an - auch kein Admin, denn er
* hätte sonst eine Akte erzeugt, deren Sichtbarkeit er nicht überblickt.
* Der Admin hat ohnehin Stufe 5.
*/
@@ -78,7 +78,7 @@ export function canEditCase(
*
* Die neue Stufe darf die eigene nicht übersteigen. Eine Herabstufung ist der
* heikelste Vorgang im System: sie macht Inhalte für Leute sichtbar, die sie
* vorher nicht sehen durften. Erlaubt ist sie trotzdem — sie wird nur sichtbar
* vorher nicht sehen durften. Erlaubt ist sie trotzdem - sie wird nur sichtbar
* protokolliert, im Verlauf und im Zugriffsprotokoll.
*/
export function canReclassify(
+1 -1
View File
@@ -8,7 +8,7 @@ import { visibleDepartmentWhere } from '@/lib/access/departmentFilter'
* Chat: der formlose Teil, der **an einer Akte hängt**.
*
* Es gibt keine freien Räume mehr. Ein Chat gehört zu einer Fallakte oder zu
* einer Personalakte und erbt deren Sichtbarkeit — wer die Akte nicht lesen
* einer Personalakte und erbt deren Sichtbarkeit - wer die Akte nicht lesen
* darf, sieht auch nicht, dass darüber geredet wird. Damit gibt es keinen Ort
* im System, an dem über einen Vorgang gesprochen werden kann, ohne dass das
* Gespräch beim Vorgang steht. Siehe docs/13-entscheidungen.md.

Some files were not shown because too many files have changed in this diff Show More