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.
268 lines
9.2 KiB
TypeScript
268 lines
9.2 KiB
TypeScript
import type { Metadata } from 'next'
|
|
import { db } from '@/lib/db'
|
|
import { visibleEntryWhere } from '@/lib/access/caseFilter'
|
|
import { visibleAttachmentWhere } from '@/lib/access/attachmentFilter'
|
|
import { loadVisibleCase } from '@/lib/cases/service'
|
|
import { loadSections } from '@/lib/cases/sectionService'
|
|
import { permissionsForPrint } from '@/lib/export/printAccess'
|
|
import { isInputField, fillPlaceholders } from '@/lib/templates/fields'
|
|
import { de } from '@/lib/i18n/de'
|
|
import { NotAvailable } from '../../../NotAvailable'
|
|
import type { RenderedSection } from '@/lib/sections/render'
|
|
|
|
export const dynamic = 'force-dynamic'
|
|
|
|
export async function generateMetadata({
|
|
params,
|
|
}: {
|
|
params: Promise<{ caseNumber: string }>
|
|
}): Promise<Metadata> {
|
|
const { caseNumber } = await params
|
|
return { title: caseNumber, robots: { index: false, follow: false } }
|
|
}
|
|
|
|
/**
|
|
* Druckansicht einer Fallakte.
|
|
*
|
|
* Sie ist die einzige Vorlage für den PDF-Export: kein zweites Layout von Hand
|
|
* in einer PDF-Bibliothek. Der Playwright-Dienst ruft genau diese Seite auf.
|
|
*
|
|
* **Ein Export darf nie sauberer aussehen als die Akte ist.** Deshalb:
|
|
* gesperrte Felder erscheinen als Balken mit Vermerk statt zu fehlen,
|
|
* geänderte Einträge sind als geändert gekennzeichnet, zurückgezogene stehen
|
|
* als Grabstein mit Begründung. Siehe docs/08-suche-und-export.md.
|
|
*/
|
|
export default async function PrintCasePage({
|
|
params,
|
|
searchParams,
|
|
}: {
|
|
params: Promise<{ caseNumber: string }>
|
|
searchParams: Promise<{ token?: string }>
|
|
}) {
|
|
const { caseNumber } = await params
|
|
const { token } = await searchParams
|
|
|
|
const zugang = await permissionsForPrint(token)
|
|
if (!zugang) return <NotAvailable />
|
|
const { permissions, displayName } = zugang
|
|
|
|
const akte = await loadVisibleCase(permissions, caseNumber)
|
|
if (!akte) return <NotAvailable />
|
|
|
|
const [sections, entries, anhaenge, levels, autoren] = await Promise.all([
|
|
loadSections(permissions, akte.id, akte.templateId),
|
|
db.caseEntry.findMany({
|
|
where: { AND: [visibleEntryWhere(permissions), { caseId: akte.id }] },
|
|
orderBy: [{ createdAt: 'asc' }, { id: 'asc' }],
|
|
select: {
|
|
id: true,
|
|
type: true,
|
|
body: true,
|
|
authorId: true,
|
|
createdAt: true,
|
|
version: true,
|
|
editedAt: true,
|
|
editVisible: true,
|
|
retractedAt: true,
|
|
retractionReason: true,
|
|
},
|
|
}),
|
|
db.attachment.findMany({
|
|
where: { AND: [visibleAttachmentWhere(permissions), { caseId: akte.id }] },
|
|
orderBy: { createdAt: 'asc' },
|
|
select: { id: true, originalName: true, sha256: true, sizeBytes: true, scanStatus: true },
|
|
}),
|
|
db.clearanceLevelConfig.findMany({ orderBy: { level: 'asc' } }),
|
|
db.user.findMany({ select: { id: true, displayName: true } }),
|
|
])
|
|
|
|
const levelName = new Map(levels.map((l) => [l.level, l.name]))
|
|
const nameVon = new Map(autoren.map((a) => [a.id, a.displayName]))
|
|
const stand = new Date()
|
|
|
|
return (
|
|
<main className="print-sheet">
|
|
<section className="print-cover">
|
|
<span className="print-stamp">
|
|
{akte.classification} · {levelName.get(akte.classification) ?? de.common.unknown}
|
|
</span>
|
|
<h1 style={{ marginTop: '4mm' }}>{akte.title}</h1>
|
|
<p style={{ fontFamily: 'var(--font-mono, monospace)', margin: 0 }}>{akte.caseNumber}</p>
|
|
|
|
<table className="print-meta">
|
|
<tbody>
|
|
<Zeile label={de.case.status} value={de.case.statusNames[akte.status] ?? akte.status} />
|
|
<Zeile label={de.case.type} value={akte.templateKey} />
|
|
<Zeile
|
|
label={de.case.createdBy}
|
|
value={nameVon.get(akte.createdById) ?? akte.createdById}
|
|
/>
|
|
<Zeile
|
|
label={de.case.dueAt}
|
|
value={akte.dueAt ? akte.dueAt.toLocaleDateString('de-DE') : '-'}
|
|
/>
|
|
<Zeile label={de.export.retrievedBy} value={displayName} />
|
|
<Zeile
|
|
label={de.export.asOf}
|
|
value={`${stand.toLocaleString('de-DE')} · ${de.clearance.label} ${permissions.clearanceLevel}`}
|
|
/>
|
|
</tbody>
|
|
</table>
|
|
|
|
<p className="print-note" style={{ marginTop: '4mm' }}>
|
|
{de.export.coverNote}
|
|
</p>
|
|
</section>
|
|
|
|
<h2>{de.section.heading}</h2>
|
|
<PrintSections sections={sections} levelName={levelName} />
|
|
|
|
<h2>{de.case.timeline}</h2>
|
|
{entries.length === 0 ? (
|
|
<p className="print-note">{de.entry.empty}</p>
|
|
) : (
|
|
entries.map((eintrag) => (
|
|
<div key={eintrag.id} className="print-entry">
|
|
<p className="print-label">
|
|
{de.entry.types[eintrag.type] ?? eintrag.type} ·{' '}
|
|
{nameVon.get(eintrag.authorId) ?? eintrag.authorId} ·{' '}
|
|
{eintrag.createdAt.toLocaleString('de-DE')}
|
|
{eintrag.editVisible && eintrag.editedAt
|
|
? ` · ${de.export.editedAt(eintrag.version, eintrag.editedAt.toLocaleDateString('de-DE'))}`
|
|
: ''}
|
|
</p>
|
|
{eintrag.retractedAt ? (
|
|
<p style={{ margin: '1mm 0', fontStyle: 'italic' }}>
|
|
{de.entry.retracted(
|
|
eintrag.retractedAt.toLocaleDateString('de-DE'),
|
|
nameVon.get(eintrag.authorId) ?? eintrag.authorId,
|
|
eintrag.retractionReason ?? '',
|
|
)}
|
|
</p>
|
|
) : (
|
|
<p style={{ margin: '1mm 0', whiteSpace: 'pre-wrap' }}>{eintrag.body}</p>
|
|
)}
|
|
</div>
|
|
))
|
|
)}
|
|
|
|
<h2>{de.attachment.heading}</h2>
|
|
{anhaenge.length === 0 ? (
|
|
<p className="print-note">{de.attachment.empty}</p>
|
|
) : (
|
|
<table className="print-meta">
|
|
<thead>
|
|
<tr>
|
|
<th>{de.export.fileName}</th>
|
|
<th>{de.export.fileSize}</th>
|
|
<th>{de.export.fileHash}</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
{anhaenge.map((anhang) => (
|
|
<tr key={anhang.id}>
|
|
<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. */}
|
|
<td style={{ fontFamily: 'monospace', fontSize: '7pt', wordBreak: 'break-all' }}>
|
|
{anhang.sha256}
|
|
</td>
|
|
</tr>
|
|
))}
|
|
</tbody>
|
|
</table>
|
|
)}
|
|
</main>
|
|
)
|
|
}
|
|
|
|
function Zeile({ label, value }: { label: string; value: string }) {
|
|
return (
|
|
<tr>
|
|
<th>{label}</th>
|
|
<td>{value}</td>
|
|
</tr>
|
|
)
|
|
}
|
|
|
|
/**
|
|
* 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
|
|
* nicht, dass er eine gekürzte Fassung in der Hand hält.
|
|
*/
|
|
function PrintSections({
|
|
sections,
|
|
levelName,
|
|
}: {
|
|
sections: RenderedSection[]
|
|
levelName: Map<number, string>
|
|
}) {
|
|
const werte = Object.fromEntries(
|
|
sections.filter((s) => s.visible).map((s) => [s.fieldKey, s.value]),
|
|
)
|
|
|
|
return (
|
|
<div>
|
|
{sections.map((section) => {
|
|
const field = section.field
|
|
|
|
if (!isInputField(field)) {
|
|
if (field.type === 'pagebreak') {
|
|
return <div key={field.key} style={{ breakAfter: 'page' }} />
|
|
}
|
|
const text = fillPlaceholders(field.text ?? '', werte)
|
|
return field.type === 'heading' ? (
|
|
<h3 key={field.key}>{text || field.label}</h3>
|
|
) : (
|
|
<p key={field.key} style={{ whiteSpace: 'pre-wrap' }}>
|
|
{text}
|
|
</p>
|
|
)
|
|
}
|
|
|
|
return (
|
|
<div key={field.key} style={{ marginBottom: '3mm', breakInside: 'avoid' }}>
|
|
<p className="print-label">{field.label}</p>
|
|
{!section.visible ? (
|
|
<p style={{ margin: '1mm 0' }}>
|
|
<span className="print-redacted"> </span>{' '}
|
|
<span className="print-note">
|
|
{de.export.redactedAt(
|
|
section.value.requiredLevel,
|
|
levelName.get(section.value.requiredLevel) ?? '',
|
|
)}
|
|
</span>
|
|
</p>
|
|
) : section.segments ? (
|
|
<p style={{ margin: '1mm 0', whiteSpace: 'pre-wrap' }}>
|
|
{section.segments.map((segment, index) =>
|
|
segment.kind === 'text' ? (
|
|
<span key={index}>{segment.text}</span>
|
|
) : (
|
|
// Balken statt Auslassung, mit der Stufe daneben: der
|
|
// Empfaenger soll sehen, dass hier etwas fehlt.
|
|
<span key={index}>
|
|
<span
|
|
className="print-redacted"
|
|
style={{ width: `${segment.blockLength}ch` }}
|
|
>
|
|
|
|
</span>
|
|
<span className="print-note"> [{segment.requiredLevel}]</span>
|
|
</span>
|
|
),
|
|
)}
|
|
</p>
|
|
) : (
|
|
<p style={{ margin: '1mm 0', whiteSpace: 'pre-wrap' }}>
|
|
{String(section.value ?? '-')}
|
|
</p>
|
|
)}
|
|
</div>
|
|
)
|
|
})}
|
|
</div>
|
|
)
|
|
}
|