feat(scheduling): prefill participant availability

This commit is contained in:
2026-07-20 18:18:18 +02:00
parent ec538f524d
commit 6f298c36fe
3 changed files with 67 additions and 4 deletions

View File

@@ -36,6 +36,7 @@ import {
createSchedulingRequest,
decideSchedulingRequest,
evaluateSchedulingFreeBusy,
getSchedulingAvailabilityResponse,
listSchedulingNotifications,
listSchedulingRequests,
openSchedulingRequest,
@@ -176,6 +177,7 @@ export default function SchedulingPage({ settings, auth }: { settings: ApiSettin
const [slots, setSlots] = useState<SlotDraft[]>(() => initialSlots(translateText));
const [participants, setParticipants] = useState<ParticipantDraft[]>(() => [emptyParticipant()]);
const [availability, setAvailability] = useState<Record<string, SchedulingAvailabilityValue | "">>({});
const [availabilityLoading, setAvailabilityLoading] = useState(false);
const canWrite = hasScope(auth, "scheduling:schedule:write");
const canRespond = hasScope(auth, "scheduling:availability:write");
@@ -240,6 +242,39 @@ export default function SchedulingPage({ settings, auth }: { settings: ApiSettin
void loadDetails(selected.id);
}, [creating, selected?.id]);
useEffect(() => {
if (!selected?.id || !selectedParticipant || !canRespond || creating || selected.status !== "collecting") {
setAvailabilityLoading(false);
return undefined;
}
let cancelled = false;
setAvailabilityLoading(true);
void getSchedulingAvailabilityResponse(settings, selected.id)
.then((response) => {
if (cancelled) return;
setAvailability(Object.fromEntries(response.answers.map((answer) => [answer.slot_id, answer.value])));
})
.catch((err) => {
if (!cancelled) setError(errorMessage(err, translateText(I18N.requestFailed)));
})
.finally(() => {
if (!cancelled) setAvailabilityLoading(false);
});
return () => {
cancelled = true;
};
}, [
canRespond,
creating,
selected?.id,
selected?.status,
selectedParticipant?.id,
settings.accessToken,
settings.apiBaseUrl,
settings.apiKey,
translateText
]);
useEffect(() => {
if (!creating || !draftDirty) return undefined;
const preventUnsavedExit = (event: BeforeUnloadEvent) => event.preventDefault();
@@ -366,7 +401,7 @@ export default function SchedulingPage({ settings, auth }: { settings: ApiSettin
async function sendAvailability(event: FormEvent<HTMLFormElement>) {
event.preventDefault();
if (!selected || !selectedParticipant || !canRespond) return;
if (!selected || !selectedParticipant || !canRespond || availabilityLoading) return;
const answers = selected.slots
.map((slot) => ({ slot_id: slot.id, value: availability[slot.id] }))
.filter((answer): answer is { slot_id: string; value: SchedulingAvailabilityValue } => Boolean(answer.value));
@@ -578,12 +613,16 @@ export default function SchedulingPage({ settings, auth }: { settings: ApiSettin
variant="primary"
type="submit"
form="scheduling-response-form"
disabled={saving || !canRespond || (selectedParticipant.status === "responded" && !selected.allow_participant_updates)}>
disabled={saving || availabilityLoading || !canRespond || (selectedParticipant.status === "responded" && !selected.allow_participant_updates)}>
<Send aria-hidden="true" size={16} />
{selectedParticipant.status === "responded" ? I18N.updateResponse : I18N.sendResponse}
</Button>
)}>
<form id="scheduling-response-form" className="scheduling-response-card" onSubmit={(event) => void sendAvailability(event)}>
<form
id="scheduling-response-form"
className="scheduling-response-card"
aria-busy={availabilityLoading}
onSubmit={(event) => void sendAvailability(event)}>
<p>{selectedParticipant.status === "responded"
? (selected.allow_participant_updates ? I18N.responseReplace : I18N.responseRecorded)
: I18N.invitationHelp}</p>
@@ -596,7 +635,7 @@ export default function SchedulingPage({ settings, auth }: { settings: ApiSettin
<span><strong>{slot.label}</strong><small>{formatRange(slot.start_at, slot.end_at)}</small></span>
<select
value={availability[slot.id] ?? ""}
disabled={selectedParticipant.status === "responded" && !selected.allow_participant_updates}
disabled={availabilityLoading || (selectedParticipant.status === "responded" && !selected.allow_participant_updates)}
onChange={(event) => setAvailability((current) => ({
...current,
[slot.id]: event.target.value as SchedulingAvailabilityValue | ""