Integrate committee ballots with Voting
This commit is contained in:
@@ -113,3 +113,40 @@ export function finalizeProviderBallot(
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
export async function finalizeVotingBallot(
|
||||
settings: ApiSettings,
|
||||
record: CommitteeRecord,
|
||||
input: {
|
||||
approvalId: string;
|
||||
changeReason: string;
|
||||
}
|
||||
): Promise<CommitteeRecord> {
|
||||
const ballotId = String(record.attributes.voting_ballot_id ?? "").trim();
|
||||
const ballot = await apiFetch<{ revision: number }>(
|
||||
settings,
|
||||
`/api/v1/voting/${encodeURIComponent(ballotId)}`
|
||||
);
|
||||
return apiFetch<CommitteeRecord>(
|
||||
settings,
|
||||
`/api/v1/committee/workspace/vote/${encodeURIComponent(record.object_id)}/finalize-voting`,
|
||||
{
|
||||
method: "POST",
|
||||
body: JSON.stringify({
|
||||
voting_ballot_id: ballotId,
|
||||
voting_expected_revision: ballot.revision,
|
||||
approval_ref: {
|
||||
kind: "approval",
|
||||
owner_module: "approvals",
|
||||
object_id: input.approvalId.trim(),
|
||||
tenant_id: record.tenant_id,
|
||||
version: "1"
|
||||
},
|
||||
expected_revision: record.revision,
|
||||
recorded_at: new Date().toISOString(),
|
||||
change_reason: input.changeReason.trim(),
|
||||
idempotency_key: crypto.randomUUID()
|
||||
})
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
} from "@govoplan/core-webui";
|
||||
import {
|
||||
finalizeProviderBallot,
|
||||
finalizeVotingBallot,
|
||||
type CommitteeRecord
|
||||
} from "../../api/committee";
|
||||
|
||||
@@ -43,11 +44,13 @@ export default function CommitteeBallotDialog({
|
||||
setBusy(true);
|
||||
setError("");
|
||||
try {
|
||||
const saved = await finalizeProviderBallot(settings, record, {
|
||||
providerBallotRef,
|
||||
approvalId,
|
||||
changeReason
|
||||
});
|
||||
const saved = votingBallotId
|
||||
? await finalizeVotingBallot(settings, record, { approvalId, changeReason })
|
||||
: await finalizeProviderBallot(settings, record, {
|
||||
providerBallotRef,
|
||||
approvalId,
|
||||
changeReason
|
||||
});
|
||||
onSaved(saved);
|
||||
onClose();
|
||||
} catch (reason) {
|
||||
@@ -58,10 +61,11 @@ export default function CommitteeBallotDialog({
|
||||
}
|
||||
|
||||
const providerId = String(record.attributes.provider_id ?? "");
|
||||
const votingBallotId = String(record.attributes.voting_ballot_id ?? "").trim();
|
||||
return (
|
||||
<Dialog
|
||||
open={open}
|
||||
title="Finalize provider ballot"
|
||||
title={votingBallotId ? "Finalize Voting ballot" : "Finalize provider ballot"}
|
||||
onClose={onClose}
|
||||
closeDisabled={busy}
|
||||
portal
|
||||
@@ -71,7 +75,7 @@ export default function CommitteeBallotDialog({
|
||||
<Button disabled={busy} onClick={onClose}>Cancel</Button>
|
||||
<Button
|
||||
variant="primary"
|
||||
disabled={busy || !providerBallotRef.trim() || !approvalId.trim() || !changeReason.trim()}
|
||||
disabled={busy || (!votingBallotId && !providerBallotRef.trim()) || !approvalId.trim() || !changeReason.trim()}
|
||||
onClick={() => void finalize()}
|
||||
>
|
||||
{busy ? "Importing" : "Finalize"}
|
||||
@@ -82,12 +86,13 @@ export default function CommitteeBallotDialog({
|
||||
<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.
|
||||
{votingBallotId
|
||||
? <>Voting ballot <strong>{votingBallotId}</strong> will be closed and its aggregate result recorded in the Committee minutes.</>
|
||||
: <>Provider <strong>{providerId}</strong> returns only the verified aggregate result, receipt hash and evidence.</>}
|
||||
</p>
|
||||
<FormField label="Provider ballot reference">
|
||||
{!votingBallotId ? <FormField label="Provider ballot reference">
|
||||
<input value={providerBallotRef} disabled={busy} onChange={(event) => setProviderBallotRef(event.target.value)} />
|
||||
</FormField>
|
||||
</FormField> : null}
|
||||
<FormField label="Approval ID">
|
||||
<input value={approvalId} disabled={busy} onChange={(event) => setApprovalId(event.target.value)} />
|
||||
</FormField>
|
||||
|
||||
@@ -377,7 +377,10 @@ function voteSummary(record: CommitteeRecord): string {
|
||||
}
|
||||
|
||||
function isProviderBallotReady(record: CommitteeRecord): boolean {
|
||||
return record.state === "open" && Boolean(String(record.attributes.provider_id ?? "").trim());
|
||||
return record.state === "open" && Boolean(
|
||||
String(record.attributes.voting_ballot_id ?? "").trim()
|
||||
|| String(record.attributes.provider_id ?? "").trim()
|
||||
);
|
||||
}
|
||||
|
||||
function statusTone(state: string): "active" | "inactive" | "warning" {
|
||||
|
||||
@@ -32,6 +32,7 @@ type Draft = {
|
||||
approvalId: string;
|
||||
evidenceId: string;
|
||||
providerId: string;
|
||||
votingBallotId: string;
|
||||
contentRecordId: string;
|
||||
decisionId: string;
|
||||
};
|
||||
@@ -206,6 +207,9 @@ export default function CommitteeRecordDialog({
|
||||
<input value={draft.providerId} disabled={busy} onChange={(event) => setDraft({ ...draft, providerId: event.target.value })} />
|
||||
</FormField>
|
||||
</div>
|
||||
<FormField label="Voting ballot ID (optional)">
|
||||
<input value={draft.votingBallotId} disabled={busy} onChange={(event) => setDraft({ ...draft, votingBallotId: event.target.value })} />
|
||||
</FormField>
|
||||
<FormField label="Choices (comma separated)">
|
||||
<input value={draft.choices} disabled={busy} onChange={(event) => setDraft({ ...draft, choices: event.target.value })} />
|
||||
</FormField>
|
||||
@@ -310,6 +314,7 @@ function draftFromRecord(kind: CommitteeObjectKind, record?: CommitteeRecord | n
|
||||
approvalId: text(approval.object_id),
|
||||
evidenceId: text(firstMapping(record?.evidence).evidence_id),
|
||||
providerId: text(attributes.provider_id),
|
||||
votingBallotId: text(attributes.voting_ballot_id),
|
||||
contentRecordId: text(content.object_id),
|
||||
decisionId: text(decision.object_id)
|
||||
};
|
||||
@@ -373,6 +378,7 @@ function attributesFromDraft(tenantId: string, kind: CommitteeObjectKind, draft:
|
||||
eligible_count: Number(draft.eligibleCount),
|
||||
cast_count: Number(draft.castCount),
|
||||
...(draft.providerId.trim() ? { provider_id: draft.providerId.trim() } : {}),
|
||||
...(draft.votingBallotId.trim() ? { voting_ballot_id: draft.votingBallotId.trim() } : {}),
|
||||
...(draft.state === "closed" ? {
|
||||
counts: Object.fromEntries(choices.map((choice) => [choice, Number(draft.counts[choice] ?? 0)])),
|
||||
quorum_met: draft.quorumMet,
|
||||
|
||||
Reference in New Issue
Block a user