2026-05-18 16:25:22 +07:00
|
|
|
'use client'
|
|
|
|
|
|
|
|
|
|
import styles from '@/app/styles/module/login.module.scss'
|
2026-05-20 16:23:19 +07:00
|
|
|
import { useActionState, useState, useEffect } from 'react';
|
2026-05-18 16:25:22 +07:00
|
|
|
import { createStuff } from '@/app/actions/stuffActions';
|
|
|
|
|
import { useTranslations } from 'next-intl';
|
2026-05-21 16:33:01 +07:00
|
|
|
import { IconEye, IconDiscet, IconDelete } from '@/app/ui/icons/icons';
|
2026-05-18 16:25:22 +07:00
|
|
|
import { useQueryClient } from '@tanstack/react-query';
|
|
|
|
|
import { toast } from 'sonner';
|
|
|
|
|
import { z } from 'zod';
|
2026-05-21 16:33:01 +07:00
|
|
|
import { useStuffTemplates } from '@/app/hooks/react-query/stuff/useStuffTemplates';
|
|
|
|
|
import DropDownList from '@/app/components/dropDownList';
|
|
|
|
|
import { createStuffTemplates, removeStuffTemplates } from '@/app/actions/stuffActions';
|
|
|
|
|
import ModalWindow from '@/app/components/modalWindow';
|
2026-05-18 16:25:22 +07:00
|
|
|
|
|
|
|
|
const createStuffSchema = z.object({
|
|
|
|
|
fullName: z.string()
|
|
|
|
|
.min(2, 'full-name-too-short')
|
|
|
|
|
.max(100, 'full-name-too-long')
|
|
|
|
|
.trim(),
|
|
|
|
|
email: z.string()
|
2026-05-20 16:23:19 +07:00
|
|
|
.email('register-error-email-invalid')
|
2026-05-18 16:25:22 +07:00
|
|
|
.toLowerCase()
|
|
|
|
|
.trim(),
|
|
|
|
|
password: z.string()
|
2026-05-20 16:23:19 +07:00
|
|
|
.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,
|
|
|
|
|
})
|
2026-05-18 16:25:22 +07:00
|
|
|
})
|
|
|
|
|
|
2026-05-20 16:23:19 +07:00
|
|
|
export function CreateStaffForm({ onSuccess }: { onSuccess?: () => void }) {
|
2026-05-18 16:25:22 +07:00
|
|
|
const [state, formAction, isPending] = useActionState(
|
|
|
|
|
createStuffAction,
|
|
|
|
|
undefined,
|
|
|
|
|
);
|
|
|
|
|
const t = useTranslations('Login-register-form');
|
2026-05-20 16:23:19 +07:00
|
|
|
const tGeneral = useTranslations('Global');
|
|
|
|
|
const tPermissions = useTranslations('Permissions');
|
2026-05-21 16:33:01 +07:00
|
|
|
const { data: templates, refetch: refetchTemplates } = useStuffTemplates();
|
2026-05-18 16:25:22 +07:00
|
|
|
|
|
|
|
|
const [showPassword, setShowPassword] = useState(false);
|
2026-05-20 16:23:19 +07:00
|
|
|
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,
|
|
|
|
|
});
|
|
|
|
|
|
2026-05-21 16:33:01 +07:00
|
|
|
const [selectedTemplateId, setSelectedTemplateId] = useState<string>('');
|
|
|
|
|
const [isSavingTemplate, setIsSavingTemplate] = useState(false);
|
|
|
|
|
const [newTemplateName, setNewTemplateName] = useState('');
|
|
|
|
|
const [showSaveDialog, setShowSaveDialog] = useState(false);
|
|
|
|
|
const [showDeleteConfirm, setShowDeleteConfirm] = useState(false);
|
|
|
|
|
const [templateToDelete, setTemplateToDelete] = useState<number | null>(null);
|
|
|
|
|
|
2026-05-20 16:23:19 +07:00
|
|
|
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
|
|
|
|
|
}));
|
2026-05-21 16:33:01 +07:00
|
|
|
|
|
|
|
|
if (selectedTemplateId) {
|
|
|
|
|
setSelectedTemplateId('');
|
|
|
|
|
}
|
2026-05-20 16:23:19 +07:00
|
|
|
};
|
2026-05-18 16:25:22 +07:00
|
|
|
|
|
|
|
|
function toggleShowPassword() {
|
|
|
|
|
setShowPassword(!showPassword);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const queryClient = useQueryClient();
|
|
|
|
|
|
2026-05-21 16:33:01 +07:00
|
|
|
const applyTemplate = (templateId: string) => {
|
|
|
|
|
if (!templates) return;
|
|
|
|
|
|
|
|
|
|
const template = templates.find(t => t.id.toString() === templateId);
|
|
|
|
|
if (template) {
|
|
|
|
|
setPermissions(template.permissions);
|
|
|
|
|
setSelectedTemplateId(templateId);
|
|
|
|
|
toast.success(`${tGeneral('template-applied')}: ${template.name}`);
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const saveCurrentAsTemplate = async () => {
|
|
|
|
|
if (!newTemplateName.trim()) {
|
|
|
|
|
toast.error(tGeneral('template-name-required'));
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
setIsSavingTemplate(true);
|
|
|
|
|
try {
|
|
|
|
|
const result = await createStuffTemplates(newTemplateName, changePassword, permissions);
|
|
|
|
|
if (result) {
|
|
|
|
|
toast.success(`${tGeneral('template-saved-successfully')}: ${newTemplateName}`);
|
|
|
|
|
setNewTemplateName('');
|
|
|
|
|
setShowSaveDialog(false);
|
|
|
|
|
await refetchTemplates();
|
|
|
|
|
|
|
|
|
|
if (result.id) {
|
|
|
|
|
setSelectedTemplateId(result.id.toString());
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
toast.error(tGeneral('template-save-failed'));
|
|
|
|
|
}
|
|
|
|
|
} catch (error) {
|
|
|
|
|
toast.error(tGeneral('template-save-failed'));
|
|
|
|
|
} finally {
|
|
|
|
|
setIsSavingTemplate(false);
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const deleteTemplate = async () => {
|
|
|
|
|
if (!templateToDelete) return;
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
const result = await removeStuffTemplates(templateToDelete);
|
|
|
|
|
if (result) {
|
|
|
|
|
toast.success(tGeneral('template-deleted-successfully'));
|
|
|
|
|
await refetchTemplates();
|
|
|
|
|
if (selectedTemplateId === templateToDelete.toString()) {
|
|
|
|
|
setSelectedTemplateId('');
|
|
|
|
|
}
|
|
|
|
|
setShowDeleteConfirm(false);
|
|
|
|
|
setTemplateToDelete(null);
|
|
|
|
|
} else {
|
|
|
|
|
toast.error(tGeneral('template-delete-failed'));
|
|
|
|
|
}
|
|
|
|
|
} catch (error) {
|
|
|
|
|
toast.error(tGeneral('template-delete-failed'));
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
2026-05-18 16:25:22 +07:00
|
|
|
useEffect(() => {
|
|
|
|
|
if (state?.success) {
|
2026-05-20 16:23:19 +07:00
|
|
|
toast.success(tGeneral('staff-created-successfully'));
|
2026-05-18 16:25:22 +07:00
|
|
|
queryClient.invalidateQueries({ queryKey: ['stuffList'] });
|
|
|
|
|
if (onSuccess) {
|
|
|
|
|
onSuccess();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if (state?.error?.server) {
|
|
|
|
|
toast.error(t(state.error.server));
|
|
|
|
|
}
|
2026-05-20 16:23:19 +07:00
|
|
|
}, [state, queryClient, onSuccess, t, tGeneral]);
|
2026-05-18 16:25:22 +07:00
|
|
|
|
|
|
|
|
return (
|
2026-05-21 16:33:01 +07:00
|
|
|
<div className="create-staff-window">
|
2026-05-20 16:23:19 +07:00
|
|
|
<form
|
|
|
|
|
className={`${styles['']}`}
|
|
|
|
|
action={formAction}
|
|
|
|
|
autoComplete="off"
|
2026-05-18 16:25:22 +07:00
|
|
|
>
|
2026-05-20 16:23:19 +07:00
|
|
|
{/* Поле Email */}
|
|
|
|
|
<div className={`${styles['form-group']}`}>
|
|
|
|
|
<label className={`${styles['form-label']}`}>
|
|
|
|
|
{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>
|
2026-05-18 16:25:22 +07:00
|
|
|
</div>
|
2026-05-20 16:23:19 +07:00
|
|
|
{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>
|
|
|
|
|
|
2026-05-21 16:33:01 +07:00
|
|
|
<div className="templates-section">
|
|
|
|
|
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '10px' }}>
|
|
|
|
|
<h3 style={{ margin: 0 }}>{tGeneral('template-management')}</h3>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div style={{ display: 'flex', gap: '10px', alignItems: 'center' }}>
|
|
|
|
|
<div style={{ flex: 1 }}>
|
|
|
|
|
<DropDownList
|
|
|
|
|
value={selectedTemplateId}
|
|
|
|
|
callBack={applyTemplate}
|
|
|
|
|
translatedValue={
|
|
|
|
|
selectedTemplateId && templates
|
|
|
|
|
? templates.find(t => t.id.toString() === selectedTemplateId)?.name
|
|
|
|
|
: tGeneral('select-template')
|
|
|
|
|
}
|
|
|
|
|
>
|
|
|
|
|
<option value="">{tGeneral('select-template')}</option>
|
|
|
|
|
{templates?.map((template) => (
|
|
|
|
|
<option key={template.id} value={template.id.toString()}>
|
|
|
|
|
{template.name}
|
|
|
|
|
</option>
|
|
|
|
|
))}
|
|
|
|
|
</DropDownList>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
{selectedTemplateId && (
|
|
|
|
|
<button
|
|
|
|
|
type="button"
|
|
|
|
|
onClick={() => {
|
|
|
|
|
const template = templates?.find(t => t.id.toString() === selectedTemplateId);
|
|
|
|
|
if (template) {
|
|
|
|
|
setTemplateToDelete(template.id);
|
|
|
|
|
setShowDeleteConfirm(true);
|
|
|
|
|
}
|
|
|
|
|
}}
|
|
|
|
|
className="btn-icon btn-danger"
|
|
|
|
|
title={tGeneral('delete-template')}
|
|
|
|
|
disabled={isPending}
|
|
|
|
|
>
|
|
|
|
|
<IconDelete />
|
|
|
|
|
</button>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
2026-05-20 16:23:19 +07:00
|
|
|
<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>
|
2026-05-21 16:33:01 +07:00
|
|
|
{/* <p className="permission-description">{perm.description}</p> */}
|
2026-05-20 16:23:19 +07:00
|
|
|
</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>
|
2026-05-18 16:25:22 +07:00
|
|
|
)}
|
2026-05-20 16:23:19 +07:00
|
|
|
|
2026-05-22 17:04:52 +07:00
|
|
|
<div style={{ display: 'flex', gap: '8px' }}>
|
|
|
|
|
<button
|
|
|
|
|
type="button"
|
|
|
|
|
onClick={() => setShowSaveDialog(true)}
|
|
|
|
|
className={`${styles['btn']} ${styles['save']}`}
|
|
|
|
|
title={tGeneral('save-current-as-template')}
|
|
|
|
|
disabled={isPending}
|
|
|
|
|
>
|
|
|
|
|
{tGeneral('save-template')}
|
|
|
|
|
</button>
|
|
|
|
|
|
|
|
|
|
<button
|
|
|
|
|
type="submit"
|
|
|
|
|
className={`${styles['btn']}`}
|
|
|
|
|
disabled={isPending}
|
|
|
|
|
>
|
|
|
|
|
{tGeneral('create-stuff')}
|
|
|
|
|
{isPending && (
|
|
|
|
|
<div className="loading-animation">
|
|
|
|
|
<div className="global-spinner"></div>
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
</button>
|
|
|
|
|
</div>
|
2026-05-20 16:23:19 +07:00
|
|
|
</form>
|
2026-05-21 16:33:01 +07:00
|
|
|
|
|
|
|
|
<ModalWindow state={showSaveDialog} callBack={setShowSaveDialog}>
|
|
|
|
|
<div
|
|
|
|
|
className="min-w-100"
|
|
|
|
|
>
|
|
|
|
|
<h3>{tGeneral('save-template')}</h3>
|
|
|
|
|
<input
|
|
|
|
|
type="text"
|
|
|
|
|
value={newTemplateName}
|
|
|
|
|
onChange={(e) => setNewTemplateName(e.target.value)}
|
|
|
|
|
placeholder={tGeneral('enter-template-name')}
|
|
|
|
|
className="template-name-input"
|
|
|
|
|
autoFocus
|
|
|
|
|
/>
|
|
|
|
|
<div className="modal-buttons">
|
|
|
|
|
<button onClick={() => setShowSaveDialog(false)} className="btn btn-secondary">
|
|
|
|
|
{tGeneral('cancel')}
|
|
|
|
|
</button>
|
|
|
|
|
<button onClick={saveCurrentAsTemplate} disabled={isSavingTemplate} className="btn btn-primary">
|
|
|
|
|
{isSavingTemplate ? tGeneral('saving') : tGeneral('save')}
|
|
|
|
|
</button>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</ModalWindow>
|
|
|
|
|
|
|
|
|
|
<ModalWindow state={showDeleteConfirm} callBack={setShowDeleteConfirm}>
|
|
|
|
|
<h3
|
|
|
|
|
className="mb-4"
|
|
|
|
|
>
|
|
|
|
|
{tGeneral('delete-template-confirm')}
|
|
|
|
|
</h3>
|
|
|
|
|
<p
|
|
|
|
|
className="mb-4"
|
|
|
|
|
>
|
|
|
|
|
{tGeneral('delete-template-warning')}
|
|
|
|
|
</p>
|
|
|
|
|
<div className="modal-buttons">
|
|
|
|
|
<button onClick={() => setShowDeleteConfirm(false)} className="btn btn-secondary">
|
|
|
|
|
{tGeneral('cancel')}
|
|
|
|
|
</button>
|
|
|
|
|
<button onClick={deleteTemplate} className="btn btn-danger">
|
|
|
|
|
{tGeneral('delete')}
|
|
|
|
|
</button>
|
|
|
|
|
</div>
|
|
|
|
|
</ModalWindow>
|
2026-05-20 16:23:19 +07:00
|
|
|
</div>
|
2026-05-18 16:25:22 +07:00
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function createStuffAction(
|
|
|
|
|
state: {
|
|
|
|
|
previousState: {
|
|
|
|
|
fullName?: string;
|
|
|
|
|
email?: string;
|
|
|
|
|
password?: string;
|
|
|
|
|
}
|
|
|
|
|
error: Record<string, string>
|
|
|
|
|
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 || '';
|
2026-05-20 16:23:19 +07:00
|
|
|
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,
|
|
|
|
|
};
|
2026-05-18 16:25:22 +07:00
|
|
|
|
|
|
|
|
const validatedFields = createStuffSchema.safeParse({
|
|
|
|
|
fullName,
|
|
|
|
|
email,
|
2026-05-20 16:23:19 +07:00
|
|
|
password,
|
|
|
|
|
changePassword,
|
|
|
|
|
permissions
|
2026-05-18 16:25:22 +07:00
|
|
|
});
|
|
|
|
|
|
|
|
|
|
if (!validatedFields.success) {
|
|
|
|
|
const errors: Record<string, string> = {}
|
|
|
|
|
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 {
|
2026-05-20 16:23:19 +07:00
|
|
|
const { fullName, email, password, changePassword, permissions } = validatedFields.data;
|
2026-05-18 16:25:22 +07:00
|
|
|
|
2026-05-20 16:23:19 +07:00
|
|
|
const result = await createStuff(fullName, email, password, permissions, changePassword);
|
2026-05-18 16:25:22 +07:00
|
|
|
|
|
|
|
|
if (result) {
|
|
|
|
|
return {
|
|
|
|
|
previousState: {
|
|
|
|
|
fullName: '',
|
|
|
|
|
email: '',
|
|
|
|
|
password: ''
|
|
|
|
|
},
|
|
|
|
|
error: {},
|
|
|
|
|
success: true
|
|
|
|
|
};
|
|
|
|
|
} else {
|
|
|
|
|
throw new Error('creation-failed');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
const errors: Record<string, string> = {}
|
|
|
|
|
|
2026-05-20 16:23:19 +07:00
|
|
|
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';
|
2026-05-18 16:25:22 +07:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
previousState: {
|
|
|
|
|
fullName,
|
|
|
|
|
email,
|
|
|
|
|
password
|
|
|
|
|
},
|
|
|
|
|
error: errors,
|
|
|
|
|
success: false
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
}
|