From 4e0f1c7889c2c8a533f95c0cc6500bb4322035c3 Mon Sep 17 00:00:00 2001 From: lizerk Date: Tue, 14 Apr 2026 01:57:25 +0300 Subject: [PATCH] fix(frontend): localize department member validation --- frontend/src/app/departments/[id]/page.tsx | 9 ++++ .../departments/member-modal-state.js | 28 ++++++++++++ .../departments/member-modal-state.test.js | 22 ++++++++++ frontend/src/hooks/useDepartmentPage.ts | 43 +++++++++++++++---- 4 files changed, 93 insertions(+), 9 deletions(-) diff --git a/frontend/src/app/departments/[id]/page.tsx b/frontend/src/app/departments/[id]/page.tsx index 47511674..e041d42f 100644 --- a/frontend/src/app/departments/[id]/page.tsx +++ b/frontend/src/app/departments/[id]/page.tsx @@ -406,6 +406,7 @@ function AddMemberModal({ isOpen, items, loading, + memberError, mode, optionsLoading, onClose, @@ -423,6 +424,7 @@ function AddMemberModal({ isOpen: boolean; items: User[]; loading: boolean; + memberError: string | null; mode: DepartmentMemberModalMode; optionsLoading: boolean; onClose: () => void; @@ -592,6 +594,12 @@ function AddMemberModal({ ) : null} + {!directoryError && memberError ? ( +
+

{memberError}

+
+ ) : null} + {canAssignRoles && !directoryError ? (
{ /любому активному сотруднику/i, ); }); + +test("member modal maps backend department validation to localized inline error", () => { + assert.equal( + getDepartmentMemberModalError( + "Employee already belongs to another active department: Бухгалтерия.", + "add", + ), + "Сотрудник уже состоит в другом активном отделе: Бухгалтерия.", + ); + assert.equal( + getDepartmentMemberModalError("Employee is inactive.", "add"), + "Можно выбрать только активного сотрудника.", + ); + assert.equal( + getDepartmentMemberModalError( + "Employee already belongs to another active department: Бухгалтерия.", + "assignRole", + ), + null, + ); +}); diff --git a/frontend/src/hooks/useDepartmentPage.ts b/frontend/src/hooks/useDepartmentPage.ts index 48953861..e1ab6b64 100644 --- a/frontend/src/hooks/useDepartmentPage.ts +++ b/frontend/src/hooks/useDepartmentPage.ts @@ -3,6 +3,7 @@ import { useCallback, useEffect, useMemo, useState } from "react"; import { toast } from "sonner"; +import { getDepartmentMemberModalError } from "@/components/departments/member-modal-state"; import { useUser } from "@/contexts/UserContext"; import { apiClient } from "@/lib/api"; import { loadAllPages } from "@/lib/shared"; @@ -48,6 +49,7 @@ export type DepartmentPageController = { employeesDirectoryLoading: boolean; loading: boolean; memberModalMode: DepartmentMemberModalMode; + memberModalError: string | null; members: DepartmentMemberLink[]; pendingKey: SavingKey | null; roleDraft: DepartmentRoleDraft; @@ -148,6 +150,7 @@ export function useDepartmentPage(departmentId: number): DepartmentPageControlle const [addMemberOpen, setAddMemberOpen] = useState(false); const [memberModalMode, setMemberModalMode] = useState("add"); + const [memberModalError, setMemberModalError] = useState(null); const [selectedMemberId, setSelectedMemberId] = useState(null); const [selectedRoleId, setSelectedRoleId] = useState(null); @@ -345,6 +348,7 @@ export function useDepartmentPage(departmentId: number): DepartmentPageControlle const openAddMember = useCallback(() => { setMemberModalMode("add"); + setMemberModalError(null); setSelectedMemberId(null); setSelectedRoleId(null); setAddMemberOpen(true); @@ -357,6 +361,7 @@ export function useDepartmentPage(departmentId: number): DepartmentPageControlle const openAssignRoleModal = useCallback(() => { setMemberModalMode("assignRole"); + setMemberModalError(null); setSelectedMemberId(null); setSelectedRoleId(null); setAddMemberOpen(true); @@ -368,12 +373,23 @@ export function useDepartmentPage(departmentId: number): DepartmentPageControlle }, [allEmployees.length, loadEmployeesDirectory]); const closeAddMember = useCallback(() => { + setMemberModalError(null); setSelectedMemberId(null); setSelectedRoleId(null); setMemberModalMode("add"); setAddMemberOpen(false); }, []); + const selectMember = useCallback((id: number | null) => { + setMemberModalError(null); + setSelectedMemberId(id); + }, []); + + const selectRole = useCallback((id: number | null) => { + setMemberModalError(null); + setSelectedRoleId(id); + }, []); + const submitAddMember = useCallback(async () => { if (!department || !selectedMemberId) { toast.error("Выберите сотрудника"); @@ -403,6 +419,7 @@ export function useDepartmentPage(departmentId: number): DepartmentPageControlle } await loadCore(); setAddMemberOpen(false); + setMemberModalError(null); setSelectedMemberId(null); setSelectedRoleId(null); setMemberModalMode("add"); @@ -412,14 +429,21 @@ export function useDepartmentPage(departmentId: number): DepartmentPageControlle : "Роль назначена сотруднику", ); } catch (submitError) { - toast.error( - getErrorMessage( - submitError, - memberModalMode === "add" - ? "Не удалось добавить сотрудника" - : "Не удалось назначить роль сотруднику", - ), + const message = getErrorMessage( + submitError, + memberModalMode === "add" + ? "Не удалось добавить сотрудника" + : "Не удалось назначить роль сотруднику", ); + const inlineMessage = getDepartmentMemberModalError( + message, + memberModalMode, + ); + if (inlineMessage) { + setMemberModalError(inlineMessage); + } else { + toast.error(message); + } } finally { setPendingKey(null); } @@ -624,6 +648,7 @@ export function useDepartmentPage(departmentId: number): DepartmentPageControlle headCandidates, loading, memberModalMode, + memberModalError, members, pendingKey, roleDraft, @@ -650,8 +675,8 @@ export function useDepartmentPage(departmentId: number): DepartmentPageControlle setRoleDraft, setMemberModalMode, setSelectedHeadId, - setSelectedMemberId, - setSelectedRoleId, + setSelectedMemberId: selectMember, + setSelectedRoleId: selectRole, submitAddMember, submitDeleteDepartment, submitHeadChange,