Integrate formal decisions with cases
This commit is contained in:
@@ -96,6 +96,33 @@ export type CaseTimelineEntry = {
|
||||
payload: Record<string, unknown>;
|
||||
};
|
||||
|
||||
export type FormalDecision = {
|
||||
reference: InstitutionalReference;
|
||||
temporal: {
|
||||
revision: string;
|
||||
valid_from?: string | null;
|
||||
valid_to?: string | null;
|
||||
recorded_at?: string | null;
|
||||
change_reason?: string | null;
|
||||
};
|
||||
decision_type: string;
|
||||
state: string;
|
||||
assurance_level: string;
|
||||
operative_result?: string | null;
|
||||
reasoning?: string | null;
|
||||
conditions: string[];
|
||||
authority_context: Record<string, unknown>;
|
||||
delivery_refs: string[];
|
||||
remedy_refs: string[];
|
||||
review_refs: string[];
|
||||
};
|
||||
|
||||
export type CaseDecisionResult = {
|
||||
case: CaseRecord;
|
||||
decision: FormalDecision;
|
||||
replayed: boolean;
|
||||
};
|
||||
|
||||
export function listCases(
|
||||
settings: ApiSettings,
|
||||
options: {
|
||||
@@ -147,6 +174,42 @@ export function caseTimeline(
|
||||
return apiFetch(settings, `/api/v1/cases/${encodeURIComponent(caseId)}/timeline`, { signal });
|
||||
}
|
||||
|
||||
export function listCaseDecisions(
|
||||
settings: ApiSettings,
|
||||
caseId: string,
|
||||
signal?: AbortSignal
|
||||
): Promise<{ decisions: FormalDecision[] }> {
|
||||
return apiFetch(
|
||||
settings,
|
||||
`/api/v1/cases/${encodeURIComponent(caseId)}/decisions`,
|
||||
{ signal }
|
||||
);
|
||||
}
|
||||
|
||||
export function recordCaseDecision(
|
||||
settings: ApiSettings,
|
||||
caseId: string,
|
||||
payload: {
|
||||
expected_case_revision: number;
|
||||
effective_at: string;
|
||||
decision_type: string;
|
||||
operative_result: string;
|
||||
reasoning: string;
|
||||
conditions: string[];
|
||||
change_reason: string;
|
||||
idempotency_key: string;
|
||||
}
|
||||
): Promise<CaseDecisionResult> {
|
||||
return apiFetch(
|
||||
settings,
|
||||
`/api/v1/cases/${encodeURIComponent(caseId)}/decisions`,
|
||||
{
|
||||
method: "POST",
|
||||
body: JSON.stringify(payload)
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
export function updateCase(
|
||||
settings: ApiSettings,
|
||||
caseId: string,
|
||||
|
||||
@@ -0,0 +1,209 @@
|
||||
import { Scale } from "lucide-react";
|
||||
import { useEffect, useMemo, useRef, useState } from "react";
|
||||
import {
|
||||
Button,
|
||||
ConfirmDialog,
|
||||
Dialog,
|
||||
DismissibleAlert,
|
||||
DocumentationHelpLink,
|
||||
FormField,
|
||||
i18nMessage,
|
||||
type ApiSettings
|
||||
} from "@govoplan/core-webui";
|
||||
import {
|
||||
recordCaseDecision,
|
||||
type CaseDecisionResult,
|
||||
type CaseRecord
|
||||
} from "../../api/cases";
|
||||
import { CASES_FIELDS_DOCUMENTATION } from "./interfacePatterns";
|
||||
|
||||
|
||||
export default function CaseDecisionDialog({
|
||||
settings,
|
||||
record,
|
||||
open,
|
||||
onClose,
|
||||
onSaved
|
||||
}: {
|
||||
settings: ApiSettings;
|
||||
record: CaseRecord;
|
||||
open: boolean;
|
||||
onClose: () => void;
|
||||
onSaved: (result: CaseDecisionResult) => void;
|
||||
}) {
|
||||
const [decisionType, setDecisionType] = useState("");
|
||||
const [effectiveAt, setEffectiveAt] = useState("");
|
||||
const [operativeResult, setOperativeResult] = useState("");
|
||||
const [reasoning, setReasoning] = useState("");
|
||||
const [conditions, setConditions] = useState("");
|
||||
const [changeReason, setChangeReason] = useState("");
|
||||
const [busy, setBusy] = useState(false);
|
||||
const [error, setError] = useState("");
|
||||
const [confirmOpen, setConfirmOpen] = useState(false);
|
||||
const idempotencyKey = useRef(crypto.randomUUID());
|
||||
|
||||
useEffect(() => {
|
||||
if (!open) return;
|
||||
setDecisionType(defaultDecisionType(record));
|
||||
setEffectiveAt(dateTimeLocalValue(new Date()));
|
||||
setOperativeResult("");
|
||||
setReasoning("");
|
||||
setConditions("");
|
||||
setChangeReason("");
|
||||
setError("");
|
||||
setConfirmOpen(false);
|
||||
idempotencyKey.current = crypto.randomUUID();
|
||||
}, [open, record]);
|
||||
|
||||
const disabledReason = useMemo(() => {
|
||||
if (busy) return "The formal Decision is being recorded.";
|
||||
if (
|
||||
!decisionType.trim()
|
||||
|| !effectiveAt
|
||||
|| !operativeResult.trim()
|
||||
|| !reasoning.trim()
|
||||
|| !changeReason.trim()
|
||||
) {
|
||||
return "Complete the Decision type, effective time, result, reasoning, and change reason.";
|
||||
}
|
||||
return undefined;
|
||||
}, [busy, changeReason, decisionType, effectiveAt, operativeResult, reasoning]);
|
||||
|
||||
async function save() {
|
||||
if (disabledReason) return;
|
||||
setBusy(true);
|
||||
setError("");
|
||||
try {
|
||||
const result = await recordCaseDecision(settings, record.reference.object_id, {
|
||||
expected_case_revision: record.revision,
|
||||
effective_at: new Date(effectiveAt).toISOString(),
|
||||
decision_type: decisionType.trim(),
|
||||
operative_result: operativeResult.trim(),
|
||||
reasoning: reasoning.trim(),
|
||||
conditions: conditions.split("\n").map((item) => item.trim()).filter(Boolean),
|
||||
change_reason: changeReason.trim(),
|
||||
idempotency_key: idempotencyKey.current
|
||||
});
|
||||
setConfirmOpen(false);
|
||||
onSaved(result);
|
||||
onClose();
|
||||
} catch (reason) {
|
||||
setConfirmOpen(false);
|
||||
setError(reason instanceof Error ? reason.message : "The formal Decision could not be recorded.");
|
||||
} finally {
|
||||
setBusy(false);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Dialog
|
||||
open={open}
|
||||
title={i18nMessage("i18n:govoplan-cases.decision_title", { value0: record.case_number })}
|
||||
onClose={onClose}
|
||||
closeDisabled={busy}
|
||||
portal
|
||||
className="case-decision-dialog"
|
||||
footer={
|
||||
<>
|
||||
<Button disabled={busy} onClick={onClose}>Cancel</Button>
|
||||
<Button
|
||||
variant="primary"
|
||||
disabledReason={disabledReason}
|
||||
onClick={() => setConfirmOpen(true)}
|
||||
helpContextId="cases.action.decide"
|
||||
>
|
||||
<Scale size={16} aria-hidden="true" />
|
||||
Record decision
|
||||
</Button>
|
||||
</>
|
||||
}
|
||||
>
|
||||
<div className="case-decision-content" data-help-context-id="cases.decision.editor">
|
||||
<DocumentationHelpLink reference={CASES_FIELDS_DOCUMENTATION} />
|
||||
{error ? <DismissibleAlert tone="danger" resetKey={error}>{error}</DismissibleAlert> : null}
|
||||
<p className="case-decision-explanation">
|
||||
The server verifies your current acting assignment, the effective Mandate, the exact Case revision, evidence, and legal basis before recording the outcome.
|
||||
</p>
|
||||
<div className="case-decision-grid">
|
||||
<FormField label="Decision type" documentation={CASES_FIELDS_DOCUMENTATION}>
|
||||
<input
|
||||
value={decisionType}
|
||||
disabled={busy}
|
||||
maxLength={120}
|
||||
onChange={(event) => setDecisionType(event.target.value)}
|
||||
/>
|
||||
</FormField>
|
||||
<FormField label="Effective at" documentation={CASES_FIELDS_DOCUMENTATION}>
|
||||
<input
|
||||
type="datetime-local"
|
||||
value={effectiveAt}
|
||||
disabled={busy}
|
||||
onChange={(event) => setEffectiveAt(event.target.value)}
|
||||
/>
|
||||
</FormField>
|
||||
</div>
|
||||
<FormField label="Operative result" documentation={CASES_FIELDS_DOCUMENTATION}>
|
||||
<textarea
|
||||
rows={4}
|
||||
value={operativeResult}
|
||||
disabled={busy}
|
||||
maxLength={20_000}
|
||||
onChange={(event) => setOperativeResult(event.target.value)}
|
||||
/>
|
||||
</FormField>
|
||||
<FormField label="Reasoning" documentation={CASES_FIELDS_DOCUMENTATION}>
|
||||
<textarea
|
||||
rows={7}
|
||||
value={reasoning}
|
||||
disabled={busy}
|
||||
maxLength={50_000}
|
||||
onChange={(event) => setReasoning(event.target.value)}
|
||||
/>
|
||||
</FormField>
|
||||
<FormField label="Conditions (one per line)" documentation={CASES_FIELDS_DOCUMENTATION}>
|
||||
<textarea
|
||||
rows={3}
|
||||
value={conditions}
|
||||
disabled={busy}
|
||||
onChange={(event) => setConditions(event.target.value)}
|
||||
/>
|
||||
</FormField>
|
||||
<FormField label="Change reason" documentation={CASES_FIELDS_DOCUMENTATION}>
|
||||
<input
|
||||
value={changeReason}
|
||||
disabled={busy}
|
||||
maxLength={1_000}
|
||||
onChange={(event) => setChangeReason(event.target.value)}
|
||||
/>
|
||||
</FormField>
|
||||
</div>
|
||||
</Dialog>
|
||||
<ConfirmDialog
|
||||
open={confirmOpen}
|
||||
title="Record formal Decision"
|
||||
message="Record this formal outcome against the exact Case revision? The protected result and reasoning become an immutable Decisions revision."
|
||||
confirmLabel="Record decision"
|
||||
busy={busy}
|
||||
onCancel={() => setConfirmOpen(false)}
|
||||
onConfirm={() => void save()}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
function defaultDecisionType(record: CaseRecord): string {
|
||||
const resultRefs = Array.isArray(record.metadata.result_refs)
|
||||
? record.metadata.result_refs
|
||||
: [];
|
||||
const result = resultRefs.find((item): item is string => typeof item === "string");
|
||||
if (result) return result.replace(/^decision:/, "");
|
||||
return record.case_type_key.replace(/-application$/, "");
|
||||
}
|
||||
|
||||
|
||||
function dateTimeLocalValue(value: Date): string {
|
||||
const local = new Date(value.getTime() - value.getTimezoneOffset() * 60_000);
|
||||
return local.toISOString().slice(0, 16);
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
import { ArrowLeft, Save, Share2 } from "lucide-react";
|
||||
import { Archive, ArrowLeft, Save, Scale, Share2 } from "lucide-react";
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||
import { useParams } from "react-router";
|
||||
import {
|
||||
@@ -12,6 +12,7 @@ import {
|
||||
PageScrollViewport,
|
||||
StatusBadge,
|
||||
hasScope,
|
||||
usePlatformModuleInstalled,
|
||||
useGuardedNavigate,
|
||||
useUnsavedDraftGuard,
|
||||
type PlatformRouteContext
|
||||
@@ -20,14 +21,17 @@ import {
|
||||
caseHistory,
|
||||
caseTimeline,
|
||||
getCase,
|
||||
listCaseDecisions,
|
||||
listCaseCatalog,
|
||||
updateCase,
|
||||
type CaseCatalog,
|
||||
type CaseRecord,
|
||||
type CaseTimelineEntry,
|
||||
type FormalDecision,
|
||||
type InstitutionalReference
|
||||
} from "../../api/cases";
|
||||
import CaseShareDialog from "./CaseShareDialog";
|
||||
import CaseDecisionDialog from "./CaseDecisionDialog";
|
||||
import {
|
||||
CASES_DOCUMENTATION,
|
||||
CASES_FIELDS_DOCUMENTATION,
|
||||
@@ -42,6 +46,7 @@ export default function CaseDetailPage({ settings, auth }: PlatformRouteContext)
|
||||
const [catalog, setCatalog] = useState<CaseCatalog>({ statuses: [], types: [] });
|
||||
const [history, setHistory] = useState<CaseRecord[]>([]);
|
||||
const [timeline, setTimeline] = useState<CaseTimelineEntry[]>([]);
|
||||
const [decisions, setDecisions] = useState<FormalDecision[]>([]);
|
||||
const [title, setTitle] = useState("");
|
||||
const [status, setStatus] = useState("");
|
||||
const [changeReason, setChangeReason] = useState("");
|
||||
@@ -49,10 +54,26 @@ export default function CaseDetailPage({ settings, auth }: PlatformRouteContext)
|
||||
const [saving, setSaving] = useState(false);
|
||||
const [error, setError] = useState("");
|
||||
const [shareOpen, setShareOpen] = useState(false);
|
||||
const [decisionOpen, setDecisionOpen] = 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");
|
||||
const decisionsAvailable = usePlatformModuleInstalled("decisions");
|
||||
const mandatesAvailable = usePlatformModuleInstalled("mandates");
|
||||
const accessAvailable = usePlatformModuleInstalled("access");
|
||||
const recordsAvailable = usePlatformModuleInstalled("records");
|
||||
const canReadDecisions = decisionsAvailable && hasScope(auth, "decisions:decision:read");
|
||||
const decisionDisabledReason = !canUpdate
|
||||
? "Your account may not update this Case."
|
||||
: !hasScope(auth, "decisions:decision:write")
|
||||
? "Your account may not record formal Decisions."
|
||||
: !accessAvailable
|
||||
? "Enable Access to verify the acting assignment."
|
||||
: !mandatesAvailable
|
||||
? "Enable Mandates to verify formal authority."
|
||||
: undefined;
|
||||
const canFileRecords = recordsAvailable && hasScope(auth, "records:workspace:write");
|
||||
|
||||
const load = useCallback((signal?: AbortSignal) => {
|
||||
setLoading(true);
|
||||
@@ -61,19 +82,23 @@ export default function CaseDetailPage({ settings, auth }: PlatformRouteContext)
|
||||
getCase(settings, caseId, signal),
|
||||
listCaseCatalog(settings, signal),
|
||||
caseHistory(settings, caseId, signal),
|
||||
caseTimeline(settings, caseId, signal)
|
||||
caseTimeline(settings, caseId, signal),
|
||||
canReadDecisions
|
||||
? listCaseDecisions(settings, caseId, signal)
|
||||
: Promise.resolve({ decisions: [] as FormalDecision[] })
|
||||
]).
|
||||
then(([nextRecord, nextCatalog, nextHistory, nextTimeline]) => {
|
||||
then(([nextRecord, nextCatalog, nextHistory, nextTimeline, nextDecisions]) => {
|
||||
setRecord(nextRecord);
|
||||
setCatalog(nextCatalog);
|
||||
setHistory(nextHistory.revisions);
|
||||
setTimeline(nextTimeline.entries);
|
||||
setDecisions(nextDecisions.decisions);
|
||||
setTitle(nextRecord.title);
|
||||
setStatus(nextRecord.status_key);
|
||||
setChangeReason("");
|
||||
}).
|
||||
finally(() => setLoading(false));
|
||||
}, [caseId, settings]);
|
||||
}, [canReadDecisions, caseId, settings]);
|
||||
|
||||
useEffect(() => {
|
||||
const controller = new AbortController();
|
||||
@@ -162,13 +187,33 @@ export default function CaseDetailPage({ settings, auth }: PlatformRouteContext)
|
||||
Cases
|
||||
</button>
|
||||
{record && <span>{record.case_number}</span>}
|
||||
{record ? <IconButton
|
||||
label="Manage case access"
|
||||
icon={<Share2 size={16} />}
|
||||
className="case-share-button"
|
||||
disabledReason={!canShare ? CASES_I18N.shareReason : undefined}
|
||||
onClick={() => setShareOpen(true)}
|
||||
/> : null}
|
||||
{record ? (
|
||||
<div className="case-detail-actions">
|
||||
{decisionsAvailable ? (
|
||||
<IconButton
|
||||
label="Record formal decision"
|
||||
icon={<Scale size={16} />}
|
||||
disabledReason={decisionDisabledReason}
|
||||
onClick={() => setDecisionOpen(true)}
|
||||
helpContextId="cases.action.decide"
|
||||
/>
|
||||
) : null}
|
||||
{canFileRecords ? (
|
||||
<IconButton
|
||||
label="File Case in eAkte"
|
||||
icon={<Archive size={16} />}
|
||||
onClick={() => navigate(recordFilingPath(record.reference, record.case_number, "cases", "case_revision"))}
|
||||
helpContextId="records.action.file"
|
||||
/>
|
||||
) : null}
|
||||
<IconButton
|
||||
label="Manage case access"
|
||||
icon={<Share2 size={16} />}
|
||||
disabledReason={!canShare ? CASES_I18N.shareReason : undefined}
|
||||
onClick={() => setShareOpen(true)}
|
||||
/>
|
||||
</div>
|
||||
) : null}
|
||||
<DocumentationHelpLink reference={CASES_DOCUMENTATION} />
|
||||
</div>
|
||||
<PageScrollViewport className="case-detail-viewport">
|
||||
@@ -250,7 +295,20 @@ export default function CaseDetailPage({ settings, auth }: PlatformRouteContext)
|
||||
|
||||
<ReferenceSection title="Parties" references={record.party_refs} />
|
||||
<ReferenceSection title="Assignments" references={record.assignment_refs} />
|
||||
<ReferenceSection title="Decisions" references={record.decision_refs} />
|
||||
{canReadDecisions ? (
|
||||
<DecisionSection
|
||||
decisions={decisions}
|
||||
canFile={canFileRecords}
|
||||
onFile={(decision) => navigate(recordFilingPath(
|
||||
decision.reference,
|
||||
`${humanize(decision.decision_type)} Decision`,
|
||||
"decisions",
|
||||
"decision_revision"
|
||||
))}
|
||||
/>
|
||||
) : (
|
||||
<ReferenceSection title="Decisions" references={record.decision_refs} />
|
||||
)}
|
||||
<ReferenceSection title="Records" references={record.record_refs} />
|
||||
</section>
|
||||
|
||||
@@ -298,6 +356,24 @@ export default function CaseDetailPage({ settings, auth }: PlatformRouteContext)
|
||||
}}
|
||||
/>
|
||||
) : null}
|
||||
{record ? (
|
||||
<CaseDecisionDialog
|
||||
settings={settings}
|
||||
record={record}
|
||||
open={decisionOpen}
|
||||
onClose={() => setDecisionOpen(false)}
|
||||
onSaved={(result) => {
|
||||
setRecord(result.case);
|
||||
setDecisions((current) => [
|
||||
result.decision,
|
||||
...current.filter((item) => item.reference.object_id !== result.decision.reference.object_id)
|
||||
]);
|
||||
void load().catch((reason) => {
|
||||
setError(reason instanceof Error ? reason.message : "Case could not be reloaded.");
|
||||
});
|
||||
}}
|
||||
/>
|
||||
) : null}
|
||||
</main>
|
||||
);
|
||||
}
|
||||
@@ -323,6 +399,68 @@ function ReferenceSection({ title, references }: { title: string; references: In
|
||||
);
|
||||
}
|
||||
|
||||
function DecisionSection({
|
||||
decisions,
|
||||
canFile,
|
||||
onFile
|
||||
}: {
|
||||
decisions: FormalDecision[];
|
||||
canFile: boolean;
|
||||
onFile: (decision: FormalDecision) => void;
|
||||
}) {
|
||||
if (decisions.length === 0) return null;
|
||||
return (
|
||||
<section className="case-reference-section">
|
||||
<h2>Decisions</h2>
|
||||
<div className="case-decision-list">
|
||||
{decisions.map((decision) => (
|
||||
<article key={`${decision.reference.object_id}:${decision.reference.version ?? "current"}`}>
|
||||
<div className="case-decision-heading">
|
||||
<div>
|
||||
<strong>{humanize(decision.decision_type)}</strong>
|
||||
<span>Revision {decision.reference.version ?? decision.temporal.revision}</span>
|
||||
</div>
|
||||
<StatusBadge status={decision.state === "effective" || decision.state === "decided" ? "active" : "inactive"} label={humanize(decision.state)} />
|
||||
{canFile ? (
|
||||
<IconButton
|
||||
label="File Decision in eAkte"
|
||||
icon={<Archive size={16} />}
|
||||
onClick={() => onFile(decision)}
|
||||
helpContextId="records.action.file"
|
||||
/>
|
||||
) : null}
|
||||
</div>
|
||||
{decision.operative_result ? <p><strong>Operative result</strong>{decision.operative_result}</p> : null}
|
||||
{decision.reasoning ? <p><strong>Reasoning</strong>{decision.reasoning}</p> : null}
|
||||
{!decision.operative_result && !decision.reasoning ? (
|
||||
<p className="case-decision-protected">Protected Decision details require the sensitive-read permission.</p>
|
||||
) : null}
|
||||
{decision.conditions.length > 0 ? (
|
||||
<ul>{decision.conditions.map((condition) => <li key={condition}>{condition}</li>)}</ul>
|
||||
) : null}
|
||||
</article>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
function recordFilingPath(
|
||||
reference: InstitutionalReference,
|
||||
label: string,
|
||||
sourceModule: string,
|
||||
resourceType: string
|
||||
): string {
|
||||
const query = new URLSearchParams({
|
||||
sourceModule,
|
||||
resourceType,
|
||||
resourceId: reference.object_id,
|
||||
sourceRevision: reference.version ?? "",
|
||||
sourceLabel: label
|
||||
});
|
||||
return `/records?${query.toString()}`;
|
||||
}
|
||||
|
||||
function formatDateTime(value?: string | null): string {
|
||||
return value ? new Intl.DateTimeFormat(undefined, { dateStyle: "medium", timeStyle: "short" }).format(new Date(value)) : "-";
|
||||
}
|
||||
|
||||
@@ -12,6 +12,8 @@ const en = {
|
||||
"i18n:govoplan-cases.timeline": "Case timeline",
|
||||
"i18n:govoplan-cases.history": "Immutable case history",
|
||||
"i18n:govoplan-cases.access": "Case access",
|
||||
"i18n:govoplan-cases.decision": "Record formal Decision",
|
||||
"i18n:govoplan-cases.decision_title": "Record formal Decision - {value0}",
|
||||
"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.",
|
||||
@@ -78,7 +80,27 @@ const en = {
|
||||
"Target": "Target",
|
||||
"Permission": "Permission",
|
||||
"Save access": "Save access",
|
||||
"No explicit access grants.": "No explicit access grants."
|
||||
"No explicit access grants.": "No explicit access grants.",
|
||||
"Record formal decision": "Record formal decision",
|
||||
"File Case in eAkte": "File Case in eAkte",
|
||||
"File Decision in eAkte": "File Decision in eAkte",
|
||||
"Record decision": "Record decision",
|
||||
"The formal Decision is being recorded.": "The formal Decision is being recorded.",
|
||||
"Complete the Decision type, effective time, result, reasoning, and change reason.": "Complete the Decision type, effective time, result, reasoning, and change reason.",
|
||||
"The formal Decision could not be recorded.": "The formal Decision could not be recorded.",
|
||||
"The server verifies your current acting assignment, the effective Mandate, the exact Case revision, evidence, and legal basis before recording the outcome.": "The server verifies your current acting assignment, the effective Mandate, the exact Case revision, evidence, and legal basis before recording the outcome.",
|
||||
"Decision type": "Decision type",
|
||||
"Effective at": "Effective at",
|
||||
"Operative result": "Operative result",
|
||||
"Reasoning": "Reasoning",
|
||||
"Conditions (one per line)": "Conditions (one per line)",
|
||||
"Record formal Decision": "Record formal Decision",
|
||||
"Record this formal outcome against the exact Case revision? The protected result and reasoning become an immutable Decisions revision.": "Record this formal outcome against the exact Case revision? The protected result and reasoning become an immutable Decisions revision.",
|
||||
"Protected Decision details require the sensitive-read permission.": "Protected Decision details require the sensitive-read permission.",
|
||||
"Your account may not update this Case.": "Your account may not update this Case.",
|
||||
"Your account may not record formal Decisions.": "Your account may not record formal Decisions.",
|
||||
"Enable Access to verify the acting assignment.": "Enable Access to verify the acting assignment.",
|
||||
"Enable Mandates to verify formal authority.": "Enable Mandates to verify formal authority."
|
||||
} as const;
|
||||
|
||||
const de: Record<keyof typeof en, string> = {
|
||||
@@ -93,6 +115,8 @@ const de: Record<keyof typeof en, string> = {
|
||||
"i18n:govoplan-cases.timeline": "Vorgangszeitachse",
|
||||
"i18n:govoplan-cases.history": "Unveränderliche Vorgangshistorie",
|
||||
"i18n:govoplan-cases.access": "Vorgangszugriff",
|
||||
"i18n:govoplan-cases.decision": "Formelle Entscheidung erfassen",
|
||||
"i18n:govoplan-cases.decision_title": "Formelle Entscheidung erfassen - {value0}",
|
||||
"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.",
|
||||
@@ -159,7 +183,27 @@ const de: Record<keyof typeof en, string> = {
|
||||
"Target": "Ziel",
|
||||
"Permission": "Berechtigung",
|
||||
"Save access": "Zugriff speichern",
|
||||
"No explicit access grants.": "Keine ausdrücklichen Zugriffsfreigaben."
|
||||
"No explicit access grants.": "Keine ausdrücklichen Zugriffsfreigaben.",
|
||||
"Record formal decision": "Formelle Entscheidung erfassen",
|
||||
"File Case in eAkte": "Vorgang in eAkte verakten",
|
||||
"File Decision in eAkte": "Entscheidung in eAkte verakten",
|
||||
"Record decision": "Entscheidung erfassen",
|
||||
"The formal Decision is being recorded.": "Die formelle Entscheidung wird erfasst.",
|
||||
"Complete the Decision type, effective time, result, reasoning, and change reason.": "Vervollständigen Sie Entscheidungsart, Gültigkeitszeitpunkt, Entscheidungssatz, Begründung und Änderungsbegründung.",
|
||||
"The formal Decision could not be recorded.": "Die formelle Entscheidung konnte nicht erfasst werden.",
|
||||
"The server verifies your current acting assignment, the effective Mandate, the exact Case revision, evidence, and legal basis before recording the outcome.": "Der Server prüft vor der Erfassung den aktuellen Funktionskontext, das wirksame Mandat, die exakte Vorgangsrevision, die Nachweise und die Rechtsgrundlage.",
|
||||
"Decision type": "Entscheidungsart",
|
||||
"Effective at": "Gültig ab",
|
||||
"Operative result": "Entscheidungssatz",
|
||||
"Reasoning": "Begründung",
|
||||
"Conditions (one per line)": "Nebenbestimmungen (eine pro Zeile)",
|
||||
"Record formal Decision": "Formelle Entscheidung erfassen",
|
||||
"Record this formal outcome against the exact Case revision? The protected result and reasoning become an immutable Decisions revision.": "Diese formelle Entscheidung zur exakten Vorgangsrevision erfassen? Entscheidungssatz und Begründung werden als unveränderliche Entscheidungsrevision gespeichert.",
|
||||
"Protected Decision details require the sensitive-read permission.": "Geschützte Entscheidungsinhalte erfordern die Berechtigung zum Lesen sensibler Entscheidungen.",
|
||||
"Your account may not update this Case.": "Ihr Konto darf diesen Vorgang nicht ändern.",
|
||||
"Your account may not record formal Decisions.": "Ihr Konto darf keine formellen Entscheidungen erfassen.",
|
||||
"Enable Access to verify the acting assignment.": "Aktivieren Sie Access, damit der Funktionskontext geprüft werden kann.",
|
||||
"Enable Mandates to verify formal authority.": "Aktivieren Sie Mandates, damit die formelle Zuständigkeit geprüft werden kann."
|
||||
};
|
||||
|
||||
export const generatedTranslations: PlatformTranslations = { en, de };
|
||||
|
||||
+4
-2
@@ -19,7 +19,8 @@ export const casesModule: PlatformWebModule = {
|
||||
"mandates",
|
||||
"decisions",
|
||||
"forms_runtime",
|
||||
"workflow_engine"
|
||||
"workflow_engine",
|
||||
"records"
|
||||
],
|
||||
translations: generatedTranslations,
|
||||
routes: [
|
||||
@@ -58,7 +59,8 @@ export const casesModule: PlatformWebModule = {
|
||||
{ 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 }
|
||||
{ id: "cases.detail.access", moduleId: "cases", kind: "action", label: "i18n:govoplan-cases.access", parentId: "cases.detail", order: 60 },
|
||||
{ id: "cases.detail.decision", moduleId: "cases", kind: "action", label: "i18n:govoplan-cases.decision", parentId: "cases.detail", order: 70 }
|
||||
]
|
||||
};
|
||||
|
||||
|
||||
@@ -52,7 +52,10 @@
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
.case-share-button {
|
||||
.case-detail-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
@@ -265,6 +268,80 @@
|
||||
max-height: min(760px, calc(100vh - 32px));
|
||||
}
|
||||
|
||||
.case-decision-dialog {
|
||||
width: min(900px, calc(100vw - 32px));
|
||||
max-height: min(820px, calc(100vh - 32px));
|
||||
}
|
||||
|
||||
.case-decision-content {
|
||||
display: flex;
|
||||
min-height: 0;
|
||||
flex-direction: column;
|
||||
gap: 14px;
|
||||
}
|
||||
|
||||
.case-decision-explanation,
|
||||
.case-decision-protected {
|
||||
margin: 0;
|
||||
color: var(--text-soft);
|
||||
font-size: 0.84rem;
|
||||
}
|
||||
|
||||
.case-decision-grid {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1fr) minmax(210px, 0.7fr);
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.case-decision-content textarea {
|
||||
resize: vertical;
|
||||
}
|
||||
|
||||
.case-decision-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.case-decision-list > article {
|
||||
padding: 12px;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 6px;
|
||||
background: var(--surface-raised);
|
||||
}
|
||||
|
||||
.case-decision-heading {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.case-decision-heading > div:first-child {
|
||||
display: flex;
|
||||
min-width: 0;
|
||||
flex: 1;
|
||||
flex-direction: column;
|
||||
gap: 2px;
|
||||
}
|
||||
|
||||
.case-decision-heading span {
|
||||
color: var(--text-soft);
|
||||
font-size: 0.78rem;
|
||||
}
|
||||
|
||||
.case-decision-list p {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 3px;
|
||||
margin: 10px 0 0;
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
|
||||
.case-decision-list ul {
|
||||
margin: 10px 0 0;
|
||||
padding-left: 20px;
|
||||
}
|
||||
|
||||
.case-share-content {
|
||||
display: flex;
|
||||
min-height: 0;
|
||||
@@ -352,6 +429,14 @@
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.case-detail-toolbar {
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.case-decision-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.case-share-add-row .icon-button {
|
||||
justify-self: end;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user