"use client" import { useCallback, useEffect, useState } from "react" import { PageHeader } from "@/components/page-header" import { DataPageCard } from "@/components/data-page-card" import { UsersDataGrid } from "@/components/data-grids/users-data-grid" import { KpiStatGrid } from "@/components/reui-kit/kpi-stat-grid" import { UserSheet } from "@/components/users/user-sheet" import { Button } from "@/components/ui/button" import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogMedia, AlertDialogTitle, } from "@/components/ui/alert-dialog" import { servers as mockServers } from "@/lib/data" import { useDataSource } from "@/lib/data-source" import { ALL_SECTIONS, INIT_USERS, bindingDiffKey, userInitials, type AppUser, type AppUserForm, type UserServerOption, } from "@/lib/users" import { listServers } from "@/shared/api/servers" import { createAppUser, createUserBinding, deleteAppUser, deleteUserBinding, listAppUsers, updateAppUser, } from "@/shared/api/users" import { ApiClientError } from "@/shared/api/http-client" import { toast } from "sonner" import { CableIcon, PlusIcon, TrashIcon, UserCheckIcon, UsersIcon, } from "lucide-react" function toServerOptionsFromMock(): UserServerOption[] { return mockServers.map((s) => ({ id: s.id, name: s.name, host: s.host, site: s.site, country: s.country, status: s.status, })) } export default function UsersPage() { const { mode, backendUrl } = useDataSource() const isLive = mode === "live" const [users, setUsers] = useState(INIT_USERS) const [serverOptions, setServerOptions] = useState(toServerOptionsFromMock) const [loading, setLoading] = useState(false) const [saving, setSaving] = useState(false) const [sheetOpen, setSheetOpen] = useState(false) const [editUser, setEditUser] = useState(null) const [deleteTarget, setDeleteTarget] = useState(null) const loadLive = useCallback(async () => { setLoading(true) try { const [list, srvs] = await Promise.all([ listAppUsers(backendUrl), listServers(backendUrl), ]) setUsers(list) setServerOptions(srvs.map((s) => ({ id: String(s.id), name: s.name || s.host, host: s.host, site: s.site, country: s.country, status: s.status, }))) } catch (err) { toast.error(err instanceof Error ? err.message : "Не удалось загрузить пользователей") } finally { setLoading(false) } }, [backendUrl]) useEffect(() => { if (!isLive) { setUsers(INIT_USERS) setServerOptions(toServerOptionsFromMock()) return } void loadLive() }, [isLive, loadLive]) const boundCount = users.reduce((n, u) => n + u.bindings.length, 0) const activeCount = users.filter((u) => u.active).length const applyBindingsDiff = async (userId: string, next: AppUserForm["bindings"], prev: AppUser["bindings"]) => { const nextKeys = new Set(next.map(bindingDiffKey)) const prevKeys = new Map(prev.map((b) => [bindingDiffKey(b), b] as const)) for (const b of prev) { if (!nextKeys.has(bindingDiffKey(b))) { await deleteUserBinding(backendUrl, userId, b.id) } } for (const b of next) { if (!prevKeys.has(bindingDiffKey(b))) { await createUserBinding(backendUrl, userId, { serverId: Number(b.serverId), interfaceName: b.interfaceName, interfaceType: b.interfaceType, peerPublicKey: b.peerPublicKey, peerName: b.peerName, comment: b.comment, }) } } } const handleSave = async (form: AppUserForm) => { if (!isLive) { if (!editUser) { const id = `u${Date.now()}` const created: AppUser = { id, ...form, last: "только что", avatar: userInitials(form.name), bindings: form.bindings.map((b, i) => ({ ...b, id: `b${id}-${i}`, userId: id })), } setUsers((prev) => [...prev, created]) } else { setUsers((prev) => prev.map((u) => u.id === editUser.id ? { ...u, ...form, avatar: userInitials(form.name) } : u)) } setSheetOpen(false) toast.success(editUser ? "Пользователь сохранён" : "Пользователь создан") return } setSaving(true) try { if (!editUser) { const created = await createAppUser(backendUrl, { name: form.name, login: form.login, email: form.email, role: form.role, active: form.active, avatar: userInitials(form.name), sections: form.sections, servers: form.servers, }) await applyBindingsDiff(created.id, form.bindings, []) await loadLive() } else { await updateAppUser(backendUrl, editUser.id, { name: form.name, login: form.login, email: form.email, role: form.role, active: form.active, avatar: userInitials(form.name), sections: form.sections, servers: form.servers, }) await applyBindingsDiff(editUser.id, form.bindings, editUser.bindings) await loadLive() } setSheetOpen(false) toast.success(editUser ? "Пользователь сохранён" : "Пользователь создан") } catch (err) { const msg = err instanceof ApiClientError ? err.message : err instanceof Error ? err.message : "Ошибка сохранения" toast.error(msg) } finally { setSaving(false) } } const handleDelete = async () => { if (!deleteTarget) return if (!isLive) { setUsers((prev) => prev.filter((u) => u.id !== deleteTarget.id)) setDeleteTarget(null) toast.success("Пользователь удалён") return } try { await deleteAppUser(backendUrl, deleteTarget.id) setDeleteTarget(null) await loadLive() toast.success("Пользователь удалён") } catch (err) { toast.error(err instanceof Error ? err.message : "Не удалось удалить") } } return (
{ setEditUser(null); setSheetOpen(true) }}> Пригласить } />
{/* Preview: https://reui.io/preview/base/stats-12 · https://reui.io/docs/components/base/icon-tile */} , iconClassName: "text-muted-foreground", }, { id: "active", label: "Активные", value: activeCount, icon: , iconClassName: "text-success", }, { id: "ifaces", label: "Привязанные ifaces", value: boundCount, icon: , iconClassName: "text-info", }, ]} /> {/* Preview: https://reui.io/preview/base/data-grid-filtering-2 · https://reui.io/docs/components/base/data-grid · https://reui.io/docs/components/base/frame */} { setEditUser(u); setSheetOpen(true) }} onDelete={setDeleteTarget} />
{ void handleSave(f) }} onClose={() => setSheetOpen(false)} /> {deleteTarget && ( { if (!v) setDeleteTarget(null) }}> Удалить пользователя? {deleteTarget.name} · {deleteTarget.email || deleteTarget.login}. Привязки интерфейсов будут удалены. setDeleteTarget(null)}>Отмена { void handleDelete() }}> Удалить )}
) }