add remove and add tariff modal windows
This commit is contained in:
@@ -56,7 +56,6 @@ export async function updateTariffs(
|
||||
accountType: 'b2c' | 'b2b' | 'token'
|
||||
) {
|
||||
const token = await getSessionData('token');
|
||||
console.log(`updateTariffs - ${id}`);
|
||||
|
||||
try {
|
||||
const response = await fetch(`${API_BASE_URL}/api/admin/control`, {
|
||||
@@ -65,7 +64,7 @@ export async function updateTariffs(
|
||||
version: 1,
|
||||
msg_id: 40003,
|
||||
message_body: {
|
||||
action: 'update',
|
||||
action: 'add',
|
||||
id: id,
|
||||
tariff_price: tariffPrice,
|
||||
tariff_name: tariffName,
|
||||
@@ -88,7 +87,104 @@ export async function updateTariffs(
|
||||
|
||||
if (response.ok) {
|
||||
const parsed = await response.json();
|
||||
console.log(parsed);
|
||||
|
||||
if (parsed.message_code === 0) {
|
||||
return parsed.message_body.message_body;
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
|
||||
} else {
|
||||
throw new Error(`${response.status}`);
|
||||
}
|
||||
} catch (error) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export async function addTariffs(
|
||||
tariffPrice: number,
|
||||
tariffName: string,
|
||||
type: string,
|
||||
tariffTokens: number,
|
||||
maxFiles: number,
|
||||
diskSize: number,
|
||||
maxUsers: number,
|
||||
description: string,
|
||||
tariffTerm: 'MONTHLY' | 'YEAR',
|
||||
accountType: 'b2c' | 'b2b' | 'token'
|
||||
) {
|
||||
const token = await getSessionData('token');
|
||||
|
||||
try {
|
||||
const response = await fetch(`${API_BASE_URL}/api/admin/control`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
version: 1,
|
||||
msg_id: 40003,
|
||||
message_body: {
|
||||
action: 'add',
|
||||
tariff_price: tariffPrice,
|
||||
tariff_name: tariffName,
|
||||
type: type,
|
||||
tariff_tokens: tariffTokens,
|
||||
max_files: maxFiles,
|
||||
disk_size: diskSize,
|
||||
max_users: maxUsers,
|
||||
description: description,
|
||||
tariff_term: tariffTerm,
|
||||
account_type: accountType
|
||||
}
|
||||
}),
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Accept': 'application/json',
|
||||
'Authorization': `Bearer ${token}`
|
||||
}
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
const parsed = await response.json();
|
||||
|
||||
if (parsed.message_code === 0) {
|
||||
return parsed.message_body.message_body;
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
|
||||
} else {
|
||||
throw new Error(`${response.status}`);
|
||||
}
|
||||
} catch (error) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export async function removeTariffs(
|
||||
id: number
|
||||
) {
|
||||
const token = await getSessionData('token');
|
||||
|
||||
try {
|
||||
const response = await fetch(`${API_BASE_URL}/api/admin/control`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
version: 1,
|
||||
msg_id: 40003,
|
||||
message_body: {
|
||||
action: 'remove',
|
||||
id: id
|
||||
}
|
||||
}),
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Accept': 'application/json',
|
||||
'Authorization': `Bearer ${token}`
|
||||
}
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
const parsed = await response.json();
|
||||
|
||||
if (parsed.message_code === 0) {
|
||||
return parsed.message_body.message_body;
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
@use './animation.scss';
|
||||
@use './file-moderation-modal.scss';
|
||||
@use './tariff-edit-modal.scss';
|
||||
@use './tariff-remove-modal.scss';
|
||||
|
||||
:root {
|
||||
--primary-color: #2563eb;
|
||||
@@ -11,8 +12,8 @@
|
||||
--secondary-hover: #4c5869;
|
||||
--success-color: #10b981;
|
||||
--success-hover: #0d8a60;
|
||||
--danger-color: #ef4444;
|
||||
--danger-hover: #bd3434;
|
||||
--danger-color: #dc2626;
|
||||
--danger-hover: #b91c1c;
|
||||
--warning-color: #f59e0b;
|
||||
--warning-hover: #b3750b;
|
||||
--info-color: #3b82f6;
|
||||
@@ -297,6 +298,11 @@
|
||||
&:hover {
|
||||
background: var(--danger-hover);
|
||||
}
|
||||
|
||||
&:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
}
|
||||
|
||||
.btn {
|
||||
@@ -574,6 +580,8 @@
|
||||
border-top: 1px solid var(--border);
|
||||
font-size: 12px;
|
||||
color: var(--text-secondary);
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.btn-outline {
|
||||
|
||||
@@ -0,0 +1,68 @@
|
||||
.tariff-delete-modal {
|
||||
min-width: 400px;
|
||||
max-width: 500px;
|
||||
|
||||
.tariff-delete-modal__title {
|
||||
margin-bottom: 20px;
|
||||
color: #dc2626;
|
||||
}
|
||||
|
||||
.tariff-delete-modal__warning {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
padding: 16px;
|
||||
background: #fef2f2;
|
||||
border-radius: 8px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.warning-icon {
|
||||
font-size: 24px;
|
||||
}
|
||||
|
||||
.warning-text {
|
||||
margin: 0;
|
||||
color: #991b1b;
|
||||
}
|
||||
|
||||
.tariff-delete-modal__info {
|
||||
padding: 12px;
|
||||
background: #f3f4f6;
|
||||
border-radius: 6px;
|
||||
margin-bottom: 20px;
|
||||
font-size: 14px;
|
||||
color: #4b5563;
|
||||
}
|
||||
|
||||
.tariff-delete-modal__confirm {
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.confirm-label {
|
||||
display: block;
|
||||
margin-bottom: 8px;
|
||||
font-size: 14px;
|
||||
color: #374151;
|
||||
}
|
||||
|
||||
.confirm-input {
|
||||
width: 100%;
|
||||
padding: 10px;
|
||||
border: 1px solid #d1d5db;
|
||||
border-radius: 6px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.confirm-input:focus {
|
||||
outline: none;
|
||||
border-color: #dc2626;
|
||||
ring: 2px solid #dc2626;
|
||||
}
|
||||
|
||||
.tariff-delete-modal__actions {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 12px;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,232 @@
|
||||
import { useState } from 'react';
|
||||
import { useMutation } from '@tanstack/react-query';
|
||||
import { addTariffs } from '@/app/actions/tariffActions';
|
||||
import { toast } from 'sonner';
|
||||
import DropDownList from '@/app/components/dropDownList';
|
||||
|
||||
interface NewTariff {
|
||||
name: string;
|
||||
price: number;
|
||||
tokens: number;
|
||||
maxFilesCount: number;
|
||||
diskSize: number;
|
||||
maxUsers: number;
|
||||
description: string;
|
||||
tariffTerm: 'MONTHLY' | 'YEAR';
|
||||
accountType: 'b2c' | 'b2b' | 'token';
|
||||
type: string;
|
||||
}
|
||||
|
||||
export function TariffCreateModal({ onClose, onSuccess }: {
|
||||
onClose: () => void;
|
||||
onSuccess: () => void;
|
||||
}) {
|
||||
const [formData, setFormData] = useState<NewTariff>({
|
||||
price: 0,
|
||||
name: '',
|
||||
tokens: 0,
|
||||
maxFilesCount: 0,
|
||||
diskSize: 0,
|
||||
maxUsers: 0,
|
||||
description: '',
|
||||
tariffTerm: 'MONTHLY',
|
||||
accountType: 'b2c',
|
||||
type: ''
|
||||
});
|
||||
|
||||
const [typeError, setTypeError] = useState('');
|
||||
|
||||
const createTariffMutation = useMutation({
|
||||
mutationFn: async () => {
|
||||
return await addTariffs(
|
||||
formData.price,
|
||||
formData.name,
|
||||
formData.type,
|
||||
formData.tokens,
|
||||
formData.maxFilesCount,
|
||||
formData.diskSize,
|
||||
formData.maxUsers,
|
||||
formData.description,
|
||||
formData.tariffTerm,
|
||||
formData.accountType
|
||||
);
|
||||
},
|
||||
onSuccess: (data) => {
|
||||
if (data !== null && data !== undefined) {
|
||||
toast.success('Тариф успешно создан');
|
||||
onSuccess();
|
||||
} else {
|
||||
toast.error('Не удалось создать тариф');
|
||||
}
|
||||
},
|
||||
onError: () => {
|
||||
toast.error('Произошла ошибка при создании тарифа');
|
||||
},
|
||||
});
|
||||
|
||||
const handleSubmit = () => {
|
||||
if (!formData.type.trim()) {
|
||||
setTypeError('Введите тип тарифа');
|
||||
return;
|
||||
}
|
||||
setTypeError('');
|
||||
createTariffMutation.mutate();
|
||||
};
|
||||
|
||||
const handleChange = (field: string, value: any) => {
|
||||
setFormData(prev => ({ ...prev, [field]: value }));
|
||||
if (field === 'type') {
|
||||
setTypeError('');
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="tariff-edit-modal">
|
||||
<h2 className="tariff-edit-modal__title">Создание нового тарифа</h2>
|
||||
|
||||
<div className="tariff-edit-modal__content">
|
||||
<div className="tariff-edit-modal__info-section">
|
||||
<label className="tariff-edit-modal__label">Тип тарифа *</label>
|
||||
<input
|
||||
type="text"
|
||||
className={`tariff-edit-modal__input ${typeError ? 'error' : ''}`}
|
||||
value={formData.type}
|
||||
onChange={(e) => handleChange('type', e.target.value)}
|
||||
placeholder="Например: BASE, PREMIUM, TOKEN_1000"
|
||||
/>
|
||||
{typeError && <div className="error-message">{typeError}</div>}
|
||||
</div>
|
||||
|
||||
<div className="tariff-edit-modal__info-section">
|
||||
<label className="tariff-edit-modal__label">Название</label>
|
||||
<input
|
||||
type="text"
|
||||
className="tariff-edit-modal__input"
|
||||
value={formData.name}
|
||||
onChange={(e) => handleChange('name', e.target.value)}
|
||||
placeholder="Введите название тарифа"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="tariff-edit-modal__info-section">
|
||||
<label className="tariff-edit-modal__label">Цена (₽)</label>
|
||||
<input
|
||||
type="number"
|
||||
className="tariff-edit-modal__input"
|
||||
value={formData.price}
|
||||
onChange={(e) => handleChange('price', Number(e.target.value))}
|
||||
placeholder="Введите цену"
|
||||
min="0"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="tariff-edit-modal__info-section">
|
||||
<label className="tariff-edit-modal__label">Количество токенов</label>
|
||||
<input
|
||||
type="number"
|
||||
className="tariff-edit-modal__input"
|
||||
value={formData.tokens}
|
||||
onChange={(e) => handleChange('tokens', Number(e.target.value))}
|
||||
placeholder="Введите количество токенов"
|
||||
min="0"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="tariff-edit-modal__info-section">
|
||||
<label className="tariff-edit-modal__label">Максимальное количество файлов</label>
|
||||
<input
|
||||
type="number"
|
||||
className="tariff-edit-modal__input"
|
||||
value={formData.maxFilesCount}
|
||||
onChange={(e) => handleChange('maxFilesCount', Number(e.target.value))}
|
||||
placeholder="Введите максимальное количество файлов"
|
||||
min="0"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="tariff-edit-modal__info-section">
|
||||
<label className="tariff-edit-modal__label">Размер диска (байт)</label>
|
||||
<input
|
||||
type="number"
|
||||
className="tariff-edit-modal__input"
|
||||
value={formData.diskSize}
|
||||
onChange={(e) => handleChange('diskSize', Number(e.target.value))}
|
||||
placeholder="Введите размер диска в байтах"
|
||||
min="0"
|
||||
step="1"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="tariff-edit-modal__info-section">
|
||||
<label className="tariff-edit-modal__label">Максимальное количество пользователей</label>
|
||||
<input
|
||||
type="number"
|
||||
className="tariff-edit-modal__input"
|
||||
value={formData.maxUsers}
|
||||
onChange={(e) => handleChange('maxUsers', Number(e.target.value))}
|
||||
placeholder="Введите максимальное количество пользователей"
|
||||
min="0"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="tariff-edit-modal__info-section">
|
||||
<label className="tariff-edit-modal__label">Срок тарифа</label>
|
||||
<DropDownList
|
||||
value={formData.tariffTerm === 'YEAR' ? 'Год' : 'Месяц'}
|
||||
callBack={(value: string) => {
|
||||
handleChange('tariffTerm', value);
|
||||
}}
|
||||
>
|
||||
<li value="MONTHLY">Месяц</li>
|
||||
<li value="YEAR">Год</li>
|
||||
</DropDownList>
|
||||
</div>
|
||||
|
||||
<div className="tariff-edit-modal__info-section">
|
||||
<label className="tariff-edit-modal__label">Тип аккаунта</label>
|
||||
<DropDownList
|
||||
value={
|
||||
formData.accountType === 'b2c' ? 'B2C' :
|
||||
formData.accountType === 'b2b' ? 'B2B' : 'Token'
|
||||
}
|
||||
callBack={(value: string) => {
|
||||
handleChange('accountType', value);
|
||||
}}
|
||||
>
|
||||
<li value="b2c">B2C</li>
|
||||
<li value="b2b">B2B</li>
|
||||
<li value="token">Token</li>
|
||||
</DropDownList>
|
||||
</div>
|
||||
|
||||
<div className="tariff-edit-modal__info-section">
|
||||
<label className="tariff-edit-modal__label">Описание</label>
|
||||
<textarea
|
||||
className="tariff-edit-modal__textarea"
|
||||
value={formData.description}
|
||||
onChange={(e) => handleChange('description', e.target.value)}
|
||||
placeholder="Введите описание тарифа"
|
||||
rows={4}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="tariff-edit-modal__actions">
|
||||
<button
|
||||
onClick={onClose}
|
||||
className="btn btn-secondary"
|
||||
disabled={createTariffMutation.isPending}
|
||||
>
|
||||
Отмена
|
||||
</button>
|
||||
<button
|
||||
onClick={handleSubmit}
|
||||
className="btn btn-primary"
|
||||
disabled={createTariffMutation.isPending}
|
||||
>
|
||||
{createTariffMutation.isPending ? 'Создание...' : 'Создать'}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
import { useState } from 'react';
|
||||
import { useMutation } from '@tanstack/react-query';
|
||||
import { removeTariffs } from '@/app/actions/tariffActions';
|
||||
import { toast } from 'sonner';
|
||||
|
||||
interface TariffDeleteModalProps {
|
||||
tariffId: number;
|
||||
tariffName: string;
|
||||
onClose: () => void;
|
||||
onSuccess: () => void;
|
||||
}
|
||||
|
||||
export function TariffDeleteModal({ tariffId, tariffName, onClose, onSuccess }: TariffDeleteModalProps) {
|
||||
const [isConfirmText, setIsConfirmText] = useState('');
|
||||
const confirmText = 'УДАЛИТЬ';
|
||||
|
||||
const deleteTariffMutation = useMutation({
|
||||
mutationFn: async () => {
|
||||
return await removeTariffs(tariffId);
|
||||
},
|
||||
onSuccess: (data) => {
|
||||
if (data !== null && data !== undefined) {
|
||||
toast.success('Тариф успешно удален');
|
||||
onSuccess();
|
||||
} else {
|
||||
toast.error('Не удалось удалить тариф');
|
||||
}
|
||||
},
|
||||
onError: () => {
|
||||
toast.error('Произошла ошибка при удалении тарифа');
|
||||
},
|
||||
});
|
||||
|
||||
const handleDelete = () => {
|
||||
deleteTariffMutation.mutate();
|
||||
};
|
||||
|
||||
const getDisplayName = (name: string) => {
|
||||
if (name) {
|
||||
return name.toUpperCase().replace(/_/g, ' ');
|
||||
}
|
||||
return `ID: ${tariffId}`;
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="tariff-delete-modal">
|
||||
<h2 className="tariff-delete-modal__title">Удаление тарифа</h2>
|
||||
|
||||
<div className="tariff-delete-modal__content">
|
||||
<div className="tariff-delete-modal__warning">
|
||||
<p className="warning-text">
|
||||
Вы действительно хотите удалить тариф <strong>"{getDisplayName(tariffName)}"</strong>?
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="tariff-delete-modal__info">
|
||||
<p>Это действие невозможно отменить.</p>
|
||||
</div>
|
||||
|
||||
<div className="tariff-delete-modal__confirm">
|
||||
<label className="confirm-label">
|
||||
Для подтверждения введите <strong>{confirmText}</strong>
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
className="confirm-input"
|
||||
value={isConfirmText}
|
||||
onChange={(e) => setIsConfirmText(e.target.value)}
|
||||
placeholder={`Введите "${confirmText}"`}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="tariff-delete-modal__actions">
|
||||
<button
|
||||
onClick={onClose}
|
||||
className="btn btn-secondary"
|
||||
disabled={deleteTariffMutation.isPending}
|
||||
>
|
||||
Отмена
|
||||
</button>
|
||||
<button
|
||||
onClick={handleDelete}
|
||||
className="btn btn-danger"
|
||||
disabled={isConfirmText !== confirmText || deleteTariffMutation.isPending}
|
||||
>
|
||||
{deleteTariffMutation.isPending ? 'Удаление...' : 'Удалить'}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -6,12 +6,17 @@ import { convertBytes } from '@/app/lib/convertBytes';
|
||||
import { IconDoubleArrowRight, IconArrowRight, IconDoubleArrowLeft, IconArrowLeft } from '@/app/ui/icons/icons';
|
||||
import ModalWindow from '@/app/components/modalWindow';
|
||||
import { useQueryClient } from '@tanstack/react-query';
|
||||
import { EditingTariff, TariffEditModal } from './TariffEditModal'
|
||||
import { EditingTariff, TariffEditModal } from './TariffEditModal';
|
||||
import { TariffCreateModal } from './TariffCreateModal';
|
||||
import { TariffDeleteModal } from './TariffDeleteModal';
|
||||
|
||||
export default function TariffManagment() {
|
||||
const [currentPage, setCurrentPage] = useState(0);
|
||||
const [isModalOpen, setIsModalOpen] = useState(false);
|
||||
const [isCreateModalOpen, setIsCreateModalOpen] = useState(false);
|
||||
const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false);
|
||||
const [editingTariff, setEditingTariff] = useState<EditingTariff | null>(null);
|
||||
const [deletingTariff, setDeletingTariff] = useState<{ id: number; name: string } | null>(null);
|
||||
|
||||
const { data: tariffData, isLoading, isError, isFetching, error } = useTariffsData(currentPage);
|
||||
const queryClient = useQueryClient();
|
||||
@@ -48,6 +53,14 @@ export default function TariffManagment() {
|
||||
setIsModalOpen(true);
|
||||
};
|
||||
|
||||
const handleDeleteClick = (tariff: any) => {
|
||||
setDeletingTariff({
|
||||
id: tariff.id,
|
||||
name: tariff.name || tariff.type
|
||||
});
|
||||
setIsDeleteModalOpen(true);
|
||||
};
|
||||
|
||||
const formatPrice = (price: number) => {
|
||||
return new Intl.NumberFormat('ru-RU').format(price);
|
||||
};
|
||||
@@ -92,7 +105,10 @@ export default function TariffManagment() {
|
||||
<div className="content-header">
|
||||
<h3>Управление тарифными планами</h3>
|
||||
<div className="content-header-actions">
|
||||
<button className="btn btn-primary">
|
||||
<button
|
||||
className="btn btn-primary"
|
||||
onClick={() => setIsCreateModalOpen(true)}
|
||||
>
|
||||
<span>➕</span>
|
||||
<span>Создать тариф</span>
|
||||
</button>
|
||||
@@ -124,6 +140,12 @@ export default function TariffManagment() {
|
||||
>
|
||||
Редактировать
|
||||
</button>
|
||||
<button
|
||||
className="btn btn-outline btn-sm btn-danger"
|
||||
onClick={() => handleDeleteClick(tariff)}
|
||||
>
|
||||
Удалить
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
@@ -205,12 +227,7 @@ export default function TariffManagment() {
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{isFetching && !isLoading && (
|
||||
<div className="refetching-indicator">Обновление...</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Модальное окно редактирования тарифа */}
|
||||
<ModalWindow state={isModalOpen} callBack={setIsModalOpen}>
|
||||
{editingTariff && (
|
||||
@@ -224,6 +241,31 @@ export default function TariffManagment() {
|
||||
/>
|
||||
)}
|
||||
</ModalWindow>
|
||||
{/* Модальное окно создания тарифа */}
|
||||
<ModalWindow state={isCreateModalOpen} callBack={setIsCreateModalOpen}>
|
||||
<TariffCreateModal
|
||||
onClose={() => setIsCreateModalOpen(false)}
|
||||
onSuccess={() => {
|
||||
queryClient.invalidateQueries({ queryKey: ['tariffsData'] });
|
||||
setIsCreateModalOpen(false);
|
||||
}}
|
||||
/>
|
||||
</ModalWindow>
|
||||
{/* Модальное окно удаления тарифа */}
|
||||
<ModalWindow state={isDeleteModalOpen} callBack={setIsDeleteModalOpen}>
|
||||
{deletingTariff && (
|
||||
<TariffDeleteModal
|
||||
tariffId={deletingTariff.id}
|
||||
tariffName={deletingTariff.name}
|
||||
onClose={() => setIsDeleteModalOpen(false)}
|
||||
onSuccess={() => {
|
||||
queryClient.invalidateQueries({ queryKey: ['tariffsData'] });
|
||||
setIsDeleteModalOpen(false);
|
||||
setDeletingTariff(null);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</ModalWindow>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user