// Availability is computed by the server; never infer presence from HTTP success. import { supportCalendarPayload } from "@/lib/support-availability" import { request } from "@/lib/api/client" import type { ServiceStatus } from "@/lib/generated/enums" import { readSupportChatRuntimeConfig } from "@/lib/sdk/runtime-config" export type SupportPeriod = { start: string; end: string } export type SupportCalendar = { teamId: number mode: "legacy" | "recurring" timezone: string effectiveTimezone: string deploymentTimezone: string weekly: (SupportPeriod & { weekday: number })[] exceptions: { date: string; closed: boolean; periods: SupportPeriod[] }[] } export type AvailabilityReason = "eligible" | "profile_disabled" | "user_disabled" | "team_disabled" | "outside_hours" | "offline" | "busy" | "break" | "unavailable" | "vacation" | "auto_assign_disabled" | "capacity" export type OperatorAvailability = { userId: number profileId: number teamId: number displayName?: string serviceStatus: ServiceStatus effectiveServiceStatus: ServiceStatus returnServiceStatus?: ServiceStatus absenceStartAt?: string absenceEndAt?: string online: boolean eligible: boolean reason: AvailabilityReason calendar: SupportCalendar calendarOpen: boolean nextOpeningAt?: string } export type AvailabilityUpdate = { serviceStatus: ServiceStatus; absenceStartAt?: string; absenceEndAt?: string } export type CalendarPreview = { open: boolean; timezone: string; nextOpeningAt?: string; mode: SupportCalendar["mode"] } export type PublicSupportStatus = { available: boolean; reason: "available" | "outside_hours" | "temporarily_absent" | "capacity"; timezone: string; nextOpeningAt?: string } const base = "/api/dashboard/support_availability" // Bound background reads so one hung request cannot freeze polling forever. async function availabilityRead(path: string, options: Parameters[1] = {}) { const controller = new AbortController() const abort = () => controller.abort() const source = options.signal if (source?.aborted) controller.abort() else source?.addEventListener("abort", abort, { once: true }) const deadline = setTimeout(abort, 10000) try { return await request(path, { ...options, signal: controller.signal }) } finally { clearTimeout(deadline); source?.removeEventListener("abort", abort) } } export function fetchMyAvailability(signal?: AbortSignal) { return availabilityRead(`${base}/me`, { signal }) } export function updateMyAvailability(payload: AvailabilityUpdate) { return request(`${base}/me`, { method: "POST", body: JSON.stringify(payload) }) } export function fetchAvailabilityAgents(conversationId?: number, signal?: AbortSignal) { return availabilityRead(`${base}/agents${conversationId ? `?conversationId=${conversationId}` : ""}`, { signal }) } export function updateAgentAvailability(userId: number, payload: AvailabilityUpdate) { return request(`${base}/agent`, { method: "POST", body: JSON.stringify({ ...payload, userId }) }) } export async function fetchSupportCalendar(teamId: number) { const data = await availabilityRead(`${base}/calendar?teamId=${teamId}`) return { ...data, weekly: data.weekly ?? [], exceptions: (data.exceptions ?? []).map(item => ({ ...item, periods: item.periods ?? [] })) } } export function updateSupportCalendar(calendar: SupportCalendar) { return request(`${base}/calendar`, { method: "POST", body: JSON.stringify(supportCalendarPayload(calendar)) }) } export function previewSupportCalendar(calendar: SupportCalendar, at?: string) { return availabilityRead(`${base}/calendar/preview`, { method: "POST", body: JSON.stringify({ calendar: supportCalendarPayload(calendar), at }) }) } export function fetchPublicSupportStatus(signal?: AbortSignal) { const config = readSupportChatRuntimeConfig() return availabilityRead("/api/support/status", { skipAuth: true, signal, baseUrl: (config.apiBaseUrl || config.baseUrl || process.env.NEXT_PUBLIC_API_BASE_URL || "").trim().replace(/\/$/, ""), headers: { "X-Channel-Id": config.channelId || process.env.NEXT_PUBLIC_OPEN_IM_CHANNEL_ID?.trim() || "" }, }) }