Migrate Committee interface patterns
This commit is contained in:
@@ -1,9 +1,14 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import {
|
||||
Button,
|
||||
ConfirmDialog,
|
||||
Dialog,
|
||||
DocumentationHelpLink,
|
||||
DismissibleAlert,
|
||||
FormField,
|
||||
i18nMessage,
|
||||
useUnsavedChanges,
|
||||
useUnsavedDraftGuard,
|
||||
type ApiSettings
|
||||
} from "@govoplan/core-webui";
|
||||
import {
|
||||
@@ -11,6 +16,10 @@ import {
|
||||
finalizeVotingBallot,
|
||||
type CommitteeRecord
|
||||
} from "../../api/committee";
|
||||
import {
|
||||
COMMITTEE_FIELD_DOCUMENTATION,
|
||||
COMMITTEE_INTERFACE_I18N
|
||||
} from "./interfacePatterns";
|
||||
|
||||
|
||||
export default function CommitteeBallotDialog({
|
||||
@@ -31,6 +40,8 @@ export default function CommitteeBallotDialog({
|
||||
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;
|
||||
@@ -38,9 +49,12 @@ export default function CommitteeBallotDialog({
|
||||
setApprovalId("");
|
||||
setChangeReason("Imported verified ballot aggregate.");
|
||||
setError("");
|
||||
setConfirming(false);
|
||||
}, [open, record.object_id]);
|
||||
|
||||
async function finalize() {
|
||||
const dirty = Boolean(providerBallotRef || approvalId || changeReason !== "Imported verified ballot aggregate.");
|
||||
|
||||
async function finalize(closeAfter = true): Promise<boolean> {
|
||||
setBusy(true);
|
||||
setError("");
|
||||
try {
|
||||
@@ -52,31 +66,54 @@ export default function CommitteeBallotDialog({
|
||||
changeReason
|
||||
});
|
||||
onSaved(saved);
|
||||
onClose();
|
||||
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"}
|
||||
onClose={onClose}
|
||||
onClose={requestClose}
|
||||
closeDisabled={busy}
|
||||
portal
|
||||
className="committee-ballot-dialog"
|
||||
footer={
|
||||
<>
|
||||
<Button disabled={busy} onClick={onClose}>Cancel</Button>
|
||||
<Button disabled={busy} disabledReason={busy ? COMMITTEE_INTERFACE_I18N.busy : undefined} onClick={requestClose}>Cancel</Button>
|
||||
<Button
|
||||
variant="primary"
|
||||
disabled={busy || (!votingBallotId && !providerBallotRef.trim()) || !approvalId.trim() || !changeReason.trim()}
|
||||
onClick={() => void finalize()}
|
||||
disabled={busy || incomplete}
|
||||
disabledReason={busy ? COMMITTEE_INTERFACE_I18N.busy : incomplete ? COMMITTEE_INTERFACE_I18N.incomplete : undefined}
|
||||
onClick={() => setConfirming(true)}
|
||||
>
|
||||
{busy ? "Importing" : "Finalize"}
|
||||
</Button>
|
||||
@@ -84,22 +121,36 @@ export default function CommitteeBallotDialog({
|
||||
}
|
||||
>
|
||||
<div className="committee-record-form">
|
||||
<div className="committee-dialog-help"><DocumentationHelpLink reference={COMMITTEE_FIELD_DOCUMENTATION} /></div>
|
||||
{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">
|
||||
{!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">
|
||||
<FormField label="Approval ID" documentation={COMMITTEE_FIELD_DOCUMENTATION}>
|
||||
<input value={approvalId} disabled={busy} onChange={(event) => setApprovalId(event.target.value)} />
|
||||
</FormField>
|
||||
<FormField label="Change reason">
|
||||
<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)}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -11,13 +11,17 @@ import {
|
||||
} from "lucide-react";
|
||||
import { useCallback, useEffect, useMemo, useState, type ReactNode } from "react";
|
||||
import {
|
||||
ActionBlockerHint,
|
||||
Button,
|
||||
DocumentationHelpLink,
|
||||
DismissibleAlert,
|
||||
IconButton,
|
||||
LoadingIndicator,
|
||||
PageScrollViewport,
|
||||
StatusBadge,
|
||||
hasScope,
|
||||
i18nMessage,
|
||||
usePlatformLanguage,
|
||||
type PlatformRouteContext
|
||||
} from "@govoplan/core-webui";
|
||||
import {
|
||||
@@ -27,6 +31,11 @@ import {
|
||||
} from "../../api/committee";
|
||||
import CommitteeBallotDialog from "./CommitteeBallotDialog";
|
||||
import CommitteeRecordDialog from "./CommitteeRecordDialog";
|
||||
import {
|
||||
COMMITTEE_DOCUMENTATION,
|
||||
COMMITTEE_INTERFACE_I18N,
|
||||
committeeDisabledReason
|
||||
} from "./interfacePatterns";
|
||||
import { canReviseCommitteeRecord } from "./lifecycle";
|
||||
|
||||
|
||||
@@ -37,6 +46,7 @@ type EditorTarget = {
|
||||
};
|
||||
|
||||
export default function CommitteePage({ settings, auth }: PlatformRouteContext) {
|
||||
const { language, translateText } = usePlatformLanguage();
|
||||
const tenantId = auth.active_tenant?.id ?? auth.tenant.id;
|
||||
const canWrite = hasScope(auth, "committee:workspace:write");
|
||||
const canFinalizeBallot = hasScope(auth, "committee:ballot:finalize");
|
||||
@@ -165,9 +175,9 @@ export default function CommitteePage({ settings, auth }: PlatformRouteContext)
|
||||
|
||||
const meetingTime = useMemo(
|
||||
() => selectedMeeting
|
||||
? `${formatDateTime(selectedMeeting.attributes.starts_at)} - ${formatTime(selectedMeeting.attributes.ends_at)}`
|
||||
? `${formatDateTime(selectedMeeting.attributes.starts_at, language)} - ${formatTime(selectedMeeting.attributes.ends_at, language)}`
|
||||
: "",
|
||||
[selectedMeeting]
|
||||
[language, selectedMeeting]
|
||||
);
|
||||
|
||||
return (
|
||||
@@ -178,20 +188,42 @@ export default function CommitteePage({ settings, auth }: PlatformRouteContext)
|
||||
<Search size={16} aria-hidden="true" />
|
||||
<input value={query} onChange={(event) => setQuery(event.target.value)} placeholder="Search bodies" aria-label="Search committee bodies" />
|
||||
</form>
|
||||
<Button variant="ghost" onClick={() => void refresh()} disabled={loading}>
|
||||
<Button variant="ghost" onClick={() => void refresh()} disabled={loading} disabledReason={loading ? COMMITTEE_INTERFACE_I18N.loading : undefined}>
|
||||
<RefreshCw size={16} aria-hidden="true" />
|
||||
Refresh
|
||||
</Button>
|
||||
{canWrite ? (
|
||||
<Button variant="primary" onClick={() => setEditor({ kind: "body" })}>
|
||||
<Plus size={16} aria-hidden="true" />
|
||||
New body
|
||||
</Button>
|
||||
) : null}
|
||||
<Button
|
||||
variant="primary"
|
||||
disabled={!canWrite || loading}
|
||||
disabledReason={committeeDisabledReason({ loading, permitted: canWrite })}
|
||||
onClick={() => setEditor({ kind: "body" })}
|
||||
>
|
||||
<Plus size={16} aria-hidden="true" />
|
||||
New body
|
||||
</Button>
|
||||
<DocumentationHelpLink reference={COMMITTEE_DOCUMENTATION} />
|
||||
</div>
|
||||
|
||||
{error ? <DismissibleAlert tone="danger" resetKey={error} className="committee-alert">{error}</DismissibleAlert> : null}
|
||||
{loading && bodies.length === 0 ? <LoadingIndicator label="Loading committee workspace" /> : null}
|
||||
{!canWrite ? (
|
||||
<ActionBlockerHint
|
||||
tone="info"
|
||||
reason={{
|
||||
summary: "No Committee management permission",
|
||||
details: COMMITTEE_INTERFACE_I18N.writeReason,
|
||||
requiredAction: COMMITTEE_INTERFACE_I18N.permissionAction,
|
||||
actor: COMMITTEE_INTERFACE_I18N.permissionActor,
|
||||
target: COMMITTEE_INTERFACE_I18N.permissionDestination
|
||||
}}
|
||||
labels={{
|
||||
requiredAction: COMMITTEE_INTERFACE_I18N.requiredAction,
|
||||
actor: COMMITTEE_INTERFACE_I18N.actor,
|
||||
target: COMMITTEE_INTERFACE_I18N.destination
|
||||
}}
|
||||
documentation={COMMITTEE_DOCUMENTATION}
|
||||
/>
|
||||
) : null}
|
||||
|
||||
<div className="committee-workspace">
|
||||
<section className="committee-panel committee-body-panel" aria-label="Committee bodies">
|
||||
@@ -199,10 +231,21 @@ export default function CommitteePage({ settings, auth }: PlatformRouteContext)
|
||||
<PageScrollViewport className="committee-panel-scroll">
|
||||
<RecordList records={bodies} selectedId={bodyId} onSelect={setBodyId} />
|
||||
</PageScrollViewport>
|
||||
{selectedBody && canWrite ? (
|
||||
{selectedBody ? (
|
||||
<div className="committee-panel-actions">
|
||||
{canReviseCommitteeRecord(selectedBody) ? <IconButton label="Edit body" icon={<Pencil size={16} />} onClick={() => setEditor({ kind: "body", record: selectedBody })} /> : null}
|
||||
<Button variant="primary" onClick={() => setEditor({ kind: "meeting", parentId: selectedBody.object_id })}>
|
||||
<IconButton
|
||||
label="Edit body"
|
||||
icon={<Pencil size={16} />}
|
||||
disabled={!canWrite || !canReviseCommitteeRecord(selectedBody)}
|
||||
disabledReason={committeeDisabledReason({ permitted: canWrite, lifecycleBlocked: !canReviseCommitteeRecord(selectedBody) })}
|
||||
onClick={() => setEditor({ kind: "body", record: selectedBody })}
|
||||
/>
|
||||
<Button
|
||||
variant="primary"
|
||||
disabled={!canWrite}
|
||||
disabledReason={committeeDisabledReason({ permitted: canWrite })}
|
||||
onClick={() => setEditor({ kind: "meeting", parentId: selectedBody.object_id })}
|
||||
>
|
||||
<CalendarPlus size={16} aria-hidden="true" />
|
||||
New meeting
|
||||
</Button>
|
||||
@@ -213,7 +256,7 @@ export default function CommitteePage({ settings, auth }: PlatformRouteContext)
|
||||
<section className="committee-panel committee-meeting-panel" aria-label="Meetings">
|
||||
<PanelHeading title="Meetings" count={meetings.length} />
|
||||
<PageScrollViewport className="committee-panel-scroll">
|
||||
<RecordList records={meetings} selectedId={meetingId} onSelect={setMeetingId} secondary={meetingSecondary} />
|
||||
<RecordList records={meetings} selectedId={meetingId} onSelect={setMeetingId} secondary={(record) => meetingSecondary(record, language, translateText)} />
|
||||
</PageScrollViewport>
|
||||
</section>
|
||||
|
||||
@@ -227,27 +270,44 @@ export default function CommitteePage({ settings, auth }: PlatformRouteContext)
|
||||
<span>{meetingTime}</span>
|
||||
<h1>{selectedMeeting.title}</h1>
|
||||
</div>
|
||||
<StatusBadge status={statusTone(selectedMeeting.state)} label={humanize(selectedMeeting.state)} />
|
||||
{canWrite && canReviseCommitteeRecord(selectedMeeting) ? <IconButton label="Edit meeting" icon={<Pencil size={16} />} onClick={() => setEditor({ kind: "meeting", record: selectedMeeting })} /> : null}
|
||||
<StatusBadge status={statusTone(selectedMeeting.state)} label={stateLabel(selectedMeeting.state)} />
|
||||
<IconButton
|
||||
label="Edit meeting"
|
||||
icon={<Pencil size={16} />}
|
||||
disabled={!canWrite || !canReviseCommitteeRecord(selectedMeeting)}
|
||||
disabledReason={committeeDisabledReason({ permitted: canWrite, lifecycleBlocked: !canReviseCommitteeRecord(selectedMeeting) })}
|
||||
onClick={() => setEditor({ kind: "meeting", record: selectedMeeting })}
|
||||
/>
|
||||
</div>
|
||||
<PageScrollViewport className="committee-detail-scroll">
|
||||
<WorkspaceSection
|
||||
title="Agenda"
|
||||
action={canWrite ? (
|
||||
<Button variant="ghost" onClick={() => setEditor({ kind: "agenda_item", parentId: selectedMeeting.object_id })}>
|
||||
action={(
|
||||
<Button
|
||||
variant="ghost"
|
||||
disabled={!canWrite}
|
||||
disabledReason={committeeDisabledReason({ permitted: canWrite })}
|
||||
onClick={() => setEditor({ kind: "agenda_item", parentId: selectedMeeting.object_id })}
|
||||
>
|
||||
<ListPlus size={16} aria-hidden="true" />
|
||||
Add item
|
||||
</Button>
|
||||
) : null}
|
||||
)}
|
||||
>
|
||||
<div className="committee-agenda-list">
|
||||
{agendaItems.map((item) => (
|
||||
<div key={item.object_id} className={item.object_id === agendaId ? "is-selected" : ""}>
|
||||
<button type="button" onClick={() => setAgendaId(item.object_id)}>
|
||||
<span className="committee-agenda-position">{String(item.attributes.position ?? "-")}</span>
|
||||
<span><strong>{item.title}</strong><small>{humanize(item.state)}</small></span>
|
||||
<span><strong>{item.title}</strong><small>{stateLabel(item.state)}</small></span>
|
||||
</button>
|
||||
{canWrite && canReviseCommitteeRecord(item) ? <IconButton label="Edit agenda item" icon={<Pencil size={15} />} onClick={() => setEditor({ kind: "agenda_item", record: item })} /> : null}
|
||||
<IconButton
|
||||
label="Edit agenda item"
|
||||
icon={<Pencil size={15} />}
|
||||
disabled={!canWrite || !canReviseCommitteeRecord(item)}
|
||||
disabledReason={committeeDisabledReason({ permitted: canWrite, lifecycleBlocked: !canReviseCommitteeRecord(item) })}
|
||||
onClick={() => setEditor({ kind: "agenda_item", record: item })}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
{agendaItems.length === 0 ? <div className="committee-empty compact">No agenda items.</div> : null}
|
||||
@@ -256,23 +316,30 @@ export default function CommitteePage({ settings, auth }: PlatformRouteContext)
|
||||
|
||||
{selectedAgenda ? (
|
||||
<WorkspaceSection
|
||||
title={`Votes - ${selectedAgenda.title}`}
|
||||
action={canWrite ? (
|
||||
<Button variant="ghost" onClick={() => setEditor({ kind: "vote", parentId: selectedAgenda.object_id })}>
|
||||
title={i18nMessage("i18n:govoplan-committee.votes_for", { title: selectedAgenda.title })}
|
||||
action={(
|
||||
<Button
|
||||
variant="ghost"
|
||||
disabled={!canWrite}
|
||||
disabledReason={committeeDisabledReason({ permitted: canWrite })}
|
||||
onClick={() => setEditor({ kind: "vote", parentId: selectedAgenda.object_id })}
|
||||
>
|
||||
<Vote size={16} aria-hidden="true" />
|
||||
Add vote
|
||||
</Button>
|
||||
) : null}
|
||||
)}
|
||||
>
|
||||
<RecordRows
|
||||
records={votes}
|
||||
canEdit={(record) => canWrite && canReviseCommitteeRecord(record)}
|
||||
editDisabledReason={(record) => committeeDisabledReason({ permitted: canWrite, lifecycleBlocked: !canReviseCommitteeRecord(record) })}
|
||||
onEdit={(record) => setEditor({ kind: "vote", record })}
|
||||
detail={voteSummary}
|
||||
secondaryAction={(record) => canFinalizeBallot && isProviderBallotReady(record) ? (
|
||||
detail={(record) => voteSummary(record, translateText)}
|
||||
secondaryAction={(record) => isProviderBallotReady(record) ? (
|
||||
<IconButton
|
||||
label={`Finalize ${record.title} from ballot provider`}
|
||||
label={i18nMessage("i18n:govoplan-committee.finalize_provider_record", { title: record.title })}
|
||||
icon={<ShieldCheck size={15} />}
|
||||
disabled={!canFinalizeBallot}
|
||||
disabledReason={!canFinalizeBallot ? COMMITTEE_INTERFACE_I18N.finalizeReason : undefined}
|
||||
onClick={() => setBallotRecord(record)}
|
||||
/>
|
||||
) : null}
|
||||
@@ -282,14 +349,24 @@ export default function CommitteePage({ settings, auth }: PlatformRouteContext)
|
||||
|
||||
<WorkspaceSection
|
||||
title="Minutes"
|
||||
action={canWrite ? (
|
||||
<Button variant="ghost" onClick={() => setEditor({ kind: "minute", parentId: selectedMeeting.object_id })}>
|
||||
action={(
|
||||
<Button
|
||||
variant="ghost"
|
||||
disabled={!canWrite}
|
||||
disabledReason={committeeDisabledReason({ permitted: canWrite })}
|
||||
onClick={() => setEditor({ kind: "minute", parentId: selectedMeeting.object_id })}
|
||||
>
|
||||
<FilePlus2 size={16} aria-hidden="true" />
|
||||
Add minutes
|
||||
</Button>
|
||||
) : null}
|
||||
)}
|
||||
>
|
||||
<RecordRows records={minutes} canEdit={(record) => canWrite && canReviseCommitteeRecord(record)} onEdit={(record) => setEditor({ kind: "minute", record })} detail={(record) => `Record ${String((record.attributes.content_ref as Record<string, unknown> | undefined)?.object_id ?? "-")}`} />
|
||||
<RecordRows
|
||||
records={minutes}
|
||||
editDisabledReason={(record) => committeeDisabledReason({ permitted: canWrite, lifecycleBlocked: !canReviseCommitteeRecord(record) })}
|
||||
onEdit={(record) => setEditor({ kind: "minute", record })}
|
||||
detail={(record) => i18nMessage("i18n:govoplan-committee.record_reference", { id: String((record.attributes.content_ref as Record<string, unknown> | undefined)?.object_id ?? "-") })}
|
||||
/>
|
||||
</WorkspaceSection>
|
||||
</PageScrollViewport>
|
||||
</>
|
||||
@@ -337,7 +414,7 @@ function RecordList({ records, selectedId, onSelect, secondary }: {
|
||||
return <div className="committee-record-list">{records.map((record) => (
|
||||
<button key={record.object_id} type="button" className={record.object_id === selectedId ? "is-selected" : ""} onClick={() => onSelect(record.object_id)}>
|
||||
<strong>{record.title}</strong>
|
||||
<span>{secondary?.(record) ?? humanize(record.state)}</span>
|
||||
<span>{secondary?.(record) ?? stateLabel(record.state)}</span>
|
||||
</button>
|
||||
))}</div>;
|
||||
}
|
||||
@@ -346,9 +423,9 @@ function WorkspaceSection({ title, action, children }: { title: string; action?:
|
||||
return <section className="committee-workspace-section"><div><h2>{title}</h2>{action}</div>{children}</section>;
|
||||
}
|
||||
|
||||
function RecordRows({ records, canEdit, onEdit, detail, secondaryAction }: {
|
||||
function RecordRows({ records, editDisabledReason, onEdit, detail, secondaryAction }: {
|
||||
records: CommitteeRecord[];
|
||||
canEdit: (record: CommitteeRecord) => boolean;
|
||||
editDisabledReason: (record: CommitteeRecord) => string | undefined;
|
||||
onEdit: (record: CommitteeRecord) => void;
|
||||
detail: (record: CommitteeRecord) => string;
|
||||
secondaryAction?: (record: CommitteeRecord) => ReactNode;
|
||||
@@ -357,23 +434,33 @@ function RecordRows({ records, canEdit, onEdit, detail, secondaryAction }: {
|
||||
return <div className="committee-record-rows">{records.map((record) => (
|
||||
<div key={record.object_id}>
|
||||
<span><strong>{record.title}</strong><small>{detail(record)}</small></span>
|
||||
<StatusBadge status={statusTone(record.state)} label={humanize(record.state)} />
|
||||
<StatusBadge status={statusTone(record.state)} label={stateLabel(record.state)} />
|
||||
<span className="committee-row-actions">
|
||||
{secondaryAction?.(record)}
|
||||
{canEdit(record) ? <IconButton label={`Edit ${record.title}`} icon={<Pencil size={15} />} onClick={() => onEdit(record)} /> : null}
|
||||
<IconButton
|
||||
label={i18nMessage("i18n:govoplan-committee.edit_record", { title: record.title })}
|
||||
icon={<Pencil size={15} />}
|
||||
disabled={Boolean(editDisabledReason(record))}
|
||||
disabledReason={editDisabledReason(record)}
|
||||
onClick={() => onEdit(record)}
|
||||
/>
|
||||
</span>
|
||||
</div>
|
||||
))}</div>;
|
||||
}
|
||||
|
||||
function meetingSecondary(record: CommitteeRecord): string {
|
||||
return `${formatDateTime(record.attributes.starts_at)} - ${humanize(record.state)}`;
|
||||
function meetingSecondary(record: CommitteeRecord, locale: string, translateText: (value: string) => string): string {
|
||||
return `${formatDateTime(record.attributes.starts_at, locale)} - ${translateText(stateLabel(record.state))}`;
|
||||
}
|
||||
|
||||
function voteSummary(record: CommitteeRecord): string {
|
||||
function voteSummary(record: CommitteeRecord, translateText: (value: string) => string): string {
|
||||
const cast = Number(record.attributes.cast_count ?? 0);
|
||||
const eligible = Number(record.attributes.eligible_count ?? 0);
|
||||
return `${humanize(String(record.attributes.method ?? "recorded"))}, ${cast}/${eligible} cast`;
|
||||
return i18nMessage("i18n:govoplan-committee.vote_summary", {
|
||||
method: translateText(domainLabel(String(record.attributes.method ?? "recorded"))),
|
||||
cast,
|
||||
eligible
|
||||
});
|
||||
}
|
||||
|
||||
function isProviderBallotReady(record: CommitteeRecord): boolean {
|
||||
@@ -389,16 +476,20 @@ function statusTone(state: string): "active" | "inactive" | "warning" {
|
||||
return "warning";
|
||||
}
|
||||
|
||||
function formatDateTime(value: unknown): string {
|
||||
function formatDateTime(value: unknown, locale?: string): string {
|
||||
if (!value) return "Date not set";
|
||||
return new Intl.DateTimeFormat(undefined, { dateStyle: "medium", timeStyle: "short" }).format(new Date(String(value)));
|
||||
return new Intl.DateTimeFormat(locale, { dateStyle: "medium", timeStyle: "short" }).format(new Date(String(value)));
|
||||
}
|
||||
|
||||
function formatTime(value: unknown): string {
|
||||
function formatTime(value: unknown, locale?: string): string {
|
||||
if (!value) return "-";
|
||||
return new Intl.DateTimeFormat(undefined, { timeStyle: "short" }).format(new Date(String(value)));
|
||||
return new Intl.DateTimeFormat(locale, { timeStyle: "short" }).format(new Date(String(value)));
|
||||
}
|
||||
|
||||
function humanize(value: string): string {
|
||||
return value.replace(/[_:.-]+/g, " ").replace(/\b\w/g, (letter) => letter.toUpperCase());
|
||||
function stateLabel(value: string): string {
|
||||
return `i18n:govoplan-committee.state_${value}`;
|
||||
}
|
||||
|
||||
function domainLabel(value: string): string {
|
||||
return `i18n:govoplan-committee.domain_${value}`;
|
||||
}
|
||||
|
||||
@@ -1,9 +1,15 @@
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import {
|
||||
Button,
|
||||
ConfirmDialog,
|
||||
Dialog,
|
||||
DocumentationHelpLink,
|
||||
DismissibleAlert,
|
||||
FormField,
|
||||
i18nMessage,
|
||||
usePlatformLanguage,
|
||||
useUnsavedChanges,
|
||||
useUnsavedDraftGuard,
|
||||
type ApiSettings
|
||||
} from "@govoplan/core-webui";
|
||||
import {
|
||||
@@ -12,6 +18,11 @@ import {
|
||||
type CommitteeRecord
|
||||
} from "../../api/committee";
|
||||
import { COMMITTEE_STATES, committeeStateOptions } from "./lifecycle";
|
||||
import {
|
||||
COMMITTEE_FIELD_DOCUMENTATION,
|
||||
COMMITTEE_INTERFACE_I18N,
|
||||
committeeDisabledReason
|
||||
} from "./interfacePatterns";
|
||||
|
||||
type Draft = {
|
||||
title: string;
|
||||
@@ -56,9 +67,14 @@ export default function CommitteeRecordDialog({
|
||||
onClose: () => void;
|
||||
onSaved: (record: CommitteeRecord) => void;
|
||||
}) {
|
||||
const [draft, setDraft] = useState<Draft>(() => draftFromRecord(kind, record));
|
||||
const { translateText } = usePlatformLanguage();
|
||||
const initialDraft = useMemo(() => draftFromRecord(kind, record), [kind, record]);
|
||||
const [draft, setDraft] = useState<Draft>(initialDraft);
|
||||
const [baseline, setBaseline] = useState<Draft>(initialDraft);
|
||||
const [busy, setBusy] = useState(false);
|
||||
const [error, setError] = useState("");
|
||||
const [confirmLifecycleChange, setConfirmLifecycleChange] = useState(false);
|
||||
const { requestDiscard } = useUnsavedChanges();
|
||||
const choices = useMemo(
|
||||
() => draft.choices.split(",").map((item) => item.trim()).filter(Boolean),
|
||||
[draft.choices]
|
||||
@@ -70,11 +86,16 @@ export default function CommitteeRecordDialog({
|
||||
|
||||
useEffect(() => {
|
||||
if (!open) return;
|
||||
setDraft(draftFromRecord(kind, record));
|
||||
setDraft(initialDraft);
|
||||
setBaseline(initialDraft);
|
||||
setError("");
|
||||
}, [kind, open, record]);
|
||||
setConfirmLifecycleChange(false);
|
||||
}, [initialDraft, open]);
|
||||
|
||||
async function save() {
|
||||
const dirty = draftKey(draft) !== draftKey(baseline);
|
||||
const incomplete = !draft.title.trim() || !draft.changeReason.trim();
|
||||
|
||||
async function save(closeAfter = true): Promise<boolean> {
|
||||
setBusy(true);
|
||||
setError("");
|
||||
try {
|
||||
@@ -91,30 +112,61 @@ export default function CommitteeRecordDialog({
|
||||
payload,
|
||||
record?.revision
|
||||
);
|
||||
const nextDraft = draftFromRecord(kind, saved);
|
||||
setDraft(nextDraft);
|
||||
setBaseline(nextDraft);
|
||||
onSaved(saved);
|
||||
onClose();
|
||||
if (closeAfter) onClose();
|
||||
return true;
|
||||
} catch (reason) {
|
||||
setError(reason instanceof Error ? reason.message : "Committee record could not be saved.");
|
||||
return false;
|
||||
} finally {
|
||||
setBusy(false);
|
||||
}
|
||||
}
|
||||
|
||||
useUnsavedDraftGuard({
|
||||
dirty: open && dirty,
|
||||
onSave: () => save(false),
|
||||
onDiscard: () => setDraft(baseline),
|
||||
title: "i18n:govoplan-committee.unsaved_title",
|
||||
message: "i18n:govoplan-committee.unsaved_message"
|
||||
});
|
||||
|
||||
function requestClose() {
|
||||
if (busy) return;
|
||||
if (dirty) requestDiscard(onClose);
|
||||
else onClose();
|
||||
}
|
||||
|
||||
function requestSave() {
|
||||
if (record && draft.state !== record.state) {
|
||||
setConfirmLifecycleChange(true);
|
||||
return;
|
||||
}
|
||||
void save();
|
||||
}
|
||||
|
||||
return (
|
||||
<Dialog
|
||||
open={open}
|
||||
title={`${record ? "Edit" : "New"} ${labelForKind(kind)}`}
|
||||
onClose={onClose}
|
||||
title={i18nMessage("i18n:govoplan-committee.record_dialog_title", {
|
||||
action: translateText(record ? "Edit" : "New"),
|
||||
kind: translateText(kindLabel(kind))
|
||||
})}
|
||||
onClose={requestClose}
|
||||
closeDisabled={busy}
|
||||
portal
|
||||
className="committee-record-dialog"
|
||||
footer={
|
||||
<>
|
||||
<Button disabled={busy} onClick={onClose}>Cancel</Button>
|
||||
<Button disabled={busy} disabledReason={committeeDisabledReason({ busy })} onClick={requestClose}>Cancel</Button>
|
||||
<Button
|
||||
variant="primary"
|
||||
disabled={busy || !draft.title.trim() || !draft.changeReason.trim()}
|
||||
onClick={() => void save()}
|
||||
disabled={busy || incomplete}
|
||||
disabledReason={busy ? COMMITTEE_INTERFACE_I18N.busy : incomplete ? COMMITTEE_INTERFACE_I18N.incomplete : undefined}
|
||||
onClick={requestSave}
|
||||
>
|
||||
{busy ? "Saving" : "Save"}
|
||||
</Button>
|
||||
@@ -122,9 +174,10 @@ export default function CommitteeRecordDialog({
|
||||
}
|
||||
>
|
||||
<div className="committee-record-form">
|
||||
<div className="committee-dialog-help"><DocumentationHelpLink reference={COMMITTEE_FIELD_DOCUMENTATION} /></div>
|
||||
{error ? <DismissibleAlert tone="danger" resetKey={error}>{error}</DismissibleAlert> : null}
|
||||
<div className="committee-form-grid">
|
||||
<FormField label="Title">
|
||||
<FormField label="Title" documentation={COMMITTEE_FIELD_DOCUMENTATION}>
|
||||
<input
|
||||
value={draft.title}
|
||||
maxLength={500}
|
||||
@@ -132,19 +185,19 @@ export default function CommitteeRecordDialog({
|
||||
onChange={(event) => setDraft({ ...draft, title: event.target.value })}
|
||||
/>
|
||||
</FormField>
|
||||
<FormField label="State">
|
||||
<FormField label="State" documentation={COMMITTEE_FIELD_DOCUMENTATION}>
|
||||
<select
|
||||
value={draft.state}
|
||||
disabled={busy}
|
||||
onChange={(event) => setDraft({ ...draft, state: event.target.value })}
|
||||
>
|
||||
{stateOptions.map((state) => <option key={state} value={state}>{humanize(state)}</option>)}
|
||||
{stateOptions.map((state) => <option key={state} value={state}>{stateLabel(state)}</option>)}
|
||||
</select>
|
||||
</FormField>
|
||||
</div>
|
||||
|
||||
{kind === "body" ? (
|
||||
<FormField label="Responsible organization unit ID">
|
||||
<FormField label="Responsible organization unit ID" documentation={COMMITTEE_FIELD_DOCUMENTATION}>
|
||||
<input
|
||||
value={draft.organizationUnitId}
|
||||
disabled={busy}
|
||||
@@ -183,7 +236,7 @@ export default function CommitteeRecordDialog({
|
||||
</FormField>
|
||||
</div>
|
||||
{draft.state === "decided" ? (
|
||||
<FormField label="Formal Decision ID">
|
||||
<FormField label="Formal Decision ID" documentation={COMMITTEE_FIELD_DOCUMENTATION}>
|
||||
<input value={draft.decisionId} disabled={busy} onChange={(event) => setDraft({ ...draft, decisionId: event.target.value })} />
|
||||
</FormField>
|
||||
) : null}
|
||||
@@ -203,11 +256,11 @@ export default function CommitteeRecordDialog({
|
||||
<FormField label="Eligible voters">
|
||||
<input type="number" min="0" value={draft.eligibleCount} disabled={busy} onChange={(event) => setDraft({ ...draft, eligibleCount: event.target.value })} />
|
||||
</FormField>
|
||||
<FormField label="Ballot provider (optional)">
|
||||
<FormField label="Ballot provider (optional)" documentation={COMMITTEE_FIELD_DOCUMENTATION}>
|
||||
<input value={draft.providerId} disabled={busy} onChange={(event) => setDraft({ ...draft, providerId: event.target.value })} />
|
||||
</FormField>
|
||||
</div>
|
||||
<FormField label="Voting ballot ID (optional)">
|
||||
<FormField label="Voting ballot ID (optional)" documentation={COMMITTEE_FIELD_DOCUMENTATION}>
|
||||
<input value={draft.votingBallotId} disabled={busy} onChange={(event) => setDraft({ ...draft, votingBallotId: event.target.value })} />
|
||||
</FormField>
|
||||
<FormField label="Choices (comma separated)">
|
||||
@@ -250,7 +303,7 @@ export default function CommitteeRecordDialog({
|
||||
|
||||
{kind === "minute" ? (
|
||||
<>
|
||||
<FormField label="Minutes record ID">
|
||||
<FormField label="Minutes record ID" documentation={COMMITTEE_FIELD_DOCUMENTATION}>
|
||||
<input value={draft.contentRecordId} disabled={busy} onChange={(event) => setDraft({ ...draft, contentRecordId: event.target.value })} />
|
||||
</FormField>
|
||||
{draft.state === "accepted" || draft.state === "corrected" ? (
|
||||
@@ -259,7 +312,7 @@ export default function CommitteeRecordDialog({
|
||||
</>
|
||||
) : null}
|
||||
|
||||
<FormField label="Change reason">
|
||||
<FormField label="Change reason" documentation={COMMITTEE_FIELD_DOCUMENTATION}>
|
||||
<input
|
||||
value={draft.changeReason}
|
||||
maxLength={1000}
|
||||
@@ -268,6 +321,23 @@ export default function CommitteeRecordDialog({
|
||||
/>
|
||||
</FormField>
|
||||
</div>
|
||||
<ConfirmDialog
|
||||
open={confirmLifecycleChange}
|
||||
title="i18n:govoplan-committee.save_state_title"
|
||||
message={record ? i18nMessage("i18n:govoplan-committee.confirm_state_change", {
|
||||
kind: translateText(kindLabel(kind)),
|
||||
from: translateText(stateLabel(record.state)),
|
||||
to: translateText(stateLabel(draft.state))
|
||||
}) : ""}
|
||||
confirmLabel="Save"
|
||||
tone={["cancelled", "retired", "withdrawn"].includes(draft.state) ? "danger" : "default"}
|
||||
busy={busy}
|
||||
onConfirm={() => {
|
||||
setConfirmLifecycleChange(false);
|
||||
void save();
|
||||
}}
|
||||
onCancel={() => setConfirmLifecycleChange(false)}
|
||||
/>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
@@ -275,10 +345,10 @@ export default function CommitteeRecordDialog({
|
||||
function EvidenceFields({ draft, busy, setDraft }: { draft: Draft; busy: boolean; setDraft: (draft: Draft) => void }) {
|
||||
return (
|
||||
<div className="committee-form-grid">
|
||||
<FormField label="Approval ID">
|
||||
<FormField label="Approval ID" documentation={COMMITTEE_FIELD_DOCUMENTATION}>
|
||||
<input value={draft.approvalId} disabled={busy} onChange={(event) => setDraft({ ...draft, approvalId: event.target.value })} />
|
||||
</FormField>
|
||||
<FormField label="Evidence record ID">
|
||||
<FormField label="Evidence record ID" documentation={COMMITTEE_FIELD_DOCUMENTATION}>
|
||||
<input value={draft.evidenceId} disabled={busy} onChange={(event) => setDraft({ ...draft, evidenceId: event.target.value })} />
|
||||
</FormField>
|
||||
</div>
|
||||
@@ -432,6 +502,18 @@ function labelForKind(kind: CommitteeObjectKind): string {
|
||||
return humanize(kind);
|
||||
}
|
||||
|
||||
function kindLabel(kind: CommitteeObjectKind): string {
|
||||
return `i18n:govoplan-committee.kind_${kind}`;
|
||||
}
|
||||
|
||||
function stateLabel(state: string): string {
|
||||
return `i18n:govoplan-committee.state_${state}`;
|
||||
}
|
||||
|
||||
function draftKey(draft: Draft): string {
|
||||
return JSON.stringify(draft);
|
||||
}
|
||||
|
||||
function humanize(value: string): string {
|
||||
return value.replace(/[_:.-]+/g, " ").replace(/\b\w/g, (letter) => letter.toUpperCase());
|
||||
}
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
import type { DocumentationHelpReference } from "@govoplan/core-webui";
|
||||
|
||||
export const COMMITTEE_DOCUMENTATION = {
|
||||
topicId: "committee.module-boundary",
|
||||
documentationType: "user"
|
||||
} satisfies DocumentationHelpReference;
|
||||
|
||||
export const COMMITTEE_FIELD_DOCUMENTATION = {
|
||||
topicId: "committee.reference.fields-and-consequences",
|
||||
documentationType: "admin"
|
||||
} satisfies DocumentationHelpReference;
|
||||
|
||||
export const COMMITTEE_INTERFACE_I18N = {
|
||||
loading: "i18n:govoplan-committee.loading_reason",
|
||||
busy: "i18n:govoplan-committee.busy_reason",
|
||||
writeReason: "i18n:govoplan-committee.write_permission_reason",
|
||||
finalizeReason: "i18n:govoplan-committee.finalize_permission_reason",
|
||||
lifecycleReason: "i18n:govoplan-committee.lifecycle_reason",
|
||||
incomplete: "i18n:govoplan-committee.incomplete_reason",
|
||||
requiredAction: "i18n:govoplan-committee.required_action",
|
||||
actor: "i18n:govoplan-committee.responsible_actor",
|
||||
destination: "i18n:govoplan-committee.destination",
|
||||
permissionAction: "i18n:govoplan-committee.permission_action",
|
||||
permissionActor: "i18n:govoplan-committee.permission_actor",
|
||||
permissionDestination: "i18n:govoplan-committee.permission_destination"
|
||||
} as const;
|
||||
|
||||
export function committeeDisabledReason({
|
||||
loading = false,
|
||||
busy = false,
|
||||
permitted = true,
|
||||
lifecycleBlocked = false
|
||||
}: {
|
||||
loading?: boolean;
|
||||
busy?: boolean;
|
||||
permitted?: boolean;
|
||||
lifecycleBlocked?: boolean;
|
||||
}): string | undefined {
|
||||
if (loading) return COMMITTEE_INTERFACE_I18N.loading;
|
||||
if (busy) return COMMITTEE_INTERFACE_I18N.busy;
|
||||
if (!permitted) return COMMITTEE_INTERFACE_I18N.writeReason;
|
||||
if (lifecycleBlocked) return COMMITTEE_INTERFACE_I18N.lifecycleReason;
|
||||
return undefined;
|
||||
}
|
||||
@@ -0,0 +1,223 @@
|
||||
import type { PlatformTranslations } from "@govoplan/core-webui";
|
||||
|
||||
const en = {
|
||||
"i18n:govoplan-committee.committee": "Committee",
|
||||
"i18n:govoplan-committee.loading_reason": "Committee data is still loading.",
|
||||
"i18n:govoplan-committee.busy_reason": "Another Committee action is still running.",
|
||||
"i18n:govoplan-committee.write_permission_reason": "Your account may not manage Committee records.",
|
||||
"i18n:govoplan-committee.finalize_permission_reason": "Your account may not finalize governed ballot results.",
|
||||
"i18n:govoplan-committee.lifecycle_reason": "This record is immutable in its current lifecycle state.",
|
||||
"i18n:govoplan-committee.incomplete_reason": "Complete the required title and change reason first.",
|
||||
"i18n:govoplan-committee.required_action": "Required action",
|
||||
"i18n:govoplan-committee.responsible_actor": "Responsible actor",
|
||||
"i18n:govoplan-committee.destination": "Destination",
|
||||
"i18n:govoplan-committee.permission_action": "Ask for the corresponding Committee management permission.",
|
||||
"i18n:govoplan-committee.permission_actor": "An Access or tenant administrator",
|
||||
"i18n:govoplan-committee.permission_destination": "Access role assignments",
|
||||
"i18n:govoplan-committee.confirm_state_change": "Save {kind} and change its state from {from} to {to}? The reason and revision are retained as evidence.",
|
||||
"i18n:govoplan-committee.confirm_ballot_finalization": "Finalize {title}? The aggregate result, approval and evidence become part of the governed Committee record.",
|
||||
"i18n:govoplan-committee.finalize_title": "Finalize ballot result",
|
||||
"i18n:govoplan-committee.save_state_title": "Confirm lifecycle change",
|
||||
"i18n:govoplan-committee.unsaved_title": "Unsaved Committee record",
|
||||
"i18n:govoplan-committee.unsaved_message": "Save or discard the Committee record before leaving this surface.",
|
||||
"i18n:govoplan-committee.votes_for": "Votes - {title}",
|
||||
"i18n:govoplan-committee.finalize_provider_record": "Finalize {title} from ballot provider",
|
||||
"i18n:govoplan-committee.record_reference": "Record {id}",
|
||||
"i18n:govoplan-committee.edit_record": "Edit {title}",
|
||||
"i18n:govoplan-committee.vote_summary": "{method}, {cast}/{eligible} cast",
|
||||
"i18n:govoplan-committee.record_dialog_title": "{action} {kind}",
|
||||
"i18n:govoplan-committee.kind_body": "body",
|
||||
"i18n:govoplan-committee.kind_meeting": "meeting",
|
||||
"i18n:govoplan-committee.kind_agenda_item": "agenda item",
|
||||
"i18n:govoplan-committee.kind_vote": "vote",
|
||||
"i18n:govoplan-committee.kind_minute": "minutes",
|
||||
"i18n:govoplan-committee.state_draft": "Draft",
|
||||
"i18n:govoplan-committee.state_active": "Active",
|
||||
"i18n:govoplan-committee.state_suspended": "Suspended",
|
||||
"i18n:govoplan-committee.state_retired": "Retired",
|
||||
"i18n:govoplan-committee.state_scheduled": "Scheduled",
|
||||
"i18n:govoplan-committee.state_open": "Open",
|
||||
"i18n:govoplan-committee.state_closed": "Closed",
|
||||
"i18n:govoplan-committee.state_cancelled": "Cancelled",
|
||||
"i18n:govoplan-committee.state_deliberating": "Deliberating",
|
||||
"i18n:govoplan-committee.state_decided": "Decided",
|
||||
"i18n:govoplan-committee.state_withdrawn": "Withdrawn",
|
||||
"i18n:govoplan-committee.state_proposed": "Proposed",
|
||||
"i18n:govoplan-committee.state_accepted": "Accepted",
|
||||
"i18n:govoplan-committee.state_corrected": "Corrected",
|
||||
"i18n:govoplan-committee.domain_recorded": "Recorded",
|
||||
"i18n:govoplan-committee.domain_public": "Public",
|
||||
"i18n:govoplan-committee.domain_secret": "Secret",
|
||||
"Committee": "Committee",
|
||||
"Search bodies": "Search bodies",
|
||||
"Search committee bodies": "Search committee bodies",
|
||||
"Refresh": "Refresh",
|
||||
"New body": "New body",
|
||||
"Loading committee workspace": "Loading committee workspace",
|
||||
"Committee bodies": "Committee bodies",
|
||||
"Bodies": "Bodies",
|
||||
"Edit body": "Edit body",
|
||||
"New meeting": "New meeting",
|
||||
"Meetings": "Meetings",
|
||||
"Meeting workspace": "Meeting workspace",
|
||||
"Select or create a meeting.": "Select or create a meeting.",
|
||||
"Edit meeting": "Edit meeting",
|
||||
"Agenda": "Agenda",
|
||||
"Add item": "Add item",
|
||||
"No agenda items.": "No agenda items.",
|
||||
"Add vote": "Add vote",
|
||||
"Minutes": "Minutes",
|
||||
"Add minutes": "Add minutes",
|
||||
"No records.": "No records.",
|
||||
"Cancel": "Cancel",
|
||||
"Save": "Save",
|
||||
"Saving": "Saving",
|
||||
"Title": "Title",
|
||||
"State": "State",
|
||||
"Responsible organization unit ID": "Responsible organization unit ID",
|
||||
"Starts": "Starts",
|
||||
"Ends": "Ends",
|
||||
"Position": "Position",
|
||||
"Subject type": "Subject type",
|
||||
"Subject ID": "Subject ID",
|
||||
"Formal Decision ID": "Formal Decision ID",
|
||||
"Method": "Method",
|
||||
"Eligible voters": "Eligible voters",
|
||||
"Ballot provider (optional)": "Ballot provider (optional)",
|
||||
"Voting ballot ID (optional)": "Voting ballot ID (optional)",
|
||||
"Choices (comma separated)": "Choices (comma separated)",
|
||||
"Votes cast": "Votes cast",
|
||||
"Quorum": "Quorum",
|
||||
"Met": "Met",
|
||||
"Not met": "Not met",
|
||||
"Approval ID": "Approval ID",
|
||||
"Evidence record ID": "Evidence record ID",
|
||||
"Minutes record ID": "Minutes record ID",
|
||||
"Change reason": "Change reason",
|
||||
"Provider ballot reference": "Provider ballot reference",
|
||||
"Importing": "Importing",
|
||||
"Finalize": "Finalize",
|
||||
"Date not set": "Date not set",
|
||||
"No Committee management permission": "No Committee management permission",
|
||||
"Edit": "Edit",
|
||||
"New": "New",
|
||||
"Recorded": "Recorded",
|
||||
"Public": "Public",
|
||||
"Secret": "Secret",
|
||||
"Case": "Case",
|
||||
"Service": "Service",
|
||||
"Work Item": "Work item",
|
||||
"Record": "Record"
|
||||
} as const;
|
||||
|
||||
const de: Record<keyof typeof en, string> = {
|
||||
"i18n:govoplan-committee.committee": "Gremien",
|
||||
"i18n:govoplan-committee.loading_reason": "Gremiendaten werden noch geladen.",
|
||||
"i18n:govoplan-committee.busy_reason": "Eine andere Gremienaktion läuft noch.",
|
||||
"i18n:govoplan-committee.write_permission_reason": "Ihr Konto darf Gremiendatensätze nicht verwalten.",
|
||||
"i18n:govoplan-committee.finalize_permission_reason": "Ihr Konto darf geregelte Abstimmungsergebnisse nicht abschließen.",
|
||||
"i18n:govoplan-committee.lifecycle_reason": "Dieser Datensatz ist in seinem aktuellen Lebenszyklus unveränderlich.",
|
||||
"i18n:govoplan-committee.incomplete_reason": "Füllen Sie zuerst Titel und Änderungsgrund aus.",
|
||||
"i18n:govoplan-committee.required_action": "Erforderliche Aktion",
|
||||
"i18n:govoplan-committee.responsible_actor": "Verantwortliche Stelle",
|
||||
"i18n:govoplan-committee.destination": "Ziel",
|
||||
"i18n:govoplan-committee.permission_action": "Fordern Sie die entsprechende Berechtigung zur Gremienverwaltung an.",
|
||||
"i18n:govoplan-committee.permission_actor": "Eine Zugriffs- oder Mandantenadministration",
|
||||
"i18n:govoplan-committee.permission_destination": "Zugriff und Rollenzuweisungen",
|
||||
"i18n:govoplan-committee.confirm_state_change": "{kind} speichern und den Status von {from} auf {to} ändern? Grund und Revision werden als Nachweis aufbewahrt.",
|
||||
"i18n:govoplan-committee.confirm_ballot_finalization": "{title} abschließen? Gesamtergebnis, Genehmigung und Nachweis werden Bestandteil des geregelten Gremiendatensatzes.",
|
||||
"i18n:govoplan-committee.finalize_title": "Abstimmungsergebnis abschließen",
|
||||
"i18n:govoplan-committee.save_state_title": "Lebenszyklusänderung bestätigen",
|
||||
"i18n:govoplan-committee.unsaved_title": "Ungespeicherter Gremiendatensatz",
|
||||
"i18n:govoplan-committee.unsaved_message": "Speichern oder verwerfen Sie den Gremiendatensatz, bevor Sie diese Oberfläche verlassen.",
|
||||
"i18n:govoplan-committee.votes_for": "Abstimmungen - {title}",
|
||||
"i18n:govoplan-committee.finalize_provider_record": "{title} über Abstimmungsanbieter abschließen",
|
||||
"i18n:govoplan-committee.record_reference": "Datensatz {id}",
|
||||
"i18n:govoplan-committee.edit_record": "{title} bearbeiten",
|
||||
"i18n:govoplan-committee.vote_summary": "{method}, {cast}/{eligible} abgegeben",
|
||||
"i18n:govoplan-committee.record_dialog_title": "{kind}: {action}",
|
||||
"i18n:govoplan-committee.kind_body": "Gremium",
|
||||
"i18n:govoplan-committee.kind_meeting": "Sitzung",
|
||||
"i18n:govoplan-committee.kind_agenda_item": "Tagesordnungspunkt",
|
||||
"i18n:govoplan-committee.kind_vote": "Abstimmung",
|
||||
"i18n:govoplan-committee.kind_minute": "Protokoll",
|
||||
"i18n:govoplan-committee.state_draft": "Entwurf",
|
||||
"i18n:govoplan-committee.state_active": "Aktiv",
|
||||
"i18n:govoplan-committee.state_suspended": "Ausgesetzt",
|
||||
"i18n:govoplan-committee.state_retired": "Stillgelegt",
|
||||
"i18n:govoplan-committee.state_scheduled": "Geplant",
|
||||
"i18n:govoplan-committee.state_open": "Offen",
|
||||
"i18n:govoplan-committee.state_closed": "Geschlossen",
|
||||
"i18n:govoplan-committee.state_cancelled": "Abgesagt",
|
||||
"i18n:govoplan-committee.state_deliberating": "In Beratung",
|
||||
"i18n:govoplan-committee.state_decided": "Entschieden",
|
||||
"i18n:govoplan-committee.state_withdrawn": "Zurückgezogen",
|
||||
"i18n:govoplan-committee.state_proposed": "Vorgeschlagen",
|
||||
"i18n:govoplan-committee.state_accepted": "Angenommen",
|
||||
"i18n:govoplan-committee.state_corrected": "Berichtigt",
|
||||
"i18n:govoplan-committee.domain_recorded": "Namentlich",
|
||||
"i18n:govoplan-committee.domain_public": "Öffentlich",
|
||||
"i18n:govoplan-committee.domain_secret": "Geheim",
|
||||
"Committee": "Gremien",
|
||||
"Search bodies": "Gremien suchen",
|
||||
"Search committee bodies": "Gremien durchsuchen",
|
||||
"Refresh": "Aktualisieren",
|
||||
"New body": "Neues Gremium",
|
||||
"Loading committee workspace": "Gremienarbeitsbereich wird geladen",
|
||||
"Committee bodies": "Gremien",
|
||||
"Bodies": "Gremien",
|
||||
"Edit body": "Gremium bearbeiten",
|
||||
"New meeting": "Neue Sitzung",
|
||||
"Meetings": "Sitzungen",
|
||||
"Meeting workspace": "Sitzungsarbeitsbereich",
|
||||
"Select or create a meeting.": "Wählen oder erstellen Sie eine Sitzung.",
|
||||
"Edit meeting": "Sitzung bearbeiten",
|
||||
"Agenda": "Tagesordnung",
|
||||
"Add item": "Punkt hinzufügen",
|
||||
"No agenda items.": "Keine Tagesordnungspunkte.",
|
||||
"Add vote": "Abstimmung hinzufügen",
|
||||
"Minutes": "Protokolle",
|
||||
"Add minutes": "Protokoll hinzufügen",
|
||||
"No records.": "Keine Datensätze.",
|
||||
"Cancel": "Abbrechen",
|
||||
"Save": "Speichern",
|
||||
"Saving": "Speichern",
|
||||
"Title": "Titel",
|
||||
"State": "Status",
|
||||
"Responsible organization unit ID": "ID der zuständigen Organisationseinheit",
|
||||
"Starts": "Beginn",
|
||||
"Ends": "Ende",
|
||||
"Position": "Position",
|
||||
"Subject type": "Gegenstandsart",
|
||||
"Subject ID": "Gegenstands-ID",
|
||||
"Formal Decision ID": "Formelle Entscheidungs-ID",
|
||||
"Method": "Verfahren",
|
||||
"Eligible voters": "Stimmberechtigte",
|
||||
"Ballot provider (optional)": "Abstimmungsanbieter (optional)",
|
||||
"Voting ballot ID (optional)": "Abstimmungs-ID (optional)",
|
||||
"Choices (comma separated)": "Auswahlmöglichkeiten (kommagetrennt)",
|
||||
"Votes cast": "Abgegebene Stimmen",
|
||||
"Quorum": "Quorum",
|
||||
"Met": "Erfüllt",
|
||||
"Not met": "Nicht erfüllt",
|
||||
"Approval ID": "Genehmigungs-ID",
|
||||
"Evidence record ID": "Nachweisdatensatz-ID",
|
||||
"Minutes record ID": "Protokolldatensatz-ID",
|
||||
"Change reason": "Änderungsgrund",
|
||||
"Provider ballot reference": "Referenz des Abstimmungsanbieters",
|
||||
"Importing": "Importieren",
|
||||
"Finalize": "Abschließen",
|
||||
"Date not set": "Datum nicht festgelegt",
|
||||
"No Committee management permission": "Keine Berechtigung zur Gremienverwaltung",
|
||||
"Edit": "Bearbeiten",
|
||||
"New": "Neu",
|
||||
"Recorded": "Namentlich",
|
||||
"Public": "Öffentlich",
|
||||
"Secret": "Geheim",
|
||||
"Case": "Fall",
|
||||
"Service": "Leistung",
|
||||
"Work Item": "Arbeitsschritt",
|
||||
"Record": "Datensatz"
|
||||
};
|
||||
|
||||
export const generatedTranslations: PlatformTranslations = { en, de };
|
||||
+4
-2
@@ -1,5 +1,6 @@
|
||||
import { createElement, lazy } from "react";
|
||||
import type { PlatformWebModule } from "@govoplan/core-webui";
|
||||
import { generatedTranslations } from "./i18n/generatedTranslations";
|
||||
import "./styles/committee.css";
|
||||
|
||||
|
||||
@@ -7,13 +8,14 @@ const CommitteePage = lazy(() => import("./features/committee/CommitteePage"));
|
||||
|
||||
export const committeeModule: PlatformWebModule = {
|
||||
id: "committee",
|
||||
name: "Committee",
|
||||
label: "i18n:govoplan-committee.committee",
|
||||
version: "0.1.8",
|
||||
optionalDependencies: ["calendar", "files", "mandates", "decisions", "approvals"],
|
||||
translations: generatedTranslations,
|
||||
navItems: [
|
||||
{
|
||||
to: "/committee",
|
||||
label: "Committee",
|
||||
label: "i18n:govoplan-committee.committee",
|
||||
iconName: "gavel",
|
||||
anyOf: ["committee:workspace:read"],
|
||||
order: 38
|
||||
|
||||
@@ -38,6 +38,10 @@
|
||||
margin: 10px 16px 0;
|
||||
}
|
||||
|
||||
.committee-shell > .action-blocker-hint {
|
||||
margin: 10px 16px 0;
|
||||
}
|
||||
|
||||
.committee-workspace {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(220px, 0.7fr) minmax(260px, 0.9fr) minmax(420px, 2fr);
|
||||
@@ -259,6 +263,11 @@
|
||||
gap: 13px;
|
||||
}
|
||||
|
||||
.committee-dialog-help {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.committee-form-grid,
|
||||
.committee-count-grid {
|
||||
display: grid;
|
||||
|
||||
Reference in New Issue
Block a user