add remove and add tariff modal windows

This commit is contained in:
smanylov
2026-06-01 12:14:27 +07:00
parent 15115c5114
commit c74ef84d37
7 changed files with 551 additions and 13 deletions
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "no-copy-admin-panel-frontend", "name": "no-copy-admin-panel-frontend",
"version": "0.15.0", "version": "0.16.0",
"private": true, "private": true,
"scripts": { "scripts": {
"dev": "next dev -p 2996", "dev": "next dev -p 2996",
+99 -3
View File
@@ -56,7 +56,6 @@ export async function updateTariffs(
accountType: 'b2c' | 'b2b' | 'token' accountType: 'b2c' | 'b2b' | 'token'
) { ) {
const token = await getSessionData('token'); const token = await getSessionData('token');
console.log(`updateTariffs - ${id}`);
try { try {
const response = await fetch(`${API_BASE_URL}/api/admin/control`, { const response = await fetch(`${API_BASE_URL}/api/admin/control`, {
@@ -65,7 +64,7 @@ export async function updateTariffs(
version: 1, version: 1,
msg_id: 40003, msg_id: 40003,
message_body: { message_body: {
action: 'update', action: 'add',
id: id, id: id,
tariff_price: tariffPrice, tariff_price: tariffPrice,
tariff_name: tariffName, tariff_name: tariffName,
@@ -88,7 +87,104 @@ export async function updateTariffs(
if (response.ok) { if (response.ok) {
const parsed = await response.json(); 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) { if (parsed.message_code === 0) {
return parsed.message_body.message_body; return parsed.message_body.message_body;
+10 -2
View File
@@ -3,6 +3,7 @@
@use './animation.scss'; @use './animation.scss';
@use './file-moderation-modal.scss'; @use './file-moderation-modal.scss';
@use './tariff-edit-modal.scss'; @use './tariff-edit-modal.scss';
@use './tariff-remove-modal.scss';
:root { :root {
--primary-color: #2563eb; --primary-color: #2563eb;
@@ -11,8 +12,8 @@
--secondary-hover: #4c5869; --secondary-hover: #4c5869;
--success-color: #10b981; --success-color: #10b981;
--success-hover: #0d8a60; --success-hover: #0d8a60;
--danger-color: #ef4444; --danger-color: #dc2626;
--danger-hover: #bd3434; --danger-hover: #b91c1c;
--warning-color: #f59e0b; --warning-color: #f59e0b;
--warning-hover: #b3750b; --warning-hover: #b3750b;
--info-color: #3b82f6; --info-color: #3b82f6;
@@ -297,6 +298,11 @@
&:hover { &:hover {
background: var(--danger-hover); background: var(--danger-hover);
} }
&:disabled {
opacity: 0.5;
cursor: not-allowed;
}
} }
.btn { .btn {
@@ -574,6 +580,8 @@
border-top: 1px solid var(--border); border-top: 1px solid var(--border);
font-size: 12px; font-size: 12px;
color: var(--text-secondary); color: var(--text-secondary);
display: flex;
gap: 10px;
} }
.btn-outline { .btn-outline {
+68
View File
@@ -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 { IconDoubleArrowRight, IconArrowRight, IconDoubleArrowLeft, IconArrowLeft } from '@/app/ui/icons/icons';
import ModalWindow from '@/app/components/modalWindow'; import ModalWindow from '@/app/components/modalWindow';
import { useQueryClient } from '@tanstack/react-query'; 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() { export default function TariffManagment() {
const [currentPage, setCurrentPage] = useState(0); const [currentPage, setCurrentPage] = useState(0);
const [isModalOpen, setIsModalOpen] = useState(false); const [isModalOpen, setIsModalOpen] = useState(false);
const [isCreateModalOpen, setIsCreateModalOpen] = useState(false);
const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false);
const [editingTariff, setEditingTariff] = useState<EditingTariff | null>(null); 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 { data: tariffData, isLoading, isError, isFetching, error } = useTariffsData(currentPage);
const queryClient = useQueryClient(); const queryClient = useQueryClient();
@@ -48,6 +53,14 @@ export default function TariffManagment() {
setIsModalOpen(true); setIsModalOpen(true);
}; };
const handleDeleteClick = (tariff: any) => {
setDeletingTariff({
id: tariff.id,
name: tariff.name || tariff.type
});
setIsDeleteModalOpen(true);
};
const formatPrice = (price: number) => { const formatPrice = (price: number) => {
return new Intl.NumberFormat('ru-RU').format(price); return new Intl.NumberFormat('ru-RU').format(price);
}; };
@@ -92,7 +105,10 @@ export default function TariffManagment() {
<div className="content-header"> <div className="content-header">
<h3>Управление тарифными планами</h3> <h3>Управление тарифными планами</h3>
<div className="content-header-actions"> <div className="content-header-actions">
<button className="btn btn-primary"> <button
className="btn btn-primary"
onClick={() => setIsCreateModalOpen(true)}
>
<span></span> <span></span>
<span>Создать тариф</span> <span>Создать тариф</span>
</button> </button>
@@ -124,6 +140,12 @@ export default function TariffManagment() {
> >
Редактировать Редактировать
</button> </button>
<button
className="btn btn-outline btn-sm btn-danger"
onClick={() => handleDeleteClick(tariff)}
>
Удалить
</button>
</div> </div>
</div> </div>
))} ))}
@@ -205,12 +227,7 @@ export default function TariffManagment() {
</div> </div>
</div> </div>
)} )}
{isFetching && !isLoading && (
<div className="refetching-indicator">Обновление...</div>
)}
</div> </div>
{/* Модальное окно редактирования тарифа */} {/* Модальное окно редактирования тарифа */}
<ModalWindow state={isModalOpen} callBack={setIsModalOpen}> <ModalWindow state={isModalOpen} callBack={setIsModalOpen}>
{editingTariff && ( {editingTariff && (
@@ -224,6 +241,31 @@ export default function TariffManagment() {
/> />
)} )}
</ModalWindow> </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> </div>
) )
} }