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,
|
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">
|
||||||
|
|||||||
Reference in New Issue
Block a user