Migrate Cases interface patterns

This commit is contained in:
2026-08-03 14:43:37 +02:00
parent c7821a5cb0
commit 43b4cc8b86
10 changed files with 639 additions and 48 deletions
+99 -26
View File
@@ -1,15 +1,19 @@
import { ArrowLeft, Save, Share2 } from "lucide-react";
import { useCallback, useEffect, useMemo, useState } from "react";
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import { useParams } from "react-router";
import {
ActionBlockerHint,
Button,
DocumentationHelpLink,
DismissibleAlert,
FormField,
IconButton,
LoadingIndicator,
PageScrollViewport,
StatusBadge,
hasScope,
useGuardedNavigate,
useUnsavedDraftGuard,
type PlatformRouteContext
} from "@govoplan/core-webui";
import {
@@ -24,6 +28,11 @@ import {
type InstitutionalReference
} from "../../api/cases";
import CaseShareDialog from "./CaseShareDialog";
import {
CASES_DOCUMENTATION,
CASES_FIELDS_DOCUMENTATION,
CASES_I18N
} from "./interfacePatterns";
export default function CaseDetailPage({ settings, auth }: PlatformRouteContext) {
@@ -40,6 +49,7 @@ export default function CaseDetailPage({ settings, auth }: PlatformRouteContext)
const [saving, setSaving] = useState(false);
const [error, setError] = useState("");
const [shareOpen, setShareOpen] = useState(false);
const idempotencyKey = useRef(crypto.randomUUID());
const canUpdate = hasScope(auth, "cases:case:update");
const canClose = hasScope(auth, "cases:case:close");
const canShare = hasScope(auth, "cases:case:share");
@@ -84,28 +94,65 @@ export default function CaseDetailPage({ settings, auth }: PlatformRouteContext)
);
}, [canClose, catalog, record]);
const changed = Boolean(record && (title.trim() !== record.title || status !== record.status_key));
const draftDirty = Boolean(record && canUpdate && (changed || changeReason.trim()));
async function save() {
if (!record || !changed || !changeReason.trim()) return;
function discardDraft() {
if (!record) return;
setTitle(record.title);
setStatus(record.status_key);
setChangeReason("");
}
async function save(): Promise<boolean> {
if (!record || !changed || !title.trim() || !changeReason.trim()) return false;
setSaving(true);
setError("");
try {
await updateCase(settings, caseId, {
const saved = await updateCase(settings, caseId, {
expected_revision: record.revision,
recorded_at: new Date().toISOString(),
change_reason: changeReason.trim(),
idempotency_key: crypto.randomUUID(),
idempotency_key: idempotencyKey.current,
...(title.trim() !== record.title ? { title: title.trim() } : {}),
...(status !== record.status_key ? { status_key: status } : {})
});
await load();
setRecord(saved);
setTitle(saved.title);
setStatus(saved.status_key);
setChangeReason("");
idempotencyKey.current = crypto.randomUUID();
try {
await load();
} catch (reloadError) {
setError(reloadError instanceof Error
? `The case was saved, but its history could not be refreshed. ${reloadError.message}`
: "The case was saved, but its history could not be refreshed.");
}
return true;
} catch (reason) {
setError(reason instanceof Error ? reason.message : "Case could not be saved.");
return false;
} finally {
setSaving(false);
}
}
useUnsavedDraftGuard({
dirty: draftDirty,
title: "i18n:govoplan-cases.unsaved_title",
message: "i18n:govoplan-cases.unsaved_message",
onSave: save,
onDiscard: discardDraft
});
const saveDisabledReason = saving
? CASES_I18N.saving
: !changed
? CASES_I18N.noChanges
: !title.trim() || !changeReason.trim()
? CASES_I18N.incomplete
: undefined;
return (
<main className="cases-page">
<div className="case-detail-shell">
@@ -115,14 +162,14 @@ export default function CaseDetailPage({ settings, auth }: PlatformRouteContext)
Cases
</button>
{record && <span>{record.case_number}</span>}
{record && canShare ? (
<IconButton
label="Manage case access"
icon={<Share2 size={16} />}
className="case-share-button"
onClick={() => setShareOpen(true)}
/>
) : null}
{record ? <IconButton
label="Manage case access"
icon={<Share2 size={16} />}
className="case-share-button"
disabledReason={!canShare ? CASES_I18N.shareReason : undefined}
onClick={() => setShareOpen(true)}
/> : null}
<DocumentationHelpLink reference={CASES_DOCUMENTATION} />
</div>
<PageScrollViewport className="case-detail-viewport">
{error &&
@@ -142,26 +189,52 @@ export default function CaseDetailPage({ settings, auth }: PlatformRouteContext)
<StatusBadge status={record.closed_at ? "inactive" : "active"} label={humanize(record.status_key)} />
</div>
{!canUpdate ? (
<ActionBlockerHint
tone="info"
reason={{
summary: "Case editing is read-only",
details: CASES_I18N.updateReason,
requiredAction: "Ask a case manager to make the required lifecycle change.",
actor: "A user with the Cases update permission",
target: "Case role or object access assignment"
}}
documentation={CASES_FIELDS_DOCUMENTATION}
/>
) : null}
{canUpdate && !canClose ? (
<ActionBlockerHint
tone="info"
reason={{
summary: "Terminal case states are unavailable",
details: CASES_I18N.closeReason,
requiredAction: "Ask a case closer to complete the lifecycle transition.",
actor: "A user with the Cases close permission",
target: "Case role assignment"
}}
documentation={CASES_FIELDS_DOCUMENTATION}
/>
) : null}
{canUpdate &&
<div className="case-edit-panel">
<label>
<span>Title</span>
<FormField label="Title" documentation={CASES_FIELDS_DOCUMENTATION}>
<input value={title} onChange={(event) => setTitle(event.target.value)} />
</label>
<label>
<span>Status</span>
</FormField>
<FormField label="Status" documentation={CASES_FIELDS_DOCUMENTATION}>
<select value={status} onChange={(event) => setStatus(event.target.value)}>
{statuses.map((item) => <option key={item.status_key} value={item.status_key}>{item.label}</option>)}
</select>
</label>
<label className="case-change-reason">
<span>Change reason</span>
<input value={changeReason} onChange={(event) => setChangeReason(event.target.value)} />
</label>
</FormField>
<div className="case-change-reason">
<FormField label="Change reason" documentation={CASES_FIELDS_DOCUMENTATION}>
<input value={changeReason} onChange={(event) => setChangeReason(event.target.value)} />
</FormField>
</div>
<Button
variant="primary"
disabled={!changed || !title.trim() || !changeReason.trim() || saving}
onClick={save}>
disabledReason={saveDisabledReason}
onClick={() => void save()}>
<Save size={16} aria-hidden="true" />
{saving ? "Saving" : "Save"}
</Button>
+94 -15
View File
@@ -1,13 +1,18 @@
import { Plus, Trash2 } from "lucide-react";
import { useEffect, useMemo, useState } from "react";
import { useEffect, useMemo, useRef, useState } from "react";
import {
Button,
ConfirmDialog,
Dialog,
DocumentationHelpLink,
DismissibleAlert,
FormField,
IconButton,
ReferenceSelect,
ToggleSwitch,
i18nMessage,
useUnsavedChanges,
useUnsavedDraftGuard,
type ApiSettings
} from "@govoplan/core-webui";
import {
@@ -16,6 +21,10 @@ import {
type CaseGrant,
type CaseRecord
} from "../../api/cases";
import {
CASES_FIELDS_DOCUMENTATION,
CASES_I18N
} from "./interfacePatterns";
type TargetType = "user" | "group";
@@ -42,6 +51,9 @@ export default function CaseShareDialog({
const [changeReason, setChangeReason] = useState("");
const [busy, setBusy] = useState(false);
const [error, setError] = useState("");
const [confirmOpen, setConfirmOpen] = useState(false);
const idempotencyKey = useRef(crypto.randomUUID());
const { requestDiscard } = useUnsavedChanges();
const targetProvider = useMemo(
() => caseShareTargetProvider(settings, record.reference.object_id, targetType),
[record.reference.object_id, settings, targetType]
@@ -51,14 +63,28 @@ export default function CaseShareDialog({
if (!open) return;
setRestricted(record.access_mode === "restricted");
setGrants(record.access_grants);
setTargetType("user");
setTargetId("");
setPermission("read");
setChangeReason("");
setError("");
setConfirmOpen(false);
idempotencyKey.current = crypto.randomUUID();
}, [open, record]);
const changed = restricted !== (record.access_mode === "restricted")
|| JSON.stringify(grants) !== JSON.stringify(record.access_grants);
const draftDirty = changed || Boolean(targetId.trim() || changeReason.trim());
function discardDraft() {
setRestricted(record.access_mode === "restricted");
setGrants(record.access_grants);
setTargetType("user");
setTargetId("");
setPermission("read");
setChangeReason("");
setError("");
}
function addGrant() {
const subjectId = targetId.trim();
@@ -77,8 +103,8 @@ export default function CaseShareDialog({
setTargetId("");
}
async function save() {
if (!changed || !changeReason.trim()) return;
async function save(): Promise<boolean> {
if (!changed || !changeReason.trim()) return false;
setBusy(true);
setError("");
try {
@@ -86,34 +112,70 @@ export default function CaseShareDialog({
expected_revision: record.revision,
recorded_at: new Date().toISOString(),
change_reason: changeReason.trim(),
idempotency_key: crypto.randomUUID(),
idempotency_key: idempotencyKey.current,
access_mode: restricted ? "restricted" : "tenant",
access_grants: grants
});
onSaved(saved);
onClose();
setRestricted(saved.access_mode === "restricted");
setGrants(saved.access_grants);
setTargetId("");
setChangeReason("");
idempotencyKey.current = crypto.randomUUID();
return true;
} catch (reason) {
setError(reason instanceof Error ? reason.message : "Case access could not be saved.");
return false;
} finally {
setBusy(false);
}
}
useUnsavedDraftGuard({
dirty: open && draftDirty,
title: "i18n:govoplan-cases.unsaved_access_title",
message: "i18n:govoplan-cases.unsaved_access_message",
onSave: save,
onDiscard: discardDraft
});
function close() {
if (busy) return;
if (draftDirty) requestDiscard(onClose);
else onClose();
}
async function confirmSave() {
const saved = await save();
if (!saved) return;
setConfirmOpen(false);
onClose();
}
const saveDisabledReason = busy
? CASES_I18N.saving
: !changed
? CASES_I18N.noChanges
: !changeReason.trim()
? CASES_I18N.incomplete
: undefined;
return (
<>
<Dialog
open={open}
title={`Case access - ${record.case_number}`}
onClose={onClose}
title={i18nMessage("i18n:govoplan-cases.case_access_title", { value0: record.case_number })}
onClose={close}
closeDisabled={busy}
portal
className="case-share-dialog"
footer={
<>
<Button disabled={busy} onClick={onClose}>Cancel</Button>
<Button disabled={busy} onClick={close}>Cancel</Button>
<Button
variant="primary"
disabled={busy || !changed || !changeReason.trim()}
onClick={() => void save()}
disabledReason={saveDisabledReason}
onClick={() => setConfirmOpen(true)}
>
{busy ? "Saving" : "Save access"}
</Button>
@@ -121,6 +183,7 @@ export default function CaseShareDialog({
}
>
<div className="case-share-content">
<DocumentationHelpLink reference={CASES_FIELDS_DOCUMENTATION} />
{error ? (
<DismissibleAlert tone="danger" resetKey={error}>{error}</DismissibleAlert>
) : null}
@@ -139,7 +202,7 @@ export default function CaseShareDialog({
</p>
<div className="case-share-add-row">
<FormField label="Target type">
<FormField label="Target type" documentation={CASES_FIELDS_DOCUMENTATION}>
<select
value={targetType}
disabled={busy}
@@ -152,7 +215,7 @@ export default function CaseShareDialog({
<option value="group">Group</option>
</select>
</FormField>
<FormField label="Target">
<FormField label="Target" documentation={CASES_FIELDS_DOCUMENTATION}>
<ReferenceSelect
value={targetId}
onChange={setTargetId}
@@ -162,7 +225,7 @@ export default function CaseShareDialog({
aria-label={`Case access ${targetType}`}
/>
</FormField>
<FormField label="Permission">
<FormField label="Permission" documentation={CASES_FIELDS_DOCUMENTATION}>
<select
value={permission}
disabled={busy}
@@ -178,7 +241,7 @@ export default function CaseShareDialog({
label="Add access grant"
icon={<Plus size={16} />}
variant="primary"
disabled={busy || !targetId.trim()}
disabledReason={busy ? CASES_I18N.saving : !targetId.trim() ? CASES_I18N.targetRequired : undefined}
onClick={addGrant}
/>
</div>
@@ -221,7 +284,7 @@ export default function CaseShareDialog({
))}
</div>
<FormField label="Change reason">
<FormField label="Change reason" documentation={CASES_FIELDS_DOCUMENTATION}>
<input
value={changeReason}
disabled={busy}
@@ -232,6 +295,22 @@ export default function CaseShareDialog({
</FormField>
</div>
</Dialog>
<ConfirmDialog
open={confirmOpen}
title="i18n:govoplan-cases.access_confirm_title"
message={i18nMessage("i18n:govoplan-cases.access_confirm_message", {
value0: record.case_number,
value1: restricted
? "i18n:govoplan-cases.visibility_restricted"
: "i18n:govoplan-cases.visibility_tenant",
value2: grants.length
})}
confirmLabel="Save access"
busy={busy}
onCancel={() => setConfirmOpen(false)}
onConfirm={() => void confirmSave()}
/>
</>
);
}
+13 -2
View File
@@ -1,10 +1,13 @@
import { Search } from "lucide-react";
import { useEffect, useMemo, useState, type FormEvent } from "react";
import {
Button,
DocumentationHelpLink,
DismissibleAlert,
LoadingIndicator,
PageScrollViewport,
StatusBadge,
i18nMessage,
useGuardedNavigate,
type PlatformRouteContext
} from "@govoplan/core-webui";
@@ -14,6 +17,7 @@ import {
type CaseCatalog,
type CaseRecord
} from "../../api/cases";
import { CASES_DOCUMENTATION, CASES_I18N } from "./interfacePatterns";
export default function CasesPage({ settings }: PlatformRouteContext) {
@@ -79,7 +83,13 @@ export default function CasesPage({ settings }: PlatformRouteContext) {
aria-label="Search cases"
placeholder="Search cases"
/>
<button type="submit" className="btn btn-primary">Search</button>
<Button
type="submit"
variant="primary"
disabledReason={loading ? CASES_I18N.loading : undefined}
>
Search
</Button>
</form>
<label className="cases-status-filter">
<span>Status</span>
@@ -90,7 +100,8 @@ export default function CasesPage({ settings }: PlatformRouteContext) {
)}
</select>
</label>
<span className="cases-count">{total} cases</span>
<span className="cases-count">{i18nMessage("i18n:govoplan-cases.case_count", { value0: total })}</span>
<DocumentationHelpLink reference={CASES_DOCUMENTATION} />
</div>
<PageScrollViewport className="cases-list-viewport">
{error &&
@@ -0,0 +1,22 @@
import type { DocumentationHelpReference } from "@govoplan/core-webui";
export const CASES_DOCUMENTATION = {
topicId: "cases.institutional-context",
documentationType: "user"
} satisfies DocumentationHelpReference;
export const CASES_FIELDS_DOCUMENTATION = {
topicId: "cases.reference.lifecycle-access-and-evidence",
documentationType: "admin"
} satisfies DocumentationHelpReference;
export const CASES_I18N = {
loading: "i18n:govoplan-cases.loading_reason",
saving: "i18n:govoplan-cases.saving_reason",
updateReason: "i18n:govoplan-cases.update_reason",
closeReason: "i18n:govoplan-cases.close_reason",
shareReason: "i18n:govoplan-cases.share_reason",
noChanges: "i18n:govoplan-cases.no_changes_reason",
incomplete: "i18n:govoplan-cases.incomplete_reason",
targetRequired: "i18n:govoplan-cases.target_required_reason"
} as const;
+165
View File
@@ -0,0 +1,165 @@
import type { PlatformTranslations } from "@govoplan/core-webui";
const en = {
"i18n:govoplan-cases.cases": "Cases",
"i18n:govoplan-cases.navigation": "Cases navigation",
"i18n:govoplan-cases.list": "Case list",
"i18n:govoplan-cases.filters": "Case search and filters",
"i18n:govoplan-cases.detail": "Case details",
"i18n:govoplan-cases.summary": "Case summary",
"i18n:govoplan-cases.editor": "Case lifecycle editor",
"i18n:govoplan-cases.references": "Institutional references",
"i18n:govoplan-cases.timeline": "Case timeline",
"i18n:govoplan-cases.history": "Immutable case history",
"i18n:govoplan-cases.access": "Case access",
"i18n:govoplan-cases.loading_reason": "The case is still loading.",
"i18n:govoplan-cases.saving_reason": "The case change is still being saved.",
"i18n:govoplan-cases.update_reason": "Your account may inspect this case but may not change its title or lifecycle state.",
"i18n:govoplan-cases.close_reason": "Closing a case requires the case-close permission.",
"i18n:govoplan-cases.share_reason": "Changing case visibility or grants requires the case-share permission.",
"i18n:govoplan-cases.no_changes_reason": "There are no case changes to save.",
"i18n:govoplan-cases.incomplete_reason": "Enter a title and a reason that explains the recorded change.",
"i18n:govoplan-cases.target_required_reason": "Select a user or group before adding an access grant.",
"i18n:govoplan-cases.unsaved_title": "Unsaved case change",
"i18n:govoplan-cases.unsaved_message": "Save or discard the case title, status, and reason before leaving.",
"i18n:govoplan-cases.unsaved_access_title": "Unsaved case access change",
"i18n:govoplan-cases.unsaved_access_message": "Save or discard the visibility and grant changes before leaving this dialog.",
"i18n:govoplan-cases.access_confirm_title": "Change case access",
"i18n:govoplan-cases.access_confirm_message": "Change {value0} to {value1} visibility with {value2} explicit grant(s)? This appends an immutable case revision and timeline entry.",
"i18n:govoplan-cases.case_access_title": "Case access - {value0}",
"i18n:govoplan-cases.case_count": "{value0} cases",
"i18n:govoplan-cases.visibility_tenant": "tenant",
"i18n:govoplan-cases.visibility_restricted": "restricted",
"Case editing is read-only": "Case editing is read-only",
"Ask a case manager to make the required lifecycle change.": "Ask a case manager to make the required lifecycle change.",
"A user with the Cases update permission": "A user with the Cases update permission",
"Case role or object access assignment": "Case role or object access assignment",
"Terminal case states are unavailable": "Terminal case states are unavailable",
"Ask a case closer to complete the lifecycle transition.": "Ask a case closer to complete the lifecycle transition.",
"A user with the Cases close permission": "A user with the Cases close permission",
"Case role assignment": "Case role assignment",
"Cases": "Cases",
"Loading cases": "Loading cases",
"Loading case": "Loading case",
"User": "User",
"Group": "Group",
"Read": "Read",
"Update": "Update",
"Share": "Share",
"Administer": "Administer",
"Cancel": "Cancel",
"Add access grant": "Add access grant",
"Explicit access grants": "Explicit access grants",
"Why is case access changing?": "Why is case access changing?",
"Search cases": "Search cases",
"Search": "Search",
"Status": "Status",
"All statuses": "All statuses",
"No matching cases.": "No matching cases.",
"Manage case access": "Manage case access",
"Title": "Title",
"Change reason": "Change reason",
"Save": "Save",
"Saving": "Saving",
"Opened": "Opened",
"Deadline": "Deadline",
"Revision": "Revision",
"Last change": "Last change",
"Parties": "Parties",
"Assignments": "Assignments",
"Decisions": "Decisions",
"Records": "Records",
"Timeline": "Timeline",
"History": "History",
"Case visibility": "Case visibility",
"Tenant": "Tenant",
"Restricted": "Restricted",
"Target type": "Target type",
"Target": "Target",
"Permission": "Permission",
"Save access": "Save access",
"No explicit access grants.": "No explicit access grants."
} as const;
const de: Record<keyof typeof en, string> = {
"i18n:govoplan-cases.cases": "Vorgänge",
"i18n:govoplan-cases.navigation": "Vorgangsnavigation",
"i18n:govoplan-cases.list": "Vorgangsliste",
"i18n:govoplan-cases.filters": "Vorgangssuche und Filter",
"i18n:govoplan-cases.detail": "Vorgangsdetails",
"i18n:govoplan-cases.summary": "Vorgangszusammenfassung",
"i18n:govoplan-cases.editor": "Vorgangsstatus bearbeiten",
"i18n:govoplan-cases.references": "Institutionelle Referenzen",
"i18n:govoplan-cases.timeline": "Vorgangszeitachse",
"i18n:govoplan-cases.history": "Unveränderliche Vorgangshistorie",
"i18n:govoplan-cases.access": "Vorgangszugriff",
"i18n:govoplan-cases.loading_reason": "Der Vorgang wird noch geladen.",
"i18n:govoplan-cases.saving_reason": "Die Vorgangsänderung wird noch gespeichert.",
"i18n:govoplan-cases.update_reason": "Ihr Konto darf diesen Vorgang einsehen, aber Titel und Status nicht ändern.",
"i18n:govoplan-cases.close_reason": "Zum Schließen eines Vorgangs ist die Berechtigung zum Vorgangsabschluss erforderlich.",
"i18n:govoplan-cases.share_reason": "Zum Ändern von Sichtbarkeit oder Freigaben ist die Freigabeberechtigung erforderlich.",
"i18n:govoplan-cases.no_changes_reason": "Es gibt keine Vorgangsänderungen zu speichern.",
"i18n:govoplan-cases.incomplete_reason": "Geben Sie einen Titel und eine Begründung für die protokollierte Änderung ein.",
"i18n:govoplan-cases.target_required_reason": "Wählen Sie eine Person oder Gruppe aus, bevor Sie eine Zugriffsfreigabe hinzufügen.",
"i18n:govoplan-cases.unsaved_title": "Ungespeicherte Vorgangsänderung",
"i18n:govoplan-cases.unsaved_message": "Speichern oder verwerfen Sie Titel, Status und Begründung, bevor Sie fortfahren.",
"i18n:govoplan-cases.unsaved_access_title": "Ungespeicherte Zugriffsänderung",
"i18n:govoplan-cases.unsaved_access_message": "Speichern oder verwerfen Sie Sichtbarkeit und Freigaben, bevor Sie den Dialog verlassen.",
"i18n:govoplan-cases.access_confirm_title": "Vorgangszugriff ändern",
"i18n:govoplan-cases.access_confirm_message": "Sichtbarkeit von {value0} auf {value1} mit {value2} ausdrücklichen Freigabe(n) ändern? Dadurch werden eine unveränderliche Vorgangsrevision und ein Zeitachseneintrag angelegt.",
"i18n:govoplan-cases.case_access_title": "Vorgangszugriff - {value0}",
"i18n:govoplan-cases.case_count": "{value0} Vorgänge",
"i18n:govoplan-cases.visibility_tenant": "mandantenweit",
"i18n:govoplan-cases.visibility_restricted": "eingeschränkt",
"Case editing is read-only": "Der Vorgang kann nur gelesen werden",
"Ask a case manager to make the required lifecycle change.": "Bitten Sie eine Vorgangsverwaltung, die erforderliche Statusänderung vorzunehmen.",
"A user with the Cases update permission": "Eine Person mit der Berechtigung zur Vorgangsänderung",
"Case role or object access assignment": "Vorgangsrolle oder Objektfreigabe",
"Terminal case states are unavailable": "Abschließende Vorgangsstatus sind nicht verfügbar",
"Ask a case closer to complete the lifecycle transition.": "Bitten Sie eine berechtigte Person, den Vorgangsabschluss vorzunehmen.",
"A user with the Cases close permission": "Eine Person mit der Berechtigung zum Vorgangsabschluss",
"Case role assignment": "Vorgangsrollenzuweisung",
"Cases": "Vorgänge",
"Loading cases": "Vorgänge werden geladen",
"Loading case": "Vorgang wird geladen",
"User": "Person",
"Group": "Gruppe",
"Read": "Lesen",
"Update": "Ändern",
"Share": "Freigeben",
"Administer": "Verwalten",
"Cancel": "Abbrechen",
"Add access grant": "Zugriffsfreigabe hinzufügen",
"Explicit access grants": "Ausdrückliche Zugriffsfreigaben",
"Why is case access changing?": "Warum wird der Vorgangszugriff geändert?",
"Search cases": "Vorgänge suchen",
"Search": "Suchen",
"Status": "Status",
"All statuses": "Alle Status",
"No matching cases.": "Keine passenden Vorgänge.",
"Manage case access": "Vorgangszugriff verwalten",
"Title": "Titel",
"Change reason": "Änderungsbegründung",
"Save": "Speichern",
"Saving": "Speichert",
"Opened": "Eröffnet",
"Deadline": "Frist",
"Revision": "Revision",
"Last change": "Letzte Änderung",
"Parties": "Beteiligte",
"Assignments": "Zuweisungen",
"Decisions": "Entscheidungen",
"Records": "Akten",
"Timeline": "Zeitachse",
"History": "Historie",
"Case visibility": "Vorgangssichtbarkeit",
"Tenant": "Mandant",
"Restricted": "Eingeschränkt",
"Target type": "Zieltyp",
"Target": "Ziel",
"Permission": "Berechtigung",
"Save access": "Zugriff speichern",
"No explicit access grants.": "Keine ausdrücklichen Zugriffsfreigaben."
};
export const generatedTranslations: PlatformTranslations = { en, de };
+14 -5
View File
@@ -1,5 +1,6 @@
import { createElement, lazy } from "react";
import type { PlatformWebModule } from "@govoplan/core-webui";
import { generatedTranslations } from "./i18n/generatedTranslations";
import "./styles/cases.css";
@@ -8,7 +9,7 @@ const CaseDetailPage = lazy(() => import("./features/cases/CaseDetailPage"));
export const casesModule: PlatformWebModule = {
id: "cases",
label: "Cases",
label: "i18n:govoplan-cases.cases",
version: "0.1.8",
optionalDependencies: [
"access",
@@ -20,6 +21,7 @@ export const casesModule: PlatformWebModule = {
"forms_runtime",
"workflow_engine"
],
translations: generatedTranslations,
routes: [
{
path: "/cases",
@@ -39,7 +41,7 @@ export const casesModule: PlatformWebModule = {
navItems: [
{
to: "/cases",
label: "Cases",
label: "i18n:govoplan-cases.cases",
iconName: "briefcase-business",
anyOf: ["cases:case:read"],
order: 35,
@@ -47,9 +49,16 @@ export const casesModule: PlatformWebModule = {
}
],
viewSurfaces: [
{ id: "cases.navigation", moduleId: "cases", kind: "navigation", label: "Cases navigation", order: 10 },
{ id: "cases.list", moduleId: "cases", kind: "route", label: "Case list", order: 20 },
{ id: "cases.detail", moduleId: "cases", kind: "route", label: "Case details", order: 30 }
{ id: "cases.navigation", moduleId: "cases", kind: "navigation", label: "i18n:govoplan-cases.navigation", order: 10 },
{ id: "cases.list", moduleId: "cases", kind: "route", label: "i18n:govoplan-cases.list", order: 20 },
{ id: "cases.list.filters", moduleId: "cases", kind: "section", label: "i18n:govoplan-cases.filters", parentId: "cases.list", order: 10 },
{ id: "cases.detail", moduleId: "cases", kind: "route", label: "i18n:govoplan-cases.detail", order: 30 },
{ id: "cases.detail.summary", moduleId: "cases", kind: "section", label: "i18n:govoplan-cases.summary", parentId: "cases.detail", order: 10 },
{ id: "cases.detail.editor", moduleId: "cases", kind: "section", label: "i18n:govoplan-cases.editor", parentId: "cases.detail", order: 20 },
{ id: "cases.detail.references", moduleId: "cases", kind: "section", label: "i18n:govoplan-cases.references", parentId: "cases.detail", order: 30 },
{ id: "cases.detail.timeline", moduleId: "cases", kind: "section", label: "i18n:govoplan-cases.timeline", parentId: "cases.detail", order: 40 },
{ id: "cases.detail.history", moduleId: "cases", kind: "section", label: "i18n:govoplan-cases.history", parentId: "cases.detail", order: 50 },
{ id: "cases.detail.access", moduleId: "cases", kind: "action", label: "i18n:govoplan-cases.access", parentId: "cases.detail", order: 60 }
]
};
+4
View File
@@ -143,6 +143,10 @@
letter-spacing: 0;
}
.case-detail-main > .action-blocker-hint {
margin-top: 16px;
}
.case-detail-eyebrow {
color: var(--text-soft);
font-size: 0.8rem;