// Modified in this independent fork for support availability; see docs/hardening/README.md. "use client"; import { PanelLeftCloseIcon, PanelLeftOpenIcon, UserCogIcon, } from "lucide-react"; import { useCallback, useEffect, useMemo, useState } from "react"; import { DashboardCrudPage, type DashboardCrudColumn, type DashboardCrudFilter, } from "@/components/dashboard/crud"; import { isSameSession } from "@/lib/session-request-guard"; import type { AuthSession } from "@/lib/auth"; import { useAuth } from "@/components/auth-provider"; import { AVAILABILITY_UPDATED_EVENT, notifyAvailabilityUpdated, OperatorStatusDialog } from "@/components/support-availability/operator-status"; import { fetchAvailabilityAgents, type OperatorAvailability } from "@/lib/api/support-availability"; import { availabilityReasonLabel, availabilityStatusLabel, availabilityStatusOptions } from "@/lib/support-availability-i18n"; import { Badge } from "@/components/ui/badge"; import { Button } from "@/components/ui/button"; import { createAgentProfile, deleteAgentProfile, fetchAgentProfiles, updateAgentProfile, type AdminAgentProfile, type AdminAgentTeam, type CreateAdminAgentProfilePayload, } from "@/lib/api/admin"; import { useI18n } from "@/i18n/provider"; import { formatDateTime } from "@/lib/utils"; import { EditDialog } from "./_components/edit"; import { AgentTeamSidebar } from "./_components/team-sidebar"; type TFunction = (key: string, values?: Record) => string; function getServiceStatusOptions(t: TFunction) { return [ { value: "all", label: t("agentProfile.allStatuses") }, ...availabilityStatusOptions(t), ]; } export default function DashboardAgentsPage() { const t = useI18n(); const { session } = useAuth(); const accountId = session?.user.id; const canUpdateAvailability = Boolean(session?.permissions.includes("agent.update")); const [availability, setAvailability] = useState<{ accountId?: number; items: OperatorAvailability[] }>({ items: [] }); const [statusDialog, setStatusDialog] = useState<{ session: AuthSession; value: OperatorAvailability } | null>(null); useEffect(() => { let active = true; let pending = false; async function refresh() { if (pending || !accountId) return; pending = true; try { const items = await fetchAvailabilityAgents(); if (active) setAvailability({ accountId, items }); } catch { if (active) setAvailability({ accountId, items: [] }); } finally { pending = false; } } void refresh(); const onRefresh = () => { if (document.visibilityState === "visible") void refresh(); }; window.addEventListener(AVAILABILITY_UPDATED_EVENT, onRefresh); const timer = window.setInterval(onRefresh, 30000); return () => { active = false; window.clearInterval(timer); window.removeEventListener(AVAILABILITY_UPDATED_EVENT, onRefresh); }; }, [accountId]); const serviceStatusOptions = useMemo(() => getServiceStatusOptions(t), [t]); const [selectedTeam, setSelectedTeam] = useState(null); const [sidebarCollapsed, setSidebarCollapsed] = useState(false); const handleTeamsChange = useCallback((nextTeams: AdminAgentTeam[]) => { setSelectedTeam((current) => { if (nextTeams.length === 0) { return null; } if (!current) { return nextTeams[0]; } return nextTeams.find((item) => item.id === current.id) ?? nextTeams[0]; }); }, []); const filters = useMemo( () => [ { name: "displayName", label: t("agentProfile.filterDisplayName"), placeholder: t("agentProfile.filterDisplayName"), defaultValue: "", trim: true, className: "w-full sm:w-72", }, { name: "agentCode", label: t("agentProfile.filterAgentCode"), placeholder: t("agentProfile.filterAgentCode"), defaultValue: "", trim: true, className: "w-full sm:w-44", }, { name: "serviceStatus", label: t("agentProfile.allStatuses"), type: "select", defaultValue: "all", allValue: "all", options: serviceStatusOptions, className: "w-full sm:w-36", }, ], [serviceStatusOptions, t], ); const columns = useMemo[]>( () => [ { key: "agent", label: t("agentProfile.columnAgent"), render: (item) => (
{item.avatar ? ( {item.displayName} ) : ( )}
{item.displayName}
{item.nickname || item.username || t("agentProfile.userFallback", { id: item.userId })}
{t("agentProfile.agentCode", { code: item.agentCode })}
), }, { key: "rules", label: t("agentProfile.columnRules"), render: (item) => { const current = availability.accountId === accountId ? availability.items.find(value => value.userId === item.userId) : undefined; return <>
{current ? availabilityStatusLabel(current.effectiveServiceStatus, t) : t("supportAvailability.status.unknown")} {current ? {t(current.online ? "supportAvailability.online" : "supportAvailability.offline")} : null}

{current ? availabilityReasonLabel(current.reason, t) : t("supportAvailability.reason.unknown")}

{current && canUpdateAvailability ? : null}
{t("agentProfile.capacityPriority", { capacity: item.maxConcurrentCount, priority: item.priorityLevel, })}
; }, }, { key: "dispatch", label: t("agentProfile.columnDispatch"), render: (item) => (
{item.autoAssignEnabled ? t("agentProfile.autoAssign") : t("agentProfile.noAutoAssign")}
), }, { key: "recent", label: t("agentProfile.columnRecent"), render: (item) => ( <>
{t("agentProfile.onlineAt", { time: formatDateTime(item.lastOnlineAt), })}
{t("agentProfile.statusAt", { time: formatDateTime(item.lastStatusAt), })}
), }, ], [t, availability, accountId, canUpdateAvailability, session], ); return (
{statusDialog && isSameSession(statusDialog.session, session) ? setStatusDialog(null)} onSaved={() => notifyAvailabilityUpdated()} /> : null}
key={selectedTeam?.id ?? "all"} layout="fragment" tableShellClassName="min-h-0" filters={filters} columns={columns} fetchList={(query) => fetchAgentProfiles({ teamId: selectedTeam?.id, displayName: typeof query.displayName === "string" ? query.displayName : undefined, agentCode: typeof query.agentCode === "string" ? query.agentCode : undefined, serviceStatus: typeof query.serviceStatus === "string" ? query.serviceStatus : undefined, page: Number(query.page), limit: Number(query.limit), }) } getItemId={(item) => item.id} createItem={createAgentProfile} updateItem={(item, payload) => updateAgentProfile({ id: item.id, ...payload }) } deleteItem={(item) => deleteAgentProfile(item.id)} renderEditDialog={({ open, saving, itemId, onOpenChange, onSubmit, }) => ( )} labels={{ refresh: t("agentProfile.refresh"), create: t("agentProfile.new"), query: t("agentProfile.query"), loading: t("agentProfile.loadingRows"), empty: selectedTeam ? t("agentProfile.emptyTeamRows") : t("agentProfile.emptyRows"), actions: t("agentProfile.columnActions"), edit: t("agentProfile.edit"), delete: t("agentProfile.delete"), processing: t("agentProfile.deleting"), moreActions: (item) => t("agentProfile.moreActions", { name: item.displayName }), loadFailed: t("agentProfile.loadFailed"), saveFailed: t("agentProfile.saveFailed"), deleteFailed: t("agentProfile.deleteFailed"), created: (payload) => t("agentProfile.created", { name: payload.displayName }), updated: (item) => t("agentProfile.updated", { name: item.displayName }), deleted: (item) => t("agentProfile.deleted", { name: item.displayName }), }} />
); }