// financial-aid.jsx — Поворотна фінансова допомога (ПФД) для ТОВ. // Облік внесків від учасника / стороннього ТОВ + контроль строків повернення. // Рух коштів — у «Грошах» (cash_ops: loan_in при отриманні, loan_out при поверненні). const LENDER_TYPE = { participant: "Учасник / засновник", company: "Стороннє ТОВ" }; function AidForm({ accounts, editing, onSave, onCancel }) { const val = (id) => id === null ? "cash" : id; const toId = (v) => v === "cash" ? null : v; const today = window.SYS_DATE; const isEdit = !!editing; // Строк = 12 календарних місяців від дати отримання: пізніше сума рахується доходом. const plus12 = (ds) => { try { const d = new Date((ds || today) + "T00:00:00"); d.setMonth(d.getMonth() + 12); return d.toISOString().slice(0, 10); } catch (e) { return ds || today; } }; const [f, setF] = React.useState(editing ? { lenderType: editing.lenderType || "participant", lender: editing.lender || "", amount: String(editing.amount || ""), account: val(editing.account || null), dateIn: editing.dateIn || today, dueDate: editing.dueDate || plus12(editing.dateIn || today), note: editing.note || "" } : { lenderType: "participant", lender: "", amount: "", account: val(accounts[0] ? accounts[0].id : null), dateIn: today, dueDate: plus12(today), note: "" }); const [busy, setBusy] = React.useState(false); const set = (k, v) => setF(p => ({ ...p, [k]: v })); const num = parseFloat(String(f.amount).replace(/\s/g, "").replace(",", ".")) || 0; const canSave = f.lender.trim() && num > 0 && f.dateIn && f.dueDate; const save = async () => { if (!canSave || busy) return; setBusy(true); const ok = await onSave({ lenderType: f.lenderType, lender: f.lender.trim(), amount: Math.round(num), account: toId(f.account), dateIn: f.dateIn, dueDate: f.dueDate, note: f.note.trim() || null }); setBusy(false); if (ok !== false) onCancel(); }; return (
set("lender", e.target.value)} placeholder={f.lenderType === "participant" ? "напр. Підкапка М.І." : "напр. ТОВ «Партнер»"} />
set("amount", e.target.value)} inputMode="numeric" placeholder="напр. 200000" />
setF(p => ({ ...p, dateIn: e.target.value, dueDate: plus12(e.target.value) }))} />
set("dueDate", e.target.value)} />
авто: 12 міс від дати отримання
set("note", e.target.value)} placeholder="договір/призначення (необов'язково)" />
{isEdit ? "Зміни синхронізуються з відповідним надходженням у «Грошах» (рахунок/сума/дата)." : "При збереженні на обраний рахунок одразу додасться надходження — баланс зросте."}
); } function FinancialAidPage({ role }) { const live = !!(window.API && window.API.mode === "live"); const m = window.formatMoney; const today = window.SYS_DATE; const canEdit = role === "director" || role === "accountant"; const [aids, setAids] = React.useState(live ? [] : (window.DATA.FINANCIAL_AID || [])); const [accounts, setAccounts] = React.useState([]); const [adding, setAdding] = React.useState(false); const [editingAid, setEditingAid] = React.useState(null); const [busy, setBusy] = React.useState(false); const reload = React.useCallback(() => { if (!live) return Promise.resolve(); return Promise.all([ window.API.list("financial-aid").then(r => setAids(r || [])).catch(() => {}), window.API.list("cash-accounts").then(r => setAccounts(r || [])).catch(() => {}), ]); }, [live]); React.useEffect(() => { reload(); }, [reload]); const accountList = [{ id: null, label: "Готівка", entity: "cash" }, ...accounts]; const acctLabel = (id) => (accountList.find(a => (a.id || null) === (id || null)) || {}).label || "—"; const daysLeft = (due) => { if (!due) return null; try { return Math.round((new Date(due + "T00:00:00") - new Date(today + "T00:00:00")) / 86400000); } catch (e) { return null; } }; const statusOf = (a) => { if (a.status === "returned") return { key: "returned", label: "повернено", cls: "tone-neutral" }; const d = daysLeft(a.dueDate); if (d != null && d < 0) return { key: "overdue", label: `прострочено ${-d} дн`, cls: "tone-warn" }; if (d != null && d <= 14) return { key: "soon", label: `${d} дн до повернення`, cls: "tone-amber" }; return { key: "active", label: "активна", cls: "tone-live" }; }; const active = aids.filter(a => a.status !== "returned"); const totalActive = active.reduce((s, a) => s + (a.amount || 0), 0); const overdue = active.filter(a => statusOf(a).key === "overdue"); const totalOverdue = overdue.reduce((s, a) => s + (a.amount || 0), 0); const returned = aids.filter(a => a.status === "returned"); const sorted = [...aids].sort((a, b) => { if ((a.status === "returned") !== (b.status === "returned")) return a.status === "returned" ? 1 : -1; return (a.dueDate || "").localeCompare(b.dueDate || ""); }); const addAid = async (data) => { if (live) { try { const created = await window.API.post("/financial-aid", { ...data, status: "active" }); await window.API.post("/cash-ops", { date: data.dateIn, type: "in", category: "loan_in", amount: data.amount, counterparty: data.lender, note: "Поворотна фін. допомога" + (data.note ? " · " + data.note : ""), account: data.account || null, ref: "fa:" + created.id + ":in" }); await reload(); } catch (e) { alert("Не вдалося додати ПФД: " + (e.message || e)); return false; } } else { setAids(prev => [{ ...data, id: "fa-" + Math.random().toString(36).slice(2, 7), status: "active" }, ...prev]); } return true; }; // Редагування ПФД: оновлюємо запис реєстру + синхронізуємо прив'язане надходження в «Грошах» // (нога fa::in) і, якщо вже повернено, видаток (fa::out). const editAid = async (data) => { const id = editingAid.id; if (live) { try { await window.API.patch("/financial-aid/" + id, data); const ops = await window.API.list("cash-ops").catch(() => []); const inLeg = (ops || []).find(o => o.ref === "fa:" + id + ":in"); const outLeg = (ops || []).find(o => o.ref === "fa:" + id + ":out"); if (inLeg) await window.API.patch("/cash-ops/" + inLeg.id, { date: data.dateIn, amount: data.amount, counterparty: data.lender, account: data.account || null, note: "Поворотна фін. допомога" + (data.note ? " · " + data.note : "") }); if (outLeg) await window.API.patch("/cash-ops/" + outLeg.id, { amount: data.amount, counterparty: data.lender, account: data.account || null }); await reload(); } catch (e) { alert("Не вдалося зберегти зміни: " + (e.message || e)); return false; } } else { setAids(prev => prev.map(x => x.id === id ? { ...x, ...data } : x)); } return true; }; const returnAid = async (a) => { if (!confirm(`Повернути ПФД ${m(a.amount)} ₴ (${a.lender})? Створиться видаток з рахунку «${acctLabel(a.account)}».`)) return; setBusy(true); if (live) { try { await window.API.post("/cash-ops", { date: today, type: "out", category: "loan_out", amount: a.amount, counterparty: a.lender, note: "Повернення фін. допомоги", account: a.account || null, ref: "fa:" + a.id + ":out" }); await window.API.patch("/financial-aid/" + a.id, { status: "returned", returnDate: today }); await reload(); } catch (e) { alert("Помилка повернення: " + (e.message || e)); } } else { setAids(prev => prev.map(x => x.id === a.id ? { ...x, status: "returned", returnDate: today } : x)); } setBusy(false); }; if (!window.canSeeCash || !window.canSeeCash(role)) { return (

Доступ обмежено

Фінанси бачать лише директор і бухгалтер.

); } return (

Поворотна фінансова допомога

ПФД для ТОВ {active.length} активних {overdue.length > 0 && <>{overdue.length} прострочено}
Непогашено (активні)
{m(totalActive)}
{active.length} внесків
0 ? "is-warn" : ""}`}>
Прострочено
{m(totalOverdue)}
{overdue.length} потребують повернення
Повернено
{returned.length}
закритих внесків
ПФД треба повернути протягом 12 календарних місяців від отримання — інакше сума рахується в дохід ТОВ. Бейдж «прострочено» підсвічує те, що пора повертати.
{canEdit && ( )}
{adding && setAdding(false)} />} {editingAid && setEditingAid(null)} />} {sorted.length === 0 && ( )} {sorted.map(a => { const st = statusOf(a); return ( ); })}
Позикодавець Тип Рахунок Сума Отримано Строк повернення Статус
Ще немає записів. Натисніть «Додати ПФД».
{a.lender}
{a.note &&
{a.note}
}
{LENDER_TYPE[a.lenderType] || a.lenderType} {acctLabel(a.account)} {m(a.amount)} ₴ {window.formatDate(a.dateIn)} {window.formatDate(a.dueDate)} {a.status === "returned" && a.returnDate &&
повернено {window.formatDate(a.returnDate)}
}
{st.label}
{canEdit && a.status !== "returned" && ( )} {canEdit && ( )}
); } window.FinancialAidPage = FinancialAidPage;