Files
agent-desk-ng/web/app/(dashboard)/dashboard/setup/_components/setup-wizard-view.tsx
mono 63e0457886 fix: harden identity, live access, drafts and setup (review only)
Source-reviewed hardening changes and regression tests. UNBUILT / UNTESTED: not a deployable release. Go formatting, compilation, automated regression tests, full frontend validation and staging acceptance remain required. See docs/hardening/VALIDATION.md. Main remains the original clean snapshot; no upstream Git history imported.
2026-10-16 12:15:20 +00:00

836 lines
32 KiB
TypeScript

// Modified in AgentDeskFixes: safe, reviewed setup repairs and honest routing diagnostics.
"use client"
import {
AlertTriangleIcon,
ArrowRightIcon,
CheckCircle2Icon,
ExternalLinkIcon,
Loader2Icon,
PlayIcon,
RefreshCwIcon,
ShieldAlertIcon,
SparklesIcon,
UserCheckIcon,
XCircleIcon,
} from "lucide-react"
import Link from "next/link"
import { useCallback, useEffect, useMemo, useRef, useState } from "react"
import { toast } from "sonner"
import { DashboardPage } from "@/components/dashboard-page"
import {
notifySetupReadinessUpdated,
SETUP_READINESS_UPDATED_EVENT,
} from "@/components/operator-readiness-indicator"
import { OptionCombobox } from "@/components/option-combobox"
import { Badge } from "@/components/ui/badge"
import { Button } from "@/components/ui/button"
import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from "@/components/ui/dialog"
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from "@/components/ui/table"
import { useI18n } from "@/i18n/provider"
import {
applySetupQuickFix,
fetchSetupReadiness,
simulateSetupRoute,
type SetupQuickFixAction,
type SetupQuickFixResponse,
type SetupReadinessResponse,
type SetupSimulateRouteResponse,
type SetupStepStatus,
} from "@/lib/api/setup-readiness"
import { cn } from "@/lib/utils"
function StatusIcon({ status }: { status: SetupStepStatus }) {
if (status === "ok") {
return (
<CheckCircle2Icon className="size-4 shrink-0 text-emerald-600 dark:text-emerald-400" />
)
}
if (status === "warning") {
return (
<AlertTriangleIcon className="size-4 shrink-0 text-amber-600 dark:text-amber-400" />
)
}
return (
<XCircleIcon className="size-4 shrink-0 text-destructive" />
)
}
function StatusBadge({ status }: { status: SetupStepStatus }) {
const t = useI18n()
if (status === "ok") {
return (
<Badge
variant="outline"
className="rounded-md border-emerald-500/30 bg-emerald-500/10 text-emerald-700 dark:text-emerald-300"
>
{t("setupWizard.stepBadgeOk")}
</Badge>
)
}
if (status === "warning") {
return (
<Badge
variant="outline"
className="rounded-md border-amber-500/40 bg-amber-500/10 text-amber-700 dark:text-amber-300"
>
{t("setupWizard.stepBadgeWarning")}
</Badge>
)
}
return (
<Badge
variant="outline"
className="rounded-md border-destructive/40 bg-destructive/10 text-destructive"
>
{t("setupWizard.stepBadgeError")}
</Badge>
)
}
export function SetupWizardView() {
const t = useI18n()
const [readiness, setReadiness] = useState<SetupReadinessResponse | null>(null)
const [loading, setLoading] = useState(true)
const [fixingAction, setFixingAction] = useState<string | null>(null)
const fixingRef = useRef(false)
const selectedChannelRef = useRef("")
const simulationRequestRef = useRef(0)
const readinessRequestRef = useRef(0)
const [pendingFix, setPendingFix] = useState<{
action: SetupQuickFixAction
entityId?: number
preview: SetupQuickFixResponse
} | null>(null)
const [selectedChannelId, setSelectedChannelId] = useState<string>("")
const [simulation, setSimulation] =
useState<SetupSimulateRouteResponse | null>(null)
const [simulating, setSimulating] = useState(false)
const runSimulation = useCallback(async (channelIdNum?: number) => {
const requestId = ++simulationRequestRef.current
setSimulating(true)
setSimulation(null)
try {
const sim = await simulateSetupRoute({
channel_id: channelIdNum && channelIdNum > 0 ? channelIdNum : undefined,
bypass_ai: false,
})
if (requestId !== simulationRequestRef.current) return
setSimulation({ ...sim, candidate_operators: sim.candidate_operators ?? [] })
if (sim.channel_id > 0) {
selectedChannelRef.current = String(sim.channel_id)
setSelectedChannelId(String(sim.channel_id))
}
} catch (error) {
if (requestId !== simulationRequestRef.current) return
toast.error(
error instanceof Error ? error.message : t("common.operationFailed"),
)
} finally {
if (requestId === simulationRequestRef.current) setSimulating(false)
}
}, [t])
const loadAll = useCallback(async () => {
const requestId = ++readinessRequestRef.current
setLoading(true)
try {
const data = await fetchSetupReadiness()
if (requestId !== readinessRequestRef.current) return
setReadiness(data)
const currentNum = Number(selectedChannelRef.current)
const targetChannelId =
currentNum > 0
? currentNum
: data.channels.find((ch) => ch.status === 0)?.id ??
data.channels[0]?.id ??
0
if (targetChannelId > 0) {
setSelectedChannelId(String(targetChannelId))
}
await runSimulation(targetChannelId)
} catch (error) {
if (requestId !== readinessRequestRef.current) return
toast.error(
error instanceof Error ? error.message : t("common.loadFailed"),
)
} finally {
if (requestId === readinessRequestRef.current) setLoading(false)
}
}, [runSimulation, t])
useEffect(() => {
void loadAll()
const handleUpdated = () => {
void loadAll()
}
window.addEventListener(SETUP_READINESS_UPDATED_EVENT, handleUpdated)
return () => {
window.removeEventListener(SETUP_READINESS_UPDATED_EVENT, handleUpdated)
simulationRequestRef.current++
readinessRequestRef.current++
}
}, [loadAll])
async function handleQuickFix(action: SetupQuickFixAction, entityId?: number) {
if (fixingRef.current) return
fixingRef.current = true
setFixingAction(entityId ? `${action}:${entityId}` : action)
try {
const preview = await applySetupQuickFix({ action, entity_id: entityId })
setPendingFix({ action, entityId, preview })
} catch (error) {
toast.error(error instanceof Error ? error.message : t("common.operationFailed"))
} finally {
fixingRef.current = false
setFixingAction(null)
}
}
async function confirmQuickFix() {
if (!pendingFix || fixingRef.current) return
fixingRef.current = true
setFixingAction(pendingFix.action)
try {
const result = await applySetupQuickFix({
action: pendingFix.action,
entity_id: pendingFix.entityId,
apply: true,
preview_token: pendingFix.preview.preview_token,
})
if (!result.applied) throw new Error(t("setupWizard.preview.notApplied"))
if (result.readiness) setReadiness(result.readiness)
setPendingFix(null)
notifySetupReadinessUpdated()
if (result.warning_codes.length) {
toast.warning(result.warning_codes.map((code) => t(`setupWizard.repairWarnings.${code}`)).join(" "))
} else {
toast.success(t("setupWizard.quickFixSuccess"))
}
} catch (error) {
toast.error(error instanceof Error ? error.message : t("common.operationFailed"))
// A failed/stale preview must be reviewed again, not silently reapplied.
setPendingFix(null)
} finally {
fixingRef.current = false
setFixingAction(null)
}
}
const canQuickSetupAll = useMemo(() => {
return (
readiness?.steps.some((s) =>
s.quick_fix_actions.includes("quick_setup_all"),
) ?? false
)
}, [readiness])
const canEnableMyProfile = useMemo(() => {
return (
readiness?.steps.some((s) =>
s.quick_fix_actions.includes("enable_my_operator_profile"),
) ?? false
)
}, [readiness])
const channelOptions = useMemo(() => {
if (!readiness) {
return []
}
return readiness.channels.map((ch) => ({
value: String(ch.id),
label: `${ch.name} (${ch.channel_type})`,
}))
}, [readiness])
if (loading && !readiness) {
return (
<DashboardPage>
<div className="flex min-h-64 items-center justify-center rounded-md border border-border/70 bg-card p-8">
<div className="flex items-center gap-2 text-sm text-muted-foreground">
<Loader2Icon className="size-4 animate-spin" />
<span>{t("common.loadingData")}</span>
</div>
</div>
</DashboardPage>
)
}
if (!readiness) {
return null
}
const progressPercent = Math.round(
(readiness.ready_steps / Math.max(readiness.total_steps, 1)) * 100,
)
return (
<DashboardPage className="space-y-5">
{/* Top Overview Header */}
<div className="rounded-md border border-border/70 bg-card p-4 lg:p-5">
<div className="flex flex-col gap-4 lg:flex-row lg:items-center lg:justify-between">
<div className="space-y-1.5">
<div className="flex flex-wrap items-center gap-2.5">
<h1 className="text-lg font-semibold text-foreground">
{t("setupWizard.title")}
</h1>
<Badge
variant="outline"
className={cn(
"rounded-md text-xs",
readiness.can_receive_requests_now
? "border-emerald-500/30 bg-emerald-500/10 text-emerald-700 dark:text-emerald-300"
: "border-amber-500/40 bg-amber-500/10 text-amber-700 dark:text-amber-300",
)}
>
{readiness.can_receive_requests_now
? t("setupWizard.statusReady")
: t("setupWizard.statusNeedsSetup", {
ready: readiness.ready_steps,
total: readiness.total_steps,
})}
</Badge>
</div>
<p className="text-sm text-muted-foreground">
{t("setupWizard.description")}
</p>
</div>
<div className="flex shrink-0 flex-wrap items-center gap-2">
<Button
variant="outline"
size="sm"
className="gap-1.5 rounded-md"
disabled={loading || fixingAction !== null}
onClick={() => void loadAll()}
>
<RefreshCwIcon
className={cn("size-4", loading && "animate-spin")}
/>
<span>{t("setupWizard.refresh")}</span>
</Button>
{canQuickSetupAll ? (
<Button
size="sm"
className="gap-1.5 rounded-md"
disabled={fixingAction !== null}
onClick={() => void handleQuickFix("quick_setup_all")}
>
{fixingAction === "quick_setup_all" ? (
<Loader2Icon className="size-4 animate-spin" />
) : (
<SparklesIcon className="size-4" />
)}
<span>
{fixingAction === "quick_setup_all"
? t("setupWizard.quickSetupRunning")
: t("setupWizard.quickSetupAll")}
</span>
</Button>
) : null}
</div>
</div>
{/* Progress Bar */}
<div className="mt-4 space-y-1.5">
<div className="flex items-center justify-between text-xs text-muted-foreground">
<span>
{t("setupWizard.stepsProgress", {
ready: readiness.ready_steps,
total: readiness.total_steps,
})}
</span>
<span>{progressPercent}%</span>
</div>
<div className="h-2 w-full overflow-hidden rounded-md bg-muted">
<div
className={cn(
"h-full transition-all duration-300",
readiness.can_receive_requests_now
? "bg-emerald-500"
: "bg-amber-500",
)}
style={{ width: `${progressPercent}%` }}
/>
</div>
</div>
</div>
{/* Current Operator Personal Diagnostic Card */}
<div
className={cn(
"rounded-md border p-4",
readiness.current_operator.can_receive_now
? "border-emerald-500/30 bg-emerald-500/5"
: "border-amber-500/40 bg-amber-500/5",
)}
>
<div className="flex flex-col gap-3 lg:flex-row lg:items-center lg:justify-between">
<div className="space-y-1.5">
<div className="flex flex-wrap items-center gap-2">
<UserCheckIcon
className={cn(
"size-4 shrink-0",
readiness.current_operator.can_receive_now
? "text-emerald-600 dark:text-emerald-400"
: "text-amber-600 dark:text-amber-400",
)}
/>
<h2 className="text-sm font-semibold text-foreground">
{t("setupWizard.currentOperatorTitle")}:{" "}
{readiness.current_operator.nickname ||
readiness.current_operator.username}
</h2>
<Badge variant="outline" className="rounded-md text-xs">
{t("setupWizard.currentOperatorTeam", {
team:
readiness.current_operator.team_name ||
t("setupWizard.currentOperatorNoTeam"),
})}
</Badge>
<Badge variant="outline" className="rounded-md text-xs">
{t("setupWizard.currentOperatorLoad", {
active: readiness.current_operator.active_conversation_count,
max: readiness.current_operator.max_concurrent_count,
})}
</Badge>
</div>
{readiness.current_operator.can_receive_now ? (
<p className="text-xs text-emerald-700 dark:text-emerald-300">
{t("setupWizard.currentOperatorReady")}
</p>
) : (
<div className="space-y-1">
<p className="text-xs font-medium text-amber-700 dark:text-amber-300">
{t("setupWizard.currentOperatorBlocked")}
</p>
<div className="flex flex-wrap gap-1.5 pt-0.5">
{readiness.current_operator.blocker_codes.map((code) => (
<Badge
key={code}
variant="secondary"
className="gap-1 rounded-md text-xs font-normal"
>
<ShieldAlertIcon className="size-3 text-amber-600 dark:text-amber-400" />
<span>{t(`setupWizard.operatorBlockers.${code}`)}</span>
</Badge>
))}
</div>
</div>
)}
</div>
{!readiness.current_operator.can_receive_now && canEnableMyProfile ? (
<div className="flex shrink-0 items-center gap-2">
<Button
size="sm"
className="gap-1.5 rounded-md"
disabled={fixingAction !== null}
onClick={() =>
void handleQuickFix("enable_my_operator_profile")
}
>
{fixingAction === "enable_my_operator_profile" ? (
<Loader2Icon className="size-4 animate-spin" />
) : (
<SparklesIcon className="size-4" />
)}
<span>{t("setupWizard.enableMyReceptionBtn")}</span>
</Button>
</div>
) : null}
</div>
</div>
{/* 6-Step Checklist Grid */}
<div className="space-y-3">
<h2 className="text-base font-semibold text-foreground">
{t("setupWizard.checklistSectionTitle")}
</h2>
<div className="grid gap-3 md:grid-cols-2">
{readiness.steps.map((step) => {
const primaryQuickFix =
(step.key === "ai_agents" || step.key === "channels") && step.total_count > 0
? undefined
: step.quick_fix_actions[0]
return (
<div
key={step.key}
className="flex flex-col justify-between rounded-md border border-border/70 bg-card p-4"
>
<div className="space-y-2.5">
<div className="flex items-start justify-between gap-2">
<div className="flex items-center gap-2">
<StatusIcon status={step.status} />
<h3 className="text-sm font-semibold text-foreground">
{step.step_number}.{" "}
{t(`setupWizard.steps.${step.key}.title`)}
</h3>
</div>
<div className="flex shrink-0 items-center gap-1.5">
<Badge
variant="secondary"
className="rounded-md text-xs font-normal"
>
{t("setupWizard.readyCountLabel", {
ready: step.ready_count,
total: step.total_count,
})}
</Badge>
<StatusBadge status={step.status} />
</div>
</div>
<p className="text-xs text-muted-foreground">
{t(`setupWizard.steps.${step.key}.description`)}
</p>
{step.issues.length > 0 ? (
<div className="space-y-1.5 rounded-md border border-border/60 bg-muted/30 p-2.5">
{step.issues.map((issue, idx) => (
<div
key={`${issue.code}-${issue.entity_id}-${idx}`}
className="flex items-start justify-between gap-2 text-xs"
>
<div className="flex items-start gap-1.5 text-foreground">
<AlertTriangleIcon className="mt-0.5 size-3.5 shrink-0 text-amber-600 dark:text-amber-400" />
<span>
{issue.entity_name ? (
<strong className="font-medium">
[{issue.entity_name}]{" "}
</strong>
) : null}
{t(`setupWizard.issues.${issue.code}`)}
</span>
</div>
{issue.quick_fix_action && issue.entity_id > 0 && issue.entity_type !== "operator" ? (
<Button size="sm" variant="outline" className="shrink-0 rounded-md" disabled={fixingAction !== null} onClick={() => void handleQuickFix(issue.quick_fix_action!, issue.entity_id)}>
{t("setupWizard.preview.review")}
</Button>
) : null}
</div>
))}
</div>
) : null}
</div>
<div className="mt-4 flex flex-wrap items-center justify-end gap-2 border-t border-border/50 pt-3">
{step.key !== "end_to_end" ? (
<Button
variant="outline"
size="sm"
className="h-8 gap-1.5 rounded-md text-xs"
render={
<Link
href={
step.key === "schedules"
? "/dashboard/agent-team-schedules"
: step.key === "ai_agents"
? "/dashboard/ai-agents"
: step.key === "channels"
? "/dashboard/channels"
: "/dashboard/agents"
}
/>
}
>
<span>{t("setupWizard.openSettings")}</span>
<ExternalLinkIcon className="size-3.5" />
</Button>
) : null}
{primaryQuickFix ? (
<Button
size="sm"
className="h-8 gap-1.5 rounded-md text-xs"
disabled={fixingAction !== null}
onClick={() => void handleQuickFix(primaryQuickFix)}
>
{fixingAction === primaryQuickFix ? (
<Loader2Icon className="size-3.5 animate-spin" />
) : (
<SparklesIcon className="size-3.5" />
)}
<span>
{t(`setupWizard.quickFixLabels.${primaryQuickFix}`)}
</span>
</Button>
) : null}
</div>
</div>
)
})}
</div>
</div>
{/* Interactive Route Simulator */}
<div className="space-y-4 rounded-md border border-border/70 bg-card p-4 lg:p-5">
<div className="flex flex-col gap-3 lg:flex-row lg:items-center lg:justify-between">
<div className="space-y-1">
<h2 className="text-base font-semibold text-foreground">
{t("setupWizard.simulator.title")}
</h2>
<p className="text-xs text-muted-foreground">
{t("setupWizard.simulator.description")}
</p>
</div>
<div className="flex flex-wrap items-center gap-2">
{channelOptions.length > 0 ? (
<OptionCombobox
options={channelOptions}
value={selectedChannelId}
onChange={(val) => {
selectedChannelRef.current = val
setSelectedChannelId(val)
void runSimulation(Number(val))
}}
placeholder={t("setupWizard.simulator.selectChannelPlaceholder")}
triggerClassName="w-64 rounded-md"
/>
) : null}
<Button
size="sm"
variant="outline"
className="gap-1.5 rounded-md"
disabled={simulating}
onClick={() => void runSimulation(Number(selectedChannelId))}
>
{simulating ? (
<Loader2Icon className="size-4 animate-spin" />
) : (
<PlayIcon className="size-4" />
)}
<span>
{simulating
? t("setupWizard.simulator.running")
: t("setupWizard.simulator.runSimulation")}
</span>
</Button>
</div>
</div>
{simulation ? (
<div className="space-y-4">
{/* Outcome Banner */}
<div
className={cn(
"flex flex-col gap-2 rounded-md border p-3 sm:flex-row sm:items-center sm:justify-between",
simulation.will_reach_operator
? "border-emerald-500/30 bg-emerald-500/10 text-emerald-800 dark:text-emerald-200"
: "border-destructive/40 bg-destructive/10 text-destructive",
)}
>
<div className="flex items-center gap-2 text-sm font-medium">
{simulation.will_reach_operator ? (
<CheckCircle2Icon className="size-4 shrink-0 text-emerald-600 dark:text-emerald-400" />
) : (
<XCircleIcon className="size-4 shrink-0 text-destructive" />
)}
<span>
{simulation.will_reach_operator
? t("setupWizard.simulator.outcomeSuccess", {
operator:
simulation.matched_operator?.display_name ?? "",
})
: t("setupWizard.simulator.outcomeBlocked")}
</span>
</div>
<span className="text-xs opacity-90">
{t(
`setupWizard.simulator.outcomes.${simulation.final_outcome_code}`,
)}
</span>
</div>
{/* 5-Hop Visual Trace Pipeline */}
<div className="grid gap-2.5 lg:grid-cols-5">
{simulation.hops.map((hop, idx) => (
<div
key={`${hop.hop_key}-${idx}`}
className={cn(
"flex flex-col justify-between rounded-md border p-3 text-xs",
hop.status === "ok"
? "border-emerald-500/30 bg-emerald-500/5"
: hop.status === "warning"
? "border-amber-500/40 bg-amber-500/5"
: "border-destructive/40 bg-destructive/5",
)}
>
<div className="space-y-1.5">
<div className="flex items-center justify-between gap-1.5">
<span className="font-semibold text-foreground">
{idx + 1}.{" "}
{t(`setupWizard.simulator.hops.${hop.hop_key}`)}
</span>
<StatusIcon status={hop.status} />
</div>
{hop.entity_name ? (
<p className="truncate font-medium text-foreground">
{hop.entity_name}
</p>
) : null}
<p className="text-muted-foreground">
{t(`setupWizard.simulator.hopReasons.${hop.reason_code}`)}
</p>
</div>
{hop.quick_fix_action && hop.status !== "ok" ? (
<Button
size="sm"
variant="outline"
className="mt-2.5 h-7 w-full gap-1 rounded-md text-[11px]"
disabled={fixingAction !== null}
onClick={() =>
void handleQuickFix(
hop.quick_fix_action!,
hop.entity_id,
)
}
>
<SparklesIcon className="size-3" />
<span className="truncate">
{t(
`setupWizard.quickFixLabels.${hop.quick_fix_action}`,
)}
</span>
</Button>
) : null}
</div>
))}
</div>
{/* Candidate Operators Evaluation Table */}
{simulation.candidate_operators.length > 0 ? (
<div className="space-y-2">
<h3 className="text-xs font-semibold uppercase tracking-wider text-muted-foreground">
{t("setupWizard.simulator.candidateTableTitle")}
</h3>
<div className="overflow-hidden rounded-md border border-border/70">
<Table>
<TableHeader>
<TableRow>
<TableHead>
{t("setupWizard.simulator.colOperator")}
</TableHead>
<TableHead>
{t("setupWizard.simulator.colTeam")}
</TableHead>
<TableHead>
{t("setupWizard.simulator.colStatus")}
</TableHead>
<TableHead>
{t("setupWizard.simulator.colAutoAssign")}
</TableHead>
<TableHead>
{t("setupWizard.simulator.colLoad")}
</TableHead>
<TableHead>
{t("setupWizard.simulator.colResult")}
</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{simulation.candidate_operators.map((cand) => (
<TableRow key={cand.user_id}>
<TableCell className="font-medium">
{cand.display_name}
</TableCell>
<TableCell>
{cand.team_name ||
t("setupWizard.currentOperatorNoTeam")}
</TableCell>
<TableCell>
<Badge
variant="outline"
className="rounded-md text-xs"
>
{cand.service_status === 0
? t("setupWizard.simulator.statusIdle")
: t("setupWizard.simulator.statusBusy")}
</Badge>
</TableCell>
<TableCell>
{cand.auto_assign_enabled
? t("common.enabled")
: t("common.disabled")}
</TableCell>
<TableCell>
{cand.active_count} / {cand.max_concurrent_count}
</TableCell>
<TableCell>
{cand.eligible ? (
<Badge
variant="outline"
className="rounded-md border-emerald-500/30 bg-emerald-500/10 text-xs text-emerald-700 dark:text-emerald-300"
>
{t("setupWizard.simulator.eligibleBadge")}
</Badge>
) : (
<span className="text-xs text-amber-700 dark:text-amber-300">
{t(
`setupWizard.simulator.skipReasons.${cand.skip_reason_code}`,
)}
</span>
)}
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</div>
</div>
) : null}
</div>
) : null}
</div>
<Dialog open={pendingFix !== null} onOpenChange={(open) => { if (!open && !fixingRef.current) setPendingFix(null) }}>
<DialogContent className="grid max-h-[85vh] grid-rows-[auto_minmax(0,1fr)_auto] rounded-md sm:max-w-2xl" showCloseButton={false}>
<DialogHeader>
<DialogTitle>{t("setupWizard.preview.title")}</DialogTitle>
<DialogDescription>{t("setupWizard.preview.description")}</DialogDescription>
</DialogHeader>
<div className="space-y-3 overflow-y-auto break-words">
{pendingFix?.preview.changes.length === 0 ? <p>{t("setupWizard.preview.noChanges")}</p> : null}
{pendingFix?.preview.changes.map((change, index) => (
<div key={`${change.entity_type}-${change.entity_id}-${index}`} className="space-y-2 rounded-md border p-3">
<p className="font-medium">{t(`setupWizard.preview.${change.operation}`)}: {change.entity_name}{change.entity_id > 0 ? ` (#${change.entity_id})` : ""}</p>
<dl className="space-y-1 text-xs">
{Object.entries(change.after).map(([field, value]) => (
<div key={field} className="grid gap-1 sm:grid-cols-[minmax(0,1fr)_minmax(0,2fr)]">
<dt className="text-muted-foreground">{t(`setupWizard.repairFields.${field}`)}</dt>
<dd>{change.before && field in change.before ? `${String(change.before[field])} → ` : ""}{String(value)}</dd>
</div>
))}
</dl>
</div>
))}
{pendingFix?.preview.warning_codes.map((code) => <p key={code} className="text-sm text-amber-700 dark:text-amber-300">{t(`setupWizard.repairWarnings.${code}`)}</p>)}
<Link className="text-sm underline" href="/dashboard/agent-team-schedules">{t("setupWizard.preview.manageSchedules")}</Link>
</div>
<DialogFooter>
<Button variant="outline" disabled={fixingAction !== null} onClick={() => setPendingFix(null)}>{t("common.cancel")}</Button>
<Button disabled={fixingAction !== null || !pendingFix?.preview.changes.length} onClick={() => void confirmQuickFix()}>
{fixingAction !== null ? <Loader2Icon className="size-4 animate-spin" /> : null}
{t("setupWizard.preview.apply")}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
</DashboardPage>
)
}
export { ArrowRightIcon }