From 8928960c341a1cbec69f84a37d77f9d24d4eff5a Mon Sep 17 00:00:00 2001 From: smanylov Date: Fri, 29 May 2026 18:43:37 +0700 Subject: [PATCH] add tariff table, add edit tariff modal window --- .../[locale]/pages/tariff-management/page.tsx | 76 +----- src/app/actions/tariffActions.ts | 105 ++++++++ src/app/hooks/react-query/useTariffsData.ts | 42 ++++ src/app/styles/global-styles.scss | 195 ++++++++------- src/app/styles/tariff-edit-modal.scss | 194 +++++++++++++++ .../ui/tariff-management/TariffEditModal.tsx | 223 +++++++++++++++++ .../tariff-management/tariff-management.tsx | 229 ++++++++++++++++++ 7 files changed, 900 insertions(+), 164 deletions(-) create mode 100644 src/app/actions/tariffActions.ts create mode 100644 src/app/hooks/react-query/useTariffsData.ts create mode 100644 src/app/styles/tariff-edit-modal.scss create mode 100644 src/app/ui/tariff-management/TariffEditModal.tsx create mode 100644 src/app/ui/tariff-management/tariff-management.tsx diff --git a/src/app/[locale]/pages/tariff-management/page.tsx b/src/app/[locale]/pages/tariff-management/page.tsx index e28f08a..9c1e6dc 100644 --- a/src/app/[locale]/pages/tariff-management/page.tsx +++ b/src/app/[locale]/pages/tariff-management/page.tsx @@ -1,78 +1,6 @@ +import TariffManagment from '@/app/ui/tariff-management/tariff-management'; export default function Page() { return ( -
-
-

Управление тарифными планами

-
- -
-
-
-
-
-
- FREE -
🆓
-
-
₽0
-
100 токенов
-
- -
-
- -
-
- MINI (TELEGRAM) -
✈️
-
-
₽500
-
500 токенов
-
- -
-
- -
-
- BASIC -
-
-
₽990
-
1,000 токенов
-
- -
-
- -
-
- PRO -
💎
-
-
₽2,990
-
5,000 токенов
-
- -
-
- -
-
- BUSINESS -
🏢
-
-
₽9,990
-
20,000 токенов
-
- -
-
-
-
-
+ ) } \ No newline at end of file diff --git a/src/app/actions/tariffActions.ts b/src/app/actions/tariffActions.ts new file mode 100644 index 0000000..99c96a2 --- /dev/null +++ b/src/app/actions/tariffActions.ts @@ -0,0 +1,105 @@ +'use server' + +import { getSessionData } from '@/app/actions/session'; +import { API_BASE_URL } from '@/app/actions/definitions'; + +export async function fetchTariffs(page: number) { + const token = await getSessionData('token'); + + try { + const response = await fetch(`${API_BASE_URL}/api/admin/info`, { + method: 'POST', + body: JSON.stringify({ + version: 1, + msg_id: 40004, + message_body: { + action: 'get_all', + page: page, + page_size: 8 + } + }), + 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 updateTariffs( + id: number, + 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'); + console.log(`updateTariffs - ${id}`); + + try { + const response = await fetch(`${API_BASE_URL}/api/admin/info`, { + method: 'POST', + body: JSON.stringify({ + version: 1, + msg_id: 40003, + message_body: { + action: 'update', + id: id, + 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(); + 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; + } +} \ No newline at end of file diff --git a/src/app/hooks/react-query/useTariffsData.ts b/src/app/hooks/react-query/useTariffsData.ts new file mode 100644 index 0000000..a8985d2 --- /dev/null +++ b/src/app/hooks/react-query/useTariffsData.ts @@ -0,0 +1,42 @@ +import { useQuery } from '@tanstack/react-query'; +import { fetchTariffs } from '@/app/actions/tariffActions'; + +export interface Tariff { + id: number; + type: string; + name: string; + price: number; + tokens: number; + maxFilesCount: number; + diskSize: number; + maxUsers: number; + tariffTerm: string; + description: string; +} + +export interface TariffsData { + page: number; + page_size: number; + sort_by: string; + sort_direction: 'asc' | 'desc'; + total_pages: number; + total_elements: number; + tariffs: Tariff[]; +} + +export const useTariffsData = (page: number) => { + return useQuery({ + queryKey: ['tariffsData', page], + queryFn: () => { + return fetchTariffs(page); + }, + select: (data: TariffsData | null) => { + if (!data) { + return null; + } + return data; + }, + refetchInterval: 30000, + placeholderData: (previousData) => previousData + }); +}; \ No newline at end of file diff --git a/src/app/styles/global-styles.scss b/src/app/styles/global-styles.scss index 87f4b79..c651a87 100644 --- a/src/app/styles/global-styles.scss +++ b/src/app/styles/global-styles.scss @@ -2,6 +2,7 @@ @use './edit-permissions-modal.scss'; @use './animation.scss'; @use './file-moderation-modal.scss'; +@use './tariff-edit-modal.scss'; :root { --primary-color: #2563eb; @@ -528,13 +529,27 @@ } .stat-card-title { - font-size: 13px; + font-size: 14px; color: var(--text-secondary); font-weight: 500; text-transform: uppercase; letter-spacing: 0.5px; } + .stat-card-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 5px; + } + + .tariff-term-badge { + background: #eff6ff; + padding: 2px 12px; + border-radius: 8px; + font-size: 11px; + } + .stat-card-value { /* width: 40px; */ width: auto; @@ -786,95 +801,6 @@ } } - .tanstak-table-pagination { - display: flex; - flex-direction: column; - align-items: flex-end; - justify-content: space-between; - margin-top: 1.5rem; - gap: 1rem; - - @media (min-width: 768px) { - flex-direction: row; - } - - .pagination-info { - display: flex; - align-items: center; - gap: 0.5rem; - font-size: 0.875rem; - - &-pages { - color: v.$text-p; - } - - &-files { - color: v.$text-s; - } - } - - .pagination-controls { - display: flex; - align-items: center; - gap: 0.5rem; - - .arrow { - padding-left: 0.5rem; - padding-right: 0.5rem; - padding-top: 0.25rem; - padding-bottom: 0.25rem; - border: 2px solid v.$border-color-1; - border-radius: 10px; - box-shadow: 0 1px 2px #0000000d; - color: var(--primary-color); - - .icon { - width: 18px; - } - - /* */ - - &:hover { - background-color: v.$bg-light; - } - - &:disabled { - opacity: 0.5; - cursor: not-allowed; - } - } - - &-pages { - display: flex; - align-items: center; - gap: 0.25rem; - - button { - border: 2px solid v.$border-color-1; - padding: 0.25rem 0.75rem; - border-radius: 10px; - color: var(--primary-color); - - &:disabled { - opacity: 0.5; - cursor: not-allowed; - } - - &.current { - background-color: var(--primary-color); - color: v.$white; - } - - &.other { - &:hover { - background-color: v.$bg-light; - } - } - } - } - } - } - .tanstak-table-block { overflow-x: auto; border-radius: 0.5rem; @@ -1217,6 +1143,95 @@ } } +.tanstak-table-pagination { + display: flex; + flex-direction: column; + align-items: flex-end; + justify-content: space-between; + margin-top: 1.5rem; + gap: 1rem; + + @media (min-width: 768px) { + flex-direction: row; + } + + .pagination-info { + display: flex; + align-items: center; + gap: 0.5rem; + font-size: 0.875rem; + + &-pages { + color: v.$text-p; + } + + &-files { + color: v.$text-s; + } + } + + .pagination-controls { + display: flex; + align-items: center; + gap: 0.5rem; + + .arrow { + padding-left: 0.5rem; + padding-right: 0.5rem; + padding-top: 0.25rem; + padding-bottom: 0.25rem; + border: 2px solid v.$border-color-1; + border-radius: 10px; + box-shadow: 0 1px 2px #0000000d; + color: var(--primary-color); + + .icon { + width: 18px; + } + + /* */ + + &:hover { + background-color: v.$bg-light; + } + + &:disabled { + opacity: 0.5; + cursor: not-allowed; + } + } + + &-pages { + display: flex; + align-items: center; + gap: 0.25rem; + + button { + border: 2px solid v.$border-color-1; + padding: 0.25rem 0.75rem; + border-radius: 10px; + color: var(--primary-color); + + &:disabled { + opacity: 0.5; + cursor: not-allowed; + } + + &.current { + background-color: var(--primary-color); + color: v.$white; + } + + &.other { + &:hover { + background-color: v.$bg-light; + } + } + } + } + } +} + .empty-state { text-align: center; padding: 60px 20px; diff --git a/src/app/styles/tariff-edit-modal.scss b/src/app/styles/tariff-edit-modal.scss new file mode 100644 index 0000000..25d3116 --- /dev/null +++ b/src/app/styles/tariff-edit-modal.scss @@ -0,0 +1,194 @@ +@use './variable.scss' as v; + +.tariff-edit-modal { + padding: 1.5rem; + min-width: 500px; + max-width: 600px; + max-height: 80vh; + overflow-y: auto; + + &__title { + font-size: 1.5rem; + font-weight: bold; + margin-bottom: 1.5rem; + color: #111827; + } + + &__content { + display: flex; + flex-direction: column; + gap: 1rem; + } + + &__info-section { + border-bottom: 1px solid #e5e7eb; + padding-bottom: 0.75rem; + + &:last-of-type { + border-bottom: none; + } + } + + &__label { + display: block; + font-size: 0.875rem; + font-weight: 500; + color: #374151; + margin-bottom: 0.5rem; + } + + &__value { + color: #111827; + word-break: break-word; + font-size: 0.875rem; + } + + &__input { + width: 100%; + padding: 0.5rem; + border: 1px solid #e5e7eb; + border-radius: 0.25rem; + font-size: 0.875rem; + font-family: inherit; + color: #111827; + background-color: #ffffff; + transition: all 0.2s; + + &:focus { + outline: none; + border-color: #3b82f6; + box-shadow: 0 0 0 2px rgba(59, 130, 246, 0.1); + } + + &::placeholder { + color: #9ca3af; + } + + &:disabled { + background-color: #f3f4f6; + cursor: not-allowed; + } + + &[type="number"] { + -moz-appearance: textfield; + + &::-webkit-inner-spin-button, + &::-webkit-outer-spin-button { + display: none; + } + } + } + + &__textarea { + width: 100%; + padding: 0.5rem; + border: 1px solid #e5e7eb; + border-radius: 0.25rem; + font-size: 0.875rem; + font-family: inherit; + color: #111827; + background-color: #ffffff; + resize: vertical; + transition: all 0.2s; + + &:focus { + outline: none; + border-color: #3b82f6; + box-shadow: 0 0 0 2px rgba(59, 130, 246, 0.1); + } + + &::placeholder { + color: #9ca3af; + } + + &:disabled { + background-color: #f3f4f6; + cursor: not-allowed; + } + } + + &__actions { + display: flex; + justify-content: flex-end; + gap: 0.75rem; + } + + // Стили для DropDownList внутри модального окна + .dropdown { + width: 100%; + } +} + +// Адаптация для мобильных устройств +@media (max-width: 768px) { + .tariff-edit-modal { + min-width: 90vw; + padding: 1rem; + + &__title { + font-size: 1.25rem; + margin-bottom: 1rem; + } + + &__info-section { + padding-bottom: 0.5rem; + } + + &__actions { + flex-direction: column-reverse; + + button { + width: 100%; + } + } + } +} + +// Стили для скроллбара (опционально) +.tariff-edit-modal { + scrollbar-width: thin; + scrollbar-color: #cbd5e1 #f1f5f9; + + &::-webkit-scrollbar { + width: 6px; + } + + &::-webkit-scrollbar-track { + background: #f1f5f9; + border-radius: 3px; + } + + &::-webkit-scrollbar-thumb { + background: #cbd5e1; + border-radius: 3px; + + &:hover { + background: #94a3b8; + } + } +} + +.tariff-edit-modal__row { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 1rem; + + @media (max-width: 768px) { + grid-template-columns: 1fr; + gap: 0.75rem; + } +} + +.tariff-edit-modal__info-section--half { + @extend .tariff-edit-modal__info-section; + + @media (min-width: 769px) { + &:first-child { + padding-right: 0.5rem; + } + + &:last-child { + padding-left: 0.5rem; + } + } +} \ No newline at end of file diff --git a/src/app/ui/tariff-management/TariffEditModal.tsx b/src/app/ui/tariff-management/TariffEditModal.tsx new file mode 100644 index 0000000..3e44ddf --- /dev/null +++ b/src/app/ui/tariff-management/TariffEditModal.tsx @@ -0,0 +1,223 @@ +import { useEffect, useState } from 'react'; +import { useMutation } from '@tanstack/react-query'; +import { updateTariffs } from '@/app/actions/tariffActions'; +import { toast } from 'sonner'; +import DropDownList from '@/app/components/dropDownList'; +import { convertBytes } from '@/app/lib/convertBytes'; + +export interface EditingTariff { + id: number; + name: string; + type: string; + price: number; + tokens: number; + maxFilesCount: number; + diskSize: number; + maxUsers: number; + description: string; + tariffTerm: string; +} + +export function TariffEditModal({ tariff, onClose, onSuccess }: { + tariff: EditingTariff; + onClose: () => void; + onSuccess: () => void; +}) { + const [formData, setFormData] = useState({ + price: tariff.price, + name: tariff.name, + tokens: tariff.tokens, + maxFilesCount: tariff.maxFilesCount, + diskSize: tariff.diskSize, + maxUsers: tariff.maxUsers, + description: tariff.description, + tariffTerm: tariff.tariffTerm === 'YEAR' ? 'YEAR' : 'MONTHLY' as 'MONTHLY' | 'YEAR', + accountType: tariff.type.includes('TOKEN') ? 'token' : tariff.type.includes('STUDIO') ? 'b2b' : 'b2c' as 'b2c' | 'b2b' | 'token' + }); + + const updateTariffMutation = useMutation({ + mutationFn: async () => { + return await updateTariffs( + tariff.id, + formData.price, + formData.name, + tariff.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 = () => { + updateTariffMutation.mutate(); + }; + + const handleChange = (field: string, value: any) => { + setFormData(prev => ({ ...prev, [field]: value })); + }; + + return ( +
+

Редактирование тарифа

+ +
+
+ +

{tariff.id}

+
+ +
+ +

{tariff.type}

+
+ +
+ + handleChange('name', e.target.value)} + placeholder="Введите название тарифа" + /> +
+ +
+ + handleChange('price', Number(e.target.value))} + placeholder="Введите цену" + min="0" + /> +
+ +
+ + handleChange('tokens', Number(e.target.value))} + placeholder="Введите количество токенов" + min="0" + /> +
+ +
+ + handleChange('maxFilesCount', Number(e.target.value))} + placeholder="Введите максимальное количество файлов" + min="0" + /> +
+ +
+ + handleChange('diskSize', Number(e.target.value))} + placeholder="Введите размер диска в ГБ" + min="0" + step="1" + /> +
+ +
+ + handleChange('maxUsers', Number(e.target.value))} + placeholder="Введите максимальное количество пользователей" + min="0" + /> +
+ +
+ + { + handleChange('tariffTerm', value); + }} + > +
  • Месяц
  • +
  • Год
  • +
    +
    + +
    + + { + handleChange('accountType', value); + }} + > +
  • B2C
  • +
  • B2B
  • +
  • Token
  • +
    +
    + +
    + +