diff --git a/package.json b/package.json index bd46beb..4209589 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "no-copy-admin-panel-frontend", - "version": "0.10.0", + "version": "0.11.0", "private": true, "scripts": { "dev": "next dev -p 2996", diff --git a/src/app/actions/stuffActions.ts b/src/app/actions/stuffActions.ts index 2963991..2cc7f99 100644 --- a/src/app/actions/stuffActions.ts +++ b/src/app/actions/stuffActions.ts @@ -208,6 +208,118 @@ export async function fetchEmployeInfo() { } }); + 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) { + throw error; + } +} + +export async function fetchStuffTemplates() { + const token = await getSessionData('token'); + + try { + const response = await fetch(`${API_BASE_URL}/api/admin`, { + method: 'POST', + body: JSON.stringify({ + version: 1, + msg_id: 200, + token: token, + message_body: { + action: 'get_all' + } + }), + 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) { + throw error; + } +} + +export async function createStuffTemplates(name: string, changePassword: boolean, permissions: Permissions) { + const token = await getSessionData('token'); + + try { + const response = await fetch(`${API_BASE_URL}/api/admin`, { + method: 'POST', + body: JSON.stringify({ + version: 1, + msg_id: 200, + token: token, + message_body: { + action: 'create', + name: name, + change_password: changePassword, + permissions: permissions + } + }), + 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) { + throw error; + } +} + +export async function removeStuffTemplates(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: 200, + token: token, + message_body: { + action: 'delete', + id: id + } + }), + headers: { + 'Content-Type': 'application/json', + 'Accept': 'application/json' + } + }); + if (response.ok) { const parsed = await response.json(); diff --git a/src/app/components/dropDownList.tsx b/src/app/components/dropDownList.tsx index d242d6b..75c673b 100644 --- a/src/app/components/dropDownList.tsx +++ b/src/app/components/dropDownList.tsx @@ -45,6 +45,7 @@ export default function DropDownList({ children, value, callBack, translatedValu setIsOpen(!isOpen) }} className="dropdown-button" + type="button" > diff --git a/src/app/hooks/react-query/stuff/useStuffTemplates.ts b/src/app/hooks/react-query/stuff/useStuffTemplates.ts new file mode 100644 index 0000000..8a87a33 --- /dev/null +++ b/src/app/hooks/react-query/stuff/useStuffTemplates.ts @@ -0,0 +1,39 @@ +import { useQuery } from '@tanstack/react-query'; +import { fetchStuffTemplates } from '@/app/actions/stuffActions'; + +interface StuffTemplates { + id: number; + change_password: boolean; + name: string; + 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 useStuffTemplates = () => { + return useQuery({ + queryKey: ['stuffTemplates'], + queryFn: () => { + return fetchStuffTemplates() + }, + select: (data: StuffTemplates[] | null) => { + if (!data) { + return null + } + return data; + }, + retry: false + }); +}; \ No newline at end of file diff --git a/src/app/styles/edit-permissions-modal.scss b/src/app/styles/edit-permissions-modal.scss index 417a315..96b1034 100644 --- a/src/app/styles/edit-permissions-modal.scss +++ b/src/app/styles/edit-permissions-modal.scss @@ -15,6 +15,35 @@ overflow: hidden; margin-bottom: 24px; padding: 1.5rem; + + .template-name-input { + width: 100%; + padding: 8px 12px; + border: 1px solid #ddd; + border-radius: 6px; + margin: 16px 0; + } + + .modal-buttons { + display: flex; + gap: 12px; + justify-content: flex-end; + } + + .btn-icon { + padding: 8px; + border: none; + background: transparent; + cursor: pointer; + border-radius: 6px; + display: inline-flex; + align-items: center; + justify-content: center; + } + + .btn-icon:hover:not(:disabled) { + background: rgba(0, 0, 0, 0.05); + } } .edit-permissions-modal, @@ -39,6 +68,13 @@ padding-top: 20px; } + .templates-section { + margin-bottom: 20px; + padding: 15px; + background: #f5f5f5; + border-radius: 8px; + } + .checkbox-label { display: flex; gap: 10px; diff --git a/src/app/ui/forms/create-staff-form.tsx b/src/app/ui/forms/create-staff-form.tsx index f3db7fd..7d7bff9 100644 --- a/src/app/ui/forms/create-staff-form.tsx +++ b/src/app/ui/forms/create-staff-form.tsx @@ -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(''); + const [isSavingTemplate, setIsSavingTemplate] = useState(false); + const [newTemplateName, setNewTemplateName] = useState(''); + const [showSaveDialog, setShowSaveDialog] = useState(false); + const [showDeleteConfirm, setShowDeleteConfirm] = useState(false); + const [templateToDelete, setTemplateToDelete] = useState(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 ( -
+
void }) { {tGeneral('permissions')} +
+
+

{tGeneral('template-management')}

+
+ +
+
+ +
+
+ t.id.toString() === selectedTemplateId)?.name + : tGeneral('select-template') + } + > + + {templates?.map((template) => ( + + ))} + +
+ + {selectedTemplateId && ( + + )} +
+
+
{permissionsConfig.map((perm) => { const currentValue = permissions[perm.key as keyof typeof permissions]; @@ -233,7 +363,7 @@ export function CreateStaffForm({ onSuccess }: { onSuccess?: () => void }) {

{perm.label}

-

{perm.description}

+ {/*

{perm.description}

*/}
@@ -295,6 +425,51 @@ export function CreateStaffForm({ onSuccess }: { onSuccess?: () => void }) { )} + + +
+

{tGeneral('save-template')}

+ setNewTemplateName(e.target.value)} + placeholder={tGeneral('enter-template-name')} + className="template-name-input" + autoFocus + /> +
+ + +
+
+
+ + +

+ {tGeneral('delete-template-confirm')} +

+

+ {tGeneral('delete-template-warning')} +

+
+ + +
+
) } diff --git a/src/app/ui/icons/icons.tsx b/src/app/ui/icons/icons.tsx index 6f3e370..4e2c13a 100644 --- a/src/app/ui/icons/icons.tsx +++ b/src/app/ui/icons/icons.tsx @@ -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 ( @@ -143,4 +141,12 @@ export function IconDelete() { return ( ) +} + +export function IconDiscet() { + return ( + + + + ) } \ No newline at end of file diff --git a/src/i18n/messages/en.json b/src/i18n/messages/en.json index fb6dac4..d21e114 100644 --- a/src/i18n/messages/en.json +++ b/src/i18n/messages/en.json @@ -204,7 +204,24 @@ "require-password-change": "Require password change on first login", "permissions": "Permissions", "creating": "Creation...", - "create": "Create" + "create": "Create", + "template-management": "Template management", + "save-current-as-template": "Save current permissions as template", + "select-template": "Select template", + "delete-template": "Delete template", + "template-applied": "Template applied", + "template-saved-successfully": "Template saved successfully", + "template-save-failed": "Failed to save template", + "template-deleted-successfully": "Template deleted successfully", + "template-delete-failed": "Failed to delete template", + "template-name-required": "Template name is required", + "save-template": "Save template", + "enter-template-name": "Enter template name", + "saving": "Saving...", + "delete-template-confirm": "Delete template", + "delete-template-warning": "Are you sure you want to delete this template?", + "save": "Save", + "delete": "Delete" }, "Login-register-form": { "and": "and", diff --git a/src/i18n/messages/ru.json b/src/i18n/messages/ru.json index 12eabbf..ac335fa 100644 --- a/src/i18n/messages/ru.json +++ b/src/i18n/messages/ru.json @@ -204,7 +204,24 @@ "require-password-change": "Требовать смену пароля при первом входе", "permissions": "Доступы", "creating": "Создание...", - "create": "Создать" + "create": "Создать", + "template-management": "Управление шаблонами", + "save-current-as-template": "Сохранить текущие разрешения как шаблон", + "select-template": "Выбрать шаблон", + "delete-template": "Удалить шаблон", + "template-applied": "Шаблон применен", + "template-saved-successfully": "Шаблон успешно сохранен", + "template-save-failed": "Не удалось сохранить шаблон", + "template-deleted-successfully": "Шаблон успешно удален", + "template-delete-failed": "Не удалось удалить шаблон", + "template-name-required": "Введите название шаблона", + "save-template": "Сохранить шаблон", + "enter-template-name": "Введите название шаблона", + "saving": "Сохранение...", + "delete-template-confirm": "Удалить шаблон", + "delete-template-warning": "Вы уверены, что хотите удалить шаблон?", + "save": "Сохранить", + "delete": "Удалить" }, "Login-register-form": { "and": "и",