import { useEffect, useMemo, useState, type FormEvent } from "react"; import { Link, useParams } from "react-router-dom"; import { Button, Card, DismissibleAlert, FormField, LoadingFrame, formatDateTime, type ApiSettings, type AuthInfo } from "@govoplan/core-webui"; import { getPublicSchedulingParticipation, submitPublicSchedulingParticipation, type SchedulingAvailabilityValue, type SchedulingPublicParticipationAccessPayload, type SchedulingPublicParticipationResponse } from "../../api/scheduling"; import { applySchedulingAvailabilityChoice } from "./schedulingViewModel"; type SchedulingPublicPageProps = { settings: ApiSettings; auth: AuthInfo | null; }; const I18N = { accessDetails: "i18n:govoplan-scheduling.access_details.79c06b89", accessHelp: "i18n:govoplan-scheduling.enter_the_details_supplied_with_the_invitation_for_privacy.81ba419c", accessRequest: "i18n:govoplan-scheduling.open_scheduling_request.31829cce", alreadySubmitted: "i18n:govoplan-scheduling.responses_may_be_updated_while_this_request_remains_open.4faecbbe", answerRequired: "i18n:govoplan-scheduling.choose_availability_for_at_least_one_candidate_slot.28d2111f", available: "i18n:govoplan-scheduling.available.7c62a142", backToScheduling: "i18n:govoplan-scheduling.open_in_scheduling.48df1541", comment: "i18n:govoplan-scheduling.comment.d03495b1", cancelled: "i18n:govoplan-scheduling.this_scheduling_request_was_cancelled.1af3c85e", cancellationNoticeUntil: "i18n:govoplan-scheduling.cancellation_notice_available_until.f840d1e6", deadline: "i18n:govoplan-scheduling.response_deadline.7fd9e3aa", email: "i18n:govoplan-scheduling.participant_email.2cadfd9e", invalidAccess: "i18n:govoplan-scheduling.this_scheduling_link_is_invalid_expired_or_the_access_details.8e7aa197", loading: "i18n:govoplan-scheduling.loading_scheduling_request.43c39c1b", maybe: "i18n:govoplan-scheduling.maybe.56dd8d0b", noLongerOpen: "i18n:govoplan-scheduling.this_scheduling_request_is_no_longer_accepting_responses.c612e78a", password: "i18n:govoplan-scheduling.guest_password.94545e82", response: "i18n:govoplan-scheduling.your_availability.f86c8215", saved: "i18n:govoplan-scheduling.your_response_has_been_recorded.b855088d", submit: "i18n:govoplan-scheduling.submit_response.a5f0c053", unavailable: "i18n:govoplan-scheduling.unavailable.2c9c1f79" } as const; function accessPayload(email: string, password: string): SchedulingPublicParticipationAccessPayload { return { participant_email: email.trim() || null, password: password || null }; } function initialAvailability(response: SchedulingPublicParticipationResponse): Record { const previous = new Map(response.answers.map((answer) => [answer.slot_id, answer.value])); return Object.fromEntries(response.slots.map((slot) => [slot.id, previous.get(slot.id) ?? ""])); } function newIdempotencyKey(): string { if (typeof crypto !== "undefined" && "randomUUID" in crypto) return crypto.randomUUID(); return `scheduling-response-${Date.now()}-${Math.random().toString(16).slice(2)}`; } export default function SchedulingPublicPage({ settings, auth }: SchedulingPublicPageProps) { const { requestId = "", token = "" } = useParams(); const [response, setResponse] = useState(null); const [email, setEmail] = useState(""); const [password, setPassword] = useState(""); const [availability, setAvailability] = useState>({}); const [comment, setComment] = useState(""); const [loading, setLoading] = useState(true); const [saving, setSaving] = useState(false); const [accessAttempted, setAccessAttempted] = useState(false); const [error, setError] = useState(""); const [success, setSuccess] = useState(""); const slotIds = useMemo(() => response?.slots.map((slot) => slot.id) ?? [], [response]); const collecting = response?.status === "collecting"; function applyResponse(next: SchedulingPublicParticipationResponse) { setResponse(next); setAvailability(initialAvailability(next)); setComment(next.comment ?? ""); setError(""); } useEffect(() => { let cancelled = false; setLoading(true); setResponse(null); setAccessAttempted(false); setError(""); void getPublicSchedulingParticipation(settings, requestId, token, {}) .then((next) => { if (!cancelled) applyResponse(next); }) .catch(() => undefined) .finally(() => { if (!cancelled) setLoading(false); }); return () => { cancelled = true; }; }, [requestId, settings.apiBaseUrl, settings.apiKey, token]); async function openRequest(event: FormEvent) { event.preventDefault(); setLoading(true); setAccessAttempted(true); setError(""); try { applyResponse(await getPublicSchedulingParticipation(settings, requestId, token, accessPayload(email, password))); } catch { setResponse(null); setError(I18N.invalidAccess); } finally { setLoading(false); } } async function saveResponse(event: FormEvent) { event.preventDefault(); if (!response) return; if (!response.slots.some((slot) => availability[slot.id])) { setError(I18N.answerRequired); return; } setSaving(true); setError(""); setSuccess(""); try { const next = await submitPublicSchedulingParticipation(settings, requestId, token, { ...accessPayload(email, password), answers: response.slots .filter((slot) => availability[slot.id]) .map((slot) => ({ slot_id: slot.id, value: availability[slot.id] as SchedulingAvailabilityValue, option_revision: slot.revision })), comment: response.allow_comments ? comment : null, idempotency_key: newIdempotencyKey() }); applyResponse(next); setSuccess(I18N.saved); } catch { setError(I18N.invalidAccess); } finally { setSaving(false); } } return (
{auth && (
{I18N.backToScheduling}
)} {!response && !loading && (

{I18N.accessHelp}

{accessAttempted && error && {error}}
setEmail(event.target.value)} /> setPassword(event.target.value)} />
)} {response && (
{response.description &&

{response.description}

}
{response.location && <>
i18n:govoplan-scheduling.location.d219c681
{response.location}
} {response.deadline_at && <>
{I18N.deadline}
{formatDateTime(response.deadline_at)}
}
{response.cancellation_notice_only ? {I18N.cancelled} : !collecting && {I18N.noLongerOpen}} {response.has_response && collecting && {I18N.alreadySubmitted}} {response.cancellation_notice_only && response.cancellation_notice_until && (

{I18N.cancellationNoticeUntil}: {formatDateTime(response.cancellation_notice_until)}

)}
{error && {error}} {success && {success}} {!response.cancellation_notice_only &&
{response.slots.map((slot) => (
{slot.label}

{formatDateTime(slot.start_at)} – {formatDateTime(slot.end_at)}

{slot.description &&

{slot.description}

} {(slot.location || response.location) &&

{slot.location || response.location}

}
{([ ["available", I18N.available], ...(response.allow_maybe ? [["maybe", I18N.maybe] as const] : []), ["unavailable", I18N.unavailable] ] as Array<[SchedulingAvailabilityValue, string]>).map(([value, label]) => ( ))}
))}
{response.allow_comments && (