feat(BillingManager): implement next payment date calculation based on payment history and exchange rates
Publish Fast Tabler Docker image / build-and-push-fast (push) Successful in 2m50s
Publish Fast Tabler Docker image / build-and-push-fast (push) Successful in 2m50s
This commit is contained in:
@@ -117,6 +117,41 @@ function BillingManager() {
|
|||||||
fetchServers();
|
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(() => {
|
useEffect(() => {
|
||||||
if (servers.length === 0 || loading) return;
|
if (servers.length === 0 || loading) return;
|
||||||
@@ -295,11 +330,14 @@ function BillingManager() {
|
|||||||
return Math.ceil((nextPayment - now) / (1000 * 60 * 60 * 24));
|
return Math.ceil((nextPayment - now) / (1000 * 60 * 60 * 24));
|
||||||
};
|
};
|
||||||
|
|
||||||
// Добавить 1 месяц к дате (для обновления nextPaymentDate после платежа)
|
// Вычислить nextPaymentDate по платежу: сумма / месячная стоимость = кол-во месяцев
|
||||||
const addMonthToDate = (dateString) => {
|
const computeNextPaymentDate = (payment, server) => {
|
||||||
if (!dateString) return '';
|
if (!payment?.date) return '';
|
||||||
const d = new Date(dateString);
|
const amountRUB = convertToRUB(payment.amount, payment.currency || 'USD');
|
||||||
d.setMonth(d.getMonth() + 1);
|
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);
|
return d.toISOString().slice(0, 10);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -467,7 +505,7 @@ function BillingManager() {
|
|||||||
const payments = [...(s.payments || [])];
|
const payments = [...(s.payments || [])];
|
||||||
payments.splice(index, 1);
|
payments.splice(index, 1);
|
||||||
const last = payments.slice().sort((a,b) => new Date(b.date) - new Date(a.date))[0];
|
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 {
|
return {
|
||||||
...s,
|
...s,
|
||||||
payments,
|
payments,
|
||||||
@@ -1091,7 +1129,7 @@ function BillingManager() {
|
|||||||
note: paymentDraft.note || ''
|
note: paymentDraft.note || ''
|
||||||
};
|
};
|
||||||
const last = payments.slice().sort((a,b) => new Date(b.date) - new Date(a.date))[0];
|
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 };
|
return { ...s, payments, lastPaymentDate: last?.date || '', lastPaymentAmount: last?.amount || 0, lastPaymentCurrency: last?.currency || 'USD', nextPaymentDate: nextPaymentDate || s.nextPaymentDate };
|
||||||
}));
|
}));
|
||||||
setEditingPayment(null);
|
setEditingPayment(null);
|
||||||
@@ -1105,7 +1143,7 @@ function BillingManager() {
|
|||||||
note: paymentDraft.note || ''
|
note: paymentDraft.note || ''
|
||||||
}];
|
}];
|
||||||
const last = payments.slice().sort((a,b) => new Date(b.date) - new Date(a.date))[0];
|
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 };
|
return { ...s, payments, lastPaymentDate: last?.date || '', lastPaymentAmount: last?.amount || 0, lastPaymentCurrency: last?.currency || 'USD', nextPaymentDate: nextPaymentDate || s.nextPaymentDate };
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user