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
Publish Fast Tabler Docker image / build-and-push-fast (push) Successful in 7m43s
This commit is contained in:
@@ -17,7 +17,8 @@ import {
|
||||
IconDownload,
|
||||
IconUpload,
|
||||
IconEye,
|
||||
IconEyeOff
|
||||
IconEyeOff,
|
||||
IconHistory
|
||||
} from '@tabler/icons-react';
|
||||
|
||||
const API_URL = '/api';
|
||||
@@ -166,7 +167,7 @@ function BillingManager() {
|
||||
|
||||
const handleAddSubmit = () => {
|
||||
if (!newItem.hostName || !newItem.country || !newItem.provider) {
|
||||
setError('Пожалуйста, заполните обязательные поля: Host Name, Country, Provider');
|
||||
setError('Пожалуйста, заполните обязательные поля: Имя хоста, Страна, Провайдер');
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -180,7 +181,7 @@ function BillingManager() {
|
||||
|
||||
const handleEditSubmit = () => {
|
||||
if (!editingItem.hostName || !editingItem.country || !editingItem.provider) {
|
||||
setError('Пожалуйста, заполните обязательные поля: Host Name, Country, Provider');
|
||||
setError('Пожалуйста, заполните обязательные поля: Имя хоста, Страна, Провайдер');
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -249,15 +250,6 @@ function BillingManager() {
|
||||
const uniqueCountries = [...new Set(billingData.map(item => item.country).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) {
|
||||
if (!dateString) return 'Не указано';
|
||||
try {
|
||||
@@ -428,7 +420,7 @@ function BillingManager() {
|
||||
>
|
||||
<option value="">Все страны</option>
|
||||
{uniqueCountries.map(country => (
|
||||
<option key={country} value={country}>{countryToFlag(country)} {country}</option>
|
||||
<option key={country} value={country}>{country}</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
@@ -615,9 +607,6 @@ function BillingManager() {
|
||||
<tr key={item.id}>
|
||||
<td>
|
||||
<div className="d-flex align-items-center">
|
||||
<span className="avatar avatar-sm me-2">
|
||||
{countryToFlag(item.country)}
|
||||
</span>
|
||||
<div>
|
||||
<div className="fw-bold">{item.hostName}</div>
|
||||
{item.loginUrl && (
|
||||
@@ -638,7 +627,6 @@ function BillingManager() {
|
||||
</td>
|
||||
<td>
|
||||
<div className="d-flex align-items-center">
|
||||
<span className="me-2">{countryToFlag(item.country)}</span>
|
||||
{item.country}
|
||||
</div>
|
||||
</td>
|
||||
@@ -743,6 +731,71 @@ function BillingManager() {
|
||||
</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
|
||||
show={showAddModal}
|
||||
@@ -775,7 +828,7 @@ function AddBillingModal({ show, item, onItemChange, onSubmit, onClose }) {
|
||||
if (!show) return null;
|
||||
|
||||
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-content">
|
||||
<div className="modal-header">
|
||||
@@ -785,7 +838,7 @@ function AddBillingModal({ show, item, onItemChange, onSubmit, onClose }) {
|
||||
<div className="modal-body">
|
||||
<div className="row g-3">
|
||||
<div className="col-md-6">
|
||||
<label className="form-label">Host Name *</label>
|
||||
<label className="form-label">Имя хоста *</label>
|
||||
<input
|
||||
type="text"
|
||||
className="form-control"
|
||||
@@ -795,7 +848,7 @@ function AddBillingModal({ show, item, onItemChange, onSubmit, onClose }) {
|
||||
/>
|
||||
</div>
|
||||
<div className="col-md-6">
|
||||
<label className="form-label">Purpose</label>
|
||||
<label className="form-label">Назначение</label>
|
||||
<select
|
||||
className="form-select"
|
||||
value={item.purpose}
|
||||
@@ -812,7 +865,7 @@ function AddBillingModal({ show, item, onItemChange, onSubmit, onClose }) {
|
||||
</select>
|
||||
</div>
|
||||
<div className="col-md-6">
|
||||
<label className="form-label">Country *</label>
|
||||
<label className="form-label">Страна *</label>
|
||||
<input
|
||||
type="text"
|
||||
className="form-control"
|
||||
@@ -822,7 +875,7 @@ function AddBillingModal({ show, item, onItemChange, onSubmit, onClose }) {
|
||||
/>
|
||||
</div>
|
||||
<div className="col-md-6">
|
||||
<label className="form-label">Provider *</label>
|
||||
<label className="form-label">Провайдер *</label>
|
||||
<input
|
||||
type="text"
|
||||
className="form-control"
|
||||
@@ -832,25 +885,27 @@ function AddBillingModal({ show, item, onItemChange, onSubmit, onClose }) {
|
||||
/>
|
||||
</div>
|
||||
<div className="col-md-6">
|
||||
<label className="form-label">Login URL</label>
|
||||
<label className="form-label">Ссылка для входа</label>
|
||||
<input
|
||||
type="text"
|
||||
className="form-control"
|
||||
value={item.loginUrl}
|
||||
onChange={(e) => onItemChange({ ...item, loginUrl: e.target.value })}
|
||||
placeholder="example.com"
|
||||
/>
|
||||
</div>
|
||||
<div className="col-md-6">
|
||||
<label className="form-label">Monthly Cost</label>
|
||||
<label className="form-label">Месячная стоимость</label>
|
||||
<input
|
||||
type="number"
|
||||
step="0.01"
|
||||
className="form-control"
|
||||
value={item.monthlyCost}
|
||||
onChange={(e) => onItemChange({ ...item, monthlyCost: parseFloat(e.target.value) || 0 })}
|
||||
/>
|
||||
</div>
|
||||
<div className="col-md-6">
|
||||
<label className="form-label">Next Payment Date</label>
|
||||
<label className="form-label">Дата следующего платежа</label>
|
||||
<input
|
||||
type="date"
|
||||
className="form-control"
|
||||
@@ -859,7 +914,7 @@ function AddBillingModal({ show, item, onItemChange, onSubmit, onClose }) {
|
||||
/>
|
||||
</div>
|
||||
<div className="col-md-6">
|
||||
<label className="form-label">Status</label>
|
||||
<label className="form-label">Статус</label>
|
||||
<select
|
||||
className="form-select"
|
||||
value={item.status}
|
||||
@@ -889,7 +944,7 @@ function EditBillingModal({ show, item, onItemChange, onSubmit, onClose }) {
|
||||
if (!show) return null;
|
||||
|
||||
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-content">
|
||||
<div className="modal-header">
|
||||
@@ -899,7 +954,7 @@ function EditBillingModal({ show, item, onItemChange, onSubmit, onClose }) {
|
||||
<div className="modal-body">
|
||||
<div className="row g-3">
|
||||
<div className="col-md-6">
|
||||
<label className="form-label">Host Name *</label>
|
||||
<label className="form-label">Имя хоста *</label>
|
||||
<input
|
||||
type="text"
|
||||
className="form-control"
|
||||
@@ -909,7 +964,7 @@ function EditBillingModal({ show, item, onItemChange, onSubmit, onClose }) {
|
||||
/>
|
||||
</div>
|
||||
<div className="col-md-6">
|
||||
<label className="form-label">Purpose</label>
|
||||
<label className="form-label">Назначение</label>
|
||||
<select
|
||||
className="form-select"
|
||||
value={item.purpose || ''}
|
||||
@@ -926,7 +981,7 @@ function EditBillingModal({ show, item, onItemChange, onSubmit, onClose }) {
|
||||
</select>
|
||||
</div>
|
||||
<div className="col-md-6">
|
||||
<label className="form-label">Country *</label>
|
||||
<label className="form-label">Страна *</label>
|
||||
<input
|
||||
type="text"
|
||||
className="form-control"
|
||||
@@ -936,7 +991,7 @@ function EditBillingModal({ show, item, onItemChange, onSubmit, onClose }) {
|
||||
/>
|
||||
</div>
|
||||
<div className="col-md-6">
|
||||
<label className="form-label">Provider *</label>
|
||||
<label className="form-label">Провайдер *</label>
|
||||
<input
|
||||
type="text"
|
||||
className="form-control"
|
||||
@@ -946,25 +1001,27 @@ function EditBillingModal({ show, item, onItemChange, onSubmit, onClose }) {
|
||||
/>
|
||||
</div>
|
||||
<div className="col-md-6">
|
||||
<label className="form-label">Login URL</label>
|
||||
<label className="form-label">Ссылка для входа</label>
|
||||
<input
|
||||
type="text"
|
||||
className="form-control"
|
||||
value={item.loginUrl || ''}
|
||||
onChange={(e) => onItemChange({ ...item, loginUrl: e.target.value })}
|
||||
placeholder="example.com"
|
||||
/>
|
||||
</div>
|
||||
<div className="col-md-6">
|
||||
<label className="form-label">Monthly Cost</label>
|
||||
<label className="form-label">Месячная стоимость</label>
|
||||
<input
|
||||
type="number"
|
||||
step="0.01"
|
||||
className="form-control"
|
||||
value={item.monthlyCost || 0}
|
||||
onChange={(e) => onItemChange({ ...item, monthlyCost: parseFloat(e.target.value) || 0 })}
|
||||
/>
|
||||
</div>
|
||||
<div className="col-md-6">
|
||||
<label className="form-label">Next Payment Date</label>
|
||||
<label className="form-label">Дата следующего платежа</label>
|
||||
<input
|
||||
type="date"
|
||||
className="form-control"
|
||||
@@ -973,7 +1030,7 @@ function EditBillingModal({ show, item, onItemChange, onSubmit, onClose }) {
|
||||
/>
|
||||
</div>
|
||||
<div className="col-md-6">
|
||||
<label className="form-label">Status</label>
|
||||
<label className="form-label">Статус</label>
|
||||
<select
|
||||
className="form-select"
|
||||
value={item.status || 'active'}
|
||||
@@ -1003,7 +1060,7 @@ function DeleteBillingModal({ show, item, onDelete, onClose }) {
|
||||
if (!show) return null;
|
||||
|
||||
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-content">
|
||||
<div className="modal-header">
|
||||
|
||||
Reference in New Issue
Block a user