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:
@@ -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.
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -13,7 +13,7 @@ import type { EffectivePermissions } from '@/lib/auth/permissions'
|
||||
* allgemein bekannt sein soll. Und sie gilt dann für alles, was **an** der
|
||||
* Abteilung hängt: ihre Vermerke und ihren Chat.
|
||||
*
|
||||
* Was in ihr aktenkundig ist, filtert dagegen weiterhin jede Liste für sich —
|
||||
* Was in ihr aktenkundig ist, filtert dagegen weiterhin jede Liste für sich -
|
||||
* die Mitglieder über `visiblePersonnelWhere`, die Fälle über
|
||||
* `visibleCaseWhere`. Eine sichtbare Abteilung ist kein Zugang zu ihrem Inhalt.
|
||||
*/
|
||||
|
||||
@@ -36,7 +36,7 @@ export function canWritePersonnel(
|
||||
/**
|
||||
* Darf dieser Nutzer ein Dokument mit dieser Klassifizierung anlegen?
|
||||
*
|
||||
* Der Besitzer ist vorher schon durch die Sichtbarkeitsprüfung gelaufen — was
|
||||
* Der Besitzer ist vorher schon durch die Sichtbarkeitsprüfung gelaufen - was
|
||||
* man nicht sieht, kann man nicht bestücken.
|
||||
*/
|
||||
export function canCreateDocument(
|
||||
|
||||
@@ -44,13 +44,13 @@ export function visiblePersonnelWhere(p: EffectivePermissions): Prisma.Personnel
|
||||
* Zwei Bedingungen, beide müssen zutreffen:
|
||||
*
|
||||
* 1. **Der Besitzer muss lesbar sein.** Ein Vertrag an einer Personalakte der
|
||||
* Stufe 4 bleibt verborgen, auch wenn der Vertrag selbst Stufe 1 trägt —
|
||||
* Stufe 4 bleibt verborgen, auch wenn der Vertrag selbst Stufe 1 trägt -
|
||||
* sonst verriete die Dokumentenliste die Existenz der Person.
|
||||
* 2. **Das Dokument selbst muss lesbar sein.** Innerhalb einer sichtbaren
|
||||
* Akte kann ein einzelnes Dokument höher klassifiziert sein.
|
||||
*
|
||||
* Wer an der besitzenden Fallakte beteiligt ist, liest ihre Dokumente
|
||||
* unabhängig von seiner Stufe — das ist derselbe ausnahmsweise Zugang wie beim
|
||||
* unabhängig von seiner Stufe - das ist derselbe ausnahmsweise Zugang wie beim
|
||||
* Verlauf, siehe docs/03-auth-und-rechte.md. Für Dokumente an einer
|
||||
* Personalakte und für Dokumente ohne Besitzer gibt es diesen Weg nicht.
|
||||
*/
|
||||
|
||||
@@ -37,7 +37,7 @@ export type AuditRow = {
|
||||
action: string
|
||||
targetType: string
|
||||
targetId: string
|
||||
/** Aktenzeichen, Name oder Dokumentnummer — was man wiedererkennt. */
|
||||
/** Aktenzeichen, Name oder Dokumentnummer - was man wiedererkennt. */
|
||||
targetLabel: string | null
|
||||
targetHref: string | null
|
||||
clearanceAtTime: number
|
||||
@@ -109,7 +109,7 @@ async function namenVon(ids: string[]): Promise<Map<string, string>> {
|
||||
* Aus Kennungen werden Aktenzeichen und Namen.
|
||||
*
|
||||
* Ohne das steht im Protokoll eine Spalte UUIDs, und niemand liest es. Der
|
||||
* Nachschlag läuft **ohne** Sichtbarkeitsfilter — das Protokoll ist die
|
||||
* Nachschlag läuft **ohne** Sichtbarkeitsfilter - das Protokoll ist die
|
||||
* Ausnahme, die es geben muss, damit die Leitung prüfen kann, ob die Regeln
|
||||
* eingehalten wurden. Genau deshalb kommt hier auch nur die Leitung hin.
|
||||
*/
|
||||
@@ -167,7 +167,7 @@ async function zieleVon(
|
||||
return karte
|
||||
}
|
||||
|
||||
/** Die Akteure, die überhaupt im Protokoll vorkommen — für die Auswahlliste. */
|
||||
/** Die Akteure, die überhaupt im Protokoll vorkommen - für die Auswahlliste. */
|
||||
export async function auditActors(): Promise<{ id: string; name: string }[]> {
|
||||
const gruppen = await db.auditLog.groupBy({ by: ['actorId'], _count: { actorId: true } })
|
||||
const namen = await namenVon(gruppen.map((g) => g.actorId))
|
||||
|
||||
@@ -23,8 +23,8 @@ export type CurrentUser = {
|
||||
* 2. Die Sitzungszeile existiert noch und ist nicht abgelaufen
|
||||
* 3. Der Nutzer trägt weiterhin eine Rolle, die zur Anmeldung berechtigt
|
||||
*
|
||||
* Schritt 3 ist doppelt gemoppelt — der Worker beendet Sitzungen beim
|
||||
* Rollenverlust bereits von sich aus — aber genau das ist der Sinn: fällt der
|
||||
* Schritt 3 ist doppelt gemoppelt - der Worker beendet Sitzungen beim
|
||||
* Rollenverlust bereits von sich aus - aber genau das ist der Sinn: fällt der
|
||||
* Worker aus, verliert trotzdem niemand unbemerkt seinen Ausschluss.
|
||||
*/
|
||||
export async function getCurrentUser(): Promise<CurrentUser | null> {
|
||||
|
||||
@@ -232,7 +232,7 @@ export async function restoreEntry(
|
||||
|
||||
/**
|
||||
* Änderungsübersicht einer Akte: alle nachträglichen Änderungen am Verlauf,
|
||||
* chronologisch. Sichtbar für alle, die die Akte lesen dürfen — das ist der
|
||||
* chronologisch. Sichtbar für alle, die die Akte lesen dürfen - das ist der
|
||||
* ganze Sinn der Sache.
|
||||
*/
|
||||
export async function loadChangeHistory(p: EffectivePermissions, caseId: string) {
|
||||
|
||||
@@ -14,7 +14,7 @@ import type { EffectivePermissions } from '@/lib/auth/permissions'
|
||||
* anderes, je nachdem, von welcher Seite man liest. `RELATED` ist symmetrisch.
|
||||
*
|
||||
* Beide Seiten laufen durch die Sichtbarkeitsprüfung. Eine Akte, die der Leser
|
||||
* nicht sehen darf, taucht auch als Verknüpfung nicht auf — sonst verriete die
|
||||
* nicht sehen darf, taucht auch als Verknüpfung nicht auf - sonst verriete die
|
||||
* Gegenseite ihre Existenz. Das gilt in **beide** Richtungen, deshalb wird der
|
||||
* Filter zweimal angewandt.
|
||||
*/
|
||||
|
||||
@@ -30,7 +30,7 @@ export async function listParticipants(p: EffectivePermissions, caseId: string)
|
||||
addedById: true,
|
||||
user: { select: { id: true, displayName: true } },
|
||||
// Die Personalakte kommt nur mit, wenn der Leser sie sehen darf. Sonst
|
||||
// steht der Name da, aber der Verweis führt ins Leere — das ist richtig
|
||||
// steht der Name da, aber der Verweis führt ins Leere - das ist richtig
|
||||
// so: der Beteiligte ist Teil der Akte, seine Personalakte nicht.
|
||||
personnelFile: { select: { id: true, displayName: true } },
|
||||
},
|
||||
@@ -92,7 +92,7 @@ export async function addParticipant(
|
||||
}
|
||||
if (personnelFileId) {
|
||||
// Eine Personalakte, die der Eintragende nicht sehen darf, kann er auch
|
||||
// nicht als Beteiligte setzen — sonst verrät ihm die Fehlermeldung ihre
|
||||
// nicht als Beteiligte setzen - sonst verrät ihm die Fehlermeldung ihre
|
||||
// Existenz.
|
||||
const akte = await db.personnelFile.findFirst({
|
||||
where: { AND: [visiblePersonnelWhere(p), { id: personnelFileId }] },
|
||||
@@ -146,7 +146,7 @@ export async function addParticipant(
|
||||
|
||||
/**
|
||||
* Entfernt einen Beteiligten. Anders als ein Verlaufseintrag ist das keine
|
||||
* Aussage über den Sachverhalt, sondern eine Zuständigkeit — sie darf
|
||||
* Aussage über den Sachverhalt, sondern eine Zuständigkeit - sie darf
|
||||
* verschwinden. Das Protokoll behält sie.
|
||||
*/
|
||||
export async function removeParticipant(
|
||||
|
||||
@@ -19,7 +19,7 @@ import type { EffectivePermissions } from '@/lib/auth/permissions'
|
||||
|
||||
export { AccessError } from '@/lib/errors'
|
||||
|
||||
/** Lädt eine Akte, sofern der Nutzer sie sehen darf. Sonst `null` — nie 403. */
|
||||
/** Lädt eine Akte, sofern der Nutzer sie sehen darf. Sonst `null` - nie 403. */
|
||||
export async function loadVisibleCase(p: EffectivePermissions, caseNumber: string) {
|
||||
return db.case.findFirst({
|
||||
where: { AND: [visibleCaseWhere(p), { caseNumber }] },
|
||||
@@ -303,14 +303,14 @@ async function missingRequiredSections(caseId: string, templateId: string): Prom
|
||||
/**
|
||||
* Wiedervorlage setzen oder abräumen.
|
||||
*
|
||||
* Das Feld gab es von Anfang an — die Übersicht zeigt darauf ihre Fristen, die
|
||||
* Das Feld gab es von Anfang an - die Übersicht zeigt darauf ihre Fristen, die
|
||||
* Suche filtert darauf, der Ausdruck druckt es. Nur setzen konnte es niemand:
|
||||
* es fehlte jede Stelle, an der man ein Datum einträgt. Deshalb stand unter
|
||||
* „Fristen" immer „Nichts steht an". Siehe docs/13-entscheidungen.md.
|
||||
*
|
||||
* Kein Verlaufseintrag: eine Wiedervorlage ist eine Merkhilfe für die
|
||||
* Bearbeitung und keine Aussage über den Fall. Protokolliert wird sie
|
||||
* trotzdem — sie ist eine Änderung an der Akte, und dafür gilt harte Regel 3.
|
||||
* trotzdem - sie ist eine Änderung an der Akte, und dafür gilt harte Regel 3.
|
||||
*/
|
||||
export async function setDueDate(
|
||||
p: EffectivePermissions,
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/**
|
||||
* Feste Wertelisten der Akte — Rollen der Beteiligten und Arten der
|
||||
* Feste Wertelisten der Akte - Rollen der Beteiligten und Arten der
|
||||
* Verknüpfung.
|
||||
*
|
||||
* **Ohne Abhängigkeiten.** Diese Datei wird auch im Client geladen, und die
|
||||
@@ -28,7 +28,7 @@ export function isParticipantRole(value: string): value is ParticipantRoleName {
|
||||
}
|
||||
|
||||
/**
|
||||
* Wer an der Akte **arbeitet** — und nur diese drei Rollen öffnen sie auch.
|
||||
* Wer an der Akte **arbeitet** - und nur diese drei Rollen öffnen sie auch.
|
||||
*
|
||||
* Die Beteiligtenliste beantwortet zwei ganz verschiedene Fragen: *wer bearbeitet
|
||||
* diesen Fall* und *um wen geht es darin*. Sie standen bisher gleichberechtigt
|
||||
|
||||
@@ -10,7 +10,7 @@ import type { EffectivePermissions } from '@/lib/auth/permissions'
|
||||
* Wer eine Akte beobachtet, sieht sie auf der Übersicht und bekommt ihre
|
||||
* Meldungen, auch ohne beteiligt zu sein. **Zugriff verschafft das nicht**:
|
||||
* beobachten kann man nur, was man ohnehin sehen darf, und fällt die Akte
|
||||
* später über die eigene Stufe, verschwindet sie auch aus der Liste — die
|
||||
* später über die eigene Stufe, verschwindet sie auch aus der Liste - die
|
||||
* Abfrage läuft durch dieselbe Sichtbarkeitsprüfung wie die Fallliste.
|
||||
*/
|
||||
export async function listWatched(p: EffectivePermissions, take = 20) {
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
* Client schickt Text, der Server bestimmt, wer gemeint ist.
|
||||
*/
|
||||
|
||||
/** `@name` — Buchstaben, Ziffern, Punkt, Unterstrich, Bindestrich. */
|
||||
/** `@name` - Buchstaben, Ziffern, Punkt, Unterstrich, Bindestrich. */
|
||||
export const MENTION = /@([a-z0-9._-]{2,32})/gi
|
||||
|
||||
export function extractMentionNames(body: string): string[] {
|
||||
|
||||
@@ -6,7 +6,7 @@ import type { EffectivePermissions } from '@/lib/auth/permissions'
|
||||
* Vorschaukarten für Aktenzeichen im Chattext.
|
||||
*
|
||||
* **Serverseitig aufgelöst, mit der Stufe des Lesers.** Wer die Akte nicht
|
||||
* sehen darf, bekommt eine neutrale Karte ohne Titel und ohne Status — nie den
|
||||
* sehen darf, bekommt eine neutrale Karte ohne Titel und ohne Status - nie den
|
||||
* Titel mitschicken und im Client ausblenden. Siehe docs/06.
|
||||
*
|
||||
* Ob eine Akte existiert, verrät die Karte damit auch nicht: eine unbekannte
|
||||
|
||||
@@ -22,7 +22,7 @@ import type { Prisma } from '@afis/db'
|
||||
/**
|
||||
* Der Aktenchat.
|
||||
*
|
||||
* Er hängt immer an einer Akte — einer Fallakte oder einer Personalakte — und
|
||||
* Er hängt immer an einer Akte - einer Fallakte oder einer Personalakte - und
|
||||
* ist **ausdrücklich kein Aktenbestandteil**: er ist der formlose Teil, den
|
||||
* die Abteilung bisher auf Discord hatte. Deshalb darf hier bearbeitet und
|
||||
* zurückgezogen werden, ohne dass eine Version entsteht.
|
||||
@@ -213,7 +213,7 @@ export async function sendMessage(
|
||||
select: { id: true },
|
||||
})
|
||||
|
||||
// Für den Worker. Hier steht nur, was hinausgehen darf — nie der Text der
|
||||
// Für den Worker. Hier steht nur, was hinausgehen darf - nie der Text der
|
||||
// Nachricht. Wer sie bekommt, entscheidet diese Anwendung, nicht er.
|
||||
if (ort.caseId) {
|
||||
const akte = await tx.case.findUniqueOrThrow({
|
||||
@@ -250,7 +250,7 @@ export async function sendMessage(
|
||||
return angelegt
|
||||
}
|
||||
|
||||
/** Bearbeiten. Sichtbar über `editedAt`, ohne Versionierung — kein Aktenbestand. */
|
||||
/** Bearbeiten. Sichtbar über `editedAt`, ohne Versionierung - kein Aktenbestand. */
|
||||
export async function editMessage(
|
||||
p: EffectivePermissions,
|
||||
input: { id: string; body: string },
|
||||
@@ -328,7 +328,7 @@ export async function listReactions(p: EffectivePermissions, messageIds: string[
|
||||
* „Als Vermerk übernehmen".
|
||||
*
|
||||
* Die Brücke zwischen Discord-Gewohnheit und sauberem Aktenbestand: aus einer
|
||||
* formlosen Nachricht wird ein Verlaufseintrag, der die harten Regeln erbt —
|
||||
* formlosen Nachricht wird ein Verlaufseintrag, der die harten Regeln erbt -
|
||||
* er ist ab da nicht mehr löschbar und jede Änderung erzeugt eine Version.
|
||||
*
|
||||
* Die Nachricht bleibt stehen und wird als übernommen markiert. Der Eintrag
|
||||
@@ -436,7 +436,7 @@ function themaVon(nachricht: {
|
||||
* Wann der Nutzer diesen Ort zuletzt gesehen hat.
|
||||
*
|
||||
* Der Marker wird beim Öffnen **nicht** sofort hochgesetzt, sondern erst, wenn
|
||||
* der Nutzer die Ansicht verlässt oder ausdrücklich weiterliest — sonst wäre
|
||||
* der Nutzer die Ansicht verlässt oder ausdrücklich weiterliest - sonst wäre
|
||||
* die Trennlinie „Neu seit deinem letzten Besuch" beim Neuladen sofort weg.
|
||||
*/
|
||||
export async function loadReadMarker(
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
/**
|
||||
* Fester Satz an Reaktionen, keine freie Emoji-Auswahl.
|
||||
*
|
||||
* Ohne Abhängigkeiten, weil der Client sie lädt — derselbe Schnitt wie
|
||||
* Ohne Abhängigkeiten, weil der Client sie lädt - derselbe Schnitt wie
|
||||
* `lib/cases/terms.ts`. Die Beschriftungen stehen in `lib/i18n/cases.ts`.
|
||||
*/
|
||||
export const REACTIONS = ['gesehen', 'zustimmung', 'frage', 'wichtig'] as const
|
||||
|
||||
@@ -4,7 +4,7 @@ import type { CasePreview } from '@/lib/chat/preview'
|
||||
* Die Form, in der eine Nachricht die Grenze zur Anzeige überquert.
|
||||
*
|
||||
* Datumsangaben als Zeichenkette, weil ein `Date` diese Grenze nicht sauber
|
||||
* überquert, und der Text einer zurückgezogenen Nachricht ist **leer** — nicht
|
||||
* überquert, und der Text einer zurückgezogenen Nachricht ist **leer** - nicht
|
||||
* ausgeblendet. Was der Client nicht bekommt, kann er nicht zeigen.
|
||||
*/
|
||||
export type ChatMessageView = {
|
||||
|
||||
@@ -13,7 +13,7 @@ import type { Prisma } from '@afis/db'
|
||||
*
|
||||
* Fünf Fragen, die man morgens hat: woran war ich dran, woran arbeite ich, was
|
||||
* ist fällig, wo hat jemand geschrieben, was hat sich getan. Jede läuft durch
|
||||
* `visibleCaseWhere` — die Übersicht ist der Ort, an dem eine unachtsame
|
||||
* `visibleCaseWhere` - die Übersicht ist der Ort, an dem eine unachtsame
|
||||
* Abfrage am ehesten eine Akte verrät, die man nicht sehen darf.
|
||||
*/
|
||||
|
||||
@@ -148,7 +148,7 @@ async function ungeleseneChats(p: EffectivePermissions): Promise<UnreadChat[]> {
|
||||
if (orte.length === 0) return []
|
||||
|
||||
// **Eine** Abfrage statt einer je Ort. Vorher war das ein `count` pro Akte
|
||||
// und pro Personalakte — bei hundert Orten hundert Hin- und Rückwege zur
|
||||
// und pro Personalakte - bei hundert Orten hundert Hin- und Rückwege zur
|
||||
// Datenbank, jeder für sich schnell und in der Summe die langsamste Stelle
|
||||
// der Übersicht. Die Lesemarke steckt als Bedingung im ODER-Zweig des
|
||||
// jeweiligen Ortes, gezählt wird über `groupBy`.
|
||||
|
||||
@@ -18,7 +18,7 @@ import type { EffectivePermissions } from '@/lib/auth/permissions'
|
||||
*
|
||||
* Schmaler als der Aktenverlauf ist er trotzdem: keine Korrekturkette, keine
|
||||
* Anhänge, keine Karenzzeit. Ein Abteilungsvermerk ist ein laufendes
|
||||
* Protokoll, kein Beweismittel — die Ermittlung gehört in eine Fallakte.
|
||||
* Protokoll, kein Beweismittel - die Ermittlung gehört in eine Fallakte.
|
||||
*/
|
||||
|
||||
const AUSWAHL = {
|
||||
@@ -101,7 +101,7 @@ export async function createEntry(
|
||||
}
|
||||
|
||||
/**
|
||||
* Ändern. Der vorherige Stand wird zur Version — in derselben Transaktion,
|
||||
* Ändern. Der vorherige Stand wird zur Version - in derselben Transaktion,
|
||||
* sonst gäbe es einen Moment, in dem der alte Text nirgends mehr steht.
|
||||
*/
|
||||
export async function editEntry(
|
||||
|
||||
@@ -9,7 +9,7 @@ import type { EffectivePermissions } from '@/lib/auth/permissions'
|
||||
import type { Prisma } from '@afis/db'
|
||||
|
||||
/**
|
||||
* Abteilungen — die Ebene über der Personalakte.
|
||||
* Abteilungen - die Ebene über der Personalakte.
|
||||
*
|
||||
* Der Aufbau ist derselbe wie bei der Personalakte, nur eine Stufe höher: was
|
||||
* zu einer Abteilung aktenkundig ist, **wird abgeleitet und nicht
|
||||
@@ -17,7 +17,7 @@ import type { Prisma } from '@afis/db'
|
||||
* wenn eine beteiligte Person dieser Abteilung angehört.
|
||||
*
|
||||
* Das ist die wichtigste Festlegung hier. Eine eingetragene Abteilung an der
|
||||
* Akte wäre schnell veraltet — Leute wechseln, Beteiligte kommen dazu — und
|
||||
* Akte wäre schnell veraltet - Leute wechseln, Beteiligte kommen dazu - und
|
||||
* niemand würde sie nachpflegen. Abgeleitet stimmt sie immer.
|
||||
*
|
||||
* Jede Abfrage läuft durch dieselben Sichtbarkeitsfragmente wie überall sonst.
|
||||
@@ -31,7 +31,7 @@ export type DepartmentSummary = {
|
||||
description: string | null
|
||||
minClearance: number
|
||||
isArchived: boolean
|
||||
/** `null`, wenn der Fragende Personalakten gar nicht sehen darf. Nicht `0` —
|
||||
/** `null`, wenn der Fragende Personalakten gar nicht sehen darf. Nicht `0` -
|
||||
eine Null wäre die Auskunft „hier ist niemand", und die steht ihm nicht zu. */
|
||||
memberCount: number | null
|
||||
}
|
||||
@@ -60,7 +60,7 @@ export async function listDepartments(
|
||||
})
|
||||
if (abteilungen.length === 0) return []
|
||||
|
||||
// Die Mitgliederzahl zählt nur, was der Nutzer auch sehen dürfte — sonst
|
||||
// Die Mitgliederzahl zählt nur, was der Nutzer auch sehen dürfte - sonst
|
||||
// verriete die Zahl, wie viele Personalakten über seiner Stufe liegen.
|
||||
if (!darfPersonal(p)) {
|
||||
return abteilungen.map((abteilung) => ({ ...abteilung, memberCount: null }))
|
||||
@@ -116,7 +116,7 @@ export async function listMembers(p: EffectivePermissions, departmentId: string)
|
||||
*
|
||||
* Über die Beteiligten und deren Personalakten. Beide Richtungen zählen: ob
|
||||
* jemand Ermittler oder Betroffener ist, ändert nichts daran, dass der Fall
|
||||
* die Abteilung betrifft — für die Zugehörigkeit ist die Rolle unerheblich,
|
||||
* die Abteilung betrifft - für die Zugehörigkeit ist die Rolle unerheblich,
|
||||
* für den Zugriff nicht (siehe lib/cases/terms.ts).
|
||||
*/
|
||||
function caseOfDepartment(departmentId: string): Prisma.CaseWhereInput {
|
||||
@@ -147,7 +147,7 @@ export async function listDepartmentCases(
|
||||
}
|
||||
|
||||
/**
|
||||
* Dokumente der Abteilung — abgeleitet wie alles hier.
|
||||
* Dokumente der Abteilung - abgeleitet wie alles hier.
|
||||
*
|
||||
* Zwei Wege dorthin: das Dokument hängt an der Personalakte eines Mitglieds,
|
||||
* oder an einer Fallakte, die diese Abteilung betrifft. Ein Dokument
|
||||
@@ -190,7 +190,7 @@ export type Slice = { label: string; value: number }
|
||||
* Womit diese Abteilung aktenkundig ist, nach Fallart.
|
||||
*
|
||||
* Gezählt wird über **dieselbe** Bedingung wie die Liste. Ein Diagramm, das
|
||||
* mehr Fälle zeigt als die Liste hergibt, wäre selbst eine Auskunft — genau
|
||||
* mehr Fälle zeigt als die Liste hergibt, wäre selbst eine Auskunft - genau
|
||||
* wie ein Facettenzähler. Siehe harte Regel 6.
|
||||
*/
|
||||
export async function caseTypesOfDepartment(
|
||||
@@ -211,7 +211,7 @@ export async function caseTypesOfDepartment(
|
||||
/**
|
||||
* Die Gegenrichtung: welche Abteilung wie viele Fälle hat.
|
||||
*
|
||||
* Das ist die Frage, mit der die Übersicht der Abteilungen anfängt — wo häuft
|
||||
* Das ist die Frage, mit der die Übersicht der Abteilungen anfängt - wo häuft
|
||||
* sich etwas. Auch hier zählt nur, was der Fragende sehen darf.
|
||||
*/
|
||||
export async function casesPerDepartment(
|
||||
@@ -219,7 +219,7 @@ export async function casesPerDepartment(
|
||||
redactedLabel: string,
|
||||
): Promise<Slice[]> {
|
||||
// **Alle** Abteilungen, auch die über der eigenen Stufe. Die Zahlen zählen
|
||||
// weiterhin nur Fälle, die der Fragende sehen darf — was er hier erfährt,
|
||||
// weiterhin nur Fälle, die der Fragende sehen darf - was er hier erfährt,
|
||||
// ist also nicht, was in einer verborgenen Abteilung steht, sondern dass
|
||||
// einige der ihm bekannten Fälle zu einer gehören, deren Namen er nicht
|
||||
// kennt. Ein Diagramm, das sie stillschweigend weglässt, wäre die
|
||||
@@ -281,7 +281,7 @@ export async function createDepartment(
|
||||
input: { key: string; name: string; description?: string | null; minClearance: number },
|
||||
): Promise<{ key: string }> {
|
||||
if (!darfVerwalten(p)) throw new AccessError('no-permission')
|
||||
// Eine Abteilung anzulegen, die man selbst nicht sieht, ergibt keinen Sinn —
|
||||
// Eine Abteilung anzulegen, die man selbst nicht sieht, ergibt keinen Sinn -
|
||||
// dieselbe Regel wie bei Akten und Dokumenten.
|
||||
if (!p.isAdmin && input.minClearance > p.clearanceLevel) {
|
||||
throw new AccessError('clearance-too-low')
|
||||
@@ -367,7 +367,7 @@ export async function updateDepartment(
|
||||
}
|
||||
|
||||
/**
|
||||
* Eine Person einer Abteilung zuordnen — oder die Zuordnung aufheben.
|
||||
* Eine Person einer Abteilung zuordnen - oder die Zuordnung aufheben.
|
||||
*
|
||||
* Setzt dasselbe Recht voraus wie das Ändern der Personalakte selbst: die
|
||||
* Zugehörigkeit steht in der Akte, und wer sie nicht schreiben darf, darf auch
|
||||
|
||||
@@ -18,8 +18,8 @@ import type { EffectivePermissions } from '@/lib/auth/permissions'
|
||||
import type { Prisma } from '@afis/db'
|
||||
|
||||
/**
|
||||
* Dokumente. Eine Ausfertigung aus einer Vorlage — Vorfallsbericht,
|
||||
* Sonderarbeitsvertrag, Bußgeldbescheid — an einer Fallakte, an einer
|
||||
* Dokumente. Eine Ausfertigung aus einer Vorlage - Vorfallsbericht,
|
||||
* Sonderarbeitsvertrag, Bußgeldbescheid - an einer Fallakte, an einer
|
||||
* Personalakte oder für sich stehend.
|
||||
*
|
||||
* Eine neue Dokumentart braucht hier keine Zeile: sie ist eine Vorlage mit
|
||||
|
||||
+1
-1
@@ -3,7 +3,7 @@
|
||||
*
|
||||
* `code` ist ein Schlüssel in `de.actionErrors`, kein Text: die Meldung wird
|
||||
* erst am Rand übersetzt. Ein Fehler, den der Nutzer nicht sehen darf, kommt
|
||||
* gar nicht erst hier an — fehlender Zugriff endet in 404, nicht in 403.
|
||||
* gar nicht erst hier an - fehlender Zugriff endet in 404, nicht in 403.
|
||||
*/
|
||||
export class AccessError extends Error {
|
||||
constructor(readonly code: string) {
|
||||
|
||||
@@ -9,7 +9,7 @@ import { createHash } from 'node:crypto'
|
||||
* Dokument geben, das für eine höhere Stufe gerendert wurde.
|
||||
*
|
||||
* Der Stand (`updatedAt`) sorgt dafür, dass eine geänderte Akte neu gerendert
|
||||
* wird — ein Export darf nie einen älteren Stand zeigen, als die Akte hat.
|
||||
* wird - ein Export darf nie einen älteren Stand zeigen, als die Akte hat.
|
||||
*/
|
||||
export function exportCacheKey(input: {
|
||||
kind: string
|
||||
|
||||
@@ -6,7 +6,7 @@ import type { EffectivePermissions } from '@/lib/auth/permissions'
|
||||
/**
|
||||
* Vollständiger JSON-Abzug als Umzugs- und Notfallformat.
|
||||
*
|
||||
* **Ohne Stufenfilterung**, nur mit Recht `admin`, immer protokolliert — so
|
||||
* **Ohne Stufenfilterung**, nur mit Recht `admin`, immer protokolliert - so
|
||||
* steht es in docs/08-suche-und-export.md. Das ist die eine Stelle im System,
|
||||
* an der die Klassifizierung nicht greift, und sie ist deshalb absichtlich
|
||||
* unbequem: kein Knopf an einer Akte, sondern eine eigene Seite im
|
||||
|
||||
@@ -5,7 +5,7 @@ import type { EffectivePermissions } from '@/lib/auth/permissions'
|
||||
/**
|
||||
* Die Druckansicht dient zwei Aufrufern: dem PDF-Dienst mit Zeichen und einem
|
||||
* Menschen, der sie sich ansieht. Diese Funktion steht in einer eigenen Datei,
|
||||
* weil sie `next-auth` lädt — der Zeichenpfad soll das nicht müssen.
|
||||
* weil sie `next-auth` lädt - der Zeichenpfad soll das nicht müssen.
|
||||
*/
|
||||
export async function permissionsForPrint(
|
||||
token: string | null | undefined,
|
||||
|
||||
@@ -11,7 +11,7 @@ import type { EffectivePermissions } from '@/lib/auth/permissions'
|
||||
*
|
||||
* Die Stufe kommt dabei nicht aus dem Zeichen, sondern wird frisch aufgelöst
|
||||
* und dann auf die im Zeichen vermerkte **gedeckelt**. Damit kann ein Zeichen
|
||||
* nie mehr zeigen, als der Abrufer beim Anfragen sehen durfte — und auch nicht
|
||||
* nie mehr zeigen, als der Abrufer beim Anfragen sehen durfte - und auch nicht
|
||||
* mehr, als er jetzt noch darf, falls ihm inzwischen Rechte entzogen wurden.
|
||||
*/
|
||||
export async function permissionsFromRenderToken(
|
||||
|
||||
@@ -18,14 +18,14 @@ import type { EffectivePermissions } from '@/lib/auth/permissions'
|
||||
*
|
||||
* 1. **Der Export respektiert die Stufe des Abrufers.** Die Druckansicht
|
||||
* rendert mit seinen Rechten, gesperrte Felder erscheinen als Schwärzung
|
||||
* mit Vermerk — nicht weggelassen, sonst merkt der Empfänger nicht, dass
|
||||
* mit Vermerk - nicht weggelassen, sonst merkt der Empfänger nicht, dass
|
||||
* er eine gekürzte Fassung hat.
|
||||
* 2. **Wasserzeichen** auf jeder Seite: Name, Zeitstempel, Stufe.
|
||||
* 3. **Jeder Export wird protokolliert**, ausnahmslos und mit Stufe,
|
||||
* Zeitpunkt und Anlass.
|
||||
*
|
||||
* Wer exportieren darf, entscheidet allein die Freigabestufe: **mindestens die
|
||||
* des Ziels.** Das ist strenger als das Lesen — wer als Beteiligter
|
||||
* des Ziels.** Das ist strenger als das Lesen - wer als Beteiligter
|
||||
* ausnahmsweise in eine höher eingestufte Akte darf, darf sie deshalb noch
|
||||
* nicht ausser Haus geben. Und es ist einfacher als das frühere
|
||||
* Vier-Augen-Verfahren, das ab Stufe 3 eine zweite Unterschrift verlangte:
|
||||
@@ -38,7 +38,7 @@ export type ExportTarget =
|
||||
| { kind: 'DOCUMENT'; documentNumber: string }
|
||||
|
||||
/**
|
||||
* Legt einen Auftrag an — oder gibt einen fertigen zurück, wenn genau dieser
|
||||
* Legt einen Auftrag an - oder gibt einen fertigen zurück, wenn genau dieser
|
||||
* Stand schon einmal für diese Stufe gerendert wurde.
|
||||
*/
|
||||
export async function requestExport(
|
||||
@@ -71,7 +71,7 @@ export async function requestExport(
|
||||
return { jobId: fertig.id, status: 'READY' }
|
||||
}
|
||||
|
||||
// Läuft schon einer für denselben Stand? Dann daran hängen — aber der
|
||||
// Läuft schon einer für denselben Stand? Dann daran hängen - aber der
|
||||
// Protokolleintrag entsteht trotzdem. „Jeder Export wird protokolliert"
|
||||
// heisst jeder Abruf, nicht jeder Rendervorgang: wer sich an einen fremden
|
||||
// Auftrag hängt, bekommt dieselbe Datei und muss genauso darin stehen.
|
||||
|
||||
@@ -35,7 +35,7 @@ export function createRenderToken(claim: Omit<RenderClaim, 'expiresAt'>): string
|
||||
}
|
||||
|
||||
/**
|
||||
* Prüft das Zeichen. Gibt `null` zurück, wenn irgendetwas nicht stimmt — der
|
||||
* Prüft das Zeichen. Gibt `null` zurück, wenn irgendetwas nicht stimmt - der
|
||||
* Aufrufer macht daraus 404, nicht 403: dass es die Druckansicht gibt, geht
|
||||
* niemanden etwas an, der kein gültiges Zeichen hat.
|
||||
*/
|
||||
|
||||
@@ -4,7 +4,7 @@ import { createHash } from 'node:crypto'
|
||||
* Ein winziger ZIP-Schreiber, nur „gespeichert" ohne Kompression.
|
||||
*
|
||||
* Eigener Code statt einer Abhängigkeit, weil der Inhalt eines Sammelexports
|
||||
* PDFs und bereits neu kodierte Bilder sind — beides ist komprimiert, ein
|
||||
* PDFs und bereits neu kodierte Bilder sind - beides ist komprimiert, ein
|
||||
* Deflate darüber bringt nichts und kostet nur CPU auf einem Heimserver. Was
|
||||
* bleibt, ist das Behälterformat, und das sind achtzig Zeilen.
|
||||
*
|
||||
|
||||
@@ -5,7 +5,7 @@ import net from 'node:net'
|
||||
*
|
||||
* Es wird **INSTREAM** benutzt, nicht SCAN mit einem Pfad: dann muss der
|
||||
* Scanner die Datei nicht sehen können, und die Anbindung funktioniert auch,
|
||||
* wenn clamd anderswo läuft. Das Protokoll ist simpel — Länge als 4 Byte,
|
||||
* wenn clamd anderswo läuft. Das Protokoll ist simpel - Länge als 4 Byte,
|
||||
* Nutzdaten, dann eine Null-Länge als Abschluss.
|
||||
*/
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
* Grenzen für Uploads, aus docs/07-dateien-und-sicherheit.md.
|
||||
*
|
||||
* Ohne Abhängigkeiten, damit die Oberfläche dieselben Zahlen anzeigen kann,
|
||||
* die der Server durchsetzt — geprüft wird ausschliesslich serverseitig.
|
||||
* die der Server durchsetzt - geprüft wird ausschliesslich serverseitig.
|
||||
*/
|
||||
|
||||
/** 25 MB pro Datei. */
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
* Bewusst eine eigene Signaturtabelle statt `file-type`: die Liste der
|
||||
* erlaubten Typen ist kurz und geschlossen, und was nicht darin steht, wird
|
||||
* abgelehnt. Eine Bibliothek, die hundert Formate erkennt, hilft bei einer
|
||||
* Positivliste nicht — sie erweitert nur die Angriffsfläche und die Menge
|
||||
* Positivliste nicht - sie erweitert nur die Angriffsfläche und die Menge
|
||||
* dessen, was aktuell gehalten werden muss. Der eigentliche Schutz ist ohnehin
|
||||
* die Neukodierung danach, siehe `process.ts`.
|
||||
*
|
||||
@@ -67,7 +67,7 @@ export type Detection =
|
||||
*
|
||||
* Reine Textdateien haben keine Signatur. Sie werden deshalb zuletzt geprüft
|
||||
* und nur dann angenommen, wenn der ganze Inhalt gültiges UTF-8 ohne
|
||||
* Steuerzeichen ist — sonst wäre `text/plain` das Schlupfloch, durch das jedes
|
||||
* Steuerzeichen ist - sonst wäre `text/plain` das Schlupfloch, durch das jedes
|
||||
* Binärformat passt.
|
||||
*/
|
||||
export function detectType(content: Uint8Array): Detection {
|
||||
@@ -106,8 +106,8 @@ export function looksLikeText(content: Uint8Array): boolean {
|
||||
/**
|
||||
* Der gemeldete Typ muss zum erkannten passen.
|
||||
*
|
||||
* Nicht, weil der gemeldete etwas wert wäre — er kommt vom Browser und ist frei
|
||||
* wählbar —, sondern weil eine Abweichung ein Hinweis ist: wer eine PNG-Datei
|
||||
* Nicht, weil der gemeldete etwas wert wäre - er kommt vom Browser und ist frei
|
||||
* wählbar -, sondern weil eine Abweichung ein Hinweis ist: wer eine PNG-Datei
|
||||
* hochlädt und sie als PDF ankündigt, hat etwas vor.
|
||||
*/
|
||||
export function matchesDeclared(detected: AllowedType, declared: string | null): boolean {
|
||||
|
||||
@@ -11,7 +11,7 @@ import { getEnv } from '@/lib/env'
|
||||
* Ausgeliefert wird ausschliesslich aus `store/`. Siehe docs/07.
|
||||
*
|
||||
* Der Dateiname ist die Kennung des Anhangs, nie der Name, den der Nutzer
|
||||
* mitgeschickt hat — der steht in der Datenbank und wird erst beim Ausliefern
|
||||
* mitgeschickt hat - der steht in der Datenbank und wird erst beim Ausliefern
|
||||
* wieder gesetzt. Ein Pfad, der aus einer Eingabe entsteht, ist eine
|
||||
* Einladung.
|
||||
*/
|
||||
|
||||
@@ -26,7 +26,7 @@ export async function runCheckChain(
|
||||
/**
|
||||
* Nur für die bewusste Freigabe durch einen Administrator, wenn der
|
||||
* Scanner nicht erreichbar war. Die Typprüfung und die Neukodierung laufen
|
||||
* auch dann — sie sind der eigentliche Schutz.
|
||||
* auch dann - sie sind der eigentliche Schutz.
|
||||
*/
|
||||
skipScan?: boolean
|
||||
} = {},
|
||||
@@ -157,8 +157,8 @@ async function freigeben(
|
||||
// Gleicher Inhalt schon da: nur verweisen, nicht ein zweites Mal ablegen.
|
||||
//
|
||||
// Der Verweis gilt aber nur, wenn die Datei auch wirklich noch liegt. Eine
|
||||
// Zeile in der Datenbank ist kein Beweis dafür: geht die Ablage verloren —
|
||||
// ein geleertes Verzeichnis, ein nicht eingehängtes Volume —, zeigten sonst
|
||||
// Zeile in der Datenbank ist kein Beweis dafür: geht die Ablage verloren -
|
||||
// ein geleertes Verzeichnis, ein nicht eingehängtes Volume -, zeigten sonst
|
||||
// alle künftigen Uploads desselben Inhalts stumm ins Leere.
|
||||
const vorhanden = await db.attachment.findFirst({
|
||||
where: { sha256, scanStatus: 'CLEAN', id: { not: attachmentId } },
|
||||
|
||||
@@ -16,7 +16,7 @@ const run = promisify(execFile)
|
||||
* Das ist die wirksamste Massnahme der ganzen Kette. Ein Bild, das durch
|
||||
* `sharp` gelaufen ist, besteht nur noch aus Pixeln: EXIF, Farbprofile,
|
||||
* angehängte Nutzlast und alles, was ein Betrachter versehentlich auswerten
|
||||
* könnte, sind weg — nicht gefiltert, sondern nie übernommen.
|
||||
* könnte, sind weg - nicht gefiltert, sondern nie übernommen.
|
||||
* Siehe docs/07-dateien-und-sicherheit.md.
|
||||
*/
|
||||
|
||||
@@ -126,7 +126,7 @@ function reencodeText(content: Uint8Array): ReencodeResult {
|
||||
*
|
||||
* `--empty --pages datei 1-z --` baut das Dokument aus den Seiten neu auf.
|
||||
* Der ursprüngliche Dokumentkatalog wird dabei nicht übernommen, und mit ihm
|
||||
* verschwinden `OpenAction`, eingebettetes JavaScript und angehängte Dateien —
|
||||
* verschwinden `OpenAction`, eingebettetes JavaScript und angehängte Dateien -
|
||||
* die drei Wege, auf denen ein PDF etwas tut, statt nur dazustehen.
|
||||
*
|
||||
* Fehlt `qpdf`, wird die Datei **nicht** durchgewinkt. Sie bleibt in der
|
||||
|
||||
@@ -91,7 +91,7 @@ async function aufloesen(
|
||||
*
|
||||
* Der Datensatz entsteht **sofort** mit `PENDING`, damit die Oberfläche ihn
|
||||
* zeigen kann, während die Prüfung läuft. Der gemeldete Typ wandert
|
||||
* unverändert in `mimeType` und wird von der Prüfkette überschrieben — bis
|
||||
* unverändert in `mimeType` und wird von der Prüfkette überschrieben - bis
|
||||
* dahin ist er nichts als eine Behauptung des Browsers.
|
||||
*/
|
||||
export async function acceptUpload(
|
||||
@@ -183,7 +183,7 @@ export async function listAttachments(
|
||||
* Lädt einen Anhang zum Ausliefern.
|
||||
*
|
||||
* Nur was die Prüfkette bestanden hat oder von einem Administrator bewusst
|
||||
* freigegeben wurde. Alles andere gibt es für die Auslieferung nicht — auch
|
||||
* freigegeben wurde. Alles andere gibt es für die Auslieferung nicht - auch
|
||||
* nicht mit Recht `admin`, denn die Datei liegt dann noch in der Quarantäne.
|
||||
*/
|
||||
export async function loadDeliverable(p: EffectivePermissions, id: string) {
|
||||
@@ -207,7 +207,7 @@ export async function loadDeliverable(p: EffectivePermissions, id: string) {
|
||||
/**
|
||||
* Gibt eine Datei frei, deren Prüfung nicht abgeschlossen werden konnte.
|
||||
*
|
||||
* Nur mit Recht `admin` und nur aus `FAILED` — was als Schadsoftware oder
|
||||
* Nur mit Recht `admin` und nur aus `FAILED` - was als Schadsoftware oder
|
||||
* unzulässiger Typ erkannt wurde, bleibt blockiert. Die Freigabe wiederholt
|
||||
* die Kette ohne den Virenscanner; die Typprüfung läuft dabei **immer** mit.
|
||||
*/
|
||||
@@ -241,7 +241,7 @@ export async function releaseAttachment(
|
||||
})
|
||||
|
||||
// Die Kette noch einmal, ohne den Scanner. Typprüfung und Neukodierung
|
||||
// laufen dabei mit — die Freigabe überspringt nur das, was ausgefallen war.
|
||||
// laufen dabei mit - die Freigabe überspringt nur das, was ausgefallen war.
|
||||
await runCheckChain(id, { skipScan: true })
|
||||
}
|
||||
|
||||
@@ -249,7 +249,7 @@ export async function releaseAttachment(
|
||||
* Löst die Lichtbildfelder eines Dokuments zu Adressen auf.
|
||||
*
|
||||
* **Serverseitig**, und nur für Anhänge, die die Prüfkette bestanden haben und
|
||||
* die der Leser sehen darf. Der Browser bekommt eine Adresse oder gar nichts —
|
||||
* die der Leser sehen darf. Der Browser bekommt eine Adresse oder gar nichts -
|
||||
* nie eine Kennung, zu der er selbst probieren müsste.
|
||||
*/
|
||||
export async function resolvePhotoUrls(
|
||||
|
||||
@@ -4,7 +4,7 @@ import type { AttachmentView } from '@/components/files/Attachments'
|
||||
* Formt die Datenbankzeilen für die Oberfläche um.
|
||||
*
|
||||
* Zwei Dinge passieren hier, beide mit Absicht: `thumbKey` wird zu einem
|
||||
* Wahrheitswert — der Pfad auf der Platte geht den Browser nichts an —, und
|
||||
* Wahrheitswert - der Pfad auf der Platte geht den Browser nichts an -, und
|
||||
* `createdAt` wird zu einer Zeichenkette, weil ein `Date` die Grenze zur
|
||||
* Client-Komponente nicht sauber überquert.
|
||||
*/
|
||||
|
||||
@@ -110,7 +110,7 @@ export const adminText = {
|
||||
*
|
||||
* Nach Bereichen getrennt, weil eine **Client-Komponente die ganze Datei ins
|
||||
* Bündel zieht**, in der ihr Text steht. Lägen alle Texte in einer, trüge jede
|
||||
* Ansicht die der ganzen Anwendung mit sich herum — das Budget aus
|
||||
* Ansicht die der ganzen Anwendung mit sich herum - das Budget aus
|
||||
* docs/09-performance-und-ux.md wäre nach wenigen Phasen aufgebraucht.
|
||||
*
|
||||
* Serverkomponenten nehmen weiter `de` aus `lib/i18n/de.ts`: was dorthin
|
||||
@@ -196,7 +196,7 @@ export const settingsText = {
|
||||
export const vitalsText = {
|
||||
heading: 'Ladewerte',
|
||||
intro:
|
||||
'Gemessen in den Browsern der Nutzer, letzte sieben Tage. Nur Zahlen und das Routenmuster — wer wann welche Akte geöffnet hat, steht im Zugriffsprotokoll.',
|
||||
'Gemessen in den Browsern der Nutzer, letzte sieben Tage. Nur Zahlen und das Routenmuster - wer wann welche Akte geöffnet hat, steht im Zugriffsprotokoll.',
|
||||
empty: 'Noch nichts gemessen.',
|
||||
metric: 'Wert',
|
||||
median: 'Median',
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
*
|
||||
* Nach Bereichen getrennt, weil eine **Client-Komponente die ganze Datei ins
|
||||
* Bündel zieht**, in der ihr Text steht. Lägen alle Texte in einer, trüge jede
|
||||
* Ansicht die der ganzen Anwendung mit sich herum — das Budget aus
|
||||
* Ansicht die der ganzen Anwendung mit sich herum - das Budget aus
|
||||
* docs/09-performance-und-ux.md wäre nach wenigen Phasen aufgebraucht.
|
||||
*
|
||||
* Serverkomponenten nehmen weiter `de` aus `lib/i18n/de.ts`: was dorthin
|
||||
@@ -65,7 +65,7 @@ export const caseText = {
|
||||
} as Record<string, string>,
|
||||
/**
|
||||
* Was die Zustände bedeuten. Sie stehen im Lebenszyklus in
|
||||
* docs/05-akten-und-vorlagen.md, standen aber nirgends in der Oberfläche —
|
||||
* docs/05-akten-und-vorlagen.md, standen aber nirgends in der Oberfläche -
|
||||
* und ein Filter, dessen Bezeichnung man nicht versteht, ist keiner.
|
||||
*/
|
||||
statusHints: {
|
||||
|
||||
@@ -36,7 +36,7 @@ export const chatText = {
|
||||
} as Record<string, string>,
|
||||
// Zeichen statt Emoji: sie drucken, sie lesen sich im schmalen Schnitt und
|
||||
// sie brauchen keine Schriftart, die es vielleicht nicht gibt. Für "wichtig"
|
||||
// ein Dreieck und kein Ausrufezeichen — die Oberfläche ruft nicht.
|
||||
// ein Dreieck und kein Ausrufezeichen - die Oberfläche ruft nicht.
|
||||
reactionSymbols: {
|
||||
gesehen: '✓',
|
||||
zustimmung: '+',
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
*
|
||||
* Nach Bereichen getrennt, weil eine **Client-Komponente die ganze Datei ins
|
||||
* Bündel zieht**, in der ihr Text steht. Lägen alle Texte in einer, trüge jede
|
||||
* Ansicht die der ganzen Anwendung mit sich herum — das Budget aus
|
||||
* Ansicht die der ganzen Anwendung mit sich herum - das Budget aus
|
||||
* docs/09-performance-und-ux.md wäre nach wenigen Phasen aufgebraucht.
|
||||
*
|
||||
* Serverkomponenten nehmen weiter `de` aus `lib/i18n/de.ts`: was dorthin
|
||||
@@ -23,7 +23,7 @@ export const commonText = {
|
||||
/**
|
||||
* Meldungen zu abgelehnten Aktionen. Der Schlüssel ist der `code` eines
|
||||
* `AccessError`, siehe lib/errors.ts. Gilt für Fallakten, Personalakten und
|
||||
* Dokumente gleichermaßen — die Regeln sind dieselben.
|
||||
* Dokumente gleichermaßen - die Regeln sind dieselben.
|
||||
*/
|
||||
export const actionErrorsText = {
|
||||
'not-found': 'Die Akte gibt es nicht, oder du darfst sie nicht sehen.',
|
||||
|
||||
+1
-1
@@ -11,7 +11,7 @@
|
||||
*
|
||||
* Die Texte stehen nach Bereichen getrennt daneben; diese Datei setzt sie nur
|
||||
* wieder zusammen. Serverkomponenten duerfen `de` nehmen. Eine
|
||||
* Client-Komponente importiert **den Bereich, den sie braucht** — sonst traegt
|
||||
* Client-Komponente importiert **den Bereich, den sie braucht** - sonst traegt
|
||||
* sie die Texte der ganzen Anwendung ins Buendel.
|
||||
*/
|
||||
import {
|
||||
|
||||
@@ -25,7 +25,7 @@ export const departmentText = {
|
||||
minClearance: 'Mindeststufe',
|
||||
archived: 'Archiviert',
|
||||
derivedHint:
|
||||
'Eine Fallakte gehört zu dieser Abteilung, wenn eine beteiligte Person ihr angehört. Die Zuordnung wird nicht eingetragen, sondern ergibt sich — damit stimmt sie auch, wenn sich die Beteiligten ändern.',
|
||||
'Eine Fallakte gehört zu dieser Abteilung, wenn eine beteiligte Person ihr angehört. Die Zuordnung wird nicht eingetragen, sondern ergibt sich - damit stimmt sie auch, wenn sich die Beteiligten ändern.',
|
||||
} as const
|
||||
|
||||
export const departmentAdminText = {
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
*
|
||||
* Nach Bereichen getrennt, weil eine **Client-Komponente die ganze Datei ins
|
||||
* Bündel zieht**, in der ihr Text steht. Lägen alle Texte in einer, trüge jede
|
||||
* Ansicht die der ganzen Anwendung mit sich herum — das Budget aus
|
||||
* Ansicht die der ganzen Anwendung mit sich herum - das Budget aus
|
||||
* docs/09-performance-und-ux.md wäre nach wenigen Phasen aufgebraucht.
|
||||
*
|
||||
* Serverkomponenten nehmen weiter `de` aus `lib/i18n/de.ts`: was dorthin
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
* Export und Druck.
|
||||
*
|
||||
* Eigene Datei und nicht bei den Dokumenten: die Fallakte lädt die
|
||||
* Dokumenttexte sofort, den Export nie — er hat eine eigene Seite. Lägen sie
|
||||
* Dokumenttexte sofort, den Export nie - er hat eine eigene Seite. Lägen sie
|
||||
* zusammen, käme er doch mit. Siehe docs/13-entscheidungen.md.
|
||||
*/
|
||||
export const exportText = {
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
*
|
||||
* Nach Bereichen getrennt, weil eine **Client-Komponente die ganze Datei ins
|
||||
* Bündel zieht**, in der ihr Text steht. Lägen alle Texte in einer, trüge jede
|
||||
* Ansicht die der ganzen Anwendung mit sich herum — das Budget aus
|
||||
* Ansicht die der ganzen Anwendung mit sich herum - das Budget aus
|
||||
* docs/09-performance-und-ux.md wäre nach wenigen Phasen aufgebraucht.
|
||||
*
|
||||
* Serverkomponenten nehmen weiter `de` aus `lib/i18n/de.ts`: was dorthin
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
*
|
||||
* Nach Bereichen getrennt, weil eine **Client-Komponente die ganze Datei ins
|
||||
* Bündel zieht**, in der ihr Text steht. Lägen alle Texte in einer, trüge jede
|
||||
* Ansicht die der ganzen Anwendung mit sich herum — das Budget aus
|
||||
* Ansicht die der ganzen Anwendung mit sich herum - das Budget aus
|
||||
* docs/09-performance-und-ux.md wäre nach wenigen Phasen aufgebraucht.
|
||||
*
|
||||
* Serverkomponenten nehmen weiter `de` aus `lib/i18n/de.ts`: was dorthin
|
||||
@@ -115,7 +115,7 @@ export const healthText = {
|
||||
export const notifyText = {
|
||||
heading: 'Benachrichtigungen',
|
||||
intro:
|
||||
'Discord erfährt nie den Inhalt einer Akte. Bei Akten ab Stufe 3 auch nicht den Titel — nur, dass sich etwas getan hat, und einen Link.',
|
||||
'Discord erfährt nie den Inhalt einer Akte. Bei Akten ab Stufe 3 auch nicht den Titel - nur, dass sich etwas getan hat, und einen Link.',
|
||||
mode: 'Was soll dich erreichen?',
|
||||
save: 'Übernehmen',
|
||||
saved: 'Übernommen',
|
||||
@@ -175,7 +175,7 @@ export const paletteText = {
|
||||
} as Record<string, string>,
|
||||
/**
|
||||
* Die Sprungziele der Palette. Sie stehen hier und nicht in der Komponente,
|
||||
* weil sie Oberflächentext sind — die Adressen daneben sind es nicht.
|
||||
* weil sie Oberflächentext sind - die Adressen daneben sind es nicht.
|
||||
*/
|
||||
pages: [
|
||||
{ href: '/uebersicht', label: 'Übersicht' },
|
||||
|
||||
@@ -13,7 +13,7 @@ import type { Prisma } from '@afis/db'
|
||||
* in dieser Anwendung, und sie im Worker ein zweites Mal zu bauen hiesse, sie
|
||||
* zweimal richtig haben zu müssen.
|
||||
*
|
||||
* Deshalb steht in `meta` nur, was **stehen darf** — fertig entschieden. Ein
|
||||
* Deshalb steht in `meta` nur, was **stehen darf** - fertig entschieden. Ein
|
||||
* Titel landet dort nur, wenn er auch nach Discord darf. Der Worker setzt
|
||||
* daraus einen Satz zusammen und hat keine Entscheidung mehr zu treffen.
|
||||
* Siehe docs/06-chat-und-realtime.md.
|
||||
@@ -27,7 +27,7 @@ export type OutboxKind = 'entry.new' | 'chat.new' | 'chat.mention' | 'case.statu
|
||||
/**
|
||||
* Die Empfänger einer Meldung zu einer Akte.
|
||||
*
|
||||
* Wer die Akte nicht sehen darf, bekommt nichts — auch keinen Hinweis, dass es
|
||||
* Wer die Akte nicht sehen darf, bekommt nichts - auch keinen Hinweis, dass es
|
||||
* sie gibt. Geprüft wird mit **derselben** Abfrage wie beim Lesen:
|
||||
* `visibleCaseWhere`, je Empfänger. Das ist eine Abfrage pro Kandidat und bei
|
||||
* dieser Nutzerzahl billiger als der Fehler, die Regel ein zweites Mal zu
|
||||
@@ -136,7 +136,7 @@ export async function enqueueCaseNotification(
|
||||
* Link führt in die Anwendung, und die prüft selbst.
|
||||
*
|
||||
* Geprüft wird je Empfänger mit derselben Abfrage wie beim Lesen, plus dem
|
||||
* Recht `personnel.read` — ohne das gibt es die Akte für ihn nicht.
|
||||
* Recht `personnel.read` - ohne das gibt es die Akte für ihn nicht.
|
||||
*/
|
||||
export async function enqueuePersonnelNotification(
|
||||
tx: Prisma.TransactionClient,
|
||||
|
||||
@@ -217,7 +217,7 @@ export async function reclassifyPersonnelFile(
|
||||
* Das ist der häufigste Ort, an dem ein Aktensystem leckt: eine Personalakte
|
||||
* der Stufe 1, auf der „3 Fälle" steht, verrät die Existenz von Ermittlungen,
|
||||
* die der Leser nicht sehen darf. Deshalb steckt der Filter in der Abfrage und
|
||||
* die Zählung entsteht aus dem gefilterten Ergebnis — nie aus einem eigenen
|
||||
* die Zählung entsteht aus dem gefilterten Ergebnis - nie aus einem eigenen
|
||||
* `count` ohne Filter. Siehe docs/05-akten-und-vorlagen.md, Rückverweise.
|
||||
*/
|
||||
export async function loadBacklinks(
|
||||
@@ -295,7 +295,7 @@ export async function listStatusTags() {
|
||||
/**
|
||||
* Die AfiS-Mitglieder, die an dieser Personalakte hängen.
|
||||
*
|
||||
* Wer hier steht, liest die Akte unabhängig von seiner Freigabestufe — das ist
|
||||
* Wer hier steht, liest die Akte unabhängig von seiner Freigabestufe - das ist
|
||||
* derselbe ausnahmsweise Zugang wie `CaseParticipant` bei der Fallakte.
|
||||
*/
|
||||
export async function listAssignments(p: EffectivePermissions, personnelFileId: string) {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
/**
|
||||
* Feste Wertelisten der Personalakte.
|
||||
*
|
||||
* **Ohne Abhängigkeiten**, weil der Client sie lädt — derselbe Schnitt wie
|
||||
* **Ohne Abhängigkeiten**, weil der Client sie lädt - derselbe Schnitt wie
|
||||
* `lib/cases/terms.ts`. Die Status-Etiketten stehen bewusst **nicht** hier:
|
||||
* sie sind im Adminbereich pflegbar und kommen aus der Datenbank.
|
||||
*/
|
||||
|
||||
@@ -5,7 +5,7 @@ import type { EffectivePermissions } from '@/lib/auth/permissions'
|
||||
import type { RecentTarget } from '@afis/db'
|
||||
|
||||
/**
|
||||
* „Zuletzt geöffnet" — die persönliche Sprungliste.
|
||||
* „Zuletzt geöffnet" - die persönliche Sprungliste.
|
||||
*
|
||||
* Sie beantwortet eine Frage, die man oft hat: *woran war ich gerade dran?*
|
||||
* Und sie beantwortet ausdrücklich **keine** andere. Drei Dinge halten sie auf
|
||||
@@ -15,7 +15,7 @@ import type { RecentTarget } from '@afis/db'
|
||||
* nächsten Öffnen überschrieben wird. Aus dieser Tabelle lässt sich nicht
|
||||
* ablesen, wie oft oder in welcher Reihenfolge jemand gearbeitet hat.
|
||||
* 2. **Nur die eigene.** Es gibt in der Anwendung keinen Weg, die Liste eines
|
||||
* anderen zu lesen — keine Ansicht, kein Adminbereich, keine Funktion mit
|
||||
* anderen zu lesen - keine Ansicht, kein Adminbereich, keine Funktion mit
|
||||
* fremder Kennung als Parameter. Jede Abfrage hier nimmt `p.userId`.
|
||||
* 3. **Jederzeit löschbar**, ohne Rückfrage und ohne Spur.
|
||||
*
|
||||
@@ -30,7 +30,7 @@ export const RECENT_MAX = 20
|
||||
/**
|
||||
* Einen Sprung vormerken.
|
||||
*
|
||||
* Nur aufrufen, wenn der Nutzer den Datensatz auch wirklich sehen durfte —
|
||||
* Nur aufrufen, wenn der Nutzer den Datensatz auch wirklich sehen durfte -
|
||||
* die Aufrufer stehen hinter `loadVisible…` und 404.
|
||||
*/
|
||||
export async function noteOpened(
|
||||
@@ -71,7 +71,7 @@ export type RecentEntry = {
|
||||
targetType: RecentTarget
|
||||
targetId: string
|
||||
href: string
|
||||
/** Aktenzeichen, Dokumentnummer oder Name — das, was man wiedererkennt. */
|
||||
/** Aktenzeichen, Dokumentnummer oder Name - das, was man wiedererkennt. */
|
||||
label: string
|
||||
/** Titel der Akte, Dienstgrad der Person. Kann fehlen. */
|
||||
secondary?: string
|
||||
@@ -85,7 +85,7 @@ export type RecentEntry = {
|
||||
* Die vorgemerkten Kennungen sind für sich genommen wertlos: jede wird durch
|
||||
* dieselbe Sichtbarkeitsprüfung geholt wie überall sonst. Steigt eine Akte
|
||||
* nachträglich über die Freigabestufe des Nutzers, verschwindet sie aus der
|
||||
* Liste, ohne dass jemand aufräumen müsste — genau wie bei der
|
||||
* Liste, ohne dass jemand aufräumen müsste - genau wie bei der
|
||||
* Beobachtungsliste.
|
||||
*/
|
||||
export async function listRecent(p: EffectivePermissions, take = 8): Promise<RecentEntry[]> {
|
||||
@@ -165,7 +165,7 @@ export async function listRecent(p: EffectivePermissions, take = 8): Promise<Rec
|
||||
for (const zeile of zeilen) {
|
||||
const gefunden = treffer.get(`${zeile.targetType}:${zeile.targetId}`)
|
||||
// Nicht gefunden heisst: nicht mehr sichtbar. Kein Platzhalter, keine
|
||||
// Lücke, kein Hinweis — die Existenz ist selbst eine Information.
|
||||
// Lücke, kein Hinweis - die Existenz ist selbst eine Information.
|
||||
if (!gefunden) continue
|
||||
liste.push({ ...gefunden, openedAt: zeile.openedAt })
|
||||
if (liste.length >= take) break
|
||||
|
||||
@@ -10,7 +10,7 @@ import type { RecentTarget } from '@afis/db'
|
||||
*
|
||||
* **Vorabrufe zählen nicht.** Next holt die Seiten hinter sichtbaren Links im
|
||||
* Voraus. Ohne diese Prüfung stünde in der Liste, worüber jemand mit der Maus
|
||||
* gefahren ist — die Übersicht allein würde sie mit zehn Akten füllen, die
|
||||
* gefahren ist - die Übersicht allein würde sie mit zehn Akten füllen, die
|
||||
* niemand geöffnet hat. Das wäre nicht nur falsch, es wäre die unangenehmere
|
||||
* Sorte falsch: eine Aufzeichnung von Aufmerksamkeit statt von Handlung.
|
||||
*
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
* Der Hinweis kam von [[c4]]Wachmann Perreira, Sektor B[[/c4]] am 27.08.
|
||||
*
|
||||
* Der Server ersetzt den Inhalt für niedrigere Stufen. **Der echte Wert
|
||||
* verlässt den Server nicht** — deshalb gibt diese Datei Segmente zurück und
|
||||
* verlässt den Server nicht** - deshalb gibt diese Datei Segmente zurück und
|
||||
* nicht etwa den vollen Text mit einem Sichtbarkeits-Flag. Was der Client
|
||||
* ausblenden müsste, hat er schon.
|
||||
*
|
||||
@@ -94,7 +94,7 @@ export function stripMarkup(source: string): string {
|
||||
/**
|
||||
* Entfernt ausgezeichnete Bereiche samt Inhalt. Genau das tut auch die
|
||||
* Funktion `strip_redactions` in der Migration, bevor der Suchvektor gefüllt
|
||||
* wird — beide müssen dasselbe Ergebnis liefern.
|
||||
* wird - beide müssen dasselbe Ergebnis liefern.
|
||||
*/
|
||||
export function stripRedacted(source: string): string {
|
||||
return source.replace(MARKUP, ' ')
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
*
|
||||
* Ohne Abhängigkeiten: die Suchseite baut damit ihre Adresse, und gespeicherte
|
||||
* Ansichten sind nichts anderes als eine solche Adresse. Dass beides dieselbe
|
||||
* Darstellung benutzt, ist der Punkt — eine Ansicht ist ein Lesezeichen.
|
||||
* Darstellung benutzt, ist der Punkt - eine Ansicht ist ein Lesezeichen.
|
||||
*/
|
||||
|
||||
/** Die Arten, über die einzeln gesucht werden kann. */
|
||||
|
||||
@@ -11,17 +11,17 @@ import type { Prisma } from '@afis/db'
|
||||
*
|
||||
* Zwei Schritte, und der Unterschied ist wichtig:
|
||||
*
|
||||
* 1. **Was passt** — eine Abfrage auf `SearchIndex`, in der Zeile zur Stufe
|
||||
* 1. **Was passt** - eine Abfrage auf `SearchIndex`, in der Zeile zur Stufe
|
||||
* des Suchenden. Sie kann nichts liefern, was er nicht lesen darf, weil
|
||||
* höher eingestufte Inhalte gar nicht erst in dieser Zeile stehen.
|
||||
* 2. **Was er sehen darf und wie viele es sind** — eine Abfrage mit
|
||||
* 2. **Was er sehen darf und wie viele es sind** - eine Abfrage mit
|
||||
* `visibleCaseWhere` und den Filtern. Treffer, Trefferzahl und die
|
||||
* Facettenzähler entstehen dort **gemeinsam**, damit sich aus einer Lücke
|
||||
* nichts ableiten lässt. Siehe docs/08-suche-und-export.md.
|
||||
*
|
||||
* Der erste Schritt ist die Suchbedingung, nicht die Zugriffsprüfung. Die
|
||||
* steht ausschliesslich im zweiten und stammt aus derselben Funktion wie in
|
||||
* der Fallliste — es gibt keine zweite Fassung davon.
|
||||
* der Fallliste - es gibt keine zweite Fassung davon.
|
||||
*/
|
||||
|
||||
/** So viele Treffer holt der Volltext höchstens, nach Rang sortiert. */
|
||||
@@ -40,7 +40,7 @@ export const PREVIEW_SIZE = 5
|
||||
* Personalakten setzen neben der Freigabestufe ein eigenes Recht voraus.
|
||||
*
|
||||
* Das stand in der Kommandopalette und auf der Aktenseite, fehlte aber in der
|
||||
* Suche — ein Nutzer mit hoher Stufe und ohne `personnel.read` konnte sich
|
||||
* Suche - ein Nutzer mit hoher Stufe und ohne `personnel.read` konnte sich
|
||||
* über `/suche?kind=personnel` eine Liste aller Personalakten holen. Die
|
||||
* Prüfung gehört an **jede** Stelle, die diese Akten herausgibt, und deshalb
|
||||
* in diese Datei und nicht in die Seite.
|
||||
@@ -57,7 +57,7 @@ async function matchingIds(
|
||||
clearanceLevel: number,
|
||||
): Promise<string[] | null> {
|
||||
const begriff = q.trim()
|
||||
// Ohne Suchbegriff filtert die Seite nur — dann gibt es keine Vorauswahl.
|
||||
// Ohne Suchbegriff filtert die Seite nur - dann gibt es keine Vorauswahl.
|
||||
if (begriff.length === 0) return null
|
||||
|
||||
const rows = await db.$queryRaw<{ targetId: string }[]>`
|
||||
@@ -233,7 +233,7 @@ export async function searchPersonnel(
|
||||
filters: SearchFilters,
|
||||
options: { take?: number } = {},
|
||||
) {
|
||||
// Ohne das Recht gibt es diese Akten für ihn nicht — auch nicht als Zahl.
|
||||
// Ohne das Recht gibt es diese Akten für ihn nicht - auch nicht als Zahl.
|
||||
if (!darfPersonalakten(p)) {
|
||||
return { hits: [], total: 0, truncated: false }
|
||||
}
|
||||
@@ -285,9 +285,9 @@ function zuFacet(rows: { _count: { _all: number } }[], key: string): Facet[] {
|
||||
*
|
||||
* Die Zahlen entstehen mit **denselben** Bedingungen wie die Trefferlisten.
|
||||
* Eine Zahl, die mehr zählt als die Liste hergibt, wäre selbst eine
|
||||
* Information — siehe harte Regel 6.
|
||||
* Information - siehe harte Regel 6.
|
||||
*
|
||||
* `null` heisst: diese Art gibt es für diesen Nutzer nicht. Nicht `0` — eine
|
||||
* `null` heisst: diese Art gibt es für diesen Nutzer nicht. Nicht `0` - eine
|
||||
* Null wäre die Auskunft „hier ist nichts", und die steht ihm nicht zu.
|
||||
*/
|
||||
export async function searchCounts(
|
||||
@@ -316,7 +316,7 @@ export async function searchCounts(
|
||||
*
|
||||
* Seit die Beteiligten im Index stehen, findet die Suche nach „Jonathan
|
||||
* Fletcher" auch Akten, in denen der Name nirgends im Text vorkommt. Ohne
|
||||
* einen Hinweis sieht das aus wie ein Fehlgriff — die Trefferzeile zeigt
|
||||
* einen Hinweis sieht das aus wie ein Fehlgriff - die Trefferzeile zeigt
|
||||
* deshalb, in welcher Rolle die Person dort steht.
|
||||
*
|
||||
* Gesucht wird nur unter den Akten, die als Treffer ohnehin schon durch die
|
||||
|
||||
@@ -6,7 +6,7 @@ import { toSearchParams } from '@/lib/search/query'
|
||||
* Sie stehen im Programm und nicht in der Datenbank, weil sie von der
|
||||
* **Gegenwart** abhängen: „diese Woche" und „seit 14 Tagen" wären als
|
||||
* gespeicherte Zeitpunkte schon am nächsten Tag falsch. Selbst angelegte
|
||||
* Ansichten sind dagegen genau das — ein Lesezeichen auf eine Adresse.
|
||||
* Ansichten sind dagegen genau das - ein Lesezeichen auf eine Adresse.
|
||||
*/
|
||||
export type BuiltinView = { key: string; params: string }
|
||||
|
||||
@@ -33,7 +33,7 @@ export function builtinViews(now: Date = new Date()): BuiltinView[] {
|
||||
|
||||
/**
|
||||
* „Seit deinem letzten Besuch geändert" braucht die Lesemarke und steht
|
||||
* deshalb nicht in der Liste oben — die Seite ergänzt sie, wenn es eine gibt.
|
||||
* deshalb nicht in der Liste oben - die Seite ergänzt sie, wenn es eine gibt.
|
||||
*/
|
||||
export function sinceLastVisitView(lastReadAt: Date): BuiltinView {
|
||||
return { key: 'sinceLastVisit', params: toSearchParams({ changedSince: lastReadAt.toISOString() }) }
|
||||
|
||||
@@ -49,7 +49,7 @@ export function asJson(value: unknown): Prisma.InputJsonValue {
|
||||
* Wendet die Stufe des Lesers auf die Abschnitte an.
|
||||
*
|
||||
* Felder über seiner Stufe werden **hier**, also serverseitig, durch einen
|
||||
* Platzhalter ersetzt. Der echte Wert verlässt den Server nicht — nie im
|
||||
* Platzhalter ersetzt. Der echte Wert verlässt den Server nicht - nie im
|
||||
* Client ausblenden. Dasselbe gilt für Schwärzungen im Fließtext, die als
|
||||
* Segmente zurückkommen.
|
||||
*/
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
/**
|
||||
* Bausteine einer Vorlage — Typen und reine Funktionen.
|
||||
* Bausteine einer Vorlage - Typen und reine Funktionen.
|
||||
*
|
||||
* **Ohne zod.** Dieses Modul wird auch im Client geladen (Vorlagen-Editor,
|
||||
* Vorschau), und eine Validierungsbibliothek im Bündel kostet mehr als das
|
||||
* Budget aus docs/09-performance-und-ux.md hergibt. Die verbindliche Prüfung
|
||||
* steht in `schema.ts` und läuft ausschliesslich serverseitig — dort, wo sie
|
||||
* steht in `schema.ts` und läuft ausschliesslich serverseitig - dort, wo sie
|
||||
* ohnehin hingehört.
|
||||
*
|
||||
* Eine Vorlage ist eine **geordnete Liste von Bausteinen**. Manche nehmen eine
|
||||
@@ -54,7 +54,7 @@ export type InputField = {
|
||||
rows?: number
|
||||
/**
|
||||
* Statt einer eigenen Zelle im Raster: dieses Feld füllt einen festen Platz
|
||||
* des Aktenblatts. Bekannt ist `classbar` — der Klassifizierungsbalken.
|
||||
* des Aktenblatts. Bekannt ist `classbar` - der Klassifizierungsbalken.
|
||||
*/
|
||||
slot?: string
|
||||
/** Hebt den Wert hervor, etwa die Freigabestufen im Aktenblatt. */
|
||||
@@ -94,7 +94,7 @@ export function fillPlaceholders(
|
||||
// **Nur eigene Eigenschaften.** `values[name]` lief über die
|
||||
// Prototypkette: ein Baustein mit dem Platzhalter `{{constructor}}` oder
|
||||
// `{{toString}}` bekam den Quelltext der geerbten Funktion in den Text
|
||||
// geschrieben — die Feldschlüssel sind Kleinbuchstaben, und `constructor`
|
||||
// geschrieben - die Feldschlüssel sind Kleinbuchstaben, und `constructor`
|
||||
// ist einer. Kein Code, der ausgeführt wird, aber Innenleben, das in einem
|
||||
// Dokument nichts zu suchen hat.
|
||||
const value = Object.hasOwn(values, name) ? values[name] : undefined
|
||||
@@ -119,7 +119,7 @@ export const DOSSIER_COLUMNS = 5
|
||||
/**
|
||||
* Zerlegt die Bausteine in die Zeilen des Aktenblatts.
|
||||
*
|
||||
* Solange die Lichtbildzelle mitläuft, ist eine Zeile eine Spalte schmaler —
|
||||
* Solange die Lichtbildzelle mitläuft, ist eine Zeile eine Spalte schmaler -
|
||||
* genau so, wie das Formular es tut. Danach nimmt sie die volle Breite.
|
||||
* Felder, die in einen festen Platz gehören oder von einer Unterschrift
|
||||
* mitgezeichnet werden, tauchen im Raster nicht auf.
|
||||
@@ -184,7 +184,7 @@ export function signatureDateKeys(fields: TemplateField[]): Set<string> {
|
||||
|
||||
/**
|
||||
* Grobe Formprüfung für die Vorschau im Editor. Sagt, ob sich die Eingabe
|
||||
* überhaupt darstellen lässt — nicht, ob sie gespeichert werden darf. Das
|
||||
* überhaupt darstellen lässt - nicht, ob sie gespeichert werden darf. Das
|
||||
* entscheidet allein der Server.
|
||||
*/
|
||||
export function looksLikeFields(value: unknown): value is TemplateField[] {
|
||||
|
||||
@@ -25,7 +25,7 @@ const gemeinsam = {
|
||||
/**
|
||||
* `.strict()` und nicht die Voreinstellung: zod wuerde unbekannte Schluessel
|
||||
* **stillschweigend verwerfen**. Fuer eine Vorlage, die von Hand als JSON
|
||||
* eingegeben wird, ist das die schlechtere Antwort — wer `typ` statt `type`
|
||||
* eingegeben wird, ist das die schlechtere Antwort - wer `typ` statt `type`
|
||||
* schreibt, bekaeme eine gespeicherte Vorlage, die nicht tut, was dasteht.
|
||||
* Und was nicht ankommt, kann auch nichts einschleusen.
|
||||
*/
|
||||
|
||||
+1
-1
@@ -6,7 +6,7 @@
|
||||
* steht so ausdrücklich hier statt in fremdem Code.
|
||||
*
|
||||
* Die Eigenschaft ist: **die Kennungen sind lexikografisch sortierbar wie die
|
||||
* Zeit.** Darauf beruhen zwei Dinge — die Nachlieferung beim Wiederverbinden
|
||||
* Zeit.** Darauf beruhen zwei Dinge - die Nachlieferung beim Wiederverbinden
|
||||
* (`id > seit`) und die Reihenfolge im Chat, ohne dass eine Sortierspalte
|
||||
* nötig wäre. Siehe docs/06-chat-und-realtime.md.
|
||||
*
|
||||
|
||||
Reference in New Issue
Block a user