Files
agent-desk-ng/web/components/support-availability/public-status.tsx
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

48 lines
2.8 KiB
TypeScript

"use client"
import { useEffect, useState } from "react"
import { RefreshCwIcon } from "lucide-react"
import { Button } from "@/components/ui/button"
import { useAppLocale, useI18n } from "@/i18n/provider"
import { fetchPublicSupportStatus, type PublicSupportStatus } from "@/lib/api/support-availability"
import { formatAvailabilityTime } from "@/lib/support-availability"
// Public server aggregates only. This never reads employee identities or private absence data.
// A successful HTTP check is not a socket connection and is not itself human presence.
export function PublicSupportAvailability({ identityScope }: { identityScope: string }) {
return <PublicSupportAvailabilityContent key={identityScope} />
}
function PublicSupportAvailabilityContent() {
const t = useI18n()
const { locale } = useAppLocale()
const [status, setStatus] = useState<PublicSupportStatus | null>(null)
const [failed, setFailed] = useState(false)
const [retry, setRetry] = useState(0)
useEffect(() => {
let active = true
let inFlight = false
let controller: AbortController | null = null
async function check() {
if (inFlight) return
inFlight = true
controller = new AbortController()
try {
const result = await fetchPublicSupportStatus(controller.signal)
if (active) { setStatus(result); setFailed(false) }
} catch { if (active) { setStatus(null); setFailed(true) } }
finally { inFlight = false }
}
void check()
const refresh = () => { if (document.visibilityState === "visible") void check() }
window.addEventListener("focus", refresh)
document.addEventListener("visibilitychange", refresh)
const timer = window.setInterval(refresh, 30000)
return () => { active = false; controller?.abort(); window.clearInterval(timer); window.removeEventListener("focus", refresh); document.removeEventListener("visibilitychange", refresh) }
}, [retry])
const reason = status?.available ? "available" : status && ["outside_hours", "temporarily_absent", "capacity"].includes(status.reason) ? status.reason : "unknown"
return <div className="mt-2 flex items-start justify-between gap-2 border-t border-border/50 pt-2 text-xs text-muted-foreground" aria-live="polite">
<div className="min-w-0 space-y-1"><p>{t(!status && !failed ? "supportAvailability.publicChecking" : `supportAvailability.public.${reason}`)}</p>{status?.nextOpeningAt ? <p>{t("supportAvailability.publicNextOpening", { time: formatAvailabilityTime(status.nextOpeningAt, status.timezone, locale) })}</p> : null}</div>
{failed ? <Button type="button" variant="ghost" size="icon" className="size-6 shrink-0" aria-label={t("supportAvailability.retry")} onClick={() => setRetry(value => value + 1)}><RefreshCwIcon className="size-3" /></Button> : null}
</div>
}