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.
157 lines
5.2 KiB
TypeScript
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)}
|
|
/>
|
|
</>
|
|
);
|
|
}
|