add permisson for create admin block
This commit is contained in:
@@ -1,14 +1,13 @@
|
|||||||
'use client'
|
'use client'
|
||||||
|
|
||||||
import TanstakStuffTable from '@/app/ui/staff-management/tanstak-stuff-table';
|
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() {
|
export default function Page() {
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<StaffManagement />
|
<CreateStaffForm />
|
||||||
<TanstakStuffTable />
|
<TanstakStuffTable />
|
||||||
{/* <CreateStaffForm /> */}
|
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -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');
|
const token = await getSessionData('token');
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -60,7 +76,9 @@ export async function createStuff(fullName: string, email: string, password: str
|
|||||||
full_name: fullName,
|
full_name: fullName,
|
||||||
email: email,
|
email: email,
|
||||||
password: password,
|
password: password,
|
||||||
is_super_admin: false
|
is_super_admin: false,
|
||||||
|
change_password: changePassword,
|
||||||
|
permissions: permissions,
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
headers: {
|
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) {
|
export async function updateStuffPermissions(id: number, permissions: Permissions) {
|
||||||
const token = await getSessionData('token');
|
const token = await getSessionData('token');
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
import { usePathname } from 'next/navigation';
|
import { usePathname } from 'next/navigation';
|
||||||
import AdminNavLinks from '@/app/ui/navigation/admin-nav-links';
|
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';
|
import { useEffect, useState } from 'react';
|
||||||
|
|
||||||
export default function AdminLayoutWrapper({
|
export default function AdminLayoutWrapper({
|
||||||
|
|||||||
@@ -1,9 +1,14 @@
|
|||||||
import { QueryClient } from '@tanstack/react-query';
|
import { QueryClient } from '@tanstack/react-query';
|
||||||
import { fetchTokensGeneralStatistic, fetchProtectedContentGeneralStatistic, fetchTariffStatistic, fetchUserFilesTopStatistic, fetchUserGeneralStatistic, fetchViolationGeneralStatistic } from '@/app/actions/statisticActions';
|
import { fetchTokensGeneralStatistic, fetchProtectedContentGeneralStatistic, fetchTariffStatistic, fetchUserFilesTopStatistic, fetchUserGeneralStatistic, fetchViolationGeneralStatistic } from '@/app/actions/statisticActions';
|
||||||
import { useSubscribeGeneralStatistic } from '@/app/hooks/react-query/dashboard/useSubscribeGeneralStatistic';
|
import { useSubscribeGeneralStatistic } from '@/app/hooks/react-query/dashboard/useSubscribeGeneralStatistic';
|
||||||
|
import { fetchEmployeInfo } from '@/app/actions/stuffActions';
|
||||||
|
|
||||||
export async function prefetchLayoutQueries(queryClient: QueryClient) {
|
export async function prefetchLayoutQueries(queryClient: QueryClient) {
|
||||||
await Promise.all([
|
await Promise.all([
|
||||||
|
queryClient.prefetchQuery({
|
||||||
|
queryKey: ['fetchEmployeInfo'],
|
||||||
|
queryFn: () => fetchEmployeInfo()
|
||||||
|
}),
|
||||||
queryClient.prefetchQuery({
|
queryClient.prefetchQuery({
|
||||||
queryKey: ['incomeGeneralStatistic'],
|
queryKey: ['incomeGeneralStatistic'],
|
||||||
queryFn: () => fetchTokensGeneralStatistic()
|
queryFn: () => fetchTokensGeneralStatistic()
|
||||||
|
|||||||
@@ -6,6 +6,19 @@
|
|||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
background: white;
|
background: white;
|
||||||
border-radius: 12px;
|
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 {
|
.modal-header {
|
||||||
margin-bottom: 24px;
|
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 {
|
.permissions-list {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
@@ -27,17 +55,10 @@
|
|||||||
margin-bottom: 28px;
|
margin-bottom: 28px;
|
||||||
|
|
||||||
.permission-item {
|
.permission-item {
|
||||||
/* border: 1px solid #e9ecef; */
|
|
||||||
border-radius: 12px;
|
border-radius: 12px;
|
||||||
transition: all 0.2s ease;
|
transition: all 0.2s ease;
|
||||||
|
|
||||||
/* &:hover {
|
|
||||||
border-color: #dee2e6;
|
|
||||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
|
|
||||||
} */
|
|
||||||
|
|
||||||
.permission-header {
|
.permission-header {
|
||||||
/* padding: 20px; */
|
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
|
|||||||
@@ -289,4 +289,9 @@
|
|||||||
|
|
||||||
.password-wrapper {
|
.password-wrapper {
|
||||||
position: relative;
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.checkbox-label {
|
||||||
|
display: flex;
|
||||||
|
gap: 10px;
|
||||||
}
|
}
|
||||||
@@ -1,13 +1,12 @@
|
|||||||
'use client'
|
'use client'
|
||||||
|
|
||||||
import styles from '@/app/styles/module/login.module.scss'
|
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 { createStuff } from '@/app/actions/stuffActions';
|
||||||
import { useTranslations } from 'next-intl';
|
import { useTranslations } from 'next-intl';
|
||||||
import { IconEye } from '@/app/ui/icons/icons';
|
import { IconEye } from '@/app/ui/icons/icons';
|
||||||
import { useQueryClient } from '@tanstack/react-query';
|
import { useQueryClient } from '@tanstack/react-query';
|
||||||
import { toast } from 'sonner';
|
import { toast } from 'sonner';
|
||||||
|
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
|
|
||||||
const createStuffSchema = z.object({
|
const createStuffSchema = z.object({
|
||||||
@@ -16,23 +15,93 @@ const createStuffSchema = z.object({
|
|||||||
.max(100, 'full-name-too-long')
|
.max(100, 'full-name-too-long')
|
||||||
.trim(),
|
.trim(),
|
||||||
email: z.string()
|
email: z.string()
|
||||||
.email('invalid-email')
|
.email('register-error-email-invalid')
|
||||||
.toLowerCase()
|
.toLowerCase()
|
||||||
.trim(),
|
.trim(),
|
||||||
password: z.string()
|
password: z.string()
|
||||||
.min(6, 'password-too-short')
|
.min(4, 'register-error-password-min')
|
||||||
.max(50, 'password-too-long'),
|
.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(
|
const [state, formAction, isPending] = useActionState(
|
||||||
createStuffAction,
|
createStuffAction,
|
||||||
undefined,
|
undefined,
|
||||||
);
|
);
|
||||||
const t = useTranslations('Login-register-form');
|
const t = useTranslations('Login-register-form');
|
||||||
const tStaff = useTranslations('Staff');
|
const tGeneral = useTranslations('Global');
|
||||||
|
const tPermissions = useTranslations('Permissions');
|
||||||
|
|
||||||
const [showPassword, setShowPassword] = useState(false);
|
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() {
|
function toggleShowPassword() {
|
||||||
setShowPassword(!showPassword);
|
setShowPassword(!showPassword);
|
||||||
@@ -42,7 +111,7 @@ export default function CreateStaffForm({ onSuccess }: { onSuccess?: () => void
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (state?.success) {
|
if (state?.success) {
|
||||||
toast.success(tStaff('staff-created-successfully'));
|
toast.success(tGeneral('staff-created-successfully'));
|
||||||
queryClient.invalidateQueries({ queryKey: ['stuffList'] });
|
queryClient.invalidateQueries({ queryKey: ['stuffList'] });
|
||||||
if (onSuccess) {
|
if (onSuccess) {
|
||||||
onSuccess();
|
onSuccess();
|
||||||
@@ -51,109 +120,182 @@ export default function CreateStaffForm({ onSuccess }: { onSuccess?: () => void
|
|||||||
if (state?.error?.server) {
|
if (state?.error?.server) {
|
||||||
toast.error(t(state.error.server));
|
toast.error(t(state.error.server));
|
||||||
}
|
}
|
||||||
}, [state, queryClient, onSuccess, t, tStaff]);
|
}, [state, queryClient, onSuccess, t, tGeneral]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<form
|
<div
|
||||||
className={`${styles['form-wrapper']}`}
|
className="create-staff-window"
|
||||||
action={formAction}
|
|
||||||
autoComplete="off"
|
|
||||||
>
|
>
|
||||||
{/* Поле Email */}
|
<form
|
||||||
<div className={`${styles['form-group']}`}>
|
className={`${styles['']}`}
|
||||||
<label className={`${styles['form-label']}`}>
|
action={formAction}
|
||||||
{t('email-adress')}
|
autoComplete="off"
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
type="email"
|
|
||||||
id="email"
|
|
||||||
name="email"
|
|
||||||
className={`${styles['form-input']}`}
|
|
||||||
placeholder={t('enter-email')}
|
|
||||||
defaultValue={state?.previousState?.email || ''}
|
|
||||||
autoComplete="off"
|
|
||||||
onChange={(e) => {
|
|
||||||
e.target.value = e.target.value.toLowerCase();
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
{state?.error?.email && (
|
|
||||||
<p className={`${styles['form-error']}`}>
|
|
||||||
{t(state?.error?.email)}
|
|
||||||
</p>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Поле Password */}
|
|
||||||
<div className={`${styles['form-group']}`}>
|
|
||||||
<label className={`${styles['form-label']}`}>
|
|
||||||
{t('password')}
|
|
||||||
</label>
|
|
||||||
<div className={`${styles['password-wrapper']}`}>
|
|
||||||
<input
|
|
||||||
type={showPassword ? "text" : "password"}
|
|
||||||
id="password"
|
|
||||||
name="password"
|
|
||||||
className={`${styles['form-input']} ${styles['password']}`}
|
|
||||||
placeholder={t('enter-password')}
|
|
||||||
autoComplete="new-password"
|
|
||||||
/>
|
|
||||||
<button
|
|
||||||
onClick={toggleShowPassword}
|
|
||||||
type="button"
|
|
||||||
className={`show-password-button ${showPassword ? 'show' : ''}`}
|
|
||||||
>
|
|
||||||
<IconEye />
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
{state?.error?.password && (
|
|
||||||
<p className={`${styles['form-error']}`}>
|
|
||||||
{t(state?.error?.password)}
|
|
||||||
</p>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Поле Full Name */}
|
|
||||||
<div className={`${styles['form-group']}`}>
|
|
||||||
<label className={`${styles['form-label']}`}>
|
|
||||||
{tStaff('full-name')}
|
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
id="fullName"
|
|
||||||
name="fullName"
|
|
||||||
className={`${styles['form-input']}`}
|
|
||||||
placeholder={tStaff('enter-full-name')}
|
|
||||||
defaultValue={state?.previousState?.fullName || ''}
|
|
||||||
autoComplete="off"
|
|
||||||
/>
|
|
||||||
{state?.error?.fullName && (
|
|
||||||
<p className={`${styles['form-error']}`}>
|
|
||||||
{t(state?.error?.fullName)}
|
|
||||||
</p>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Общая ошибка сервера */}
|
|
||||||
{state?.error?.server && !state.success && (
|
|
||||||
<p className={`${styles['form-error']}`}>
|
|
||||||
{t(state?.error?.server)}
|
|
||||||
</p>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Кнопка отправки */}
|
|
||||||
<button
|
|
||||||
type="submit"
|
|
||||||
className={`${styles['btn']}`}
|
|
||||||
disabled={isPending}
|
|
||||||
>
|
>
|
||||||
{tStaff('create-staff')}
|
{/* Поле Email */}
|
||||||
{isPending && (
|
<div className={`${styles['form-group']}`}>
|
||||||
<div className="loading-animation">
|
<label className={`${styles['form-label']}`}>
|
||||||
<div className="global-spinner"></div>
|
{t('email-adress')}
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="email"
|
||||||
|
id="email"
|
||||||
|
name="email"
|
||||||
|
className={`${styles['form-input']}`}
|
||||||
|
placeholder={t('enter-email')}
|
||||||
|
defaultValue={state?.previousState?.email || ''}
|
||||||
|
autoComplete="off"
|
||||||
|
onChange={(e) => {
|
||||||
|
e.target.value = e.target.value.toLowerCase();
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
{state?.error?.email && (
|
||||||
|
<p className={`${styles['form-error']}`}>
|
||||||
|
{t(state?.error?.email)}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Поле Password */}
|
||||||
|
<div className={`${styles['form-group']}`}>
|
||||||
|
<label className={`${styles['form-label']}`}>
|
||||||
|
{t('password')}
|
||||||
|
</label>
|
||||||
|
<div className={`${styles['password-wrapper']}`}>
|
||||||
|
<input
|
||||||
|
type={showPassword ? "text" : "password"}
|
||||||
|
id="password"
|
||||||
|
name="password"
|
||||||
|
className={`${styles['form-input']} ${styles['password']}`}
|
||||||
|
placeholder={t('enter-password')}
|
||||||
|
autoComplete="new-password"
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
onClick={toggleShowPassword}
|
||||||
|
type="button"
|
||||||
|
className={`show-password-button ${showPassword ? 'show' : ''}`}
|
||||||
|
>
|
||||||
|
<IconEye />
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
{state?.error?.password && (
|
||||||
|
<p className={`${styles['form-error']}`}>
|
||||||
|
{t(state?.error?.password)}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Поле Full Name */}
|
||||||
|
<div className={`${styles['form-group']}`}>
|
||||||
|
<label className={`${styles['form-label']}`}>
|
||||||
|
{tGeneral('full-name')}
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
id="fullName"
|
||||||
|
name="fullName"
|
||||||
|
className={`${styles['form-input']}`}
|
||||||
|
placeholder={tGeneral('enter-full-name')}
|
||||||
|
defaultValue={state?.previousState?.fullName || ''}
|
||||||
|
autoComplete="off"
|
||||||
|
/>
|
||||||
|
{state?.error?.fullName && (
|
||||||
|
<p className={`${styles['form-error']}`}>
|
||||||
|
{t(state?.error?.fullName)}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Чекбокс */}
|
||||||
|
<div className={`${styles['form-group']}`}>
|
||||||
|
<label className={`${styles['form-label']} ${styles['checkbox-label']}`}>
|
||||||
|
<span>{tGeneral('require-password-change')}</span>
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
name="changePassword"
|
||||||
|
checked={changePassword}
|
||||||
|
onChange={(e) => setChangePassword(e.target.checked)}
|
||||||
|
disabled={isPending}
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="permissions-section">
|
||||||
|
<h3 className="permissions-section-title">
|
||||||
|
{tGeneral('permissions')}
|
||||||
|
</h3>
|
||||||
|
|
||||||
|
<div className="permissions-list">
|
||||||
|
{permissionsConfig.map((perm) => {
|
||||||
|
const currentValue = permissions[perm.key as keyof typeof permissions];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div key={perm.key} className="permission-item">
|
||||||
|
<div className="permission-header">
|
||||||
|
<div className="permission-info">
|
||||||
|
<h4 className="permission-title">{perm.label}</h4>
|
||||||
|
<p className="permission-description">{perm.description}</p>
|
||||||
|
</div>
|
||||||
|
<div className="permission-controls">
|
||||||
|
<div className="permission-level-buttons">
|
||||||
|
<input type="hidden" name={`permissions.${perm.key}`} value={currentValue} />
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => handlePermissionChange(perm.key as keyof typeof permissions, 0)}
|
||||||
|
className={`permission-level-btn ${currentValue === 0 ? 'active no-access' : ''}`}
|
||||||
|
title={tPermissions('no-access')}
|
||||||
|
disabled={isPending}
|
||||||
|
>
|
||||||
|
<span className="btn-text">{tPermissions('no-access')}</span>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => handlePermissionChange(perm.key as keyof typeof permissions, 1)}
|
||||||
|
className={`permission-level-btn ${currentValue === 1 ? 'active read-only' : ''}`}
|
||||||
|
title={tPermissions('read-only')}
|
||||||
|
disabled={isPending}
|
||||||
|
>
|
||||||
|
<span className="btn-text">{tPermissions('read-only')}</span>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => handlePermissionChange(perm.key as keyof typeof permissions, 3)}
|
||||||
|
className={`permission-level-btn ${currentValue === 3 ? 'active full-access' : ''}`}
|
||||||
|
title={tPermissions('full-access')}
|
||||||
|
disabled={isPending}
|
||||||
|
>
|
||||||
|
<span className="btn-text">{tPermissions('full-access')}</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{state?.error?.server && !state.success && (
|
||||||
|
<p className={`${styles['form-error']}`}>
|
||||||
|
{t(state?.error?.server)}
|
||||||
|
</p>
|
||||||
)}
|
)}
|
||||||
</button>
|
|
||||||
</form>
|
<button
|
||||||
|
type="submit"
|
||||||
|
className={`${styles['btn']}`}
|
||||||
|
disabled={isPending}
|
||||||
|
>
|
||||||
|
{tGeneral('create')}
|
||||||
|
{isPending && (
|
||||||
|
<div className="loading-animation">
|
||||||
|
<div className="global-spinner"></div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -172,11 +314,30 @@ async function createStuffAction(
|
|||||||
const fullName = formData.get('fullName') as string || '';
|
const fullName = formData.get('fullName') as string || '';
|
||||||
const email = formData.get('email') as string || '';
|
const email = formData.get('email') as string || '';
|
||||||
const password = formData.get('password') 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({
|
const validatedFields = createStuffSchema.safeParse({
|
||||||
fullName,
|
fullName,
|
||||||
email,
|
email,
|
||||||
password
|
password,
|
||||||
|
changePassword,
|
||||||
|
permissions
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!validatedFields.success) {
|
if (!validatedFields.success) {
|
||||||
@@ -205,10 +366,9 @@ async function createStuffAction(
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const { fullName, email, password } = validatedFields.data;
|
const { fullName, email, password, changePassword, permissions } = validatedFields.data;
|
||||||
|
|
||||||
// Вызов API для создания сотрудника
|
const result = await createStuff(fullName, email, password, permissions, changePassword);
|
||||||
const result = await createStuff(fullName, email, password);
|
|
||||||
|
|
||||||
if (result) {
|
if (result) {
|
||||||
return {
|
return {
|
||||||
@@ -227,16 +387,12 @@ async function createStuffAction(
|
|||||||
} catch (error) {
|
} catch (error) {
|
||||||
const errors: Record<string, string> = {}
|
const errors: Record<string, string> = {}
|
||||||
|
|
||||||
switch (error) {
|
if (error === 'email-already-exists') {
|
||||||
case 'email-already-exists':
|
errors['email'] = 'email-already-exists';
|
||||||
errors['email'] = 'email-already-exists';
|
} else if (error === 'invalid-email') {
|
||||||
break;
|
errors['email'] = 'invalid-email';
|
||||||
case 'invalid-email':
|
} else {
|
||||||
errors['email'] = 'invalid-email';
|
errors['server'] = 'request-ended-with-an-error';
|
||||||
break;
|
|
||||||
default:
|
|
||||||
errors['server'] = 'request-ended-with-an-error';
|
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
import LanguageSwitcher from '@/app/components/LanguageSwitcher';
|
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() {
|
export default function AdminHeaderPanel() {
|
||||||
return (
|
return (
|
||||||
@@ -15,15 +16,7 @@ export default function AdminHeaderPanel() {
|
|||||||
</div>
|
</div>
|
||||||
<div className="admin-header-right">
|
<div className="admin-header-right">
|
||||||
{/* <LanguageSwitcher /> */}
|
{/* <LanguageSwitcher /> */}
|
||||||
<div className="admin-user-info">
|
<HeaderUserInfoBadge />
|
||||||
<div className="admin-user-avatar">
|
|
||||||
+
|
|
||||||
</div>
|
|
||||||
<div className="admin-user-details">
|
|
||||||
<span className="name">name name name</span>
|
|
||||||
<span className="role">role</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<Link
|
<Link
|
||||||
href={'/pages/dashboard'}
|
href={'/pages/dashboard'}
|
||||||
className="btn btn-primary"
|
className="btn btn-primary"
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
'use client'
|
||||||
|
|
||||||
|
import {useEmployeInfo} from '@/app/hooks/react-query/useEmployeInfo';
|
||||||
|
|
||||||
|
export function HeaderUserInfoBadge() {
|
||||||
|
const { data } = useEmployeInfo();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="admin-user-info">
|
||||||
|
{/* <div className="admin-user-avatar">
|
||||||
|
+
|
||||||
|
</div> */}
|
||||||
|
<div className="admin-user-details">
|
||||||
|
<span className="name">{data?.full_name ? data?.full_name : '---'}</span>
|
||||||
|
<span className="role">{data?.is_super_admin ? 'super-admin' : 'stuff'}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -1,6 +1,8 @@
|
|||||||
|
/* for remove */
|
||||||
|
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import ModalWindow from '@/app/components/modalWindow';
|
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() {
|
export default function StaffManagement() {
|
||||||
const [isModalOpen, setIsModalOpen] = useState(false);
|
const [isModalOpen, setIsModalOpen] = useState(false);
|
||||||
@@ -9,7 +11,8 @@ export default function StaffManagement() {
|
|||||||
<div
|
<div
|
||||||
className="admin-content mb-4"
|
className="admin-content mb-4"
|
||||||
>
|
>
|
||||||
<div
|
<CreateStaffForm />
|
||||||
|
{/* <div
|
||||||
className="p-4"
|
className="p-4"
|
||||||
>
|
>
|
||||||
<button
|
<button
|
||||||
@@ -22,10 +25,10 @@ export default function StaffManagement() {
|
|||||||
<ModalWindow state={isModalOpen} callBack={setIsModalOpen}>
|
<ModalWindow state={isModalOpen} callBack={setIsModalOpen}>
|
||||||
<div className="p-6 min-w-[400px]">
|
<div className="p-6 min-w-[400px]">
|
||||||
<h2 className="text-2xl font-bold mb-4">Создание сотрудника</h2>
|
<h2 className="text-2xl font-bold mb-4">Создание сотрудника</h2>
|
||||||
<CreateStaffForm onSuccess={() => setIsModalOpen(false)} />
|
<CreateStaffForm />
|
||||||
</div>
|
</div>
|
||||||
</ModalWindow>
|
</ModalWindow>
|
||||||
</div>
|
</div> */}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -63,7 +63,6 @@ export default function TanstakStuffTable() {
|
|||||||
const [openWindowChildren, setOpenWindowChildren] = useState<ReactNode>(null);
|
const [openWindowChildren, setOpenWindowChildren] = useState<ReactNode>(null);
|
||||||
const [selectedStaff, setSelectedStaff] = useState<StaffMember | null>(null);
|
const [selectedStaff, setSelectedStaff] = useState<StaffMember | null>(null);
|
||||||
const [confirmRemove, setConfirmRemove] = useState<{ id: number; name: string } | null>(null);
|
const [confirmRemove, setConfirmRemove] = useState<{ id: number; name: string } | null>(null);
|
||||||
const tStaff = useTranslations('Staff');
|
|
||||||
|
|
||||||
const debouncedSetSearchQuery = useDebouncedCallback(
|
const debouncedSetSearchQuery = useDebouncedCallback(
|
||||||
(value: string) => {
|
(value: string) => {
|
||||||
@@ -117,12 +116,12 @@ export default function TanstakStuffTable() {
|
|||||||
const removeMutation = useMutation({
|
const removeMutation = useMutation({
|
||||||
mutationFn: (id: number) => removeStuff(id),
|
mutationFn: (id: number) => removeStuff(id),
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
toast.success(tStaff('staff-removed-successfully'));
|
toast.success(t('staff-removed-successfully'));
|
||||||
queryClient.invalidateQueries({ queryKey: ['stuffList'] });
|
queryClient.invalidateQueries({ queryKey: ['stuffList'] });
|
||||||
setConfirmRemove(null);
|
setConfirmRemove(null);
|
||||||
},
|
},
|
||||||
onError: (error: Error) => {
|
onError: (error: Error) => {
|
||||||
toast.error(tStaff(error.message || 'removal-failed'));
|
toast.error(t(error.message || 'removal-failed'));
|
||||||
setConfirmRemove(null);
|
setConfirmRemove(null);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -131,13 +130,13 @@ export default function TanstakStuffTable() {
|
|||||||
mutationFn: ({ id, permissions }: { id: number; permissions: StaffMember['permissions'] }) =>
|
mutationFn: ({ id, permissions }: { id: number; permissions: StaffMember['permissions'] }) =>
|
||||||
updateStuffPermissions(id, permissions),
|
updateStuffPermissions(id, permissions),
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
toast.success(tStaff('permissions-updated-successfully'));
|
toast.success(t('permissions-updated-successfully'));
|
||||||
queryClient.invalidateQueries({ queryKey: ['stuffList'] });
|
queryClient.invalidateQueries({ queryKey: ['stuffList'] });
|
||||||
queryClient.invalidateQueries({ queryKey: ['employeInfo'] });
|
queryClient.invalidateQueries({ queryKey: ['employeInfo'] });
|
||||||
setOpenWindow(false);
|
setOpenWindow(false);
|
||||||
},
|
},
|
||||||
onError: (error: Error) => {
|
onError: (error: Error) => {
|
||||||
toast.error(tStaff(error.message || 'permissions-update-failed'));
|
toast.error(t(error.message || 'permissions-update-failed'));
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
+18
-17
@@ -190,7 +190,21 @@
|
|||||||
"diskUsagePercent": "Usage",
|
"diskUsagePercent": "Usage",
|
||||||
"fileCount": "Files",
|
"fileCount": "Files",
|
||||||
"employee": "Employee",
|
"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": {
|
"Login-register-form": {
|
||||||
"and": "and",
|
"and": "and",
|
||||||
@@ -227,7 +241,7 @@
|
|||||||
"register-error-name-max": "The name length must not exceed 100 characters.",
|
"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-digit": "Password must contain at least 1 digit.",
|
||||||
"register-error-password-one-letter": "Password must contain at least 1 letter.",
|
"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-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-min": "The company name must contain at least 2 characters.",
|
||||||
"register-error-company-name-max": "The company name must exceed 200 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-phone-not-allowed-symbols": "",
|
||||||
"register-error-password-not-allowed-symbols": "Password contains invalid characters.",
|
"register-error-password-not-allowed-symbols": "Password contains invalid characters.",
|
||||||
"confirmation-code": "Confirmation code",
|
"confirmation-code": "Confirmation code",
|
||||||
"enter-confirmation-code": "Enter the 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-short": "Full name must be at least 2 characters",
|
||||||
"full-name-too-long": "Full name must not exceed 100 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"
|
|
||||||
},
|
},
|
||||||
"Permissions": {
|
"Permissions": {
|
||||||
"edit-permissions": "Edit Permissions",
|
"edit-permissions": "Edit Permissions",
|
||||||
|
|||||||
+18
-17
@@ -190,7 +190,21 @@
|
|||||||
"diskUsagePercent": "Заполнение",
|
"diskUsagePercent": "Заполнение",
|
||||||
"fileCount": "Файлы",
|
"fileCount": "Файлы",
|
||||||
"employee": "Сотрудник",
|
"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": {
|
"Login-register-form": {
|
||||||
"and": "и",
|
"and": "и",
|
||||||
@@ -227,7 +241,7 @@
|
|||||||
"register-error-name-max": "Длина имени не должна превышать 100 символов.",
|
"register-error-name-max": "Длина имени не должна превышать 100 символов.",
|
||||||
"register-error-password-one-digit": "Пароль должен содержать как минимум 1 цифру.",
|
"register-error-password-one-digit": "Пароль должен содержать как минимум 1 цифру.",
|
||||||
"register-error-password-one-letter": "Пароль должен содержать как минимум 1 букву.",
|
"register-error-password-one-letter": "Пароль должен содержать как минимум 1 букву.",
|
||||||
"register-error-password-min": "Длина пароля должна быть не менее 8 символов.",
|
"register-error-password-min": "Длина пароля должна быть не менее 4 символов.",
|
||||||
"register-error-phone-min": "Пожалуйста, введите телефонный номер.",
|
"register-error-phone-min": "Пожалуйста, введите телефонный номер.",
|
||||||
"register-error-company-name-min": "Название компании должно содержать не менее 2 символов.",
|
"register-error-company-name-min": "Название компании должно содержать не менее 2 символов.",
|
||||||
"register-error-company-name-max": "Название компании должно превышать 200 символов.",
|
"register-error-company-name-max": "Название компании должно превышать 200 символов.",
|
||||||
@@ -258,22 +272,9 @@
|
|||||||
"register-error-phone-not-allowed-symbols": "",
|
"register-error-phone-not-allowed-symbols": "",
|
||||||
"register-error-password-not-allowed-symbols": "Пароль содержит недопустимые символы.",
|
"register-error-password-not-allowed-symbols": "Пароль содержит недопустимые символы.",
|
||||||
"confirmation-code": "Код подтверждения",
|
"confirmation-code": "Код подтверждения",
|
||||||
"enter-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-short": "Полное имя должно состоять как минимум из 2 символов",
|
||||||
"full-name-too-long": "Полное имя не должно превышать 100 символов",
|
"full-name-too-long": "Полное имя не должно превышать 100 символов"
|
||||||
"passwords-do-not-match": "Пароли не совпадают",
|
|
||||||
"email-already-exists": "Пользователь с таким адресом электронной почты уже существует",
|
|
||||||
"staff-removed-successfully": "Сотрудник успешно удален",
|
|
||||||
"removal-failed": "Ошибка при удалении сотрудника",
|
|
||||||
"permissions-updated-successfully": "Права доступа успешно обновлены",
|
|
||||||
"permissions-update-failed": "Не удалось обновить права доступа"
|
|
||||||
},
|
},
|
||||||
"Permissions": {
|
"Permissions": {
|
||||||
"edit-permissions": "Редактирование прав доступа",
|
"edit-permissions": "Редактирование прав доступа",
|
||||||
|
|||||||
Reference in New Issue
Block a user