diff --git a/package.json b/package.json index 5669a5a..521bd6e 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "no-copy-admin-panel-frontend", - "version": "0.7.0", + "version": "0.8.0", "private": true, "scripts": { "dev": "next dev -p 2996", diff --git a/src/app/[locale]/layout.tsx b/src/app/[locale]/layout.tsx index 58fdfbc..d6820d2 100644 --- a/src/app/[locale]/layout.tsx +++ b/src/app/[locale]/layout.tsx @@ -2,6 +2,7 @@ import Providers from '@/app/providers/getQueryServer' import { NextIntlClientProvider, hasLocale } from 'next-intl'; import { notFound } from 'next/navigation'; import { routing } from '@/i18n/routing'; +import { ToastProvider } from '@/app/providers/ToastProvider'; import "../styles/globals.css"; import "../styles/global-styles.scss" @@ -28,6 +29,7 @@ export default async function RootLayout({ {children} + diff --git a/src/app/[locale]/pages/staff-management/page.tsx b/src/app/[locale]/pages/staff-management/page.tsx new file mode 100644 index 0000000..af3f943 --- /dev/null +++ b/src/app/[locale]/pages/staff-management/page.tsx @@ -0,0 +1,17 @@ +'use client' + +import { useStuffList } from '@/app/hooks/react-query/stuff/useStuffList'; +import { useEffect } from 'react'; +import TanstakStuffTable from '@/app/ui/staff-management/tanstak-stuff-table'; +import CreateStaffForm from '@/app/ui/forms/create-staff-form'; +import StaffManagement from '@/app/ui/staff-management/staff-add-management'; + +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 new file mode 100644 index 0000000..65688a3 --- /dev/null +++ b/src/app/actions/stuffActions.ts @@ -0,0 +1,132 @@ +'use server' + +import { getSessionData } from '@/app/actions/session'; +import { API_BASE_URL } from '@/app/actions/definitions'; + +export async function fetchStuffList(page?: number, size?: number, sortBy?: string, sortDirection?: 'asc' | 'desc' | string, nameQuery?: string) { + const token = await getSessionData('token'); + + try { + const response = await fetch(`${API_BASE_URL}/api/admin`, { + method: 'POST', + body: JSON.stringify({ + version: 1, + msg_id: 100, + token: token, + message_body: { + /* token: token, */ + action: 'get_all', + page: page, + size: size, + sort_by: sortBy || '', + sort_direction: sortDirection || 'asc', + full_name: nameQuery + } + }), + 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) { + return null; + } +} + +export async function createStuff(fullName: string, email: string, password: string) { + const token = await getSessionData('token'); + + try { + const response = await fetch(`${API_BASE_URL}/api/admin`, { + method: 'POST', + body: JSON.stringify({ + version: 1, + msg_id: 100, + token: token, + message_body: { + action: 'create', + full_name: fullName, + email: email, + password: password, + is_super_admin: false + } + }), + 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 if (parsed.message_code === 3) { + throw new Error('email-already-exists'); + } else if (parsed.message_code === 2) { + throw new Error('invalid-email'); + } else { + return null; + } + } else { + throw new Error(`${response.status}`); + } + } catch (error) { + throw error; + } +} + +export async function removeStuff(id: number) { + const token = await getSessionData('token'); + + try { + const response = await fetch(`${API_BASE_URL}/api/admin`, { + method: 'POST', + body: JSON.stringify({ + version: 1, + msg_id: 100, + token: token, + message_body: { + action: 'delete', + admin_id: id, + } + }), + 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 if (parsed.message_code === 3) { + throw new Error('email-already-exists'); + } else if (parsed.message_code === 2) { + throw new Error('invalid-email'); + } else { + return null; + } + } else { + throw new Error(`${response.status}`); + } + } catch (error) { + throw error; + } +} \ No newline at end of file diff --git a/src/app/hooks/react-query/stuff/useStuffList.ts b/src/app/hooks/react-query/stuff/useStuffList.ts new file mode 100644 index 0000000..008ead5 --- /dev/null +++ b/src/app/hooks/react-query/stuff/useStuffList.ts @@ -0,0 +1,43 @@ +import { useQuery } from '@tanstack/react-query'; +import { fetchStuffList } from '@/app/actions/stuffActions'; + +interface StaffMember { + id: number; + full_name: string; + email: string; + is_active: boolean; + is_super_admin: boolean; + created_at: string; + active_password: boolean; + permissions: { + subscriptions: number; + mailings: number; + complaints: number; + agreements: number; + staff: number; + content_moderation: number; + users: number; + kyc: number; + money: number; + claims: number; + api: number; + dash: number; + tariffs: number; + }; +} + +export const useStuffList = (page?: number, size?: number, sortBy?: string, sortDirection?: 'asc' | 'desc' | string, nameQuery?: string) => { + return useQuery({ + queryKey: ['stuffList'], + queryFn: () => { + return fetchStuffList(page, size, sortBy, sortDirection, nameQuery) + }, + select: (data: StaffMember[] | null) => { + if (!data) { + return null + } + return data; + }, + retry: false + }); +}; \ No newline at end of file diff --git a/src/app/styles/global-styles.scss b/src/app/styles/global-styles.scss index 128e21b..b756415 100644 --- a/src/app/styles/global-styles.scss +++ b/src/app/styles/global-styles.scss @@ -8,6 +8,7 @@ --success-color: #10b981; --success-hover: #0d8a60; --danger-color: #ef4444; + --danger-hover: #bd3434; --warning-color: #f59e0b; --warning-hover: #b3750b; --info-color: #3b82f6; @@ -93,29 +94,29 @@ display: flex; justify-content: center; align-items: center; +} - .show-password-button { - cursor: pointer; - position: absolute; - top: 50%; - right: 0px; - padding: 10px; - transform: translateY(-50%); +.show-password-button { + cursor: pointer; + position: absolute; + top: 50%; + right: 0px; + padding: 10px; + transform: translateY(-50%); - svg { - width: 16px; - color: var(--secondary-hover); - transition: color 0.2s ease; + svg { + width: 16px; + color: var(--secondary-hover); + transition: color 0.2s ease; - &:hover { - color: var(--primary-hover); - } + &:hover { + color: var(--primary-hover); } + } - &.show { - svg { - color: var(--primary-hover); - } + &.show { + svg { + color: var(--primary-hover); } } } @@ -285,6 +286,15 @@ } } + .btn-danger { + background: var(--danger-color); + color: white; + + &:hover { + background: var(--danger-hover); + } + } + .btn { padding: 12px 24px; border-radius: 8px; diff --git a/src/app/ui/forms/create-staff-form.tsx b/src/app/ui/forms/create-staff-form.tsx new file mode 100644 index 0000000..9a0fab4 --- /dev/null +++ b/src/app/ui/forms/create-staff-form.tsx @@ -0,0 +1,253 @@ +'use client' + +import styles from '@/app/styles/module/login.module.scss' +import { useActionState, useState, useRef, 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({ + fullName: z.string() + .min(2, 'full-name-too-short') + .max(100, 'full-name-too-long') + .trim(), + email: z.string() + .email('invalid-email') + .toLowerCase() + .trim(), + password: z.string() + .min(6, 'password-too-short') + .max(50, 'password-too-long'), +}) + +export default function CreateStaffForm({ onSuccess }: { onSuccess?: () => void }) { + const [state, formAction, isPending] = useActionState( + createStuffAction, + undefined, + ); + const t = useTranslations('Login-register-form'); + const tStaff = useTranslations('Staff'); + + const [showPassword, setShowPassword] = useState(false); + + function toggleShowPassword() { + setShowPassword(!showPassword); + } + + const queryClient = useQueryClient(); + + useEffect(() => { + if (state?.success) { + toast.success(tStaff('staff-created-successfully')); + queryClient.invalidateQueries({ queryKey: ['stuffList'] }); + if (onSuccess) { + onSuccess(); + } + } + if (state?.error?.server) { + toast.error(t(state.error.server)); + } + }, [state, queryClient, onSuccess, t, tStaff]); + + 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)} +

+ )} + + {/* Кнопка отправки */} + +
+ ) +} + +async function createStuffAction( + state: { + previousState: { + fullName?: string; + email?: string; + password?: string; + } + error: Record + success?: boolean; + } | undefined, + formData: FormData, +) { + const fullName = formData.get('fullName') as string || ''; + const email = formData.get('email') as string || ''; + const password = formData.get('password') as string || ''; + + const validatedFields = createStuffSchema.safeParse({ + fullName, + email, + password + }); + + if (!validatedFields.success) { + const errors: Record = {} + JSON.parse(validatedFields.error.message).forEach((obj: { + message: string, + path: string[] + }) => { + const fieldName = obj.path[0]; + if (errors[fieldName]) { + errors[fieldName] = `${errors[fieldName]} ${obj.message}`; + } else { + errors[fieldName] = obj.message; + } + }); + + return { + previousState: { + fullName, + email, + password + }, + error: errors, + success: false + }; + } + + try { + const { fullName, email, password } = validatedFields.data; + + // Вызов API для создания сотрудника + const result = await createStuff(fullName, email, password); + + if (result) { + return { + previousState: { + fullName: '', + email: '', + password: '' + }, + error: {}, + success: true + }; + } else { + throw new Error('creation-failed'); + } + + } 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; + } + + return { + previousState: { + fullName, + email, + password + }, + error: errors, + success: false + }; + } +} \ No newline at end of file diff --git a/src/app/ui/navigation/admin-nav-links.tsx b/src/app/ui/navigation/admin-nav-links.tsx index 921a1d3..ccefb96 100644 --- a/src/app/ui/navigation/admin-nav-links.tsx +++ b/src/app/ui/navigation/admin-nav-links.tsx @@ -23,6 +23,11 @@ export default function AdminNavLinks() { href: '/pages/users', img: 'M16 17v2H2v-2s0-4 7-4s7 4 7 4m-3.5-9.5A3.5 3.5 0 1 0 9 11a3.5 3.5 0 0 0 3.5-3.5m3.44 5.5A5.32 5.32 0 0 1 18 17v2h4v-2s0-3.63-6.06-4M15 4a3.4 3.4 0 0 0-1.93.59a5 5 0 0 1 0 5.82A3.4 3.4 0 0 0 15 11a3.5 3.5 0 0 0 0-7' }, + { + name: 'staff management', + href: '/pages/staff-management', + img: 'M16 17v2H2v-2s0-4 7-4s7 4 7 4m-3.5-9.5A3.5 3.5 0 1 0 9 11a3.5 3.5 0 0 0 3.5-3.5m3.44 5.5A5.32 5.32 0 0 1 18 17v2h4v-2s0-3.63-6.06-4M15 4a3.4 3.4 0 0 0-1.93.59a5 5 0 0 1 0 5.82A3.4 3.4 0 0 0 15 11a3.5 3.5 0 0 0 0-7' + }, { name: 'subscriptions', href: '/pages/subscriptions', diff --git a/src/app/ui/staff-management/staff-add-management.tsx b/src/app/ui/staff-management/staff-add-management.tsx new file mode 100644 index 0000000..8e4ca02 --- /dev/null +++ b/src/app/ui/staff-management/staff-add-management.tsx @@ -0,0 +1,31 @@ +import { useState } from 'react'; +import ModalWindow from '@/app/components/modalWindow'; +import CreateStaffForm from '@/app/ui/forms/create-staff-form'; + +export default function StaffManagement() { + const [isModalOpen, setIsModalOpen] = useState(false); + + return ( +
+
+ + + +
+

Создание сотрудника

+ setIsModalOpen(false)} /> +
+
+
+
+ ); +} \ 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 new file mode 100644 index 0000000..db5a190 --- /dev/null +++ b/src/app/ui/staff-management/tanstak-stuff-table.tsx @@ -0,0 +1,655 @@ +'use client'; + +import { useState, useMemo, useEffect, ReactNode, useCallback } from 'react'; +import { + useReactTable, + getCoreRowModel, + getSortedRowModel, + getPaginationRowModel, + getFilteredRowModel, + ColumnDef, + SortingState, + ColumnFiltersState, +} from '@tanstack/react-table'; +import { IconEye, IconDoubleArrowRight, IconArrowRight, IconDoubleArrowLeft, IconArrowLeft, IconArrowUp, IconArrowDown, IconFilter, IconFileDownload, IconShieldExclamation, IconDelete } from '@/app/ui/icons/icons'; +import { useTranslations, useLocale } from 'next-intl'; +import DropDownList from '@/app/components/dropDownList'; +import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; +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'; + +interface StaffMember { + id: number; + full_name: string; + email: string; + is_active: boolean; + is_super_admin: boolean; + created_at: string; + active_password: boolean; + permissions: { + subscriptions: number; + mailings: number; + complaints: number; + agreements: number; + staff: number; + content_moderation: number; + users: number; + kyc: number; + money: number; + claims: number; + api: number; + dash: number; + tariffs: number; + }; +} + +// Форматирование даты из 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([]); + const [columnFilters, setColumnFilters] = useState([]); + const [searchQuery, setSearchQuery] = useState(''); + const [pagination, setPagination] = useState({ + pageIndex: 0, + pageSize: 10, + }); + const [searchInputValue, setSearchInputValue] = useState(''); + const [openWindow, setOpenWindow] = useState(false); + 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) => { + setSearchQuery(value); + setPagination(prev => ({ ...prev, pageIndex: 0 })); + }, + 500 + ); + + const handleSearchInputChange = useCallback((e: React.ChangeEvent) => { + const value = e.target.value; + setSearchInputValue(value); + debouncedSetSearchQuery(value); + }, [debouncedSetSearchQuery]); + + const t = useTranslations("Global"); + const locale = useLocale(); + + const getSortParams = useMemo(() => { + if (sorting.length === 0) { + return { sortBy: '', sortOrder: undefined }; + } + + const sort = sorting[0]; + const sortByMap: Record = { + 'id': 'id', + 'full_name': 'full_name', + 'email': 'email', + 'is_active': 'is_active', + 'is_super_admin': 'is_super_admin', + 'created_at': 'created_at', + }; + + return { + sortBy: sortByMap[sort.id] || sort.id, + sortDirection: sort.desc ? 'desc' : 'asc' + }; + }, [sorting]); + + const { data: staffList, + isLoading, + isError, + isFetching, + error + } = useStuffList(pagination.pageIndex + 1, pagination.pageSize, getSortParams?.sortBy, getSortParams?.sortDirection, searchQuery); + + 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), + onSuccess: () => { + toast.success(tStaff('staff-removed-successfully')); + // Инвалидируем кэш списка сотрудников + queryClient.invalidateQueries({ queryKey: ['stuffList'] }); + setConfirmRemove(null); // Закрываем окно подтверждения + }, + onError: (error: Error) => { + toast.error(tStaff(error.message || 'removal-failed')); + setConfirmRemove(null); + } + }); + + // Определение колонок для сотрудников + const columns = useMemo[]>( + () => [ + { + accessorKey: 'id', + header: ({ column }) => ( +
+ ID + +
+ ), + cell: ({ row }) => ( +
+
+
+ {row.original.id} +
+
+
+ ), + enableColumnFilter: false, + }, + { + accessorKey: 'full_name', + header: ({ column }) => ( +
+ Пользователь + +
+ ), + cell: ({ row }) => { + return ( +
+ {row.original.full_name} +
+ ) + }, + }, + { + accessorKey: 'email', + header: ({ column }) => ( +
+ Email + +
+ ), + cell: ({ row }) => { + return ( +
+ {row.original.email} +
+ ) + }, + }, + { + accessorKey: 'is_active', + header: ({ column }) => ( +
+ Статус + +
+ ), + cell: ({ row }) => ( +
+ + {row.original.is_active ? 'Активен' : 'Неактивен'} + +
+ ), + }, + { + accessorKey: 'is_super_admin', + header: ({ column }) => ( +
+ Роль + +
+ ), + cell: ({ row }) => { + return ( +
+ + {row.original.is_super_admin ? 'Super Admin' : 'Staff'} + +
+ ) + }, + }, + { + accessorKey: 'created_at', + header: ({ column }) => ( +
+ Дата создания + +
+ ), + cell: ({ row }) => { + return ( +
+ {row.original.created_at ? ( + <> + {formatDate(row.original.created_at)} +
+ {formatDateTime(row.original.created_at)} + + ) : ( +
-
+ )} +
+ ) + }, + }, + { + id: 'actions', + header: () => { + return ( +
+ {t('actions')} +
+ ) + }, + cell: ({ row }) => ( +
+
+ + +
+
+ ), + enableSorting: false, + enableColumnFilter: false, + }, + ], + [t] + ); + + const handleView = (staff: StaffMember) => { + setSelectedStaff(staff); + setOpenWindowChildren(() => { + return ( + + ) + }) + setOpenWindow(true); + }; + + const handleRemove = (id: number, name: string) => { + setConfirmRemove({ id, name }); + }; + + // Фильтрация данных (если нужна клиентская фильтрация) + const filteredData = useMemo(() => { + if (!staffList) { + return []; + } + + // Если серверная фильтрация уже применена, возвращаем как есть + return staffList; + }, [staffList]); + + useEffect(() => { + const currentPageRows = table.getRowModel().rows; + const pageCount = table.getPageCount(); + + if (currentPageRows.length === 0 && pagination.pageIndex > 0 && pageCount > 0) { + table.setPageIndex(pagination.pageIndex - 1); + } + }, [filteredData, pagination.pageIndex]); + + const table = useReactTable({ + data: filteredData, + columns, + state: { + sorting, + columnFilters, + pagination + }, + manualPagination: false, + manualSorting: false, + manualFiltering: false, + pageCount: totalPages || 0, + onPaginationChange: setPagination, + onSortingChange: (updater) => { + const newSorting = typeof updater === 'function' ? updater(sorting) : updater; + setSorting(newSorting); + setPagination(prev => ({ ...prev, pageIndex: 0 })); + }, + getCoreRowModel: getCoreRowModel(), + getSortedRowModel: getSortedRowModel(), + getPaginationRowModel: getPaginationRowModel() + }); + + if (isError) { + return ( +
+
+

Ошибка загрузки данных: {error?.message}

+
+
+ ); + } + + return ( +
+
+ + {openWindowChildren} + + + setConfirmRemove(null)} + > + {confirmRemove && ( +
+

Подтверждение удаления

+

+ Вы уверены, что хотите удалить сотрудника {confirmRemove.name}? +

+
+ + +
+
+ )} +
+ + {/* Фильтры */} +
+ {/*
+
+
{t('search')}:
+ +
+
*/} + +
+
{t('items-per-page')}:
+ + {[5, 10, 20, 50, 100].map(pageSize => ( +
  • + {t('show')} {pageSize} +
  • + ))} +
    +
    +
    + + {/* Таблица */} +
    + + + {table.getHeaderGroups().map(headerGroup => ( + + {headerGroup.headers.map(header => ( + + ))} + + ))} + + + {table.getRowModel().rows.length > 0 ? ( + table.getRowModel().rows.map(row => ( + + {row.getVisibleCells().map(cell => ( + + ))} + + )) + ) : ( + + + + )} + +
    + {header.isPlaceholder + ? null + : typeof header.column.columnDef.header === 'function' + ? header.column.columnDef.header(header.getContext()) + : header.column.columnDef.header as string} +
    + {typeof cell.column.columnDef.cell === 'function' + ? cell.column.columnDef.cell(cell.getContext()) + : cell.getValue() as string} +
    + {t('no-data-for-selected-filters')} +
    +
    + + {/* Пагинация */} +
    +
    + + {t('page')}{' '} + + {(totalPages ?? 0)} {t('out-of')} {totalPages ?? 1} + + + + | {t('shown')} {table.getRowModel().rows.length} {t('out-of')} {totalItems ?? 0} + +
    + +
    + + + +
    + {(() => { + /* const totalPages = totalPages; */ + if (!totalPages || totalPages === 0) return null; + + const currentPageIndex = table.getState().pagination.pageIndex; + const maxVisiblePages = 5; + const visiblePagesCount = Math.min(maxVisiblePages, totalPages); + + let startPage = currentPageIndex - Math.floor(maxVisiblePages / 2); + startPage = Math.max(0, Math.min(startPage, totalPages - visiblePagesCount)); + + return Array.from({ length: visiblePagesCount }, (_, i) => { + const pageIndex = startPage + i; + + return ( + + ); + }); + })()} +
    + + + +
    +
    +
    +
    + ); +} + +// Компонент модального окна с информацией о сотруднике +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/i18n/messages/en.json b/src/i18n/messages/en.json index 1187e86..b6cd2f5 100644 --- a/src/i18n/messages/en.json +++ b/src/i18n/messages/en.json @@ -257,5 +257,18 @@ "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", + "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" } } \ No newline at end of file diff --git a/src/i18n/messages/ru.json b/src/i18n/messages/ru.json index 6314806..16fa6d7 100644 --- a/src/i18n/messages/ru.json +++ b/src/i18n/messages/ru.json @@ -257,5 +257,18 @@ "register-error-password-not-allowed-symbols": "Пароль содержит недопустимые символы.", "confirmation-code": "Код подтверждения", "enter-confirmation-code": "Введите код подтверждения" + }, + "Staff": { + "full-name": "Полное имя", + "enter-full-name": "Введите полное имя", + "confirm-password": "Подтвердите пароль", + "create-staff": "Создать персонал", + "staff-created-successfully": "Сотрудник успешно создан", + "full-name-too-short": "Полное имя должно состоять как минимум из 2 символов", + "full-name-too-long": "Полное имя не должно превышать 100 символов", + "passwords-do-not-match": "Пароли не совпадают", + "email-already-exists": "Пользователь с таким адресом электронной почты уже существует", + "staff-removed-successfully": "Сотрудник успешно удален", + "removal-failed": "Ошибка при удалении сотрудника" } } \ No newline at end of file