import { useEffect, useMemo, useState, type FormEvent } from "react"; import { Bell, CalendarCheck, Check, Clock, Plus, RefreshCw, Send, XCircle } from "lucide-react"; import { Button, hasScope, type ApiSettings, type AuthInfo } from "@govoplan/core-webui"; import { closeSchedulingRequest, createSchedulingCalendarEvent, createSchedulingHolds, createSchedulingNotifications, createSchedulingRequest, decideSchedulingRequest, evaluateSchedulingFreeBusy, listSchedulingNotifications, listSchedulingRequests, openSchedulingRequest, schedulingSummary, type SchedulingNotification, type SchedulingRequest, type SchedulingSummaryResponse } from "../../api/scheduling"; type SlotDraft = { label: string; start_at: string; end_at: string }; type ParticipantDraft = { display_name: string; email: string; required: boolean }; const now = new Date(); const defaultStart = new Date(now.getTime() + 24 * 60 * 60 * 1000); defaultStart.setMinutes(0, 0, 0); const defaultEnd = new Date(defaultStart.getTime() + 60 * 60 * 1000); function localValue(date: Date): string { const pad = (value: number) => value.toString().padStart(2, "0"); return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())}T${pad(date.getHours())}:${pad(date.getMinutes())}`; } function isoFromLocal(value: string): string { return new Date(value).toISOString(); } export default function SchedulingPage({ settings, auth }: { settings: ApiSettings; auth: AuthInfo }) { const [requests, setRequests] = useState([]); const [selectedId, setSelectedId] = useState(""); const [summary, setSummary] = useState(null); const [notifications, setNotifications] = useState([]); const [loading, setLoading] = useState(true); const [saving, setSaving] = useState(false); const [error, setError] = useState(""); const [title, setTitle] = useState("Scheduling poll"); const [location, setLocation] = useState(""); const [calendarId, setCalendarId] = useState(""); const [calendarEnabled, setCalendarEnabled] = useState(false); const [slots, setSlots] = useState([ { label: "Option 1", start_at: localValue(defaultStart), end_at: localValue(defaultEnd) } ]); const [participants, setParticipants] = useState([{ display_name: "", email: "", required: true }]); const canWrite = hasScope(auth, "scheduling:schedule:write"); const selected = useMemo(() => requests.find((item) => item.id === selectedId) || requests[0] || null, [requests, selectedId]); const optionResultById = useMemo(() => { const map = new Map(); for (const result of summary?.poll_summary.option_results || []) map.set(result.option_id, result); return map; }, [summary]); useEffect(() => { void loadRequests(); }, [settings.apiBaseUrl, settings.apiKey, settings.accessToken]); useEffect(() => { if (!selected?.id) { setSummary(null); setNotifications([]); return; } void loadDetails(selected.id); }, [selected?.id]); async function loadRequests() { setLoading(true); setError(""); try { const response = await listSchedulingRequests(settings); setRequests(response.requests); if (!selectedId && response.requests[0]) setSelectedId(response.requests[0].id); } catch (err) { setError(errorMessage(err)); } finally { setLoading(false); } } async function loadDetails(requestId: string) { try { const [summaryResponse, notificationResponse] = await Promise.all([ schedulingSummary(settings, requestId).catch(() => null), listSchedulingNotifications(settings, requestId).catch(() => ({ notifications: [] })) ]); setSummary(summaryResponse); setNotifications(notificationResponse.notifications); } catch (err) { setError(errorMessage(err)); } } async function createRequest(event: FormEvent) { event.preventDefault(); if (!canWrite) return; setSaving(true); setError(""); try { const request = await createSchedulingRequest(settings, { title, location: location || null, status: "collecting", calendar: { enabled: calendarEnabled, calendar_id: calendarId || null, freebusy_enabled: calendarEnabled, tentative_holds_enabled: calendarEnabled, create_event_on_decision: calendarEnabled }, slots: slots.map((slot) => ({ label: slot.label || null, start_at: isoFromLocal(slot.start_at), end_at: isoFromLocal(slot.end_at), location: location || null })), participants: participants .filter((participant) => participant.email || participant.display_name) .map((participant) => ({ ...participant, participant_type: "external" })) }); setRequests((items) => [request, ...items]); setSelectedId(request.id); } catch (err) { setError(errorMessage(err)); } finally { setSaving(false); } } async function runAction(action: () => Promise<{ request?: SchedulingRequest } | SchedulingSummaryResponse | unknown>) { if (!selected || !canWrite) return; setSaving(true); setError(""); try { const response = await action(); const updated = isRequestEnvelope(response) ? response.request : null; if (updated) { setRequests((items) => items.map((item) => (item.id === updated.id ? updated : item))); } await loadDetails(selected.id); } catch (err) { setError(errorMessage(err)); } finally { setSaving(false); } } return (
{selected?.title || "Scheduling"}
{error ?
{error}
: null}
void createRequest(event)}>
New poll
{calendarEnabled ? ( ) : null}
Slots
{slots.map((slot, index) => (
updateSlot(index, "label", event.target.value)} /> updateSlot(index, "start_at", event.target.value)} /> updateSlot(index, "end_at", event.target.value)} />
))}
Participants
{participants.map((participant, index) => (
updateParticipant(index, "display_name", event.target.value)} /> updateParticipant(index, "email", event.target.value)} />
))}
{selected ? ( <>
{selected.status} {selected.participants.length} participants {summary?.poll_summary.response_count || 0} responses
{selected.slots.map((slot) => { const result = slot.poll_option_id ? optionResultById.get(slot.poll_option_id) : null; return (
{slot.label} {formatRange(slot.start_at, slot.end_at)}
{slot.freebusy_status || "unchecked"} {result?.values.available || 0} yes {result?.values.maybe || 0} maybe {result?.values.unavailable || 0} no
); })}
Participants
{selected.participants.map((participant) => (
{participant.display_name || participant.email || participant.id} {participant.status}
))}
Outbox
{notifications.map((notification) => (
{notification.event_kind} {notification.status}
))}
) : (
No scheduling request
)}
); function updateSlot(index: number, field: keyof SlotDraft, value: string) { setSlots((items) => items.map((item, itemIndex) => (itemIndex === index ? { ...item, [field]: value } : item))); } function updateParticipant(index: number, field: keyof ParticipantDraft, value: string) { setParticipants((items) => items.map((item, itemIndex) => (itemIndex === index ? { ...item, [field]: value } : item))); } } function isRequestEnvelope(value: unknown): value is { request: SchedulingRequest } { return Boolean(value && typeof value === "object" && "request" in value); } function formatRange(start: string, end: string): string { const formatter = new Intl.DateTimeFormat(undefined, { dateStyle: "short", timeStyle: "short" }); return `${formatter.format(new Date(start))} - ${formatter.format(new Date(end))}`; } function errorMessage(error: unknown): string { return error instanceof Error ? error.message : "Request failed"; }