const { useEffect, useMemo, useState } = React; async function api(path, options = {}) { const res = await fetch(path, { 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 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("/shipping") ? : 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 }) { function cellValue(value) { if (typeof value === "boolean") return value ? "Да" : "Нет"; return String(value ?? ""); } return
{columns.map(c => )} {rows.map((row, i) => {columns.map(c => )})}
{c[1]}
{money === c[0] ? fmtMoney(row[c[0]]) : cellValue(row[c[0]])}
; } function Invoices() { const [rows, setRows] = useState([]); const [orgs, setOrgs] = useState([]); const [selected, setSelected] = 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СНомер заказаКомментарииСинхронизация
{r.organization_name}{r.number}{fmtDate(r.date)}{fmtMoney(r.amount)}{paymentLabel(r.payment_status)}{r.counterparty_name}{r.one_c_comment}{r.order_number || нет}{r.comments_count}{fmtDate(r.last_seen_at)}
{selected && { setSelected(null); load(); }} />} ; } 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 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(""); 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); } } return <>