feat(mcp): подготовить сервер и доступ к приёмке

This commit is contained in:
Denozordec committed 2026-11-03 03:12:03 +07:00
commit bf3d50e1ff
3835 files changed
+537122

No files matched your search

@@ -0,0 +1,296 @@
"use client"
import { dataGridFeatures, type DataGridFeatures } from "@/components/reui/data-grid"
import { useEffect, useMemo, useState } from "react"
import { Input } from "@/components/ui/input"
import { DataGridContainer, DataGridScrollArea, DataGridTable } from "@/components/reui/data-grid"
import {
type ColumnDef,
useTable,
} from "@tanstack/react-table"
import type { CommRec } from "@/lib/route-optimizer-data"
import { cn } from "@/lib/utils"
import { Button } from "@/components/ui/button"
import { DataGridShell } from "@/components/data-grids/shared/data-grid-shell"
import {
DATA_GRID_CELL_PAD,
DATA_GRID_CELL_PAD_FIRST,
DATA_GRID_CELL_PAD_LAST,
} from "@/components/data-grids/shared/data-grid-layout"
import { DataGridSortHeader } from "@/components/data-grids/shared/data-grid-sort-header"
import { ArrowRightIcon, PinIcon, RefreshCwIcon } from "lucide-react"
function Chip({ children, color }: { children: React.ReactNode; color?: string }) {
return (
<span
className={cn(
"inline-flex items-center rounded px-1.5 py-0.5 text-[11px] font-medium border",
color ?? "bg-muted text-muted-foreground border-border",
)}
>
{children}
</span>
)
}
function ProbChip({ prob, best }: { prob: number | null; best?: boolean }) {
if (prob === null) return <span className="text-muted-foreground" aria-label="Индекс неизвестен">—</span>
return (
<Chip
color={
best
? "bg-success/10 text-success border-success/20"
: "bg-muted text-muted-foreground border-border"
}
>
{prob} б.
</Chip>
)
}
interface RouteOptimizerCommRecsDataGridProps {
recs: CommRec[]
homeId: string
pinned: Set<string>
applied: Set<string>
applying: Set<string>
onPin: (key: string) => void
simulation?: boolean
onApply: (community: string, homeId: string) => void
}
function RouteOptimizerCommRecsDataGrid({
recs,
homeId,
pinned,
applied,
applying,
onPin,
onApply,
simulation = false,
}: RouteOptimizerCommRecsDataGridProps) {
const [query, setQuery] = useState("")
const rows = useMemo(
() => recs.map((r, idx) => ({ ...r, _rowKey: `${homeId}::${r.community}::${idx}` })),
[homeId, recs],
)
const filtered = useMemo(() => rows.filter(row =>
[row.community, row.communityName, row.current?.wan, row.current?.jh, row.current?.exit, row.current?.gateway,
row.recommended?.wan, row.recommended?.jh, row.recommended?.exit, row.recommended?.gateway]
.join(" ").toLocaleLowerCase().includes(query.trim().toLocaleLowerCase())), [rows, query])
const columns = useMemo<ColumnDef<DataGridFeatures, (typeof rows)[number]>[]>(
() => [
{
id: "community",
size: 180,
accessorKey: "community",
header: ({ column }) => (
<DataGridSortHeader column={column} title="Community" className="ml-1" />
),
cell: ({ row }) => (
<div>
<div className="font-mono text-xs font-medium">{row.original.community}</div>
<div className="text-[11px] text-muted-foreground">{row.original.communityName}</div>
</div>
),
meta: {
headerTitle: "Community",
headerClassName: DATA_GRID_CELL_PAD_FIRST,
cellClassName: DATA_GRID_CELL_PAD_FIRST,
},
},
{
id: "current",
size: 260,
header: () => (
<span className="text-[11px] font-medium text-muted-foreground">
Базовый расчёт (WAN → JH → Exit)
</span>
),
enableSorting: false,
cell: ({ row }) => {
const r = row.original
if (!r.current) return <span className="text-muted-foreground text-xs">—</span>
return (
<div className="text-xs flex items-center gap-1 flex-wrap">
<span className="font-mono font-medium text-info ">
{r.current.wan}
</span>
<ArrowRightIcon aria-hidden="true" className="size-3 text-muted-foreground shrink-0" />
<span>{r.current.jh}</span>
<ArrowRightIcon aria-hidden="true" className="size-3 text-muted-foreground shrink-0" />
<span className="text-muted-foreground">{r.current.exit}</span>
<span className="font-mono text-[10px] text-muted-foreground">
({r.current.gateway})
</span>
</div>
)
},
meta: { headerClassName: DATA_GRID_CELL_PAD, cellClassName: DATA_GRID_CELL_PAD },
},
{
id: "recommended",
size: 260,
header: () => <span className="text-[11px] font-medium text-muted-foreground">Рекомендуемый</span>,
enableSorting: false,
cell: ({ row }) => {
const r = row.original
const pinKey = `${homeId}::${r.community}`
const isPinned = pinned.has(pinKey)
if (!r.recommended) return <span className="text-muted-foreground text-xs">—</span>
return (
<div
className={cn(
"text-xs flex items-center gap-1 flex-wrap",
r.shouldSwitch && !isPinned && "text-warning ",
)}
>
<span className="font-mono font-medium">{r.recommended.wan}</span>
<ArrowRightIcon aria-hidden="true" className="size-3 shrink-0 opacity-60" />
<span>{r.recommended.jh}</span>
<ArrowRightIcon aria-hidden="true" className="size-3 shrink-0 opacity-60" />
<span>{r.recommended.exit}</span>
{r.shouldSwitch && r.current && !isPinned && (
<span className="ml-1 text-[10px] font-bold bg-warning/10 border border-warning/20 px-1.5 py-0.5 rounded">
+{r.recommended.prob - r.current.prob} б.
</span>
)}
</div>
)
},
meta: { headerClassName: DATA_GRID_CELL_PAD, cellClassName: DATA_GRID_CELL_PAD },
},
{
id: "probability",
size: 160,
header: () => (
<span className="text-[11px] font-medium text-muted-foreground block text-center">
Индекс (баз / рек)
</span>
),
enableSorting: false,
cell: ({ row }) => {
const r = row.original
const pinKey = `${homeId}::${r.community}`
return (
<div className="flex items-center justify-center gap-1">
<ProbChip prob={r.current?.prob ?? null} />
<span className="text-muted-foreground text-[10px]">/</span>
<ProbChip prob={r.recommended?.prob ?? null} best={r.shouldSwitch && !pinned.has(pinKey)} />
</div>
)
},
meta: {
headerClassName: cn(DATA_GRID_CELL_PAD, "text-center"),
cellClassName: cn(DATA_GRID_CELL_PAD, "text-center"),
},
},
{
id: "actions",
size: 240,
header: () => (
<span className="text-[11px] font-medium text-muted-foreground block text-right">
Действие
</span>
),
enableSorting: false,
cell: ({ row }) => {
const r = row.original
const pinKey = `${homeId}::${r.community}`
const isPinned = pinned.has(pinKey)
const isApplied = simulation && applied.has(pinKey)
const isApplying = simulation && applying.has(pinKey)
const canApply = simulation && r.shouldSwitch && !isPinned && !isApplied
return (
<div className="flex items-center justify-end gap-1.5" onClick={(e) => e.stopPropagation()}>
{isPinned && <PinIcon aria-hidden="true" className="size-3 text-info fill-info" />}
<Button type="button"
variant="outline"
size="sm"
className={cn("h-7 text-xs", isPinned && "text-info border-info/30")}
onClick={() => onPin(pinKey)}
>
<PinIcon aria-hidden="true" className={cn("size-3", isPinned && "fill-current")} />
{isPinned ? "Открепить" : "Закрепить"}
</Button>
{canApply && (
<Button type="button"
size="sm"
className="h-7 text-xs"
disabled={isApplying}
onClick={() => onApply(r.community, homeId)}
>
{isApplying ? (
<RefreshCwIcon aria-hidden="true" className="size-3 animate-spin" />
) : (
"Симулировать"
)}
</Button>
)}
{isApplied && (
<span className="text-[10px] text-success font-medium">
✓ симуляция завершена
</span>
)}
</div>
)
},
meta: {
headerClassName: cn(DATA_GRID_CELL_PAD_LAST, "text-right"),
cellClassName: DATA_GRID_CELL_PAD_LAST,
},
},
],
[applied, applying, homeId, onApply, onPin, pinned, simulation],
)
const table = useTable({
features: dataGridFeatures,
manualPagination: false,
autoResetPageIndex: false,
initialState: { pagination: { pageIndex: 0, pageSize: 50 }, columnPinning: { start: ["community"], end: [] } },
data: filtered,
columns,
getRowId: (row) => row._rowKey,
})
const { pageIndex, pageSize } = table.state.pagination
useEffect(() => {
const lastPage = Math.max(0, Math.ceil(filtered.length / pageSize) - 1)
if (pageIndex > lastPage) table.setPageIndex(lastPage)
}, [filtered.length, pageIndex, pageSize, table])
return (
<section aria-label="Рекомендации BGP Community" className="min-w-0">
<div className="px-5 py-3 space-y-2">
<label className="block text-xs">Поиск Community
<Input className="mt-1" value={query} onChange={event => { setQuery(event.target.value); table.setPageIndex(0) }} placeholder="Community, название или шлюз" />
</label>
<p className="text-xs text-muted-foreground">Community: {filtered.length} из {rows.length}. Индексы и пути — расчётная модель.</p>
</div>
<DataGridShell
table={table}
recordCount={filtered.length}
pagination={filtered.length > 50 || pageSize !== 50}
tableClassNames={{
headerRow: "border-b bg-muted/20",
bodyRow: cn(
"group/row hover:bg-muted/30",
"[&:has([data-comm-switch=true])]:bg-warning/5",
"[&:has([data-comm-applied=true])]:bg-success/5",
),
}}
tableLayout={{ dense: true, width: "fixed", columnsPinnable: true }}
>
<DataGridContainer border={false}>
<DataGridScrollArea orientation="horizontal"><DataGridTable /></DataGridScrollArea>
</DataGridContainer>
</DataGridShell>
</section>
)
}
export { RouteOptimizerCommRecsDataGrid, type RouteOptimizerCommRecsDataGridProps }