feat(scheduling-webui): build two-pane request workspace

This commit is contained in:
2026-07-21 21:17:26 +02:00
parent b1725b8f59
commit 82b158c170
7 changed files with 1559 additions and 627 deletions
@@ -1,4 +1,8 @@
import type { SchedulingParticipant, SchedulingRequest } from "../../api/scheduling";
import type {
SchedulingAvailabilityValue,
SchedulingParticipant,
SchedulingRequest
} from "../../api/scheduling";
export type SchedulingActor = {
accountId?: string | null;
@@ -28,20 +32,27 @@ export function schedulingActorIds(actor: SchedulingActor): string[] {
actor.membershipId,
actor.identityId,
actor.email
].filter((value): value is string => Boolean(value))));
].filter((value): value is string => Boolean(value)).flatMap((value) =>
value.includes("@") ? [value, value.trim().toLowerCase()] : [value]
)));
}
export function schedulingParticipantForActor(
request: SchedulingRequest,
actor: SchedulingActor
): SchedulingParticipant | null {
const projected = request.participants.find(
(participant) => participant.is_current_participant
);
if (projected) return projected;
const ids = new Set(schedulingActorIds(actor));
return request.participants.find((participant) =>
Boolean(
return request.participants.find((participant) => {
const email = participant.email?.trim().toLowerCase();
return Boolean(
(participant.respondent_id && ids.has(participant.respondent_id)) ||
(participant.email && ids.has(participant.email))
)
) ?? null;
(email && ids.has(email))
);
}) ?? null;
}
export function schedulingRequestIsOwned(
@@ -137,6 +148,26 @@ export function schedulingRequestIsPast(
return slotEnds.every((value) => Number.isFinite(value) && value < now.getTime());
}
export function applySchedulingAvailabilityChoice(
slotIds: string[],
current: Record<string, SchedulingAvailabilityValue | "">,
slotId: string,
value: SchedulingAvailabilityValue | "",
singleChoice: boolean
): Record<string, SchedulingAvailabilityValue | ""> {
if (!singleChoice || !["available", "maybe"].includes(value)) {
return { ...current, [slotId]: value };
}
return Object.fromEntries(slotIds.map((candidateId) => [
candidateId,
candidateId === slotId
? value
: current[candidateId] && current[candidateId] !== "unavailable"
? "unavailable"
: current[candidateId] ?? ""
]));
}
const SORT_PHASE_ORDER: Record<SchedulingSortPhase, number> = {
unanswered: 0,
answered: 1,