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.
This commit is contained in:
63
web/app/(dashboard)/dashboard/support-availability/page.tsx
Normal file
63
web/app/(dashboard)/dashboard/support-availability/page.tsx
Normal file
@@ -0,0 +1,63 @@
|
||||
"use client"
|
||||
|
||||
import { RefreshCwIcon } from "lucide-react"
|
||||
import { useEffect, useState } from "react"
|
||||
import { useAuth } from "@/components/auth-provider"
|
||||
import { useConfirm } from "@/components/confirm-provider"
|
||||
import { DashboardPage, DashboardToolbar } from "@/components/dashboard-page"
|
||||
import { OptionCombobox } from "@/components/option-combobox"
|
||||
import { OperatorStatusControl } from "@/components/support-availability/operator-status"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { useI18n } from "@/i18n/provider"
|
||||
import { fetchAgentTeamsAll, type AdminAgentTeam } from "@/lib/api/admin"
|
||||
import { fetchSupportCalendar, type SupportCalendar } from "@/lib/api/support-availability"
|
||||
import { CalendarEditor } from "./_components/calendar-editor"
|
||||
|
||||
// A scoped configuration editor, not a paginated CRUD resource.
|
||||
export default function SupportAvailabilityPage() {
|
||||
const { session } = useAuth()
|
||||
return <SupportAvailabilityContent key={session?.user.id ?? "signed-out"} />
|
||||
}
|
||||
function SupportAvailabilityContent() {
|
||||
const t = useI18n()
|
||||
const { session } = useAuth()
|
||||
const confirm = useConfirm()
|
||||
const can = (permission: string) => Boolean(session?.permissions.includes(permission))
|
||||
const canDefault = can("supportConfig.view")
|
||||
const canTeams = can("agentTeam.view") && can("agentTeamSchedule.view")
|
||||
const [teams, setTeams] = useState<AdminAgentTeam[]>([])
|
||||
const [teamId, setTeamId] = useState<number | null>(canDefault ? 0 : null)
|
||||
const [calendar, setCalendar] = useState<SupportCalendar | null>(null)
|
||||
const [error, setError] = useState("")
|
||||
const [teamError, setTeamError] = useState("")
|
||||
const [loading, setLoading] = useState(true)
|
||||
const [dirty, setDirty] = useState(false)
|
||||
const [busy, setBusy] = useState(false)
|
||||
const [revision, setRevision] = useState(0)
|
||||
useEffect(() => {
|
||||
if (!canTeams) return
|
||||
let active = true
|
||||
fetchAgentTeamsAll().then(data => { if (active) { setTeams(data); setTeamError(""); setTeamId(current => current ?? data[0]?.id ?? null); if (!data.length && !canDefault) setLoading(false) } }).catch(error => { if (active) { setTeamError(error instanceof Error ? error.message : t("supportAvailability.loadFailed")); if (!canDefault) setLoading(false) } })
|
||||
return () => { active = false }
|
||||
}, [canTeams, canDefault, revision, t])
|
||||
useEffect(() => {
|
||||
if (teamId === null) return
|
||||
let active = true
|
||||
fetchSupportCalendar(teamId).then(data => { if (active) { setCalendar(data); setError("") } }).catch(error => { if (active) { setCalendar(null); setError(error instanceof Error ? error.message : t("supportAvailability.loadFailed")) } }).finally(() => { if (active) setLoading(false) })
|
||||
return () => { active = false }
|
||||
}, [teamId, revision, t])
|
||||
async function changeScope(next: number) {
|
||||
if (busy) return
|
||||
if (dirty && !(await confirm({ title: t("supportAvailability.discardTitle"), description: t("supportAvailability.discardDescription"), confirmText: t("supportAvailability.discard") }))) return
|
||||
setDirty(false); setCalendar(null); setLoading(true); setTeamId(next)
|
||||
}
|
||||
const options = [...(canDefault ? [{ value: "0", label: t("supportAvailability.deploymentDefault") }] : []), ...teams.map(team => ({ value: String(team.id), label: team.name }))]
|
||||
return <DashboardPage>
|
||||
<DashboardToolbar actions={<OperatorStatusControl />}><div><h1 className="text-lg font-semibold">{t("supportAvailability.title")}</h1><p className="mt-1 max-w-3xl text-sm text-muted-foreground">{t("supportAvailability.description")}</p></div></DashboardToolbar>
|
||||
{!canDefault && !canTeams ? <p className="text-sm text-muted-foreground">{t("supportAvailability.noPermission")}</p> : <>
|
||||
<div className="flex max-w-xl items-center gap-2"><div className="min-w-0 flex-1"><OptionCombobox value={teamId === null ? "" : String(teamId)} disabled={busy} options={options} onChange={value => void changeScope(Number(value))} ariaLabel={t("supportAvailability.selectScope")} placeholder={t("supportAvailability.selectScope")} /></div><Button type="button" variant="outline" size="icon" aria-label={t("supportAvailability.refresh")} disabled={dirty || busy} onClick={() => { setLoading(true); setRevision(value => value + 1) }}><RefreshCwIcon className="size-4" /></Button></div>
|
||||
{teamError ? <p role="alert" className="text-sm text-destructive">{teamError}</p> : null}
|
||||
{error ? <div role="alert" className="space-y-2 rounded-md border p-4"><p className="text-sm text-destructive">{error}</p><Button variant="outline" onClick={() => { setLoading(true); setRevision(value => value + 1) }}>{t("supportAvailability.retry")}</Button></div> : calendar && calendar.teamId === teamId ? <CalendarEditor key={`${teamId}:${revision}`} initial={calendar} writable={can(teamId === 0 ? "supportConfig.update" : "agentTeamSchedule.update")} onDirtyChange={setDirty} onBusyChange={setBusy} onSaved={value => { setCalendar(value); setDirty(false); setRevision(revision => revision + 1) }} /> : <p className="p-4 text-sm text-muted-foreground" role="status">{t(teamId === null && !loading ? "supportAvailability.noTeams" : "supportAvailability.loading")}</p>}
|
||||
</>}
|
||||
</DashboardPage>
|
||||
}
|
||||
Reference in New Issue
Block a user