327 lines
15 KiB
TypeScript
327 lines
15 KiB
TypeScript
import { useMemo, useState } from "react";
|
|
import { FileText, GitBranch, Inbox, Search, ShieldCheck } from "lucide-react";
|
|
import { useLocation } from "react-router";
|
|
import QuickAccessRail from "../../../govoplan-quick-access/webui/src/components/QuickAccessRail";
|
|
import ActionToolbar from "../src/components/ActionToolbar";
|
|
import Button from "../src/components/Button";
|
|
import Card from "../src/components/Card";
|
|
import ContentGrid, { FormGrid } from "../src/components/ContentGrid";
|
|
import ContentSection from "../src/components/ContentSection";
|
|
import CountBadge from "../src/components/CountBadge";
|
|
import DefinitionNodeIcon from "../src/components/DefinitionNodeIcon";
|
|
import DefinitionPalette, { DefinitionPaletteGroup, DefinitionPaletteItem } from "../src/components/DefinitionPalette";
|
|
import DescriptionList, { DescriptionItem } from "../src/components/DescriptionList";
|
|
import Dialog from "../src/components/Dialog";
|
|
import { DialogForm, DialogSection } from "../src/components/DialogAnatomy";
|
|
import FilterBar from "../src/components/FilterBar";
|
|
import FloatingStatus from "../src/components/FloatingStatus";
|
|
import FormSection from "../src/components/FormSection";
|
|
import MetricCard from "../src/components/MetricCard";
|
|
import MetricGrid from "../src/components/MetricGrid";
|
|
import PageLayout from "../src/components/PageLayout";
|
|
import PageActionBar from "../src/components/PageActionBar";
|
|
import SelectionList, { SelectionListItem, SelectionListItemContent } from "../src/components/SelectionList";
|
|
import StatePanel from "../src/components/StatePanel";
|
|
import WorkspaceFrame from "../src/components/WorkspaceFrame";
|
|
import WorkspaceLayout from "../src/components/WorkspaceLayout";
|
|
import WorkspaceActionBar from "../src/components/WorkspaceActionBar";
|
|
import BreadcrumbBar from "../src/layout/BreadcrumbBar";
|
|
import { useGuardedNavigate } from "../src/components/UnsavedChangesGuard";
|
|
import {
|
|
createQuickAccessLaunchContext,
|
|
quickAccessLaunchState
|
|
} from "../src/platform/launchContext";
|
|
import type { ApiSettings, AuthInfo, EffectiveViewProjection, QuickAccessToolMetadata } from "../src/types";
|
|
|
|
export default function ConformanceApp() {
|
|
const location = useLocation();
|
|
const [dialogOpen, setDialogOpen] = useState(false);
|
|
const [editorDirty, setEditorDirty] = useState(true);
|
|
const [metricDrilldown, setMetricDrilldown] = useState("");
|
|
|
|
return (
|
|
<main className="conformance-root" data-conformance-id="shared-ui-lab">
|
|
<PageLayout
|
|
archetype="overview"
|
|
mode="embedded"
|
|
title="Zentrale GovOPlaN-Oberflächen"
|
|
description="Diese Prüfseite rendert reale gemeinsame Komponenten mit langen deutschen Beschriftungen, Zuständen und responsiven Zusammensetzungen."
|
|
actions={<PageActionBar variant="overview" primaryActions={<Button variant="primary" data-testid="open-dialog" onClick={() => setDialogOpen(true)}>Prüfdialog öffnen</Button>} />}
|
|
>
|
|
<section className="conformance-section" aria-labelledby="actions-heading">
|
|
<h2 id="actions-heading">Aktionen, Filter und Status</h2>
|
|
<PageActionBar
|
|
variant="editor"
|
|
refreshable
|
|
state={editorDirty ? "dirty" : "clean"}
|
|
label="Bearbeitungsaktionen"
|
|
reloadAction={{ onReload: () => undefined, label: "Neu laden" }}
|
|
contextActions={<Button>Vorschau öffnen</Button>}
|
|
destructiveActions={<Button variant="danger" disabledReason="Nur die federführende Stelle darf diesen Vorgang endgültig löschen.">Löschen</Button>}
|
|
discardAction={{ label: "Verwerfen", onClick: () => setEditorDirty(false) }}
|
|
saveAction={{ label: "Änderungen speichern", onClick: () => setEditorDirty(false) }}
|
|
/>
|
|
<FilterBar surface="control" aria-label="Vorgangsliste filtern">
|
|
<label>Suche<input type="search" placeholder="Aktenzeichen oder verantwortliche Stelle" /></label>
|
|
<label>Status<select defaultValue="open"><option value="open">Offen</option><option value="done">Abgeschlossen</option></select></label>
|
|
<Button><Search size={16} aria-hidden="true" /> Anwenden</Button>
|
|
<CountBadge aria-label="27 Treffer">27</CountBadge>
|
|
</FilterBar>
|
|
<ContentGrid columns={3} collapseAt="standard" spacing="block">
|
|
<StatePanel title="Keine Einträge" description="Für den gewählten Zeitraum liegen noch keine Einträge vor." surface="dashed" size="compact" icon={<Inbox size={22} />} />
|
|
<StatePanel title="Berechtigung erforderlich" description="Die zuständige Administration kann den fehlenden Zugriff prüfen." tone="warning" surface="subtle" size="compact" icon={<ShieldCheck size={22} />} />
|
|
<StatePanel title="Verbindung unterbrochen" description="Gespeicherte Daten bleiben erhalten. Erneut versuchen, sobald die Verbindung verfügbar ist." tone="danger" surface="subtle" size="compact" actions={<Button>Erneut versuchen</Button>} />
|
|
</ContentGrid>
|
|
</section>
|
|
|
|
<section className="conformance-section" aria-labelledby="metrics-heading">
|
|
<h2 id="metrics-heading">Kennzahlen und Eigenschaften</h2>
|
|
<MetricGrid columns={4} collapseAt="standard">
|
|
<MetricCard label="Offene Aufgaben" value="18" detail="3 heute fällig" drilldown={{ label: "Offene Aufgaben prüfen", onActivate: () => setMetricDrilldown("18 offene Aufgaben") }} />
|
|
<MetricCard label="Fristgerecht" value="94 %" tone="good" detail="Letzte 30 Tage" />
|
|
<MetricCard label="Klärung erforderlich" value="4" tone="warning" density="compact" />
|
|
<MetricCard label="Fehlgeschlagen" value="1" tone="danger" surface="subtle" />
|
|
</MetricGrid>
|
|
<p data-testid="metric-drilldown-result" role="status">{metricDrilldown}</p>
|
|
<ContentSection density="default" surface="subtle">
|
|
<h3>Nachvollziehbare Entscheidung</h3>
|
|
<DescriptionList columns={3} collapseAt="standard">
|
|
<DescriptionItem term="Aktenzeichen">A-2026-004218</DescriptionItem>
|
|
<DescriptionItem term="Verantwortliche Stelle">Fachbereich Öffentlicher Raum und nachhaltige Mobilität</DescriptionItem>
|
|
<DescriptionItem term="Letzte Änderung">18. August 2026, 14:42 Uhr</DescriptionItem>
|
|
</DescriptionList>
|
|
</ContentSection>
|
|
</section>
|
|
|
|
<section className="conformance-section" aria-labelledby="workspace-heading">
|
|
<h2 id="workspace-heading">Listen- und Arbeitsbereich</h2>
|
|
<WorkspaceFrame className="conformance-workspace" label="Vorgangsauswahl">
|
|
<WorkspaceLayout
|
|
variant="split"
|
|
primarySize="compact"
|
|
surface="contained"
|
|
primaryLabel="Vorgänge"
|
|
contentLabel="Ausgewählter Vorgang"
|
|
primary={
|
|
<>
|
|
<WorkspaceActionBar scope="collection-pane" variant="collection" refreshable reloadAction={{ onReload: () => undefined, label: "Vorgänge neu laden" }} contextActions={<strong>Vorgänge</strong>} createAction={<Button variant="primary">Neu</Button>} />
|
|
<SelectionList label="Vorgänge" variant="navigation">
|
|
<SelectionListItem selected>
|
|
<SelectionListItemContent leading={<FileText size={18} />} title="Anwohnerparkausweis" description="A-2026-004218 · Prüfung läuft" />
|
|
</SelectionListItem>
|
|
<SelectionListItem selected={false}>
|
|
<SelectionListItemContent leading={<FileText size={18} />} title="Sondernutzung öffentlicher Fläche" description="A-2026-004219 · Rückfrage offen" />
|
|
</SelectionListItem>
|
|
</SelectionList>
|
|
</>
|
|
}
|
|
>
|
|
<>
|
|
<WorkspaceActionBar scope="detail-pane" variant="detail" contextActions={<strong>Anwohnerparkausweis</strong>} primaryActions={<Button variant="primary">Bearbeiten</Button>} destructiveActions={<Button variant="danger">Schließen</Button>} />
|
|
<Card title="Anwohnerparkausweis">
|
|
<p>Die Identität wurde geprüft. Ein aktueller Wohnsitznachweis muss noch bestätigt werden.</p>
|
|
<FormSection title="Nächster Arbeitsschritt" description="Die Entscheidung bleibt nachvollziehbar und kann vor Abschluss korrigiert werden." variant="panel">
|
|
<FormGrid columns={2}>
|
|
<label>Zuständigkeit<input defaultValue="Bürgerdienste Mitte" /></label>
|
|
<label>Bearbeitungsfrist<input type="date" defaultValue="2026-08-28" /></label>
|
|
</FormGrid>
|
|
</FormSection>
|
|
</Card>
|
|
</>
|
|
</WorkspaceLayout>
|
|
</WorkspaceFrame>
|
|
</section>
|
|
|
|
<section className="conformance-section" aria-labelledby="definition-heading">
|
|
<h2 id="definition-heading">Definitionsarbeitsbereich</h2>
|
|
<div className="conformance-definition">
|
|
<DefinitionPalette label="Bausteine" description="Mit Tastatur oder Schaltfläche einfügen">
|
|
<DefinitionPaletteGroup label="Verarbeitung">
|
|
<DefinitionPaletteItem icon={<DefinitionNodeIcon><GitBranch size={16} /></DefinitionNodeIcon>} label="Bedingung prüfen" />
|
|
<DefinitionPaletteItem icon={<DefinitionNodeIcon><ShieldCheck size={16} /></DefinitionNodeIcon>} label="Freigabe anfordern" />
|
|
</DefinitionPaletteGroup>
|
|
</DefinitionPalette>
|
|
<ContentSection className="conformance-canvas" density="default" spacing="none">
|
|
<StatePanel title="Definition auswählen" description="Wählen Sie links einen Baustein oder öffnen Sie eine vorhandene Definition." size="fill" />
|
|
<FloatingStatus>Automatische Prüfung läuft …</FloatingStatus>
|
|
</ContentSection>
|
|
</div>
|
|
</section>
|
|
|
|
<LaunchContextScenario />
|
|
</PageLayout>
|
|
|
|
<Dialog
|
|
open={dialogOpen}
|
|
title="Konsequenz vor Ausführung prüfen"
|
|
description="Die Änderung betrifft nachgelagerte Aufgaben und wird mit Verantwortlichkeit und Zeitpunkt protokolliert."
|
|
size="default"
|
|
onClose={() => setDialogOpen(false)}
|
|
footer={<><Button onClick={() => setDialogOpen(false)}>Abbrechen</Button><Button variant="primary" onClick={() => setDialogOpen(false)}>Änderung bestätigen</Button></>}
|
|
>
|
|
<DialogForm onSubmit={(event) => event.preventDefault()}>
|
|
<DialogSection title="Begründung">
|
|
<label>Begründung<textarea defaultValue="Die vorliegenden Nachweise wurden vollständig geprüft." /></label>
|
|
</DialogSection>
|
|
</DialogForm>
|
|
</Dialog>
|
|
{new URLSearchParams(location.search).has("quick-access") ? <QuickAccessScenario /> : null}
|
|
</main>
|
|
);
|
|
}
|
|
|
|
function QuickAccessScenario() {
|
|
const location = useLocation();
|
|
const mode = new URLSearchParams(location.search).get("quick-access");
|
|
const viewContext = useMemo(
|
|
() => mode === "focused" || mode === "stale-focus"
|
|
? {
|
|
...CONFORMANCE_VIEW_PROJECTION,
|
|
presentation: {
|
|
quickAccessFocusedToolIds: mode === "focused"
|
|
? ["files.recent"]
|
|
: ["mail.compose"]
|
|
}
|
|
}
|
|
: null,
|
|
[mode]
|
|
);
|
|
const launchContext = useMemo(() => createQuickAccessLaunchContext({
|
|
pathname: location.pathname,
|
|
search: location.search,
|
|
hash: location.hash,
|
|
historyIndex: browserHistoryIndex(),
|
|
auth: CONFORMANCE_AUTH,
|
|
activeObject: {
|
|
ownerModule: "cases",
|
|
kind: "case",
|
|
objectId: "case-1",
|
|
tenantId: "tenant-1",
|
|
label: "RPP-2026-0001 · Anwohnerparkausweis"
|
|
},
|
|
temporalContext: { validityMode: "current", validAt: null, recordedAt: null },
|
|
viewContext
|
|
}), [location.hash, location.pathname, location.search, viewContext]);
|
|
return <QuickAccessRail
|
|
settings={CONFORMANCE_SETTINGS}
|
|
auth={CONFORMANCE_AUTH}
|
|
tools={CONFORMANCE_QUICK_ACCESS_TOOLS}
|
|
launchContext={launchContext}
|
|
/>;
|
|
}
|
|
|
|
function LaunchContextScenario() {
|
|
const location = useLocation();
|
|
const navigate = useGuardedNavigate();
|
|
const launchContext = useMemo(() => createQuickAccessLaunchContext({
|
|
pathname: location.pathname,
|
|
search: location.search,
|
|
hash: location.hash,
|
|
historyIndex: browserHistoryIndex(),
|
|
auth: CONFORMANCE_AUTH,
|
|
activeObject: {
|
|
ownerModule: "cases",
|
|
kind: "case",
|
|
objectId: "case-1",
|
|
tenantId: "tenant-1",
|
|
label: "RPP-2026-0001 · Anwohnerparkausweis",
|
|
version: "4",
|
|
path: "/cases/case-1"
|
|
},
|
|
temporalContext: { validityMode: "current", validAt: null, recordedAt: null }
|
|
}), [location.hash, location.pathname, location.search]);
|
|
|
|
return (
|
|
<section className="conformance-section" aria-labelledby="launch-heading">
|
|
<h2 id="launch-heading">Start- und Rücksprungkontext</h2>
|
|
<p>Ein begrenzter Objektverweis öffnet das vollständige Werkzeug und erhält einen eindeutigen Rücksprung zum Vorgang.</p>
|
|
<BreadcrumbBar
|
|
pathname={location.pathname === "/" ? "/cases/case-1" : location.pathname}
|
|
locationState={location.state}
|
|
/>
|
|
<ActionToolbar surface="subtle">
|
|
<Button
|
|
data-testid="launch-full-page"
|
|
onClick={() => navigate("/files", { state: quickAccessLaunchState(launchContext) })}
|
|
>
|
|
Vollständige Dateiansicht öffnen
|
|
</Button>
|
|
</ActionToolbar>
|
|
</section>
|
|
);
|
|
}
|
|
|
|
const CONFORMANCE_AUTH = {
|
|
user: { id: "user-1", account_id: "account-1", email: "case@example.test" },
|
|
tenant: { id: "tenant-1", slug: "reference", name: "Referenzkommune" },
|
|
scopes: [],
|
|
roles: [],
|
|
groups: [],
|
|
profile_loaded: true,
|
|
roles_loaded: true,
|
|
groups_loaded: true
|
|
} satisfies AuthInfo;
|
|
|
|
const CONFORMANCE_SETTINGS: ApiSettings = {
|
|
apiBaseUrl: "",
|
|
apiKey: "",
|
|
accessToken: ""
|
|
};
|
|
|
|
const CONFORMANCE_QUICK_ACCESS_TOOLS: QuickAccessToolMetadata[] = [{
|
|
contractVersion: "1",
|
|
id: "files.recent",
|
|
moduleId: "files",
|
|
categoryId: "files",
|
|
label: "Recent files",
|
|
description: "Select an authorized file without leaving this case.",
|
|
iconName: "files",
|
|
surfaceId: "files.route.files",
|
|
fullPagePath: "/files",
|
|
allOf: [],
|
|
anyOf: [],
|
|
order: 10,
|
|
defaultEnabled: true,
|
|
modes: ["select"],
|
|
availability: "global",
|
|
acceptedReferenceKinds: [],
|
|
returnedReferenceKinds: ["files.file-version"],
|
|
helpContextId: "files.quick_access.files"
|
|
}, {
|
|
contractVersion: "1",
|
|
id: "postbox.unread",
|
|
moduleId: "postbox",
|
|
categoryId: "messages",
|
|
label: "Unread messages",
|
|
description: "Open an authorized unread message.",
|
|
iconName: "messages",
|
|
surfaceId: "postbox.route.postbox",
|
|
fullPagePath: "/postbox",
|
|
allOf: [],
|
|
anyOf: [],
|
|
order: 10,
|
|
defaultEnabled: true,
|
|
modes: ["view"],
|
|
availability: "global",
|
|
acceptedReferenceKinds: [],
|
|
returnedReferenceKinds: ["postbox.message"],
|
|
helpContextId: "postbox.quick_access.messages"
|
|
}];
|
|
|
|
const CONFORMANCE_VIEW_PROJECTION: EffectiveViewProjection = {
|
|
activeViewId: "case-processing",
|
|
activeRevisionId: "case-processing-r4",
|
|
activeViewName: "Case processing",
|
|
visibleSurfaceIds: [],
|
|
projectionActive: true,
|
|
locked: false,
|
|
availableViews: [],
|
|
provenance: [{ source: "tenant-default", scopeType: "tenant", scopeId: "tenant-1" }],
|
|
diagnostics: []
|
|
};
|
|
|
|
function browserHistoryIndex(): number | null {
|
|
const value = (window.history.state as { idx?: unknown } | null)?.idx;
|
|
return typeof value === "number" && Number.isInteger(value) ? value : null;
|
|
}
|