From d8457b1497904f039480eea66e57ec2321708266 Mon Sep 17 00:00:00 2001 From: shats Date: Wed, 4 Feb 2026 11:29:34 +0700 Subject: [PATCH] feat(BillingManager): implement next payment date calculation based on payment history and exchange rates --- frontend/src/BillingManager.jsx | 54 ++++++++++++++++++++++++++++----- 1 file changed, 46 insertions(+), 8 deletions(-) diff --git a/frontend/src/BillingManager.jsx b/frontend/src/BillingManager.jsx index 204986e..da54da3 100644 --- a/frontend/src/BillingManager.jsx +++ b/frontend/src/BillingManager.jsx @@ -117,6 +117,41 @@ function BillingManager() { fetchServers(); }, []); + // Пересчёт nextPaymentDate по платежам при загрузке (для уже сохранённых данных) + useEffect(() => { + if (billingData.length === 0 || ratesLoading) return; + setBillingData(prev => { + const updated = prev.map(item => { + const payments = item.payments || []; + if (payments.length === 0) return item; + const last = payments.slice().sort((a, b) => new Date(b.date) - new Date(a.date))[0]; + const computed = (() => { + if (!last?.date) return null; + const amountRUB = (amt, ccy) => { + if (!amt) return 0; + const rub = Number(exchangeRates.RUB) || 1; + if (!ccy || ccy === 'RUB') return amt; + if (ccy === 'USD') return amt * rub; + const c = Number(exchangeRates[ccy]); + return c ? (amt / c) * rub : amt * rub; + }; + const costRUB = amountRUB(item.monthlyCost || 0, item.monthlyCostCurrency || 'USD'); + const amtRUB = amountRUB(last.amount, last.currency || 'USD'); + const months = costRUB > 0 ? Math.max(1, Math.floor(amtRUB / costRUB)) : 1; + const d = new Date(last.date); + d.setMonth(d.getMonth() + months); + return d.toISOString().slice(0, 10); + })(); + const current = new Date(item.nextPaymentDate || 0); + const now = new Date(); + if (computed && current < now) return { ...item, nextPaymentDate: computed }; + return item; + }); + const changed = updated.some((it, i) => it.nextPaymentDate !== prev[i]?.nextPaymentDate); + return changed ? updated : prev; + }); + }, [billingData.length, ratesLoading, exchangeRates]); + // Автоматическая синхронизация: добавляем серверы без записей в биллинге useEffect(() => { if (servers.length === 0 || loading) return; @@ -295,11 +330,14 @@ function BillingManager() { return Math.ceil((nextPayment - now) / (1000 * 60 * 60 * 24)); }; - // Добавить 1 месяц к дате (для обновления nextPaymentDate после платежа) - const addMonthToDate = (dateString) => { - if (!dateString) return ''; - const d = new Date(dateString); - d.setMonth(d.getMonth() + 1); + // Вычислить nextPaymentDate по платежу: сумма / месячная стоимость = кол-во месяцев + const computeNextPaymentDate = (payment, server) => { + if (!payment?.date) return ''; + const amountRUB = convertToRUB(payment.amount, payment.currency || 'USD'); + const costRUB = convertToRUB(server.monthlyCost || 0, server.monthlyCostCurrency || 'USD'); + const months = costRUB > 0 ? Math.max(1, Math.floor(amountRUB / costRUB)) : 1; + const d = new Date(payment.date); + d.setMonth(d.getMonth() + months); return d.toISOString().slice(0, 10); }; @@ -467,7 +505,7 @@ function BillingManager() { const payments = [...(s.payments || [])]; payments.splice(index, 1); const last = payments.slice().sort((a,b) => new Date(b.date) - new Date(a.date))[0]; - const nextPaymentDate = addMonthToDate(last?.date); + const nextPaymentDate = last ? computeNextPaymentDate(last, s) : ''; return { ...s, payments, @@ -1091,7 +1129,7 @@ function BillingManager() { note: paymentDraft.note || '' }; const last = payments.slice().sort((a,b) => new Date(b.date) - new Date(a.date))[0]; - const nextPaymentDate = addMonthToDate(last?.date); + const nextPaymentDate = computeNextPaymentDate(last, s); return { ...s, payments, lastPaymentDate: last?.date || '', lastPaymentAmount: last?.amount || 0, lastPaymentCurrency: last?.currency || 'USD', nextPaymentDate: nextPaymentDate || s.nextPaymentDate }; })); setEditingPayment(null); @@ -1105,7 +1143,7 @@ function BillingManager() { note: paymentDraft.note || '' }]; const last = payments.slice().sort((a,b) => new Date(b.date) - new Date(a.date))[0]; - const nextPaymentDate = addMonthToDate(last?.date); + const nextPaymentDate = computeNextPaymentDate(last, s); return { ...s, payments, lastPaymentDate: last?.date || '', lastPaymentAmount: last?.amount || 0, lastPaymentCurrency: last?.currency || 'USD', nextPaymentDate: nextPaymentDate || s.nextPaymentDate }; })); }