feat(scheduling-webui): model invitation actions

This commit is contained in:
2026-07-22 04:24:01 +02:00
parent f512784dd3
commit fc356e22c6
3 changed files with 215 additions and 0 deletions

View File

@@ -84,6 +84,7 @@ export type SchedulingRequest = {
anonymous_password_protection_enabled: boolean;
public_participation_policy_enforcement_available: boolean | null;
public_participation_policy_enforcement_reason?: string | null;
participant_invitation_delivery_available: boolean | null;
calendar_integration_enabled: boolean | null;
calendar_id?: string | null;
calendar_freebusy_enabled: boolean | null;
@@ -288,6 +289,18 @@ export type SchedulingNotification = {
export type SchedulingNotificationListResponse = { notifications: SchedulingNotification[] };
export type SchedulingInvitationAction = "copy" | "send" | "revoke";
export type SchedulingInvitationActionResponse = {
participant_id: string;
action: SchedulingInvitationAction;
status: string;
action_url?: string | null;
issued_at?: string | null;
replayed: boolean;
notification?: SchedulingNotification | null;
};
export type SchedulingPollOptionResult = {
option_id: string;
option_key: string;
@@ -446,3 +459,30 @@ export function listSchedulingNotifications(settings: ApiSettings, requestId?: s
const query = requestId ? `?request_id=${encodeURIComponent(requestId)}` : "";
return apiFetch<SchedulingNotificationListResponse>(settings, `/api/v1/scheduling/notifications${query}`);
}
export function issueSchedulingParticipantInvitation(
settings: ApiSettings,
requestId: string,
participantId: string,
participantRevision: string,
action: Exclude<SchedulingInvitationAction, "revoke">
): Promise<SchedulingInvitationActionResponse> {
return apiFetch<SchedulingInvitationActionResponse>(
settings,
`/api/v1/scheduling/requests/${encodeURIComponent(requestId)}/participants/${encodeURIComponent(participantId)}/invitation`,
json({ action, participant_revision: participantRevision })
);
}
export function revokeSchedulingParticipantInvitation(
settings: ApiSettings,
requestId: string,
participantId: string,
participantRevision: string
): Promise<SchedulingInvitationActionResponse> {
return apiFetch<SchedulingInvitationActionResponse>(
settings,
`/api/v1/scheduling/requests/${encodeURIComponent(requestId)}/participants/${encodeURIComponent(participantId)}/invitation`,
{ method: "DELETE", body: JSON.stringify({ participant_revision: participantRevision }) }
);
}

View File

@@ -42,6 +42,20 @@ export type SchedulingSortPhase =
| "determined"
| "past";
export type SchedulingInvitationActionBlock =
| "participation_policy_unavailable"
| "cancellation_notice_expired"
| "delivery_unavailable"
| "no_delivery_target"
| "no_active_invitation"
| "participant_revision_unavailable";
export type SchedulingInvitationActionBlocks = {
copy: SchedulingInvitationActionBlock | null;
send: SchedulingInvitationActionBlock | null;
revoke: SchedulingInvitationActionBlock | null;
};
type DirectorySelection = {
selection_key?: string;
kind?: PeoplePickerItem["kind"];
@@ -268,6 +282,65 @@ export function schedulingRequestIsPast(
return slotEnds.every((value) => Number.isFinite(value) && value < now.getTime());
}
export function schedulingInvitationActionBlocks(
request: SchedulingRequest,
participant: SchedulingParticipant,
now = new Date()
): SchedulingInvitationActionBlocks {
if (!participant.revision) {
return {
copy: "participant_revision_unavailable",
send: "participant_revision_unavailable",
revoke: "participant_revision_unavailable"
};
}
const policyAvailable = Boolean(
request.poll_id &&
request.public_participation_policy_enforcement_available === true
);
let issueBlock: SchedulingInvitationActionBlock | null = policyAvailable
? null
: "participation_policy_unavailable";
if (!issueBlock && request.status === "cancelled") {
const noticeUntil = request.cancellation_notice_until
? Date.parse(request.cancellation_notice_until)
: Number.NaN;
if (!Number.isFinite(noticeUntil) || noticeUntil <= now.getTime()) {
issueBlock = "cancellation_notice_expired";
}
}
const respondentId = participant.respondent_id?.trim() ?? "";
const hasDeliveryTarget = Boolean(
participant.email?.trim() ||
(respondentId && !respondentId.startsWith("scheduling-participant:"))
);
const sendBlock = issueBlock
?? (request.participant_invitation_delivery_available === true
? null
: "delivery_unavailable")
?? (hasDeliveryTarget ? null : "no_delivery_target");
const revokeBlock = participant.poll_invitation_id
? (policyAvailable ? null : "participation_policy_unavailable")
: "no_active_invitation";
return { copy: issueBlock, send: sendBlock, revoke: revokeBlock };
}
export function schedulingPublicInvitationUrl(
actionUrl: string,
applicationOrigin: string
): string | null {
try {
const origin = new URL(applicationOrigin);
const url = new URL(actionUrl, origin);
if (url.origin !== origin.origin || !url.pathname.startsWith("/scheduling/public/")) return null;
return url.toString();
} catch {
return null;
}
}
export function applySchedulingAvailabilityChoice(
slotIds: string[],
current: Record<string, SchedulingAvailabilityValue | "">,