feat: Add payment history section to BillingManager, update form labels to Russian, and remove country flag display for improved clarity
Publish Fast Tabler Docker image / build-and-push-fast (push) Successful in 7m43s

This commit is contained in:
2025-08-05 23:40:06 +07:00
parent 859395028f
commit 2c67c2b23b
+93 -36
View File
@@ -17,7 +17,8 @@ import {
IconDownload, IconDownload,
IconUpload, IconUpload,
IconEye, IconEye,
IconEyeOff IconEyeOff,
IconHistory
} from '@tabler/icons-react'; } from '@tabler/icons-react';
const API_URL = '/api'; const API_URL = '/api';
@@ -166,7 +167,7 @@ function BillingManager() {
const handleAddSubmit = () => { const handleAddSubmit = () => {
if (!newItem.hostName || !newItem.country || !newItem.provider) { if (!newItem.hostName || !newItem.country || !newItem.provider) {
setError('Пожалуйста, заполните обязательные поля: Host Name, Country, Provider'); setError('Пожалуйста, заполните обязательные поля: Имя хоста, Страна, Провайдер');
return; return;
} }
@@ -180,7 +181,7 @@ function BillingManager() {
const handleEditSubmit = () => { const handleEditSubmit = () => {
if (!editingItem.hostName || !editingItem.country || !editingItem.provider) { if (!editingItem.hostName || !editingItem.country || !editingItem.provider) {
setError('Пожалуйста, заполните обязательные поля: Host Name, Country, Provider'); setError('Пожалуйста, заполните обязательные поля: Имя хоста, Страна, Провайдер');
return; return;
} }
@@ -249,15 +250,6 @@ function BillingManager() {
const uniqueCountries = [...new Set(billingData.map(item => item.country).filter(Boolean))]; const uniqueCountries = [...new Set(billingData.map(item => item.country).filter(Boolean))];
const uniqueStatuses = [...new Set(billingData.map(item => item.status).filter(Boolean))]; const uniqueStatuses = [...new Set(billingData.map(item => item.status).filter(Boolean))];
function countryToFlag(isoCode) {
if (!isoCode) return '🌍';
const codePoints = isoCode
.toUpperCase()
.split('')
.map(char => 127397 + char.charCodeAt());
return String.fromCodePoint(...codePoints);
}
function formatDate(dateString) { function formatDate(dateString) {
if (!dateString) return 'Не указано'; if (!dateString) return 'Не указано';
try { try {
@@ -428,7 +420,7 @@ function BillingManager() {
> >
<option value="">Все страны</option> <option value="">Все страны</option>
{uniqueCountries.map(country => ( {uniqueCountries.map(country => (
<option key={country} value={country}>{countryToFlag(country)} {country}</option> <option key={country} value={country}>{country}</option>
))} ))}
</select> </select>
</div> </div>
@@ -615,9 +607,6 @@ function BillingManager() {
<tr key={item.id}> <tr key={item.id}>
<td> <td>
<div className="d-flex align-items-center"> <div className="d-flex align-items-center">
<span className="avatar avatar-sm me-2">
{countryToFlag(item.country)}
</span>
<div> <div>
<div className="fw-bold">{item.hostName}</div> <div className="fw-bold">{item.hostName}</div>
{item.loginUrl && ( {item.loginUrl && (
@@ -638,7 +627,6 @@ function BillingManager() {
</td> </td>
<td> <td>
<div className="d-flex align-items-center"> <div className="d-flex align-items-center">
<span className="me-2">{countryToFlag(item.country)}</span>
{item.country} {item.country}
</div> </div>
</td> </td>
@@ -743,6 +731,71 @@ function BillingManager() {
</div> </div>
</div> </div>
{/* История платежей */}
<div className="card mt-4">
<div className="card-header">
<h3 className="card-title">
<IconHistory size={20} className="me-2" />
История платежей
</h3>
</div>
<div className="card-body">
<div className="table-responsive">
<table className="table table-vcenter">
<thead>
<tr>
<th>Имя хостера</th>
<th>Дата платежа</th>
<th>Сумма</th>
<th>Валюта</th>
<th>Статус</th>
</tr>
</thead>
<tbody>
{billingData
.filter(item => item.lastPaymentDate && item.lastPaymentAmount)
.sort((a, b) => new Date(b.lastPaymentDate) - new Date(a.lastPaymentDate))
.slice(0, 10)
.map(item => (
<tr key={item.id}>
<td>
<div className="fw-bold">{item.hostName}</div>
<div className="text-muted small">{item.provider}</div>
</td>
<td>{formatDate(item.lastPaymentDate)}</td>
<td>
<div className="fw-bold">
{formatCurrency(item.lastPaymentAmount, item.lastPaymentCurrency)}
</div>
<div className="text-muted small">
{formatCurrencyInRUB(item.lastPaymentAmount, item.lastPaymentCurrency)}
</div>
</td>
<td>
<span className="badge bg-blue-lt text-blue">
{item.lastPaymentCurrency || 'USD'}
</span>
</td>
<td>
<span className="badge bg-success-lt text-success">
Оплачен
</span>
</td>
</tr>
))}
{billingData.filter(item => item.lastPaymentDate && item.lastPaymentAmount).length === 0 && (
<tr>
<td colSpan="5" className="text-center text-muted py-4">
История платежей пуста
</td>
</tr>
)}
</tbody>
</table>
</div>
</div>
</div>
{/* Модальные окна */} {/* Модальные окна */}
<AddBillingModal <AddBillingModal
show={showAddModal} show={showAddModal}
@@ -775,7 +828,7 @@ function AddBillingModal({ show, item, onItemChange, onSubmit, onClose }) {
if (!show) return null; if (!show) return null;
return ( return (
<div className="modal show d-block" tabIndex="-1"> <div className="modal show d-block" tabIndex="-1" style={{ backgroundColor: 'rgba(0,0,0,0.5)' }}>
<div className="modal-dialog modal-lg"> <div className="modal-dialog modal-lg">
<div className="modal-content"> <div className="modal-content">
<div className="modal-header"> <div className="modal-header">
@@ -785,7 +838,7 @@ function AddBillingModal({ show, item, onItemChange, onSubmit, onClose }) {
<div className="modal-body"> <div className="modal-body">
<div className="row g-3"> <div className="row g-3">
<div className="col-md-6"> <div className="col-md-6">
<label className="form-label">Host Name *</label> <label className="form-label">Имя хоста *</label>
<input <input
type="text" type="text"
className="form-control" className="form-control"
@@ -795,7 +848,7 @@ function AddBillingModal({ show, item, onItemChange, onSubmit, onClose }) {
/> />
</div> </div>
<div className="col-md-6"> <div className="col-md-6">
<label className="form-label">Purpose</label> <label className="form-label">Назначение</label>
<select <select
className="form-select" className="form-select"
value={item.purpose} value={item.purpose}
@@ -812,7 +865,7 @@ function AddBillingModal({ show, item, onItemChange, onSubmit, onClose }) {
</select> </select>
</div> </div>
<div className="col-md-6"> <div className="col-md-6">
<label className="form-label">Country *</label> <label className="form-label">Страна *</label>
<input <input
type="text" type="text"
className="form-control" className="form-control"
@@ -822,7 +875,7 @@ function AddBillingModal({ show, item, onItemChange, onSubmit, onClose }) {
/> />
</div> </div>
<div className="col-md-6"> <div className="col-md-6">
<label className="form-label">Provider *</label> <label className="form-label">Провайдер *</label>
<input <input
type="text" type="text"
className="form-control" className="form-control"
@@ -832,25 +885,27 @@ function AddBillingModal({ show, item, onItemChange, onSubmit, onClose }) {
/> />
</div> </div>
<div className="col-md-6"> <div className="col-md-6">
<label className="form-label">Login URL</label> <label className="form-label">Ссылка для входа</label>
<input <input
type="text" type="text"
className="form-control" className="form-control"
value={item.loginUrl} value={item.loginUrl}
onChange={(e) => onItemChange({ ...item, loginUrl: e.target.value })} onChange={(e) => onItemChange({ ...item, loginUrl: e.target.value })}
placeholder="example.com"
/> />
</div> </div>
<div className="col-md-6"> <div className="col-md-6">
<label className="form-label">Monthly Cost</label> <label className="form-label">Месячная стоимость</label>
<input <input
type="number" type="number"
step="0.01"
className="form-control" className="form-control"
value={item.monthlyCost} value={item.monthlyCost}
onChange={(e) => onItemChange({ ...item, monthlyCost: parseFloat(e.target.value) || 0 })} onChange={(e) => onItemChange({ ...item, monthlyCost: parseFloat(e.target.value) || 0 })}
/> />
</div> </div>
<div className="col-md-6"> <div className="col-md-6">
<label className="form-label">Next Payment Date</label> <label className="form-label">Дата следующего платежа</label>
<input <input
type="date" type="date"
className="form-control" className="form-control"
@@ -859,7 +914,7 @@ function AddBillingModal({ show, item, onItemChange, onSubmit, onClose }) {
/> />
</div> </div>
<div className="col-md-6"> <div className="col-md-6">
<label className="form-label">Status</label> <label className="form-label">Статус</label>
<select <select
className="form-select" className="form-select"
value={item.status} value={item.status}
@@ -889,7 +944,7 @@ function EditBillingModal({ show, item, onItemChange, onSubmit, onClose }) {
if (!show) return null; if (!show) return null;
return ( return (
<div className="modal show d-block" tabIndex="-1"> <div className="modal show d-block" tabIndex="-1" style={{ backgroundColor: 'rgba(0,0,0,0.5)' }}>
<div className="modal-dialog modal-lg"> <div className="modal-dialog modal-lg">
<div className="modal-content"> <div className="modal-content">
<div className="modal-header"> <div className="modal-header">
@@ -899,7 +954,7 @@ function EditBillingModal({ show, item, onItemChange, onSubmit, onClose }) {
<div className="modal-body"> <div className="modal-body">
<div className="row g-3"> <div className="row g-3">
<div className="col-md-6"> <div className="col-md-6">
<label className="form-label">Host Name *</label> <label className="form-label">Имя хоста *</label>
<input <input
type="text" type="text"
className="form-control" className="form-control"
@@ -909,7 +964,7 @@ function EditBillingModal({ show, item, onItemChange, onSubmit, onClose }) {
/> />
</div> </div>
<div className="col-md-6"> <div className="col-md-6">
<label className="form-label">Purpose</label> <label className="form-label">Назначение</label>
<select <select
className="form-select" className="form-select"
value={item.purpose || ''} value={item.purpose || ''}
@@ -926,7 +981,7 @@ function EditBillingModal({ show, item, onItemChange, onSubmit, onClose }) {
</select> </select>
</div> </div>
<div className="col-md-6"> <div className="col-md-6">
<label className="form-label">Country *</label> <label className="form-label">Страна *</label>
<input <input
type="text" type="text"
className="form-control" className="form-control"
@@ -936,7 +991,7 @@ function EditBillingModal({ show, item, onItemChange, onSubmit, onClose }) {
/> />
</div> </div>
<div className="col-md-6"> <div className="col-md-6">
<label className="form-label">Provider *</label> <label className="form-label">Провайдер *</label>
<input <input
type="text" type="text"
className="form-control" className="form-control"
@@ -946,25 +1001,27 @@ function EditBillingModal({ show, item, onItemChange, onSubmit, onClose }) {
/> />
</div> </div>
<div className="col-md-6"> <div className="col-md-6">
<label className="form-label">Login URL</label> <label className="form-label">Ссылка для входа</label>
<input <input
type="text" type="text"
className="form-control" className="form-control"
value={item.loginUrl || ''} value={item.loginUrl || ''}
onChange={(e) => onItemChange({ ...item, loginUrl: e.target.value })} onChange={(e) => onItemChange({ ...item, loginUrl: e.target.value })}
placeholder="example.com"
/> />
</div> </div>
<div className="col-md-6"> <div className="col-md-6">
<label className="form-label">Monthly Cost</label> <label className="form-label">Месячная стоимость</label>
<input <input
type="number" type="number"
step="0.01"
className="form-control" className="form-control"
value={item.monthlyCost || 0} value={item.monthlyCost || 0}
onChange={(e) => onItemChange({ ...item, monthlyCost: parseFloat(e.target.value) || 0 })} onChange={(e) => onItemChange({ ...item, monthlyCost: parseFloat(e.target.value) || 0 })}
/> />
</div> </div>
<div className="col-md-6"> <div className="col-md-6">
<label className="form-label">Next Payment Date</label> <label className="form-label">Дата следующего платежа</label>
<input <input
type="date" type="date"
className="form-control" className="form-control"
@@ -973,7 +1030,7 @@ function EditBillingModal({ show, item, onItemChange, onSubmit, onClose }) {
/> />
</div> </div>
<div className="col-md-6"> <div className="col-md-6">
<label className="form-label">Status</label> <label className="form-label">Статус</label>
<select <select
className="form-select" className="form-select"
value={item.status || 'active'} value={item.status || 'active'}
@@ -1003,7 +1060,7 @@ function DeleteBillingModal({ show, item, onDelete, onClose }) {
if (!show) return null; if (!show) return null;
return ( return (
<div className="modal show d-block" tabIndex="-1"> <div className="modal show d-block" tabIndex="-1" style={{ backgroundColor: 'rgba(0,0,0,0.5)' }}>
<div className="modal-dialog"> <div className="modal-dialog">
<div className="modal-content"> <div className="modal-content">
<div className="modal-header"> <div className="modal-header">