Files
agent-desk-ng/web/lib/api/support-availability.ts
mono 2f7a491666 feat: harden support, localize knowledge and add operator availability [skip ci]
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.
2026-10-16 15:21:42 +00:00

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() || "" },
})
}