add template managment

This commit is contained in:
smanylov
2026-05-21 16:33:01 +07:00
parent 921f3c0104
commit 76009e4abb
9 changed files with 413 additions and 10 deletions
+180 -5
View File
@@ -4,10 +4,14 @@ import styles from '@/app/styles/module/login.module.scss'
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 { IconEye, IconDiscet, IconDelete } from '@/app/ui/icons/icons';
import { useQueryClient } from '@tanstack/react-query';
import { toast } from 'sonner';
import { z } from 'zod';
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';
const createStuffSchema = z.object({
fullName: z.string()
@@ -61,6 +65,7 @@ export function CreateStaffForm({ onSuccess }: { onSuccess?: () => void }) {
const t = useTranslations('Login-register-form');
const tGeneral = useTranslations('Global');
const tPermissions = useTranslations('Permissions');
const { data: templates, refetch: refetchTemplates } = useStuffTemplates();
const [showPassword, setShowPassword] = useState(false);
const [changePassword, setChangePassword] = useState(true);
@@ -80,6 +85,13 @@ export function CreateStaffForm({ onSuccess }: { onSuccess?: () => void }) {
tariffs: 0,
});
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);
const permissionsConfig = [
{ key: 'subscriptions', label: tPermissions('subscriptions'), description: tPermissions('subscriptions-desc') },
{ key: 'mailings', label: tPermissions('mailings'), description: tPermissions('mailings-desc') },
@@ -101,6 +113,10 @@ export function CreateStaffForm({ onSuccess }: { onSuccess?: () => void }) {
...prev,
[key]: value
}));
if (selectedTemplateId) {
setSelectedTemplateId('');
}
};
function toggleShowPassword() {
@@ -109,6 +125,66 @@ export function CreateStaffForm({ onSuccess }: { onSuccess?: () => void }) {
const queryClient = useQueryClient();
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'));
}
};
useEffect(() => {
if (state?.success) {
toast.success(tGeneral('staff-created-successfully'));
@@ -123,9 +199,7 @@ export function CreateStaffForm({ onSuccess }: { onSuccess?: () => void }) {
}, [state, queryClient, onSuccess, t, tGeneral]);
return (
<div
className="create-staff-window"
>
<div className="create-staff-window">
<form
className={`${styles['']}`}
action={formAction}
@@ -224,6 +298,62 @@ export function CreateStaffForm({ onSuccess }: { onSuccess?: () => void }) {
{tGeneral('permissions')}
</h3>
<div className="templates-section">
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '10px' }}>
<h3 style={{ margin: 0 }}>{tGeneral('template-management')}</h3>
<div style={{ display: 'flex', gap: '8px' }}>
<button
type="button"
onClick={() => setShowSaveDialog(true)}
className="btn-icon"
title={tGeneral('save-current-as-template')}
disabled={isPending}
>
<IconDiscet />
</button>
</div>
</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>
<div className="permissions-list">
{permissionsConfig.map((perm) => {
const currentValue = permissions[perm.key as keyof typeof permissions];
@@ -233,7 +363,7 @@ export function CreateStaffForm({ onSuccess }: { onSuccess?: () => void }) {
<div className="permission-header">
<div className="permission-info">
<h4 className="permission-title">{perm.label}</h4>
<p className="permission-description">{perm.description}</p>
{/* <p className="permission-description">{perm.description}</p> */}
</div>
<div className="permission-controls">
<div className="permission-level-buttons">
@@ -295,6 +425,51 @@ export function CreateStaffForm({ onSuccess }: { onSuccess?: () => void }) {
)}
</button>
</form>
<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>
</div>
)
}
+8 -2
View File
@@ -1,7 +1,5 @@
/* https://icon-sets.iconify.design/ic/ */
import { CLIENT_STATIC_FILES_RUNTIME_WEBPACK } from 'next/dist/shared/lib/constants'
export function IconImageFile() {
return (
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" className="icon">
@@ -143,4 +141,12 @@ export function IconDelete() {
return (
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" className="icon"><path fill="currentColor" d="M6 19c0 1.1.9 2 2 2h8c1.1 0 2-.9 2-2V7H6zm2.46-7.12l1.41-1.41L12 12.59l2.12-2.12l1.41 1.41L13.41 14l2.12 2.12l-1.41 1.41L12 15.41l-2.12 2.12l-1.41-1.41L10.59 14zM15.5 4l-1-1h-5l-1 1H5v2h14V4z" /></svg>
)
}
export function IconDiscet() {
return (
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" className="icon">
<g fill="none" stroke="currentColor" strokeWidth="1.5"><path d="M3 19V5a2 2 0 0 1 2-2h11.172a2 2 0 0 1 1.414.586l2.828 2.828A2 2 0 0 1 21 7.828V19a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2Z" /><path d="M8.6 9h6.8a.6.6 0 0 0 .6-.6V3.6a.6.6 0 0 0-.6-.6H8.6a.6.6 0 0 0-.6.6v4.8a.6.6 0 0 0 .6.6ZM6 13.6V21h12v-7.4a.6.6 0 0 0-.6-.6H6.6a.6.6 0 0 0-.6.6Z" /></g>
</svg>
)
}