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
;
}
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 => | {c[1]} | )}
{rows.map((row, i) => {columns.map(c => | {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}
}
| Организация | Номер | Дата | Сумма | Оплата | Контрагент | Комментарий 1С | Номер заказа | Комментарии | Синхронизация |
{rows.map(r => setSelected(r)}>| {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 && }
| Фирма | Счет | Дата | Сумма | Оплата | Отгрузка | Подпись | Номер отгрузки | Контрагент | Номер заказа | Комментарий 1С | Синхронизация |
{rows.map(row => | {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 <>
>;
}
function Organizations() {
const empty = { name: "", odata_base_url: "", invoice_entity_set: "Document_СчетНаОплатуПокупателю", one_c_username: "", one_c_password: "", is_active: true, include_in_dashboards: true, sync_interval_minutes: 5, payment_strategy: "metadata_unknown" };
const [rows, setRows] = useState([]);
const [form, setForm] = useState(empty);
const [message, setMessage] = useState("");
async function load() { setRows(await api("/api/admin/organizations")); }
useEffect(() => { load(); }, []);
async function submit(e) { e.preventDefault(); await api("/api/admin/organizations", { method: "POST", body: JSON.stringify(form) }); setForm(empty); load(); }
async function test(id) { setMessage(JSON.stringify(await api(`/api/admin/organizations/${id}/test-connection`, { method: "POST" }), null, 2)); }
async function sync(id) { setMessage(JSON.stringify(await api(`/api/admin/organizations/${id}/sync-now`, { method: "POST" }), null, 2)); load(); }
async function toggleDashboards(org) {
await api(`/api/admin/organizations/${org.id}`, { method: "PATCH", body: JSON.stringify({ include_in_dashboards: !org.include_in_dashboards }) });
await load();
}
return <>Организации
{rows.map(o =>
{o.name}
)}
{message &&
{message}}
>;
}
function Users() {
const [rows, setRows] = useState([]);
const [form, setForm] = useState({ login: "", password: "", role: "user", is_active: true });
async function load() { setRows(await api("/api/admin/users")); }
useEffect(() => { load(); }, []);
async function submit(e) { e.preventDefault(); await api("/api/admin/users", { method: "POST", body: JSON.stringify(form) }); setForm({ login: "", password: "", role: "user", is_active: true }); load(); }
return <>Пользователи
>;
}
function Sync() {
const [result, setResult] = useState("");
async function syncAll() { setResult(JSON.stringify(await api("/api/admin/organizations/sync-all", { method: "POST" }), null, 2)); }
return <>Синхронизация
{result}>;
}
function App() {
const [user, setUser] = useState(null);
const [loading, setLoading] = useState(true);
useEffect(() => { api("/api/auth/me").then(setUser).catch(() => setUser(null)).finally(() => setLoading(false)); }, []);
if (loading) return Загрузка...
;
return user ? : ;
}
ReactDOM.createRoot(document.getElementById("root")).render();