const { useEffect, useMemo, useState } = React; async function api(path, options = {}) { const res = await fetch(path, { cache: "no-store", credentials: "include", headers: { "Content-Type": "application/json", ...(options.headers || {}) }, ...options, }); if (!res.ok) { let detail = "Ошибка запроса"; try { detail = (await res.json()).detail || detail; } catch (_) {} throw new Error(Array.isArray(detail) ? detail.map(x => x.msg).join(", ") : detail); } return res.json(); } function fmtMoney(value) { return new Intl.NumberFormat("ru-RU", { style: "currency", currency: "RUB", maximumFractionDigits: 2 }).format(Number(value || 0)); } function fmtDate(value) { return value ? new Intl.DateTimeFormat("ru-RU", { dateStyle: "short", timeStyle: "short" }).format(new Date(value)) : ""; } function fmtDateOnly(value) { if (!value) return ""; const text = String(value); const match = text.match(/^(\d{4})-(\d{2})-(\d{2})/); if (match) return `${match[3]}.${match[2]}.${match[1]}`; return new Intl.DateTimeFormat("ru-RU", { dateStyle: "short" }).format(new Date(value)); } function prettyJson(value) { return JSON.stringify(value ?? null, null, 2); } function paymentLabel(value) { return ({ unpaid: "Не оплачен", partially_paid: "Оплачен частично", paid: "Оплачен", canceled: "Отменен", unknown: "Неизвестно" })[value] || value; } function shipmentLabel(value) { return ({ not_shipped: "Не отгружен", partially_shipped: "Отгружен частично", shipped: "Отгружен", unknown: "Неизвестно" })[value] || value; } function signatureLabel(value) { return ({ not_signed: "Не подписан", signed: "Подписан", unknown: "Неизвестно" })[value] || value; } function Login({ onLogin }) { const [login, setLogin] = useState(""); const [password, setPassword] = useState(""); const [error, setError] = useState(""); async function submit(e) { e.preventDefault(); setError(""); try { onLogin(await api("/api/auth/login", { method: "POST", body: JSON.stringify({ login, password }) })); } catch (err) { setError(err.message); } } return
Score
{error &&
{error}
} setLogin(e.target.value)} autoFocus /> setPassword(e.target.value)} />
; } function Shell({ user, setUser }) { const [path, setPath] = useState(location.pathname === "/" ? "/dashboard" : location.pathname); useEffect(() => { const onPop = () => setPath(location.pathname); addEventListener("popstate", onPop); return () => removeEventListener("popstate", onPop); }, []); function nav(to) { history.pushState(null, "", to); setPath(to); } async function logout() { await api("/api/auth/logout", { method: "POST" }); setUser(null); } const page = path.startsWith("/invoices") ? : path.startsWith("/suppliers") ? : path.startsWith("/shipping") ? : path.startsWith("/ai/questions") ? : path.startsWith("/admin/users") ? : path.startsWith("/admin/organizations") ? : path.startsWith("/admin/sync") ? : ; return ; } function Dashboard() { const [data, setData] = useState(null); const [error, setError] = useState(""); useEffect(() => { api("/api/dashboard/summary").then(setData).catch(e => setError(e.message)); }, []); if (error) return
{error}
; if (!data) return
Загрузка...
; return <>

Dashboard

Неоплаченных счетов
{data.total_unpaid_count}
Сумма неоплаченных
{fmtMoney(data.total_unpaid_amount)}
Без номера заказа
{data.without_order_number_count}
Сумма без номера
{fmtMoney(data.without_order_number_amount)}

По организациям

Синхронизация

; } function SimpleTable({ rows, columns, money, footerRows = [] }) { const moneyColumns = Array.isArray(money) ? money : [money]; function cellValue(value) { if (typeof value === "boolean") return value ? "Да" : "Нет"; return String(value ?? ""); } function cells(row) { return columns.map(c => {moneyColumns.includes(c[0]) ? fmtMoney(row[c[0]]) : cellValue(row[c[0]])}); } return
{columns.map(c => )} {rows.map((row, i) => {cells(row)})} {footerRows.length > 0 && {footerRows.map((row, i) => {cells(row)})}}
{c[1]}
; } function Invoices({ user }) { const [rows, setRows] = useState([]); const [orgs, setOrgs] = useState([]); const [selected, setSelected] = useState(null); const [aiInvoice, setAiInvoice] = useState(null); const [filters, setFilters] = useState({ organization_id: "", order_number: "" }); const [error, setError] = useState(""); async function load() { const qs = new URLSearchParams(Object.entries(filters).filter(([,v]) => v)); setRows(await api(`/api/invoices?${qs}`)); } useEffect(() => { load().catch(e => setError(e.message)); }, [filters]); useEffect(() => { api("/api/organizations").then(setOrgs).catch(() => setOrgs([])); }, []); return <>

Счета

{error &&
{error}
}
{rows.map(r => setSelected(r)}>)}
ОрганизацияНомерДатаСуммаОплатаОжидаемая оплатаКонтрагентКомментарий 1СНомер заказаБ24AIКомментарииСинхронизация
{r.organization_name}{r.number}{fmtDate(r.date)}{fmtMoney(r.amount)}{paymentLabel(r.payment_status)}{r.expected_payment_date ? fmtDateOnly(r.expected_payment_date) : нет}{r.counterparty_name}{r.one_c_comment}{r.order_number || нет}{r.bitrix_deal_url ? e.stopPropagation()}>#{r.bitrix_deal_id} : нет}{user.role === "admin" ? : -}{r.comments_count}{fmtDate(r.last_seen_at)}
{selected && { setSelected(null); load(); }} />} {aiInvoice && setAiInvoice(null)} />} ; } function Shipping() { const [data, setData] = useState(null); const [error, setError] = useState(""); async function load() { setError(""); try { setData(await api("/api/shipping")); } catch (e) { setError(e.message); } } useEffect(() => { load(); }, []); const rows = data?.rows || []; return <>

Отгружено без подписи

{error &&
{error}
} {!data ?
Загрузка...
: <>
{data.summary.map(item =>
{item.label}
{item.count}
{fmtMoney(item.amount)}
)}
{data.by_organization?.length > 0 &&

По фирмам

}
{rows.map(row => )}
ФирмаСчетДатаСуммаОплатаОтгрузкаПодписьНомер отгрузкиКонтрагентНомер заказаКомментарий 1ССинхронизация
{row.organization_name}{row.number}{fmtDate(row.date)}{fmtMoney(row.amount)}{paymentLabel(row.payment_status)}{row.shipment_status_label || shipmentLabel(row.shipment_status)}{row.signature_status_label || signatureLabel(row.signature_status)}{((row.shipment_numbers || row.realization_numbers || []).join(", ")) || нет}{row.counterparty_name}{row.order_number || нет}{row.one_c_comment}{fmtDate(row.last_seen_at)}
} ; } function matchLabel(value) { return ({ advance_offset: "Зачет аванса", invoice_number: "Номер счета", comment_invoice_number: "Комментарий + счет", counterparty_amount: "Поставщик + сумма", not_found: "Не найдено" })[value] || value; } function Suppliers() { const [data, setData] = useState(null); const [error, setError] = useState(""); const [syncing, setSyncing] = useState(false); const [organizationId, setOrganizationId] = useState(""); async function load() { setError(""); try { setData(await api("/api/suppliers")); } catch (e) { setError(e.message); } } async function sync() { setError(""); setSyncing(true); try { setData(await api("/api/suppliers/sync", { method: "POST" })); } catch (e) { setError(e.message); } finally { setSyncing(false); } } useEffect(() => { load(); }, []); const rows = data?.rows || []; const organizationOptions = useMemo(() => { return (data?.by_organization || []) .map(row => { const source = (data?.by_supplier || []).find(item => item.organization_name === row.organization); return { id: String(source?.organization_id || ""), name: row.organization }; }) .filter(org => org.id) .sort((a, b) => a.name.localeCompare(b.name, "ru")); }, [data]); const selectedOrganization = organizationOptions.find(org => org.id === organizationId); const filteredRows = organizationId ? rows.filter(row => String(row.organization_id) === organizationId) : rows; const filteredAmount = filteredRows.reduce((sum, row) => sum + Number(row.amount || 0), 0); const bySupplier = useMemo(() => { const rows = data?.by_supplier || []; return (organizationId ? rows.filter(row => String(row.organization_id) === organizationId) : rows) .slice() .sort((a, b) => Number(b.open_amount || 0) - Number(a.open_amount || 0)); }, [data, organizationId]); const bySupplierTotals = useMemo(() => { return bySupplier.reduce((total, row) => ({ supplier: "ИТОГО", open_count: total.open_count + Number(row.open_count || 0), open_amount: total.open_amount + Number(row.open_amount || 0), closed_count: total.closed_count + Number(row.closed_count || 0), closed_amount: total.closed_amount + Number(row.closed_amount || 0), total_count: total.total_count + Number(row.total_count || 0), total_amount: total.total_amount + Number(row.total_amount || 0), }), { supplier: "ИТОГО", open_count: 0, open_amount: 0, closed_count: 0, closed_amount: 0, total_count: 0, total_amount: 0, }); }, [bySupplier]); const filteredByOrganization = organizationId ? (data?.by_organization || []).filter(row => row.organization === selectedOrganization?.name) : (data?.by_organization || []); return <>

Поставщики

{error &&
{error}
} {!data ?
Загрузка...
: <>
Требуют закрывающих
{filteredRows.length}
{fmtMoney(filteredAmount)}
Год оплат
{data.year}
обновлено {fmtDate(data.generated_at)}
{filteredByOrganization.length > 0 &&

По фирмам

}
{filteredRows.map((row, index) => )}
ФирмаДата оплатыПлатежСуммаПоставщикСтатусЗакрывающийВх. номерСвязьНазначение
{row.organization_name}{fmtDate(row.payment_date)}{row.payment_number}{fmtMoney(row.amount)}{row.supplier_name}{row.closing_status_label}{row.closing_number ? `${row.closing_number} от ${fmtDate(row.closing_date)}` : нет}{row.closing_incoming_number || нет}{matchLabel(row.match_method)}{row.purpose}
{bySupplier.length > 0 &&

По контрагентам

} {data.diagnostics?.length > 0 &&

Диагностика

} } ; } function InvoiceDrawer({ invoice, close }) { const [detail, setDetail] = useState(invoice); const [comments, setComments] = useState([]); const [orderNumber, setOrderNumber] = useState(invoice.order_number || ""); const [comment, setComment] = useState(""); const [error, setError] = useState(""); const [bitrixDeals, setBitrixDeals] = useState(null); const [bitrixLoading, setBitrixLoading] = useState(false); const [bitrixError, setBitrixError] = useState(""); async function load() { setDetail(await api(`/api/invoices/${invoice.id}`)); setComments(await api(`/api/invoices/${invoice.id}/comments`)); } useEffect(() => { load().catch(e => setError(e.message)); }, []); async function saveOrder() { setError(""); try { setDetail(await api(`/api/invoices/${invoice.id}/order-number`, { method: "PATCH", body: JSON.stringify({ order_number: orderNumber }) })); } catch (e) { setError(e.message); } } async function addComment() { setError(""); try { await api(`/api/invoices/${invoice.id}/comments`, { method: "POST", body: JSON.stringify({ comment }) }); setComment(""); await load(); } catch (e) { setError(e.message); } } async function findBitrixDeal() { const normalized = orderNumber.replace(/\D/g, ""); if (!normalized) { setBitrixError("Укажите номер заказа"); setBitrixDeals(null); return; } setBitrixLoading(true); setBitrixError(""); try { setBitrixDeals(await api(`/api/bitrix/deals/by-order/${encodeURIComponent(normalized)}`)); } catch (e) { setBitrixError(e.message); setBitrixDeals(null); } finally { setBitrixLoading(false); } } return <>