Files
govoplan-committee/webui/src/features/committee/CommitteeBallotDialog.tsx
T
zemion 1d6eef386d fix(ui): align contextual documentation with headings
Verified with the coordinated workspace changes by devkit full run
2026-09-08T225814-186389-0000-3e3ed7cd (all seven phases passed).
This shared UI pass does not mark the individual module reviews complete.
2026-09-09 02:03:35 +02:00

157 lines
5.2 KiB
TypeScript

import { useEffect, useState } from "react";
import {
Button,
ConfirmDialog,
Dialog,
DocumentationHelpLink,
DismissibleAlert,
FormField,
i18nMessage,
useUnsavedChanges,
useUnsavedDraftGuard,
type ApiSettings
} from "@govoplan/core-webui";
import {
finalizeProviderBallot,
finalizeVotingBallot,
type CommitteeRecord
} from "../../api/committee";
import {
COMMITTEE_FIELD_DOCUMENTATION,
COMMITTEE_INTERFACE_I18N
} from "./interfacePatterns";
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("");
const [confirming, setConfirming] = useState(false);
const { requestDiscard } = useUnsavedChanges();
useEffect(() => {
if (!open) return;
setProviderBallotRef("");
setApprovalId("");
setChangeReason("Imported verified ballot aggregate.");
setError("");
setConfirming(false);
}, [open, record.object_id]);
const dirty = Boolean(providerBallotRef || approvalId || changeReason !== "Imported verified ballot aggregate.");
async function finalize(closeAfter = true): Promise<boolean> {
setBusy(true);
setError("");
try {
const saved = votingBallotId
? await finalizeVotingBallot(settings, record, { approvalId, changeReason })
: await finalizeProviderBallot(settings, record, {
providerBallotRef,
approvalId,
changeReason
});
onSaved(saved);
if (closeAfter) onClose();
return true;
} catch (reason) {
setError(reason instanceof Error ? reason.message : "Ballot result could not be imported.");
return false;
} finally {
setBusy(false);
}
}
useUnsavedDraftGuard({
dirty: open && dirty,
onSave: () => finalize(false),
onDiscard: () => {
setProviderBallotRef("");
setApprovalId("");
setChangeReason("Imported verified ballot aggregate.");
},
title: "i18n:govoplan-committee.unsaved_title",
message: "i18n:govoplan-committee.unsaved_message"
});
function requestClose() {
if (busy) return;
if (dirty) requestDiscard(onClose);
else onClose();
}
const providerId = String(record.attributes.provider_id ?? "");
const votingBallotId = String(record.attributes.voting_ballot_id ?? "").trim();
const incomplete = (!votingBallotId && !providerBallotRef.trim()) || !approvalId.trim() || !changeReason.trim();
return (
<>
<Dialog
open={open}
title={votingBallotId ? "Finalize Voting ballot" : "Finalize provider ballot"}
titleHelp={<DocumentationHelpLink reference={COMMITTEE_FIELD_DOCUMENTATION} />}
onClose={requestClose}
closeDisabled={busy}
portal
className="committee-ballot-dialog"
footer={
<>
<Button disabled={busy} disabledReason={busy ? COMMITTEE_INTERFACE_I18N.busy : undefined} onClick={requestClose}>Cancel</Button>
<Button
variant="primary"
disabled={busy || incomplete}
disabledReason={busy ? COMMITTEE_INTERFACE_I18N.busy : incomplete ? COMMITTEE_INTERFACE_I18N.incomplete : undefined}
onClick={() => setConfirming(true)}
>
{busy ? "Importing" : "Finalize"}
</Button>
</>
}
>
<div className="committee-record-form">
{error ? <DismissibleAlert tone="danger" resetKey={error}>{error}</DismissibleAlert> : null}
<p className="committee-dialog-note">
{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>
{!votingBallotId ? <FormField label="Provider ballot reference" documentation={COMMITTEE_FIELD_DOCUMENTATION}>
<input value={providerBallotRef} disabled={busy} onChange={(event) => setProviderBallotRef(event.target.value)} />
</FormField> : null}
<FormField label="Approval ID" documentation={COMMITTEE_FIELD_DOCUMENTATION}>
<input value={approvalId} disabled={busy} onChange={(event) => setApprovalId(event.target.value)} />
</FormField>
<FormField label="Change reason" documentation={COMMITTEE_FIELD_DOCUMENTATION}>
<input value={changeReason} maxLength={1000} disabled={busy} onChange={(event) => setChangeReason(event.target.value)} />
</FormField>
</div>
</Dialog>
<ConfirmDialog
open={confirming}
title="i18n:govoplan-committee.finalize_title"
message={i18nMessage("i18n:govoplan-committee.confirm_ballot_finalization", { title: record.title })}
confirmLabel="Finalize"
busy={busy}
onConfirm={() => {
setConfirming(false);
void finalize();
}}
onCancel={() => setConfirming(false)}
/>
</>
);
}