From ced17ae4a6577b1cc93d8083afe4d34bae743b0b Mon Sep 17 00:00:00 2001 From: smanylov Date: Tue, 19 May 2026 14:44:35 +0700 Subject: [PATCH] add staff managment permission modal window --- src/app/actions/auth.ts | 2 - src/app/actions/stuffActions.ts | 54 +++++ src/app/styles/edit-permissions-modal.scss | 211 ++++++++++++++++++ src/app/styles/global-styles.scss | 1 + src/app/ui/forms/create-staff-form.tsx | 1 - .../staff-management/EditPermissionsModal.tsx | 126 +++++++++++ .../ui/staff-management/StaffInfoModal.tsx | 69 ++++++ .../staff-management/tanstak-stuff-table.tsx | 134 ++++------- src/i18n/messages/en.json | 40 +++- src/i18n/messages/ru.json | 40 +++- 10 files changed, 579 insertions(+), 99 deletions(-) create mode 100644 src/app/styles/edit-permissions-modal.scss create mode 100644 src/app/ui/staff-management/EditPermissionsModal.tsx create mode 100644 src/app/ui/staff-management/StaffInfoModal.tsx diff --git a/src/app/actions/auth.ts b/src/app/actions/auth.ts index b621eec..1aa2cc4 100644 --- a/src/app/actions/auth.ts +++ b/src/app/actions/auth.ts @@ -112,7 +112,6 @@ export async function authorization( export async function logout() { const token = await getSessionData('token'); - console.log('logout'); try { const response = await fetch(`${API_BASE_URL}/api/admin`, { @@ -133,7 +132,6 @@ export async function logout() { if (response.ok) { let parsed = await response.json(); - console.log(parsed); } } catch (error) { throw error; diff --git a/src/app/actions/stuffActions.ts b/src/app/actions/stuffActions.ts index 65688a3..bcbb012 100644 --- a/src/app/actions/stuffActions.ts +++ b/src/app/actions/stuffActions.ts @@ -129,4 +129,58 @@ export async function removeStuff(id: number) { } catch (error) { throw error; } +} + +type Permissions = { + subscriptions: number; + mailings: number; + complaints: number; + agreements: number; + staff: number; + content_moderation: number; + users: number; + money: number; + kyc: number; + claims: number; + api: number; + dash: number; + tariffs: number; +}; + +export async function updateStuffPermissions(id: number, permissions: Permissions) { + const token = await getSessionData('token'); + + try { + const response = await fetch(`${API_BASE_URL}/api/admin`, { + method: 'POST', + body: JSON.stringify({ + version: 2, + msg_id: 100, + token: token, + message_body: { + action: 'update_permissions', + admin_id: id, + permissions: permissions + } + }), + headers: { + 'Content-Type': 'application/json', + 'Accept': 'application/json' + } + }); + + if (response.ok) { + const parsed = await response.json(); + + if (parsed.message_code === 0) { + return parsed.message_body; + } else { + return null; + } + } else { + throw new Error(`${response.status}`); + } + } catch (error) { + throw error; + } } \ No newline at end of file diff --git a/src/app/styles/edit-permissions-modal.scss b/src/app/styles/edit-permissions-modal.scss new file mode 100644 index 0000000..7da721f --- /dev/null +++ b/src/app/styles/edit-permissions-modal.scss @@ -0,0 +1,211 @@ +.edit-permissions-modal { + padding: 24px; + max-width: 900px; + width: 100%; + max-height: 85vh; + overflow-y: auto; + background: white; + border-radius: 12px; + + .modal-header { + margin-bottom: 24px; + padding-bottom: 16px; + border-bottom: 2px solid #f0f0f0; + + .modal-title { + font-size: 24px; + font-weight: 700; + color: #1a1a1a; + margin: 0; + } + } + + .permissions-list { + display: flex; + flex-direction: column; + gap: 16px; + margin-bottom: 28px; + + .permission-item { + /* border: 1px solid #e9ecef; */ + border-radius: 12px; + transition: all 0.2s ease; + +/* &:hover { + border-color: #dee2e6; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05); + } */ + + .permission-header { + /* padding: 20px; */ + display: flex; + align-items: center; + justify-content: space-between; + gap: 20px; + flex-wrap: wrap; + + @media (max-width: 768px) { + flex-direction: column; + align-items: stretch; + } + + .permission-info { + flex: 1; + + .permission-title { + font-size: 16px; + font-weight: 600; + color: #212529; + margin: 0 0 4px 0; + } + + .permission-description { + font-size: 13px; + color: #6c757d; + margin: 0; + } + } + + .permission-controls { + display: flex; + align-items: center; + gap: 16px; + flex-wrap: wrap; + + .permission-level-buttons { + display: flex; + gap: 8px; + background: #f8f9fa; + padding: 4px; + border-radius: 8px; + + .permission-level-btn { + display: flex; + align-items: center; + gap: 6px; + padding: 6px 12px; + border: none; + background: transparent; + border-radius: 6px; + cursor: pointer; + transition: all 0.2s ease; + font-size: 13px; + color: #6c757d; + + .btn-icon { + font-size: 14px; + } + + .btn-text { + font-weight: 500; + } + + &:hover { + background: rgba(0, 0, 0, 0.05); + } + + &.active { + background: white; + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1); + + &.no-access { + color: #dc3545; + background: #fff5f5; + } + + &.read-only { + color: #0d6efd; + background: #f0f7ff; + } + + &.full-access { + color: #198754; + background: #f0fff4; + } + } + } + } + } + } + } + } + + .modal-footer { + display: flex; + justify-content: flex-end; + gap: 12px; + padding-top: 20px; + border-top: 1px solid #e9ecef; + + .btn { + padding: 10px 20px; + border: none; + border-radius: 8px; + font-size: 14px; + font-weight: 500; + cursor: pointer; + transition: all 0.2s ease; + display: inline-flex; + align-items: center; + gap: 8px; + + &:disabled { + opacity: 0.6; + cursor: not-allowed; + } + + &.btn-secondary { + background: #6c757d; + color: white; + + &:hover:not(:disabled) { + background: #5a6268; + } + } + + &.btn-primary { + background: #0d6efd; + color: white; + + &:hover:not(:disabled) { + background: #0b5ed7; + } + } + + .spinner { + display: inline-block; + width: 14px; + height: 14px; + border: 2px solid rgba(255, 255, 255, 0.3); + border-radius: 50%; + border-top-color: white; + animation: spin 0.6s linear infinite; + } + } + } +} + +@keyframes spin { + to { + transform: rotate(360deg); + } +} + +// Scrollbar styling +.edit-permissions-modal::-webkit-scrollbar { + width: 8px; +} + +.edit-permissions-modal::-webkit-scrollbar-track { + background: #f1f1f1; + border-radius: 10px; +} + +.edit-permissions-modal::-webkit-scrollbar-thumb { + background: #888; + border-radius: 10px; + + &:hover { + background: #555; + } +} \ No newline at end of file diff --git a/src/app/styles/global-styles.scss b/src/app/styles/global-styles.scss index b756415..41f3b94 100644 --- a/src/app/styles/global-styles.scss +++ b/src/app/styles/global-styles.scss @@ -1,4 +1,5 @@ @use './variable.scss' as v; +@use './edit-permissions-modal.scss'; :root { --primary-color: #2563eb; diff --git a/src/app/ui/forms/create-staff-form.tsx b/src/app/ui/forms/create-staff-form.tsx index 9a0fab4..984067e 100644 --- a/src/app/ui/forms/create-staff-form.tsx +++ b/src/app/ui/forms/create-staff-form.tsx @@ -227,7 +227,6 @@ async function createStuffAction( } catch (error) { const errors: Record = {} - // Обработка различных ошибок от сервера switch (error) { case 'email-already-exists': errors['email'] = 'email-already-exists'; diff --git a/src/app/ui/staff-management/EditPermissionsModal.tsx b/src/app/ui/staff-management/EditPermissionsModal.tsx new file mode 100644 index 0000000..9272cbe --- /dev/null +++ b/src/app/ui/staff-management/EditPermissionsModal.tsx @@ -0,0 +1,126 @@ +import { useState } from 'react'; +import { useTranslations } from 'next-intl'; +import { StaffMember } from './tanstak-stuff-table'; + +export function EditPermissionsModal({ + staff, + setWindowClose, + onSave, + isLoading +}: { + staff: StaffMember; + setWindowClose: (state: boolean) => void; + onSave: (permissions: StaffMember['permissions']) => Promise; + isLoading: boolean; +}) { + const t = useTranslations("Global"); + const tPermissions = useTranslations("Permissions"); + + const [permissions, setPermissions] = useState(staff.permissions); + + const permissionsConfig = [ + { key: 'subscriptions', label: tPermissions('subscriptions'), description: tPermissions('subscriptions-desc') }, + { key: 'mailings', label: tPermissions('mailings'), description: tPermissions('mailings-desc') }, + { key: 'complaints', label: tPermissions('complaints'), description: tPermissions('complaints-desc') }, + { key: 'agreements', label: tPermissions('agreements'), description: tPermissions('agreements-desc') }, + { key: 'staff', label: tPermissions('staff'), description: tPermissions('staff-desc') }, + { key: 'content_moderation', label: tPermissions('content_moderation'), description: tPermissions('content_moderation-desc') }, + { key: 'users', label: tPermissions('users'), description: tPermissions('users-desc') }, + { key: 'kyc', label: tPermissions('kyc'), description: tPermissions('kyc-desc') }, + { key: 'money', label: tPermissions('money'), description: tPermissions('money-desc') }, + { key: 'claims', label: tPermissions('claims'), description: tPermissions('claims-desc') }, + { key: 'api', label: tPermissions('api'), description: tPermissions('api-desc') }, + { key: 'dash', label: tPermissions('dash'), description: tPermissions('dash-desc') }, + { key: 'tariffs', label: tPermissions('tariffs'), description: tPermissions('tariffs-desc') }, + ]; + + const handlePermissionChange = (key: keyof StaffMember['permissions'], value: number) => { + setPermissions(prev => ({ + ...prev, + [key]: value + })); + }; + + const handleSave = async () => { + await onSave(permissions); + }; + + return ( +
+
+

+ {tPermissions('edit-permissions')} +

+
+ +
+ {permissionsConfig.map((perm) => { + const currentValue = permissions[perm.key as keyof StaffMember['permissions']]; + + return ( +
+
+
+

{perm.label}

+ {/*

{perm.description}

*/} +
+
+
+ + + + + +
+
+
+
+ ); + })} +
+ +
+ + +
+
+ ); +} \ No newline at end of file diff --git a/src/app/ui/staff-management/StaffInfoModal.tsx b/src/app/ui/staff-management/StaffInfoModal.tsx new file mode 100644 index 0000000..97632f5 --- /dev/null +++ b/src/app/ui/staff-management/StaffInfoModal.tsx @@ -0,0 +1,69 @@ +import { ReactNode } from 'react'; +import { useTranslations } from 'next-intl'; +import { StaffMember } from '@/app/ui/staff-management/tanstak-stuff-table'; +import { formatDate, formatDateTime } from '@/app/lib/formatDate'; + +export function StaffInfoModal({ + staff, + setWindowClose, + setWindowChildren +}: { + staff: StaffMember; + setWindowClose: (state: boolean) => void; + setWindowChildren: (children: ReactNode) => void; +}) { + const t = useTranslations("Global"); + + return ( +
+

Информация о сотруднике

+ +
+
+ ID: {staff.id} +
+
+ ФИО: {staff.full_name} +
+
+ Email: {staff.email} +
+
+ Статус: {staff.is_active ? 'Активен' : 'Неактивен'} +
+
+ Роль: {staff.is_super_admin ? 'Super Admin' : 'Staff'} +
+
+ Дата создания: {formatDateTime(staff.created_at)} {formatDate(staff.created_at)} +
+
+ Активный пароль: {staff.active_password ? 'Да' : 'Нет'} +
+ +
+ Права доступа: +
+ {Object.entries(staff.permissions).map(([key, value]) => ( +
+ {key}: + + {value === 1 ? 'Чтение' : value === 2 ? 'Запись' : value === 3 ? 'Полный доступ' : value} + +
+ ))} +
+
+
+ +
+ +
+
+ ); +} \ No newline at end of file diff --git a/src/app/ui/staff-management/tanstak-stuff-table.tsx b/src/app/ui/staff-management/tanstak-stuff-table.tsx index db5a190..f681425 100644 --- a/src/app/ui/staff-management/tanstak-stuff-table.tsx +++ b/src/app/ui/staff-management/tanstak-stuff-table.tsx @@ -19,9 +19,12 @@ import ModalWindow from '@/app/components/modalWindow'; import { toast } from 'sonner'; import { useDebouncedCallback } from 'use-debounce'; import { useStuffList } from '@/app/hooks/react-query/stuff/useStuffList'; -import { removeStuff } from '@/app/actions/stuffActions'; +import { removeStuff, updateStuffPermissions } from '@/app/actions/stuffActions'; +import { EditPermissionsModal } from './EditPermissionsModal' +import { StaffInfoModal } from '@/app/ui/staff-management/StaffInfoModal'; +import { formatDate, formatDateTime } from '@/app/lib/formatDate'; -interface StaffMember { +export interface StaffMember { id: number; full_name: string; email: string; @@ -46,24 +49,6 @@ interface StaffMember { }; } -// Форматирование даты из ISO строки -const formatDate = (dateString: string) => { - const date = new Date(dateString); - const day = date.getDate().toString().padStart(2, '0'); - const month = (date.getMonth() + 1).toString().padStart(2, '0'); - const year = date.getFullYear(); - - return `${day}.${month}.${year}`; -}; - -const formatDateTime = (dateString: string) => { - const date = new Date(dateString); - const hours = date.getHours().toString().padStart(2, '0'); - const minutes = date.getMinutes().toString().padStart(2, '0'); - - return `${hours}:${minutes}`; -}; - export default function TanstakStuffTable() { // Состояния const [sorting, setSorting] = useState([]); @@ -128,10 +113,6 @@ export default function TanstakStuffTable() { const totalItems = staffList?.length || 0; const totalPages = Math.ceil(totalItems / pagination.pageSize); - useEffect(() => { - console.log(staffList); - }, [staffList]) - const queryClient = useQueryClient(); const removeMutation = useMutation({ mutationFn: (id: number) => removeStuff(id), @@ -147,6 +128,19 @@ export default function TanstakStuffTable() { } }); + const updatePermissionsMutation = useMutation({ + mutationFn: ({ id, permissions }: { id: number; permissions: StaffMember['permissions'] }) => + updateStuffPermissions(id, permissions), + onSuccess: () => { + toast.success(tStaff('permissions-updated-successfully')); + queryClient.invalidateQueries({ queryKey: ['stuffList'] }); + setOpenWindow(false); + }, + onError: (error: Error) => { + toast.error(tStaff(error.message || 'permissions-update-failed')); + } + }); + // Определение колонок для сотрудников const columns = useMemo[]>( () => [ @@ -308,6 +302,13 @@ export default function TanstakStuffTable() { > + - - - ); } \ No newline at end of file diff --git a/src/i18n/messages/en.json b/src/i18n/messages/en.json index b6cd2f5..3a9f1d9 100644 --- a/src/i18n/messages/en.json +++ b/src/i18n/messages/en.json @@ -188,7 +188,9 @@ "storageInformation": "Storage Information", "diskUsage": "Disk Usage", "diskUsagePercent": "Usage", - "fileCount": "Files" + "fileCount": "Files", + "employee": "Employee", + "role": "Role" }, "Login-register-form": { "and": "and", @@ -269,6 +271,40 @@ "passwords-do-not-match": "Passwords do not match", "email-already-exists": "User with this email already exists", "staff-removed-successfully": "The employee has been successfully removed", - "removal-failed": "Error deleting employee" + "removal-failed": "Error deleting employee", + "permissions-updated-successfully": "Permissions updated successfully", + "permissions-update-failed": "Failed to update permissions" + }, + "Permissions": { + "edit-permissions": "Edit Permissions", + "no-access": "No Access", + "read-only": "Read Only", + "full-access": "Full Access", + "subscriptions": "Subscriptions", + "subscriptions-desc": "Manage user subscriptions", + "mailings": "Mailings", + "mailings-desc": "Manage email mailings", + "complaints": "Complaints", + "complaints-desc": "Process user complaints", + "agreements": "Agreements", + "agreements-desc": "Manage user agreements", + "staff": "Staff", + "staff-desc": "Manage staff members", + "content_moderation": "Content Moderation", + "content_moderation-desc": "Moderate content", + "users": "Users", + "users-desc": "Manage users", + "kyc": "Verification", + "kyc-desc": "Manage user verification", + "money": "Money", + "money-desc": "Manage financial operations", + "claims": "Claims", + "claims-desc": "Process claims", + "api": "API", + "api-desc": "Manage API access", + "dash": "Dashboard", + "dash-desc": "Access to dashboard", + "tariffs": "Tariffs", + "tariffs-desc": "Manage tariffs" } } \ No newline at end of file diff --git a/src/i18n/messages/ru.json b/src/i18n/messages/ru.json index 16fa6d7..920e8a6 100644 --- a/src/i18n/messages/ru.json +++ b/src/i18n/messages/ru.json @@ -188,7 +188,9 @@ "storageInformation": "Информация о хранилище", "diskUsage": "Использование диска", "diskUsagePercent": "Заполнение", - "fileCount": "Файлы" + "fileCount": "Файлы", + "employee": "Сотрудник", + "role": "Роль" }, "Login-register-form": { "and": "и", @@ -269,6 +271,40 @@ "passwords-do-not-match": "Пароли не совпадают", "email-already-exists": "Пользователь с таким адресом электронной почты уже существует", "staff-removed-successfully": "Сотрудник успешно удален", - "removal-failed": "Ошибка при удалении сотрудника" + "removal-failed": "Ошибка при удалении сотрудника", + "permissions-updated-successfully": "Права доступа успешно обновлены", + "permissions-update-failed": "Не удалось обновить права доступа" + }, + "Permissions": { + "edit-permissions": "Редактирование прав доступа", + "no-access": "Нет доступа", + "read-only": "Только чтение", + "full-access": "Полный доступ", + "subscriptions": "Подписки", + "subscriptions-desc": "Управление подписками пользователей", + "mailings": "Рассылки", + "mailings-desc": "Управление email рассылками", + "complaints": "Жалобы", + "complaints-desc": "Обработка жалоб пользователей", + "agreements": "Соглашения", + "agreements-desc": "Управление пользовательскими соглашениями", + "staff": "Сотрудники", + "staff-desc": "Управление сотрудниками", + "content_moderation": "Модерация контента", + "content_moderation-desc": "Модерация контента", + "users": "Пользователи", + "users-desc": "Управление пользователями", + "kyc": "Верификация", + "kyc-desc": "Управление верификацией пользователей", + "money": "Финансы", + "money-desc": "Управление финансовыми операциями", + "claims": "Претензии", + "claims-desc": "Обработка претензий", + "api": "API", + "api-desc": "Управление API доступами", + "dash": "Панель управления", + "dash-desc": "Доступ к панеле управления", + "tariffs": "Тарифы", + "tariffs-desc": "Управление тарифами" } } \ No newline at end of file