fix(frontend): localize department member validation

This commit is contained in:
lizerk 2026-04-14 01:57:25 +03:00
parent 8d22156b36
commit 4e0f1c7889
4 changed files with 93 additions and 9 deletions

View File

@ -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({
</div>
) : null}
{!directoryError && memberError ? (
<div className="app-feedback-danger rounded-xl p-3 text-sm">
<p>{memberError}</p>
</div>
) : null}
{canAssignRoles && !directoryError ? (
<div className="space-y-3">
<SearchableSelectSingle
@ -978,6 +986,7 @@ export default function DepartmentDetailPage() {
h.assignableEmployees,
)}
loading={h.pendingKey === "member"}
memberError={h.memberModalError}
mode={h.memberModalMode}
optionsLoading={h.employeesDirectoryLoading}
onClose={h.closeAddMember}

View File

@ -55,3 +55,31 @@ export function isDepartmentMemberModalSubmitDisabled({
if (mode === "assignRole" && !selectedRoleId) return true;
return false;
}
export function getDepartmentMemberModalError(message, mode) {
const normalized = String(message || "").trim();
if (!normalized) return null;
const anotherDepartmentMatch = normalized.match(
/Employee already belongs to another active department:\s*(.+?)\.?$/i,
);
if (anotherDepartmentMatch) {
const departmentName = anotherDepartmentMatch[1]?.trim();
if (mode === "assignRole") {
return null;
}
return departmentName
? `Сотрудник уже состоит в другом активном отделе: ${departmentName}.`
: "Сотрудник уже состоит в другом активном отделе.";
}
if (/Employee is inactive\.?$/i.test(normalized)) {
return "Можно выбрать только активного сотрудника.";
}
if (/Employee not found\.?$/i.test(normalized)) {
return "Сотрудник не найден.";
}
return null;
}

View File

@ -2,6 +2,7 @@ import test from "node:test";
import assert from "node:assert/strict";
import {
getDepartmentMemberModalError,
getDepartmentMemberModalEmployeeLabel,
getDepartmentMemberModalHelperText,
getDepartmentMemberModalItems,
@ -92,3 +93,24 @@ test("member modal helper texts reflect mode and loading state", () => {
/любому активному сотруднику/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,
);
});

View File

@ -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<DepartmentMemberModalMode>("add");
const [memberModalError, setMemberModalError] = useState<string | null>(null);
const [selectedMemberId, setSelectedMemberId] = useState<number | null>(null);
const [selectedRoleId, setSelectedRoleId] = useState<number | null>(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,