Source-only publication of reviewed commit 64e96a7f423673dc29ad56485be9fc79bce8af1c.
Passed in isolation: focused frontend/backend tests, race checks, TypeScript typecheck, lint, Go compilation, Flowgram build, and deterministic SDK build/tests.
Not accepted as a production release: Next.js export/native assembly remains blocked by environment memory limits; browser interaction and MySQL staging acceptance remain unverified. No production server, database/schema, runtime configuration, deployment, release tag, or workflow execution was performed.
Expected source tree: 0a4a7528e9.
68 lines
4.2 KiB
TypeScript
68 lines
4.2 KiB
TypeScript
// 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<T>(path: string, options: Parameters<typeof request>[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<T>(path, { ...options, signal: controller.signal }) }
|
|
finally { clearTimeout(deadline); source?.removeEventListener("abort", abort) }
|
|
}
|
|
export function fetchMyAvailability(signal?: AbortSignal) { return availabilityRead<OperatorAvailability | null>(`${base}/me`, { signal }) }
|
|
export function updateMyAvailability(payload: AvailabilityUpdate) { return request<OperatorAvailability>(`${base}/me`, { method: "POST", body: JSON.stringify(payload) }) }
|
|
export function fetchAvailabilityAgents(conversationId?: number, signal?: AbortSignal) { return availabilityRead<OperatorAvailability[]>(`${base}/agents${conversationId ? `?conversationId=${conversationId}` : ""}`, { signal }) }
|
|
export function updateAgentAvailability(userId: number, payload: AvailabilityUpdate) { return request<OperatorAvailability>(`${base}/agent`, { method: "POST", body: JSON.stringify({ ...payload, userId }) }) }
|
|
export async function fetchSupportCalendar(teamId: number) {
|
|
const data = await availabilityRead<SupportCalendar>(`${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<SupportCalendar>(`${base}/calendar`, { method: "POST", body: JSON.stringify(supportCalendarPayload(calendar)) }) }
|
|
export function previewSupportCalendar(calendar: SupportCalendar, at?: string) { return availabilityRead<CalendarPreview>(`${base}/calendar/preview`, { method: "POST", body: JSON.stringify({ calendar: supportCalendarPayload(calendar), at }) }) }
|
|
export function fetchPublicSupportStatus(signal?: AbortSignal) {
|
|
const config = readSupportChatRuntimeConfig()
|
|
return availabilityRead<PublicSupportStatus>("/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() || "" },
|
|
})
|
|
}
|