(function () { const { useCallback, useEffect, useMemo, useRef, useState } = React; const LS_TOKEN = "admin_access_token"; const PAGE_SIZE = 50; const DEFAULT_FORM_FIELD_TYPES = ["string", "text", "number", "boolean", "date"]; const ALL_OPERATORS = ["=", "!=", ">", "<", ">=", "<=", "~"]; const OPERATOR_LABELS = { "=": "=", "!=": "!=", ">": ">", "<": "<", ">=": ">=", "<=": "<=", "~": "~", }; const ROLE_LABELS = { ADMIN: "Администратор", LAWYER: "Юрист", }; const STATUS_LABELS = { NEW: "Новая", IN_PROGRESS: "В работе", WAITING_CLIENT: "Ожидание клиента", WAITING_COURT: "Ожидание суда", RESOLVED: "Решена", CLOSED: "Закрыта", REJECTED: "Отклонена", }; const TABLE_SERVER_CONFIG = { requests: { endpoint: "/api/admin/requests/query", sort: [{ field: "created_at", dir: "desc" }], }, quotes: { endpoint: "/api/admin/quotes/query", sort: [{ field: "sort_order", dir: "asc" }], }, topics: { endpoint: "/api/admin/config/topics/query", sort: [{ field: "sort_order", dir: "asc" }], }, statuses: { endpoint: "/api/admin/config/statuses/query", sort: [{ field: "sort_order", dir: "asc" }], }, formFields: { endpoint: "/api/admin/config/form-fields/query", sort: [{ field: "sort_order", dir: "asc" }], }, }; function createTableState() { return { filters: [], sort: null, offset: 0, total: 0, showAll: false, rows: [], }; } function decodeJwtPayload(token) { try { const payload = token.split(".")[1] || ""; const base64 = payload.replace(/-/g, "+").replace(/_/g, "/"); const json = decodeURIComponent( atob(base64) .split("") .map((c) => "%" + ("00" + c.charCodeAt(0).toString(16)).slice(-2)) .join("") ); return JSON.parse(json); } catch (_) { return null; } } function sortByName(items) { return [...items].sort((a, b) => String(a.name || a.code || "").localeCompare(String(b.name || b.code || ""), "ru")); } function roleLabel(role) { return ROLE_LABELS[role] || role || "-"; } function statusLabel(code) { return STATUS_LABELS[code] || code || "-"; } function boolLabel(value) { return value ? "Да" : "Нет"; } function boolFilterLabel(value) { return value ? "True" : "False"; } function fmtDate(value) { if (!value) return "-"; const date = new Date(value); return Number.isNaN(date.getTime()) ? String(value) : date.toLocaleString("ru-RU"); } function buildUniversalQuery(filters, sort, limit, offset) { return { filters: filters || [], sort: sort || [], page: { limit: limit ?? PAGE_SIZE, offset: offset ?? 0 }, }; } function canAccessSection(role, section) { if (section === "quotes" || section === "config") return role === "ADMIN"; return true; } function translateApiError(message) { const direct = { "Missing auth token": "Отсутствует токен авторизации", "Missing bearer token": "Отсутствует токен авторизации", "Invalid token": "Некорректный токен", Forbidden: "Недостаточно прав", "Invalid credentials": "Неверный логин или пароль", "Request not found": "Заявка не найдена", "Quote not found": "Цитата не найдена", not_found: "Запись не найдена", }; if (direct[message]) return direct[message]; if (String(message).startsWith("HTTP ")) return "Ошибка сервера (" + message + ")"; return message; } function getOperatorsForType(type) { if (type === "number" || type === "date" || type === "datetime") return ["=", "!=", ">", "<", ">=", "<="]; if (type === "boolean" || type === "reference" || type === "enum") return ["=", "!="]; return [...ALL_OPERATORS]; } function localizeRequestDetails(row) { return { ID: row.id || null, "Номер заявки": row.track_number || null, Клиент: row.client_name || null, Телефон: row.client_phone || null, "Тема (код)": row.topic_code || null, Статус: statusLabel(row.status_code), Описание: row.description || null, "Дополнительные поля": row.extra_fields || {}, "Назначенный юрист (ID)": row.assigned_lawyer_id || null, "Общий размер вложений (байт)": row.total_attachments_bytes ?? 0, Создано: fmtDate(row.created_at), Обновлено: fmtDate(row.updated_at), }; } function localizeMeta(data) { const fieldTypeMap = { string: "строка", text: "текст", boolean: "булево", number: "число", date: "дата", }; return { Сущность: data.entity, Поля: (data.fields || []).map((field) => ({ "Код поля": field.field_name, Название: field.label, Тип: fieldTypeMap[field.type] || field.type, Обязательное: boolLabel(field.required), "Только чтение": boolLabel(field.read_only), "Редактируемые роли": (field.editable_roles || []).map(roleLabel), })), }; } function StatusLine({ status }) { return

{status?.message || ""}

; } function Section({ active, children, id }) { return (
{children}
); } function DataTable({ headers, rows, emptyColspan, renderRow, onSort, sortClause }) { return (
{headers.map((header) => { const h = typeof header === "string" ? { key: header, label: header } : header; const sortable = Boolean(h.sortable && h.field && onSort); const active = Boolean(sortable && sortClause && sortClause.field === h.field); const direction = active ? sortClause.dir : ""; return ( ); })} {rows.length ? ( rows.map((row, index) => renderRow(row, index)) ) : ( )}
onSort(h.field) : undefined} title={sortable ? "Нажмите для сортировки" : undefined} > {h.label} {sortable ? {direction === "desc" ? "↓" : "↑"} : null}
Нет данных
); } function TablePager({ tableState, onPrev, onNext, onLoadAll }) { return (
{tableState.showAll ? "Всего: " + tableState.total + " • показаны все записи" : "Всего: " + tableState.total + " • смещение: " + tableState.offset}
); } function FilterToolbar({ filters, onOpen, onRemove, onEdit, getChipLabel }) { return (
{filters.length ? ( filters.map((filter, index) => (
onEdit(index)} role="button" tabIndex={0} onKeyDown={(event) => { if (event.key === "Enter" || event.key === " ") { event.preventDefault(); onEdit(index); } }} title="Редактировать фильтр" > {getChipLabel(filter)}
)) ) : ( Фильтры не заданы )}
); } function Overlay({ open, onClose, children, id }) { return (
{children}
); } function IconButton({ icon, tooltip, onClick, tone }) { return ( ); } function LoginScreen({ onSubmit, status }) { const [email, setEmail] = useState(""); const [password, setPassword] = useState(""); const submit = (event) => { event.preventDefault(); onSubmit(email, password); }; return (

Вход в админ-панель

Используйте учетную запись администратора или юриста.

setEmail(event.target.value)} />
setPassword(event.target.value)} />
); } function FilterModal({ open, tableLabel, fields, draft, status, onClose, onFieldChange, onOpChange, onValueChange, onSubmit, onClear, getOperators, getFieldOptions, }) { if (!open) return null; const selectedField = fields.find((field) => field.field === draft.field) || fields[0] || null; const operators = getOperators(selectedField?.type || "text"); const options = selectedField ? getFieldOptions(selectedField) : []; return ( event.target.id === "filter-overlay" && onClose()}>
event.stopPropagation()}>

Фильтр таблицы

{tableLabel ? (draft.editIndex !== null ? "Редактирование фильтра • " : "Новый фильтр • ") + "Таблица: " + tableLabel : "Выберите поле, оператор и значение."}

{!selectedField || selectedField.type === "text" ? ( ) : selectedField.type === "number" ? ( ) : selectedField.type === "date" ? ( ) : selectedField.type === "boolean" ? ( ) : selectedField.type === "reference" || selectedField.type === "enum" ? ( ) : ( )}
); } function QuoteModal({ open, editing, form, status, onClose, onChange, onSubmit }) { if (!open) return null; return ( event.target.id === "quote-overlay" && onClose()}>
event.stopPropagation()}>

{editing ? "Редактирование цитаты" : "Новая цитата"}

Создание и редактирование цитат.

onChange("author", event.target.value)} />