217 lines
9.0 KiB
TypeScript
217 lines
9.0 KiB
TypeScript
import { Play } from "lucide-react";
|
|
import { useCallback, useEffect, useState } from "react";
|
|
import {
|
|
Button,
|
|
Dialog,
|
|
DialogForm,
|
|
DialogSection,
|
|
DismissibleAlert,
|
|
FormField,
|
|
FormGrid,
|
|
LoadingIndicator,
|
|
ToggleSwitch,
|
|
type PlatformRouteContext
|
|
} from "@govoplan/core-webui";
|
|
import {
|
|
listAssistedIntakeProfiles,
|
|
startAssistedFormIntake,
|
|
type AssistedIntakeContext,
|
|
type FormIntakeProfile
|
|
} from "../../api/formsRuntime";
|
|
|
|
|
|
type AssistedIntakeDialogProps = {
|
|
open: boolean;
|
|
settings: PlatformRouteContext["settings"];
|
|
language: string;
|
|
onStarted: (instanceId: string) => void;
|
|
onClose: () => void;
|
|
};
|
|
|
|
export default function AssistedIntakeDialog({
|
|
open,
|
|
settings,
|
|
language,
|
|
onStarted,
|
|
onClose
|
|
}: AssistedIntakeDialogProps) {
|
|
const [profiles, setProfiles] = useState<FormIntakeProfile[]>([]);
|
|
const [profileId, setProfileId] = useState("");
|
|
const [channel, setChannel] = useState<AssistedIntakeContext["channel"]>("counter");
|
|
const [affectedPartyRef, setAffectedPartyRef] = useState("");
|
|
const [representedPartyRef, setRepresentedPartyRef] = useState("");
|
|
const [authorityBasis, setAuthorityBasis] = useState("self");
|
|
const [purpose, setPurpose] = useState("");
|
|
const [legalBasisRef, setLegalBasisRef] = useState("");
|
|
const [consentBasis, setConsentBasis] = useState("");
|
|
const [noticeGiven, setNoticeGiven] = useState(false);
|
|
const [responsibleFunctionRef, setResponsibleFunctionRef] = useState("");
|
|
const [accessibilityNeeds, setAccessibilityNeeds] = useState("");
|
|
const [loading, setLoading] = useState(false);
|
|
const [busy, setBusy] = useState(false);
|
|
const [error, setError] = useState("");
|
|
|
|
const load = useCallback(async (signal?: AbortSignal) => {
|
|
setLoading(true);
|
|
setError("");
|
|
try {
|
|
const result = await listAssistedIntakeProfiles(settings, signal);
|
|
setProfiles(result.profiles);
|
|
setProfileId((current) => current || result.profiles[0]?.profile_id || "");
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
}, [settings]);
|
|
|
|
useEffect(() => {
|
|
if (!open) return undefined;
|
|
const controller = new AbortController();
|
|
void load(controller.signal).catch((reason) => {
|
|
if ((reason as Error).name !== "AbortError") {
|
|
setError(reason instanceof Error ? reason.message : "Assisted intake profiles could not be loaded.");
|
|
}
|
|
});
|
|
return () => controller.abort();
|
|
}, [load, open]);
|
|
|
|
async function start() {
|
|
if (!valid()) return;
|
|
setBusy(true);
|
|
setError("");
|
|
try {
|
|
const result = await startAssistedFormIntake(settings, {
|
|
profileId,
|
|
channel,
|
|
affectedPartyRef: affectedPartyRef.trim(),
|
|
representedPartyRef: representedPartyRef.trim() || undefined,
|
|
authorityBasis,
|
|
purpose: purpose.trim(),
|
|
legalBasisRef: legalBasisRef.trim() || undefined,
|
|
consentBasis: consentBasis.trim() || undefined,
|
|
noticeGiven,
|
|
responsibleFunctionRef: responsibleFunctionRef.trim(),
|
|
language: language || "de",
|
|
accessibilityNeeds: accessibilityNeeds.split(/[,\n]/).map((item) => item.trim()).filter(Boolean)
|
|
});
|
|
if (!result.instance) throw new Error("The assisted session was created without a Form instance.");
|
|
onStarted(result.instance.instance_id);
|
|
} catch (reason) {
|
|
setError(reason instanceof Error ? reason.message : "The assisted intake could not be started.");
|
|
} finally {
|
|
setBusy(false);
|
|
}
|
|
}
|
|
|
|
function valid() {
|
|
return Boolean(
|
|
profileId
|
|
&& affectedPartyRef.trim()
|
|
&& authorityBasis.trim()
|
|
&& purpose.trim()
|
|
&& responsibleFunctionRef.trim()
|
|
);
|
|
}
|
|
|
|
return (
|
|
<Dialog
|
|
open={open}
|
|
title="Start assisted intake"
|
|
description="Capture who is acting, for whom, through which channel, and for what purpose before entering Form values."
|
|
size="large"
|
|
closeDisabled={busy}
|
|
onClose={onClose}
|
|
helpContextId="forms_runtime.assisted-intake"
|
|
helpTopicId="forms_runtime.submissions"
|
|
footer={
|
|
<>
|
|
<Button onClick={onClose} disabled={busy}>Cancel</Button>
|
|
<Button variant="primary" onClick={() => void start()} disabled={busy || loading || !valid()}>
|
|
<Play size={16} aria-hidden="true" />
|
|
Start session
|
|
</Button>
|
|
</>
|
|
}>
|
|
{error && <DismissibleAlert tone="danger" resetKey={error}>{error}</DismissibleAlert>}
|
|
{loading && <LoadingIndicator label="Loading assisted intake profiles" />}
|
|
{!loading && profiles.length === 0 &&
|
|
<DismissibleAlert tone="info">
|
|
No assisted intake profile is enabled. Ask a Forms Runtime administrator to add one for the published Form.
|
|
</DismissibleAlert>
|
|
}
|
|
{!loading && profiles.length > 0 &&
|
|
<DialogForm onSubmit={(event) => { event.preventDefault(); void start(); }}>
|
|
<DialogSection>
|
|
<FormGrid columns={2} gap="small" collapseAt="workspace">
|
|
<FormField label="Published Form">
|
|
<select value={profileId} onChange={(event) => setProfileId(event.target.value)} disabled={busy}>
|
|
{profiles.map((profile) =>
|
|
<option key={profile.profile_id} value={profile.profile_id}>
|
|
{profileTitle(profile)} · revision {profile.definition_ref.version}
|
|
</option>
|
|
)}
|
|
</select>
|
|
</FormField>
|
|
<FormField label="Intake channel">
|
|
<select value={channel} onChange={(event) => setChannel(event.target.value as AssistedIntakeContext["channel"])} disabled={busy}>
|
|
<option value="counter">Service counter</option>
|
|
<option value="telephone">Telephone</option>
|
|
<option value="paper">Paper</option>
|
|
<option value="email">Email</option>
|
|
<option value="mobile">Mobile service</option>
|
|
<option value="representative">Representative</option>
|
|
<option value="offline_import">Offline import</option>
|
|
</select>
|
|
</FormField>
|
|
<FormField label="Affected party reference" help="Use the governed person or organization reference; do not enter a display name only.">
|
|
<input value={affectedPartyRef} onChange={(event) => setAffectedPartyRef(event.target.value)} disabled={busy} required />
|
|
</FormField>
|
|
<FormField label="Represented party reference" help="Optional when the affected party is acting directly.">
|
|
<input value={representedPartyRef} onChange={(event) => setRepresentedPartyRef(event.target.value)} disabled={busy} />
|
|
</FormField>
|
|
<FormField label="Authority basis">
|
|
<select value={authorityBasis} onChange={(event) => setAuthorityBasis(event.target.value)} disabled={busy}>
|
|
<option value="self">Acting for self</option>
|
|
<option value="documented_representation">Documented representation</option>
|
|
<option value="legal_guardianship">Legal guardianship</option>
|
|
<option value="statutory_authority">Statutory authority</option>
|
|
</select>
|
|
</FormField>
|
|
<FormField label="Responsible function reference">
|
|
<input value={responsibleFunctionRef} onChange={(event) => setResponsibleFunctionRef(event.target.value)} disabled={busy} required />
|
|
</FormField>
|
|
<FormField label="Purpose">
|
|
<input value={purpose} onChange={(event) => setPurpose(event.target.value)} disabled={busy} required />
|
|
</FormField>
|
|
<FormField label="Legal basis reference">
|
|
<input value={legalBasisRef} onChange={(event) => setLegalBasisRef(event.target.value)} disabled={busy} />
|
|
</FormField>
|
|
<FormField label="Consent basis">
|
|
<input value={consentBasis} onChange={(event) => setConsentBasis(event.target.value)} disabled={busy} />
|
|
</FormField>
|
|
<FormField label="Accessibility or communication support" help="Separate multiple needs with commas.">
|
|
<input value={accessibilityNeeds} onChange={(event) => setAccessibilityNeeds(event.target.value)} disabled={busy} />
|
|
</FormField>
|
|
</FormGrid>
|
|
</DialogSection>
|
|
<DialogSection variant="inset">
|
|
<ToggleSwitch
|
|
label="Privacy and procedural notice was provided"
|
|
checked={noticeGiven}
|
|
onChange={setNoticeGiven}
|
|
disabled={busy}
|
|
help="Record the fact of notice here; retain any separately required evidence through its owning module."
|
|
/>
|
|
</DialogSection>
|
|
</DialogForm>
|
|
}
|
|
</Dialog>
|
|
);
|
|
}
|
|
|
|
function profileTitle(profile: FormIntakeProfile): string {
|
|
const title = profile.metadata.definition_title;
|
|
return typeof title === "string" && title.trim()
|
|
? title
|
|
: profile.definition_ref.label ?? profile.definition_ref.object_id;
|
|
}
|