diff --git a/src/app/[locale]/pages/staff-management/page.tsx b/src/app/[locale]/pages/staff-management/page.tsx index 0a150f0..1db05da 100644 --- a/src/app/[locale]/pages/staff-management/page.tsx +++ b/src/app/[locale]/pages/staff-management/page.tsx @@ -1,14 +1,13 @@ 'use client' import TanstakStuffTable from '@/app/ui/staff-management/tanstak-stuff-table'; -import StaffManagement from '@/app/ui/staff-management/staff-add-management'; +import { CreateStaffForm } from '@/app/ui/forms/create-staff-form'; export default function Page() { return (
- + - {/* */}
) } \ No newline at end of file diff --git a/src/app/actions/stuffActions.ts b/src/app/actions/stuffActions.ts index e7ed155..2963991 100644 --- a/src/app/actions/stuffActions.ts +++ b/src/app/actions/stuffActions.ts @@ -45,7 +45,23 @@ export async function fetchStuffList(page?: number, size?: number, sortBy?: stri } } -export async function createStuff(fullName: string, email: string, password: string) { +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 createStuff(fullName: string, email: string, password: string, permissions: Permissions, changePassword: boolean) { const token = await getSessionData('token'); try { @@ -60,7 +76,9 @@ export async function createStuff(fullName: string, email: string, password: str full_name: fullName, email: email, password: password, - is_super_admin: false + is_super_admin: false, + change_password: changePassword, + permissions: permissions, } }), headers: { @@ -130,22 +148,6 @@ export async function removeStuff(id: number) { } } -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'); diff --git a/src/app/components/admin-layout-wrapper.tsx b/src/app/components/admin-layout-wrapper.tsx index 7c06aeb..d32937e 100644 --- a/src/app/components/admin-layout-wrapper.tsx +++ b/src/app/components/admin-layout-wrapper.tsx @@ -2,7 +2,7 @@ import { usePathname } from 'next/navigation'; import AdminNavLinks from '@/app/ui/navigation/admin-nav-links'; -import AdminHeaderPanel from '@/app/ui/admin-header-panel'; +import AdminHeaderPanel from '@/app/ui/header/admin-header-panel'; import { useEffect, useState } from 'react'; export default function AdminLayoutWrapper({ diff --git a/src/app/lib/prefetch-queries.ts b/src/app/lib/prefetch-queries.ts index 52985d8..e0a8400 100644 --- a/src/app/lib/prefetch-queries.ts +++ b/src/app/lib/prefetch-queries.ts @@ -1,9 +1,14 @@ import { QueryClient } from '@tanstack/react-query'; import { fetchTokensGeneralStatistic, fetchProtectedContentGeneralStatistic, fetchTariffStatistic, fetchUserFilesTopStatistic, fetchUserGeneralStatistic, fetchViolationGeneralStatistic } from '@/app/actions/statisticActions'; import { useSubscribeGeneralStatistic } from '@/app/hooks/react-query/dashboard/useSubscribeGeneralStatistic'; +import { fetchEmployeInfo } from '@/app/actions/stuffActions'; export async function prefetchLayoutQueries(queryClient: QueryClient) { await Promise.all([ + queryClient.prefetchQuery({ + queryKey: ['fetchEmployeInfo'], + queryFn: () => fetchEmployeInfo() + }), queryClient.prefetchQuery({ queryKey: ['incomeGeneralStatistic'], queryFn: () => fetchTokensGeneralStatistic() diff --git a/src/app/styles/edit-permissions-modal.scss b/src/app/styles/edit-permissions-modal.scss index 7da721f..417a315 100644 --- a/src/app/styles/edit-permissions-modal.scss +++ b/src/app/styles/edit-permissions-modal.scss @@ -6,6 +6,19 @@ overflow-y: auto; background: white; border-radius: 12px; +} + +.create-staff-window { + background: var(--surface); + box-shadow: var(--shadow); + border-radius: 12px; + overflow: hidden; + margin-bottom: 24px; + padding: 1.5rem; +} + +.edit-permissions-modal, +.create-staff-window { .modal-header { margin-bottom: 24px; @@ -20,6 +33,21 @@ } } + .permissions-section-title { + border-top: 1px solid #e9ecef; + margin-bottom: 20px; + padding-top: 20px; + } + + .checkbox-label { + display: flex; + gap: 10px; + + input { + width: auto; + } + } + .permissions-list { display: flex; flex-direction: column; @@ -27,17 +55,10 @@ 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; diff --git a/src/app/styles/module/login.module.scss b/src/app/styles/module/login.module.scss index 9187940..c71ea81 100644 --- a/src/app/styles/module/login.module.scss +++ b/src/app/styles/module/login.module.scss @@ -289,4 +289,9 @@ .password-wrapper { position: relative; +} + +.checkbox-label { + display: flex; + gap: 10px; } \ No newline at end of file diff --git a/src/app/ui/forms/create-staff-form.tsx b/src/app/ui/forms/create-staff-form.tsx index 984067e..f3db7fd 100644 --- a/src/app/ui/forms/create-staff-form.tsx +++ b/src/app/ui/forms/create-staff-form.tsx @@ -1,13 +1,12 @@ 'use client' import styles from '@/app/styles/module/login.module.scss' -import { useActionState, useState, useRef, useEffect } from 'react'; +import { useActionState, useState, useEffect } from 'react'; import { createStuff } from '@/app/actions/stuffActions'; import { useTranslations } from 'next-intl'; import { IconEye } from '@/app/ui/icons/icons'; import { useQueryClient } from '@tanstack/react-query'; import { toast } from 'sonner'; - import { z } from 'zod'; const createStuffSchema = z.object({ @@ -16,23 +15,93 @@ const createStuffSchema = z.object({ .max(100, 'full-name-too-long') .trim(), email: z.string() - .email('invalid-email') + .email('register-error-email-invalid') .toLowerCase() .trim(), password: z.string() - .min(6, 'password-too-short') - .max(50, 'password-too-long'), + .min(4, 'register-error-password-min') + .max(124, 'register-error-password-max'), + changePassword: z.boolean().default(true), + permissions: z.object({ + subscriptions: z.number().min(0).max(3), + mailings: z.number().min(0).max(3), + complaints: z.number().min(0).max(3), + agreements: z.number().min(0).max(3), + staff: z.number().min(0).max(3), + content_moderation: z.number().min(0).max(3), + users: z.number().min(0).max(3), + money: z.number().min(0).max(3), + kyc: z.number().min(0).max(3), + claims: z.number().min(0).max(3), + api: z.number().min(0).max(3), + dash: z.number().min(0).max(3), + tariffs: z.number().min(0).max(3), + }).default({ + subscriptions: 0, + mailings: 0, + complaints: 0, + agreements: 0, + staff: 0, + content_moderation: 0, + users: 0, + money: 0, + kyc: 0, + claims: 0, + api: 0, + dash: 0, + tariffs: 0, + }) }) -export default function CreateStaffForm({ onSuccess }: { onSuccess?: () => void }) { +export function CreateStaffForm({ onSuccess }: { onSuccess?: () => void }) { const [state, formAction, isPending] = useActionState( createStuffAction, undefined, ); const t = useTranslations('Login-register-form'); - const tStaff = useTranslations('Staff'); + const tGeneral = useTranslations('Global'); + const tPermissions = useTranslations('Permissions'); const [showPassword, setShowPassword] = useState(false); + const [changePassword, setChangePassword] = useState(true); + const [permissions, setPermissions] = useState({ + subscriptions: 0, + mailings: 0, + complaints: 0, + agreements: 0, + staff: 0, + content_moderation: 0, + users: 0, + money: 0, + kyc: 0, + claims: 0, + api: 0, + dash: 0, + tariffs: 0, + }); + + 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 typeof permissions, value: number) => { + setPermissions(prev => ({ + ...prev, + [key]: value + })); + }; function toggleShowPassword() { setShowPassword(!showPassword); @@ -42,7 +111,7 @@ export default function CreateStaffForm({ onSuccess }: { onSuccess?: () => void useEffect(() => { if (state?.success) { - toast.success(tStaff('staff-created-successfully')); + toast.success(tGeneral('staff-created-successfully')); queryClient.invalidateQueries({ queryKey: ['stuffList'] }); if (onSuccess) { onSuccess(); @@ -51,109 +120,182 @@ export default function CreateStaffForm({ onSuccess }: { onSuccess?: () => void if (state?.error?.server) { toast.error(t(state.error.server)); } - }, [state, queryClient, onSuccess, t, tStaff]); + }, [state, queryClient, onSuccess, t, tGeneral]); return ( -
- {/* Поле Email */} -
- - { - e.target.value = e.target.value.toLowerCase(); - }} - /> - {state?.error?.email && ( -

- {t(state?.error?.email)} -

- )} -
- - {/* Поле Password */} -
- -
- - -
- {state?.error?.password && ( -

- {t(state?.error?.password)} -

- )} -
- - {/* Поле Full Name */} -
- - - {state?.error?.fullName && ( -

- {t(state?.error?.fullName)} -

- )} -
- - {/* Общая ошибка сервера */} - {state?.error?.server && !state.success && ( -

- {t(state?.error?.server)} -

- )} - - {/* Кнопка отправки */} - + {state?.error?.password && ( +

+ {t(state?.error?.password)} +

+ )} + + + {/* Поле Full Name */} +
+ + + {state?.error?.fullName && ( +

+ {t(state?.error?.fullName)} +

+ )} +
+ + {/* Чекбокс */} +
+ +
+ +
+

+ {tGeneral('permissions')} +

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

{perm.label}

+

{perm.description}

+
+
+
+ + + + + + +
+
+
+
+ ); + })} +
+
+ + {state?.error?.server && !state.success && ( +

+ {t(state?.error?.server)} +

)} - -
+ + + + ) } @@ -172,11 +314,30 @@ async function createStuffAction( const fullName = formData.get('fullName') as string || ''; const email = formData.get('email') as string || ''; const password = formData.get('password') as string || ''; + const changePassword = formData.get('changePassword') === 'on'; + + const permissions = { + subscriptions: parseInt(formData.get('permissions.subscriptions') as string) || 0, + mailings: parseInt(formData.get('permissions.mailings') as string) || 0, + complaints: parseInt(formData.get('permissions.complaints') as string) || 0, + agreements: parseInt(formData.get('permissions.agreements') as string) || 0, + staff: parseInt(formData.get('permissions.staff') as string) || 0, + content_moderation: parseInt(formData.get('permissions.content_moderation') as string) || 0, + users: parseInt(formData.get('permissions.users') as string) || 0, + money: parseInt(formData.get('permissions.money') as string) || 0, + kyc: parseInt(formData.get('permissions.kyc') as string) || 0, + claims: parseInt(formData.get('permissions.claims') as string) || 0, + api: parseInt(formData.get('permissions.api') as string) || 0, + dash: parseInt(formData.get('permissions.dash') as string) || 0, + tariffs: parseInt(formData.get('permissions.tariffs') as string) || 0, + }; const validatedFields = createStuffSchema.safeParse({ fullName, email, - password + password, + changePassword, + permissions }); if (!validatedFields.success) { @@ -205,10 +366,9 @@ async function createStuffAction( } try { - const { fullName, email, password } = validatedFields.data; + const { fullName, email, password, changePassword, permissions } = validatedFields.data; - // Вызов API для создания сотрудника - const result = await createStuff(fullName, email, password); + const result = await createStuff(fullName, email, password, permissions, changePassword); if (result) { return { @@ -227,16 +387,12 @@ async function createStuffAction( } catch (error) { const errors: Record = {} - switch (error) { - case 'email-already-exists': - errors['email'] = 'email-already-exists'; - break; - case 'invalid-email': - errors['email'] = 'invalid-email'; - break; - default: - errors['server'] = 'request-ended-with-an-error'; - break; + if (error === 'email-already-exists') { + errors['email'] = 'email-already-exists'; + } else if (error === 'invalid-email') { + errors['email'] = 'invalid-email'; + } else { + errors['server'] = 'request-ended-with-an-error'; } return { diff --git a/src/app/ui/admin-header-panel.tsx b/src/app/ui/header/admin-header-panel.tsx similarity index 66% rename from src/app/ui/admin-header-panel.tsx rename to src/app/ui/header/admin-header-panel.tsx index 650341a..a17e375 100644 --- a/src/app/ui/admin-header-panel.tsx +++ b/src/app/ui/header/admin-header-panel.tsx @@ -1,6 +1,7 @@ import Link from 'next/link'; import LanguageSwitcher from '@/app/components/LanguageSwitcher'; -import packageJson from '../../../package.json'; +import packageJson from '../../../../package.json'; +import { HeaderUserInfoBadge } from '@/app/ui/header/header-user-info'; export default function AdminHeaderPanel() { return ( @@ -15,15 +16,7 @@ export default function AdminHeaderPanel() {
{/* */} -
-
- + -
-
- name name name - role -
-
+ +{/*
+ + +
*/} +
+ {data?.full_name ? data?.full_name : '---'} + {data?.is_super_admin ? 'super-admin' : 'stuff'} +
+
+ ) +} \ No newline at end of file diff --git a/src/app/ui/staff-management/staff-add-management.tsx b/src/app/ui/staff-management/staff-add-management.tsx index 8e4ca02..bb11244 100644 --- a/src/app/ui/staff-management/staff-add-management.tsx +++ b/src/app/ui/staff-management/staff-add-management.tsx @@ -1,6 +1,8 @@ +/* for remove */ + import { useState } from 'react'; import ModalWindow from '@/app/components/modalWindow'; -import CreateStaffForm from '@/app/ui/forms/create-staff-form'; +import { CreateStaffForm } from '@/app/ui/forms/create-staff-form'; export default function StaffManagement() { const [isModalOpen, setIsModalOpen] = useState(false); @@ -9,7 +11,8 @@ export default function StaffManagement() {
-
+ {/*
+
*/}
); } \ 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 74a598b..bcee119 100644 --- a/src/app/ui/staff-management/tanstak-stuff-table.tsx +++ b/src/app/ui/staff-management/tanstak-stuff-table.tsx @@ -63,7 +63,6 @@ export default function TanstakStuffTable() { const [openWindowChildren, setOpenWindowChildren] = useState(null); const [selectedStaff, setSelectedStaff] = useState(null); const [confirmRemove, setConfirmRemove] = useState<{ id: number; name: string } | null>(null); - const tStaff = useTranslations('Staff'); const debouncedSetSearchQuery = useDebouncedCallback( (value: string) => { @@ -117,12 +116,12 @@ export default function TanstakStuffTable() { const removeMutation = useMutation({ mutationFn: (id: number) => removeStuff(id), onSuccess: () => { - toast.success(tStaff('staff-removed-successfully')); + toast.success(t('staff-removed-successfully')); queryClient.invalidateQueries({ queryKey: ['stuffList'] }); setConfirmRemove(null); }, onError: (error: Error) => { - toast.error(tStaff(error.message || 'removal-failed')); + toast.error(t(error.message || 'removal-failed')); setConfirmRemove(null); } }); @@ -131,13 +130,13 @@ export default function TanstakStuffTable() { mutationFn: ({ id, permissions }: { id: number; permissions: StaffMember['permissions'] }) => updateStuffPermissions(id, permissions), onSuccess: () => { - toast.success(tStaff('permissions-updated-successfully')); + toast.success(t('permissions-updated-successfully')); queryClient.invalidateQueries({ queryKey: ['stuffList'] }); queryClient.invalidateQueries({ queryKey: ['employeInfo'] }); setOpenWindow(false); }, onError: (error: Error) => { - toast.error(tStaff(error.message || 'permissions-update-failed')); + toast.error(t(error.message || 'permissions-update-failed')); } }); diff --git a/src/i18n/messages/en.json b/src/i18n/messages/en.json index 3a9f1d9..fb6dac4 100644 --- a/src/i18n/messages/en.json +++ b/src/i18n/messages/en.json @@ -190,7 +190,21 @@ "diskUsagePercent": "Usage", "fileCount": "Files", "employee": "Employee", - "role": "Role" + "role": "Role", + "enter-full-name": "Enter full name", + "staff-created-successfully": "Staff member created successfully", + "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", + "permissions-updated-successfully": "Permissions updated successfully", + "permissions-update-failed": "Failed to update permissions", + "staff-creation": "Staff creation", + "password": "Password", + "require-password-change": "Require password change on first login", + "permissions": "Permissions", + "creating": "Creation...", + "create": "Create" }, "Login-register-form": { "and": "and", @@ -227,7 +241,7 @@ "register-error-name-max": "The name length must not exceed 100 characters.", "register-error-password-one-digit": "Password must contain at least 1 digit.", "register-error-password-one-letter": "Password must contain at least 1 letter.", - "register-error-password-min": "Password must be at least 8 characters long.", + "register-error-password-min": "Password must be at least 4 characters long.", "register-error-phone-min": "Please enter a phone number.", "register-error-company-name-min": "The company name must contain at least 2 characters.", "register-error-company-name-max": "The company name must exceed 200 characters.", @@ -258,22 +272,9 @@ "register-error-phone-not-allowed-symbols": "", "register-error-password-not-allowed-symbols": "Password contains invalid characters.", "confirmation-code": "Confirmation code", - "enter-confirmation-code": "Enter the confirmation code" - }, - "Staff": { - "full-name": "Full Name", - "enter-full-name": "Enter full name", - "confirm-password": "Confirm Password", - "create-staff": "Create Staff", - "staff-created-successfully": "Staff member created successfully", + "enter-confirmation-code": "Enter the confirmation code", "full-name-too-short": "Full name must be at least 2 characters", - "full-name-too-long": "Full name must not exceed 100 characters", - "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", - "permissions-updated-successfully": "Permissions updated successfully", - "permissions-update-failed": "Failed to update permissions" + "full-name-too-long": "Full name must not exceed 100 characters" }, "Permissions": { "edit-permissions": "Edit Permissions", diff --git a/src/i18n/messages/ru.json b/src/i18n/messages/ru.json index 920e8a6..12eabbf 100644 --- a/src/i18n/messages/ru.json +++ b/src/i18n/messages/ru.json @@ -190,7 +190,21 @@ "diskUsagePercent": "Заполнение", "fileCount": "Файлы", "employee": "Сотрудник", - "role": "Роль" + "role": "Роль", + "enter-full-name": "Введите полное имя", + "staff-created-successfully": "Сотрудник успешно создан", + "passwords-do-not-match": "Пароли не совпадают", + "email-already-exists": "Пользователь с таким адресом электронной почты уже существует", + "staff-removed-successfully": "Сотрудник успешно удален", + "removal-failed": "Ошибка при удалении сотрудника", + "permissions-updated-successfully": "Права доступа успешно обновлены", + "permissions-update-failed": "Не удалось обновить права доступа", + "staff-creation": "Создание персонала", + "password": "Пароль", + "require-password-change": "Требовать смену пароля при первом входе", + "permissions": "Доступы", + "creating": "Создание...", + "create": "Создать" }, "Login-register-form": { "and": "и", @@ -227,7 +241,7 @@ "register-error-name-max": "Длина имени не должна превышать 100 символов.", "register-error-password-one-digit": "Пароль должен содержать как минимум 1 цифру.", "register-error-password-one-letter": "Пароль должен содержать как минимум 1 букву.", - "register-error-password-min": "Длина пароля должна быть не менее 8 символов.", + "register-error-password-min": "Длина пароля должна быть не менее 4 символов.", "register-error-phone-min": "Пожалуйста, введите телефонный номер.", "register-error-company-name-min": "Название компании должно содержать не менее 2 символов.", "register-error-company-name-max": "Название компании должно превышать 200 символов.", @@ -258,22 +272,9 @@ "register-error-phone-not-allowed-symbols": "", "register-error-password-not-allowed-symbols": "Пароль содержит недопустимые символы.", "confirmation-code": "Код подтверждения", - "enter-confirmation-code": "Введите код подтверждения" - }, - "Staff": { - "full-name": "Полное имя", - "enter-full-name": "Введите полное имя", - "confirm-password": "Подтвердите пароль", - "create-staff": "Создать персонал", - "staff-created-successfully": "Сотрудник успешно создан", + "enter-confirmation-code": "Введите код подтверждения", "full-name-too-short": "Полное имя должно состоять как минимум из 2 символов", - "full-name-too-long": "Полное имя не должно превышать 100 символов", - "passwords-do-not-match": "Пароли не совпадают", - "email-already-exists": "Пользователь с таким адресом электронной почты уже существует", - "staff-removed-successfully": "Сотрудник успешно удален", - "removal-failed": "Ошибка при удалении сотрудника", - "permissions-updated-successfully": "Права доступа успешно обновлены", - "permissions-update-failed": "Не удалось обновить права доступа" + "full-name-too-long": "Полное имя не должно превышать 100 символов" }, "Permissions": { "edit-permissions": "Редактирование прав доступа",