feat: implement committee decision workspace

This commit is contained in:
2026-08-01 17:48:25 +02:00
parent 232329ac52
commit 758b59ca03
28 changed files with 4909 additions and 32 deletions
@@ -0,0 +1,100 @@
import { useEffect, useState } from "react";
import {
Button,
Dialog,
DismissibleAlert,
FormField,
type ApiSettings
} from "@govoplan/core-webui";
import {
finalizeProviderBallot,
type CommitteeRecord
} from "../../api/committee";
export default function CommitteeBallotDialog({
settings,
record,
open,
onClose,
onSaved
}: {
settings: ApiSettings;
record: CommitteeRecord;
open: boolean;
onClose: () => void;
onSaved: (record: CommitteeRecord) => void;
}) {
const [providerBallotRef, setProviderBallotRef] = useState("");
const [approvalId, setApprovalId] = useState("");
const [changeReason, setChangeReason] = useState("Imported verified ballot aggregate.");
const [busy, setBusy] = useState(false);
const [error, setError] = useState("");
useEffect(() => {
if (!open) return;
setProviderBallotRef("");
setApprovalId("");
setChangeReason("Imported verified ballot aggregate.");
setError("");
}, [open, record.object_id]);
async function finalize() {
setBusy(true);
setError("");
try {
const saved = await finalizeProviderBallot(settings, record, {
providerBallotRef,
approvalId,
changeReason
});
onSaved(saved);
onClose();
} catch (reason) {
setError(reason instanceof Error ? reason.message : "Ballot result could not be imported.");
} finally {
setBusy(false);
}
}
const providerId = String(record.attributes.provider_id ?? "");
return (
<Dialog
open={open}
title="Finalize provider ballot"
onClose={onClose}
closeDisabled={busy}
portal
className="committee-ballot-dialog"
footer={
<>
<Button disabled={busy} onClick={onClose}>Cancel</Button>
<Button
variant="primary"
disabled={busy || !providerBallotRef.trim() || !approvalId.trim() || !changeReason.trim()}
onClick={() => void finalize()}
>
{busy ? "Importing" : "Finalize"}
</Button>
</>
}
>
<div className="committee-record-form">
{error ? <DismissibleAlert tone="danger" resetKey={error}>{error}</DismissibleAlert> : null}
<p className="committee-dialog-note">
Provider <strong>{providerId}</strong> returns only the verified aggregate result,
receipt hash and evidence. Individual secret ballots are not stored in GovOPlaN.
</p>
<FormField label="Provider ballot reference">
<input value={providerBallotRef} disabled={busy} onChange={(event) => setProviderBallotRef(event.target.value)} />
</FormField>
<FormField label="Approval ID">
<input value={approvalId} disabled={busy} onChange={(event) => setApprovalId(event.target.value)} />
</FormField>
<FormField label="Change reason">
<input value={changeReason} maxLength={1000} disabled={busy} onChange={(event) => setChangeReason(event.target.value)} />
</FormField>
</div>
</Dialog>
);
}