refactor(frontend): extend department member modal
This commit is contained in:
parent
852ac367c0
commit
d1a7eedcee
@ -18,15 +18,19 @@ import {
|
||||
} from "lucide-react";
|
||||
|
||||
import { AppShell } from "@/components/AppShell";
|
||||
import {
|
||||
getAddMemberHelperText,
|
||||
getAddMemberPlaceholder,
|
||||
isAddMemberSelectDisabled,
|
||||
} from "@/components/departments/add-member-modal-state";
|
||||
import {
|
||||
DEPARTMENT_MEMBERS_EMPTY_STATE_CLASSNAME,
|
||||
getDepartmentMembersListClassName,
|
||||
} from "@/components/departments/layout";
|
||||
import {
|
||||
getDepartmentMemberModalEmployeeLabel,
|
||||
getDepartmentMemberModalHelperText,
|
||||
getDepartmentMemberModalItems,
|
||||
getDepartmentMemberModalPlaceholder,
|
||||
getDepartmentMemberModalSubmitLabel,
|
||||
getDepartmentMemberModalTitle,
|
||||
isDepartmentMemberModalSubmitDisabled,
|
||||
} from "@/components/departments/member-modal-state";
|
||||
import { RequestAvatar } from "@/components/requests/RequestAvatar";
|
||||
import { SearchableSelectSingle } from "@/components/shared/SearchableSelect";
|
||||
import { Modal } from "@/components/ui/Modal";
|
||||
@ -34,6 +38,8 @@ import { useDepartmentPage } from "@/hooks/useDepartmentPage";
|
||||
import { displayUserName, userProfileLink } from "@/lib/shared";
|
||||
import type { DepartmentMemberLink } from "@/types/api";
|
||||
|
||||
type DepartmentMemberModalMode = "add" | "assignRole";
|
||||
|
||||
function MetaChip({
|
||||
children,
|
||||
tone = "neutral",
|
||||
@ -391,29 +397,54 @@ function DepartmentMembersEmptyState() {
|
||||
}
|
||||
|
||||
function AddMemberModal({
|
||||
canAssignRoles,
|
||||
isOpen,
|
||||
items,
|
||||
loading,
|
||||
mode,
|
||||
optionsLoading,
|
||||
onClose,
|
||||
onCreateRole,
|
||||
onSave,
|
||||
onSelect,
|
||||
onSelectRole,
|
||||
roleOptions,
|
||||
selectedId,
|
||||
selectedRoleId,
|
||||
}: {
|
||||
canAssignRoles: boolean;
|
||||
isOpen: boolean;
|
||||
items: Array<{ id: number; name: string }>;
|
||||
loading: boolean;
|
||||
mode: DepartmentMemberModalMode;
|
||||
optionsLoading: boolean;
|
||||
onClose: () => void;
|
||||
onCreateRole: () => void;
|
||||
onSave: () => Promise<void>;
|
||||
onSelect: (id: number | null) => void;
|
||||
onSelectRole: (id: number | null) => void;
|
||||
roleOptions: Array<{ id: number; name: string }>;
|
||||
selectedId: number | null;
|
||||
selectedRoleId: number | null;
|
||||
}) {
|
||||
const helperText = getDepartmentMemberModalHelperText(
|
||||
mode,
|
||||
optionsLoading,
|
||||
items.length,
|
||||
);
|
||||
const selectDisabled = optionsLoading || items.length === 0;
|
||||
const submitDisabled = isDepartmentMemberModalSubmitDisabled({
|
||||
loading,
|
||||
mode,
|
||||
selectedEmployeeId: selectedId,
|
||||
selectedRoleId,
|
||||
});
|
||||
|
||||
return (
|
||||
<Modal
|
||||
isOpen={isOpen}
|
||||
onClose={onClose}
|
||||
title="Добавить участника"
|
||||
title={getDepartmentMemberModalTitle(mode)}
|
||||
size="md"
|
||||
className="overflow-visible"
|
||||
footer={
|
||||
@ -428,26 +459,59 @@ function AddMemberModal({
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => void onSave()}
|
||||
disabled={loading || !selectedId}
|
||||
disabled={submitDisabled}
|
||||
className="app-action-primary rounded-lg px-4 py-2 text-sm disabled:opacity-50"
|
||||
>
|
||||
Добавить
|
||||
{getDepartmentMemberModalSubmitLabel(mode)}
|
||||
</button>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<section className="app-surface-muted rounded-xl p-4">
|
||||
<section className="app-surface-muted space-y-4 rounded-xl p-4">
|
||||
<SearchableSelectSingle
|
||||
label="Сотрудник"
|
||||
label={getDepartmentMemberModalEmployeeLabel(mode)}
|
||||
items={items}
|
||||
selectedId={selectedId}
|
||||
onSelect={onSelect}
|
||||
placeholder={getAddMemberPlaceholder(optionsLoading)}
|
||||
disabled={isAddMemberSelectDisabled(optionsLoading, items.length)}
|
||||
placeholder={getDepartmentMemberModalPlaceholder(mode, optionsLoading)}
|
||||
disabled={selectDisabled}
|
||||
/>
|
||||
{getAddMemberHelperText(optionsLoading, items.length) ? (
|
||||
|
||||
{canAssignRoles ? (
|
||||
<div className="space-y-3">
|
||||
<SearchableSelectSingle
|
||||
label={mode === "assignRole" ? "Роль *" : "Роль"}
|
||||
items={roleOptions}
|
||||
selectedId={selectedRoleId}
|
||||
onSelect={onSelectRole}
|
||||
placeholder={
|
||||
roleOptions.length
|
||||
? "Выберите роль"
|
||||
: "Сначала создайте роль отдела"
|
||||
}
|
||||
disabled={loading || roleOptions.length === 0}
|
||||
/>
|
||||
<div className="flex flex-wrap items-center justify-between gap-2">
|
||||
<p className="app-text-muted text-sm">
|
||||
{mode === "add"
|
||||
? "Роль можно назначить сразу при добавлении, но это необязательно."
|
||||
: "Роль можно выдать любому активному сотруднику, даже вне состава отдела."}
|
||||
</p>
|
||||
<button
|
||||
type="button"
|
||||
onClick={onCreateRole}
|
||||
className="app-action-secondary inline-flex items-center gap-2 rounded-lg px-3 py-2 text-sm"
|
||||
>
|
||||
<Plus size={14} />
|
||||
Создать роль
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{helperText ? (
|
||||
<p className="app-text-muted mt-3 text-sm">
|
||||
{getAddMemberHelperText(optionsLoading, items.length)}
|
||||
{helperText}
|
||||
</p>
|
||||
) : null}
|
||||
</section>
|
||||
@ -658,17 +722,32 @@ export default function DepartmentDetailPage() {
|
||||
{departmentMenuOpen ? (
|
||||
<div className="app-menu absolute right-0 top-full z-20 mt-2 w-56 rounded-xl py-1.5">
|
||||
{h.userPerms.can_manage ? (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
setDepartmentMenuOpen(false);
|
||||
setManagementMode(true);
|
||||
h.openAddMember();
|
||||
}}
|
||||
className="flex w-full items-center gap-2 px-3 py-2 text-left text-sm text-[var(--foreground)] transition hover:bg-[var(--surface-secondary)]"
|
||||
>
|
||||
<UserPlus size={14} className="app-text-muted" />
|
||||
Добавить участников
|
||||
</button>
|
||||
) : null}
|
||||
|
||||
{h.userPerms.can_assign_roles ? (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
setDepartmentMenuOpen(false);
|
||||
setManagementMode(true);
|
||||
h.openAddMember();
|
||||
h.openAssignRoleModal();
|
||||
}}
|
||||
className="flex w-full items-center gap-2 px-3 py-2 text-left text-sm text-[var(--foreground)] transition hover:bg-[var(--surface-secondary)]"
|
||||
>
|
||||
<UserPlus size={14} className="app-text-muted" />
|
||||
Позвать сотрудников
|
||||
<Plus size={14} className="app-text-muted" />
|
||||
Выдать роль
|
||||
</button>
|
||||
) : null}
|
||||
|
||||
@ -775,14 +854,27 @@ export default function DepartmentDetailPage() {
|
||||
/>
|
||||
|
||||
<AddMemberModal
|
||||
canAssignRoles={h.userPerms.can_assign_roles}
|
||||
isOpen={h.addMemberOpen}
|
||||
items={h.selectableEmployees}
|
||||
items={getDepartmentMemberModalItems(
|
||||
h.memberModalMode,
|
||||
h.selectableEmployees,
|
||||
h.assignableEmployees,
|
||||
)}
|
||||
loading={h.pendingKey === "member"}
|
||||
mode={h.memberModalMode}
|
||||
optionsLoading={h.employeesDirectoryLoading}
|
||||
onClose={h.closeAddMember}
|
||||
onCreateRole={() => {
|
||||
h.closeAddMember();
|
||||
h.openCreateRole();
|
||||
}}
|
||||
onSave={h.submitAddMember}
|
||||
onSelect={h.setSelectedMemberId}
|
||||
onSelectRole={h.setSelectedRoleId}
|
||||
roleOptions={roleOptions}
|
||||
selectedId={h.selectedMemberId}
|
||||
selectedRoleId={h.selectedRoleId}
|
||||
/>
|
||||
|
||||
<RoleEditorModal
|
||||
|
||||
@ -1,31 +0,0 @@
|
||||
import test from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
|
||||
import {
|
||||
getAddMemberHelperText,
|
||||
getAddMemberPlaceholder,
|
||||
isAddMemberSelectDisabled,
|
||||
} from "./add-member-modal-state.ts";
|
||||
|
||||
test("add member modal shows loading placeholder while directory loads", () => {
|
||||
assert.equal(getAddMemberPlaceholder(true), "Загружаем сотрудников...");
|
||||
assert.equal(isAddMemberSelectDisabled(true, 3), true);
|
||||
assert.equal(
|
||||
getAddMemberHelperText(true, 3),
|
||||
"Загружаем доступных сотрудников...",
|
||||
);
|
||||
});
|
||||
|
||||
test("add member modal shows empty state when there are no selectable employees", () => {
|
||||
assert.equal(getAddMemberPlaceholder(false), "Выберите сотрудника");
|
||||
assert.equal(isAddMemberSelectDisabled(false, 0), true);
|
||||
assert.equal(
|
||||
getAddMemberHelperText(false, 0),
|
||||
"В директории нет доступных сотрудников для добавления.",
|
||||
);
|
||||
});
|
||||
|
||||
test("add member modal stays enabled when options are ready", () => {
|
||||
assert.equal(isAddMemberSelectDisabled(false, 2), false);
|
||||
assert.equal(getAddMemberHelperText(false, 2), null);
|
||||
});
|
||||
@ -1,23 +0,0 @@
|
||||
export function getAddMemberPlaceholder(optionsLoading: boolean) {
|
||||
return optionsLoading ? "Загружаем сотрудников..." : "Выберите сотрудника";
|
||||
}
|
||||
|
||||
export function isAddMemberSelectDisabled(
|
||||
optionsLoading: boolean,
|
||||
itemsCount: number,
|
||||
) {
|
||||
return optionsLoading || itemsCount === 0;
|
||||
}
|
||||
|
||||
export function getAddMemberHelperText(
|
||||
optionsLoading: boolean,
|
||||
itemsCount: number,
|
||||
) {
|
||||
if (optionsLoading) {
|
||||
return "Загружаем доступных сотрудников...";
|
||||
}
|
||||
if (itemsCount === 0) {
|
||||
return "В директории нет доступных сотрудников для добавления.";
|
||||
}
|
||||
return null;
|
||||
}
|
||||
@ -1,4 +1,4 @@
|
||||
export function getDepartmentMembersListClassName(isManagementMode: boolean) {
|
||||
export function getDepartmentMembersListClassName(isManagementMode) {
|
||||
return isManagementMode
|
||||
? "space-y-3"
|
||||
: "flex flex-wrap items-center gap-2";
|
||||
@ -4,7 +4,7 @@ import assert from "node:assert/strict";
|
||||
import {
|
||||
DEPARTMENT_MEMBERS_EMPTY_STATE_CLASSNAME,
|
||||
getDepartmentMembersListClassName,
|
||||
} from "./layout.ts";
|
||||
} from "./layout.js";
|
||||
|
||||
test("showcase members layout keeps bubble flow", () => {
|
||||
assert.equal(
|
||||
|
||||
@ -1,23 +0,0 @@
|
||||
/* eslint-disable @typescript-eslint/no-require-imports */
|
||||
const test = require("node:test");
|
||||
const assert = require("node:assert/strict");
|
||||
|
||||
const {
|
||||
DEPARTMENT_MEMBERS_EMPTY_STATE_CLASSNAME,
|
||||
getDepartmentMembersListClassName,
|
||||
} = require("./layout.ts");
|
||||
|
||||
test("showcase members layout keeps bubble flow", () => {
|
||||
assert.equal(
|
||||
getDepartmentMembersListClassName(false),
|
||||
"flex flex-wrap items-center gap-2",
|
||||
);
|
||||
});
|
||||
|
||||
test("management members layout keeps stacked editing rows", () => {
|
||||
assert.equal(getDepartmentMembersListClassName(true), "space-y-3");
|
||||
});
|
||||
|
||||
test("department empty state spans full section width", () => {
|
||||
assert.match(DEPARTMENT_MEMBERS_EMPTY_STATE_CLASSNAME, /\bw-full\b/);
|
||||
});
|
||||
57
frontend/src/components/departments/member-modal-state.js
Normal file
57
frontend/src/components/departments/member-modal-state.js
Normal file
@ -0,0 +1,57 @@
|
||||
export function getDepartmentMemberModalItems(
|
||||
mode,
|
||||
selectableEmployees,
|
||||
assignableEmployees,
|
||||
) {
|
||||
return mode === "add" ? selectableEmployees : assignableEmployees;
|
||||
}
|
||||
|
||||
export function getDepartmentMemberModalSubmitLabel(mode) {
|
||||
return mode === "add" ? "Добавить" : "Выдать роль";
|
||||
}
|
||||
|
||||
export function getDepartmentMemberModalTitle(mode) {
|
||||
return mode === "add" ? "Добавить участника" : "Выдать роль сотруднику";
|
||||
}
|
||||
|
||||
export function getDepartmentMemberModalEmployeeLabel(mode) {
|
||||
return mode === "add" ? "Сотрудник" : "Сотрудник *";
|
||||
}
|
||||
|
||||
export function getDepartmentMemberModalPlaceholder(mode, optionsLoading) {
|
||||
if (optionsLoading) return "Загружаем сотрудников...";
|
||||
return mode === "add" ? "Выберите сотрудника" : "Выберите сотрудника для роли";
|
||||
}
|
||||
|
||||
export function getDepartmentMemberModalHelperText(
|
||||
mode,
|
||||
optionsLoading,
|
||||
itemsCount,
|
||||
) {
|
||||
if (optionsLoading) {
|
||||
return mode === "add"
|
||||
? "Загружаем доступных сотрудников..."
|
||||
: "Загружаем сотрудников, которым можно выдать роль...";
|
||||
}
|
||||
|
||||
if (itemsCount === 0) {
|
||||
return mode === "add"
|
||||
? "В директории нет доступных сотрудников для добавления."
|
||||
: "Нет активных сотрудников, которым можно выдать роль.";
|
||||
}
|
||||
|
||||
return mode === "add"
|
||||
? "При добавлении можно сразу назначить роль, если она нужна."
|
||||
: "Роль в отделе можно выдать любому активному сотруднику, даже если он ещё не состоит в отделе.";
|
||||
}
|
||||
|
||||
export function isDepartmentMemberModalSubmitDisabled({
|
||||
loading,
|
||||
mode,
|
||||
selectedEmployeeId,
|
||||
selectedRoleId,
|
||||
}) {
|
||||
if (loading || !selectedEmployeeId) return true;
|
||||
if (mode === "assignRole" && !selectedRoleId) return true;
|
||||
return false;
|
||||
}
|
||||
@ -0,0 +1,94 @@
|
||||
import test from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
|
||||
import {
|
||||
getDepartmentMemberModalEmployeeLabel,
|
||||
getDepartmentMemberModalHelperText,
|
||||
getDepartmentMemberModalItems,
|
||||
getDepartmentMemberModalPlaceholder,
|
||||
getDepartmentMemberModalSubmitLabel,
|
||||
getDepartmentMemberModalTitle,
|
||||
isDepartmentMemberModalSubmitDisabled,
|
||||
} from "./member-modal-state.js";
|
||||
|
||||
test("member modal uses selectable employees in add mode", () => {
|
||||
const selectable = [{ id: 1, name: "A" }];
|
||||
const assignable = [{ id: 2, name: "B" }];
|
||||
|
||||
assert.deepEqual(
|
||||
getDepartmentMemberModalItems("add", selectable, assignable),
|
||||
selectable,
|
||||
);
|
||||
assert.equal(getDepartmentMemberModalSubmitLabel("add"), "Добавить");
|
||||
assert.equal(getDepartmentMemberModalTitle("add"), "Добавить участника");
|
||||
assert.equal(getDepartmentMemberModalEmployeeLabel("add"), "Сотрудник");
|
||||
});
|
||||
|
||||
test("member modal uses full assignable directory in role mode", () => {
|
||||
const selectable = [{ id: 1, name: "A" }];
|
||||
const assignable = [{ id: 2, name: "B" }];
|
||||
|
||||
assert.deepEqual(
|
||||
getDepartmentMemberModalItems("assignRole", selectable, assignable),
|
||||
assignable,
|
||||
);
|
||||
assert.equal(
|
||||
getDepartmentMemberModalSubmitLabel("assignRole"),
|
||||
"Выдать роль",
|
||||
);
|
||||
assert.equal(
|
||||
getDepartmentMemberModalTitle("assignRole"),
|
||||
"Выдать роль сотруднику",
|
||||
);
|
||||
assert.equal(
|
||||
getDepartmentMemberModalEmployeeLabel("assignRole"),
|
||||
"Сотрудник *",
|
||||
);
|
||||
});
|
||||
|
||||
test("member modal submit stays disabled until required fields are selected", () => {
|
||||
assert.equal(
|
||||
isDepartmentMemberModalSubmitDisabled({
|
||||
loading: false,
|
||||
mode: "add",
|
||||
selectedEmployeeId: null,
|
||||
selectedRoleId: null,
|
||||
}),
|
||||
true,
|
||||
);
|
||||
|
||||
assert.equal(
|
||||
isDepartmentMemberModalSubmitDisabled({
|
||||
loading: false,
|
||||
mode: "assignRole",
|
||||
selectedEmployeeId: 1,
|
||||
selectedRoleId: null,
|
||||
}),
|
||||
true,
|
||||
);
|
||||
|
||||
assert.equal(
|
||||
isDepartmentMemberModalSubmitDisabled({
|
||||
loading: false,
|
||||
mode: "assignRole",
|
||||
selectedEmployeeId: 1,
|
||||
selectedRoleId: 2,
|
||||
}),
|
||||
false,
|
||||
);
|
||||
});
|
||||
|
||||
test("member modal helper texts reflect mode and loading state", () => {
|
||||
assert.equal(
|
||||
getDepartmentMemberModalPlaceholder("add", true),
|
||||
"Загружаем сотрудников...",
|
||||
);
|
||||
assert.equal(
|
||||
getDepartmentMemberModalPlaceholder("assignRole", false),
|
||||
"Выберите сотрудника для роли",
|
||||
);
|
||||
assert.match(
|
||||
getDepartmentMemberModalHelperText("assignRole", false, 1) || "",
|
||||
/любому активному сотруднику/i,
|
||||
);
|
||||
});
|
||||
@ -20,6 +20,8 @@ type DepartmentRoleDraft = {
|
||||
name: string;
|
||||
};
|
||||
|
||||
type DepartmentMemberModalMode = "add" | "assignRole";
|
||||
|
||||
type SavingKey =
|
||||
| "department"
|
||||
| "department-delete"
|
||||
@ -33,6 +35,7 @@ type SavingKey =
|
||||
export type DepartmentPageController = {
|
||||
addMemberOpen: boolean;
|
||||
allEmployees: User[];
|
||||
assignableEmployees: Array<{ id: number; name: string }>;
|
||||
currentUserId: number | null;
|
||||
department: Department | null;
|
||||
departmentDraft: { name: string; description: string };
|
||||
@ -43,6 +46,7 @@ export type DepartmentPageController = {
|
||||
headCandidates: Array<{ id: number; name: string }>;
|
||||
employeesDirectoryLoading: boolean;
|
||||
loading: boolean;
|
||||
memberModalMode: DepartmentMemberModalMode;
|
||||
members: DepartmentMemberLink[];
|
||||
pendingKey: SavingKey | null;
|
||||
roleDraft: DepartmentRoleDraft;
|
||||
@ -52,11 +56,13 @@ export type DepartmentPageController = {
|
||||
selectableEmployees: Array<{ id: number; name: string }>;
|
||||
selectedHeadId: number | null;
|
||||
selectedMemberId: number | null;
|
||||
selectedRoleId: number | null;
|
||||
userPerms: DepartmentUserPermissions;
|
||||
closeAddMember: () => void;
|
||||
closeDepartmentEditor: () => void;
|
||||
closeRoleEditor: () => void;
|
||||
openAddMember: () => void;
|
||||
openAssignRoleModal: () => void;
|
||||
openCreateRole: () => void;
|
||||
openDepartmentEditor: () => void;
|
||||
openEditRole: (role: DepartmentRole) => void;
|
||||
@ -64,8 +70,10 @@ export type DepartmentPageController = {
|
||||
saveDepartment: () => Promise<void>;
|
||||
saveRole: () => Promise<void>;
|
||||
setRoleDraft: (next: DepartmentRoleDraft | ((current: DepartmentRoleDraft) => DepartmentRoleDraft)) => void;
|
||||
setMemberModalMode: (mode: DepartmentMemberModalMode) => void;
|
||||
setSelectedHeadId: (id: number | null) => void;
|
||||
setSelectedMemberId: (id: number | null) => void;
|
||||
setSelectedRoleId: (id: number | null) => void;
|
||||
submitAddMember: () => Promise<void>;
|
||||
submitDeleteDepartment: () => Promise<boolean>;
|
||||
submitHeadChange: () => Promise<void>;
|
||||
@ -136,7 +144,9 @@ export function useDepartmentPage(departmentId: number): DepartmentPageControlle
|
||||
const [departmentDraft, setDepartmentDraft] = useState({ name: "", description: "" });
|
||||
|
||||
const [addMemberOpen, setAddMemberOpen] = useState(false);
|
||||
const [memberModalMode, setMemberModalMode] = useState<DepartmentMemberModalMode>("add");
|
||||
const [selectedMemberId, setSelectedMemberId] = useState<number | null>(null);
|
||||
const [selectedRoleId, setSelectedRoleId] = useState<number | null>(null);
|
||||
|
||||
const [selectedHeadId, setSelectedHeadId] = useState<number | null>(null);
|
||||
|
||||
@ -255,6 +265,13 @@ export function useDepartmentPage(departmentId: number): DepartmentPageControlle
|
||||
}));
|
||||
}, [allEmployees, members]);
|
||||
|
||||
const assignableEmployees = useMemo(() => {
|
||||
return allEmployees.map((employee) => ({
|
||||
id: employee.id,
|
||||
name: `${employee.last_name || ""} ${employee.first_name || ""}`.trim() || employee.email,
|
||||
}));
|
||||
}, [allEmployees]);
|
||||
|
||||
const headCandidates = useMemo(() => {
|
||||
return allEmployees.map((employee) => ({
|
||||
id: employee.id,
|
||||
@ -316,7 +333,21 @@ export function useDepartmentPage(departmentId: number): DepartmentPageControlle
|
||||
}, [department, departmentDraft.description, departmentDraft.name, hydrateDepartmentDraft]);
|
||||
|
||||
const openAddMember = useCallback(() => {
|
||||
setMemberModalMode("add");
|
||||
setSelectedMemberId(null);
|
||||
setSelectedRoleId(null);
|
||||
setAddMemberOpen(true);
|
||||
if (allEmployees.length === 0) {
|
||||
void loadEmployeesDirectory().catch((loadError) => {
|
||||
toast.error(getErrorMessage(loadError, "Не удалось загрузить сотрудников"));
|
||||
});
|
||||
}
|
||||
}, [allEmployees.length, loadEmployeesDirectory]);
|
||||
|
||||
const openAssignRoleModal = useCallback(() => {
|
||||
setMemberModalMode("assignRole");
|
||||
setSelectedMemberId(null);
|
||||
setSelectedRoleId(null);
|
||||
setAddMemberOpen(true);
|
||||
if (allEmployees.length === 0) {
|
||||
void loadEmployeesDirectory().catch((loadError) => {
|
||||
@ -327,6 +358,8 @@ export function useDepartmentPage(departmentId: number): DepartmentPageControlle
|
||||
|
||||
const closeAddMember = useCallback(() => {
|
||||
setSelectedMemberId(null);
|
||||
setSelectedRoleId(null);
|
||||
setMemberModalMode("add");
|
||||
setAddMemberOpen(false);
|
||||
}, []);
|
||||
|
||||
@ -336,19 +369,57 @@ export function useDepartmentPage(departmentId: number): DepartmentPageControlle
|
||||
return;
|
||||
}
|
||||
|
||||
if (memberModalMode === "assignRole" && !selectedRoleId) {
|
||||
toast.error("Выберите роль");
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
setPendingKey("member");
|
||||
await apiClient.addDepartmentMember(department.id, selectedMemberId);
|
||||
if (memberModalMode === "add") {
|
||||
await apiClient.addDepartmentMember(department.id, selectedMemberId);
|
||||
if (selectedRoleId && userPerms?.can_assign_roles) {
|
||||
await apiClient.setDepartmentMemberRole(department.id, {
|
||||
employee_id: selectedMemberId,
|
||||
role_id: selectedRoleId,
|
||||
});
|
||||
}
|
||||
} else {
|
||||
await apiClient.setDepartmentMemberRole(department.id, {
|
||||
employee_id: selectedMemberId,
|
||||
role_id: selectedRoleId,
|
||||
});
|
||||
}
|
||||
await loadCore();
|
||||
setAddMemberOpen(false);
|
||||
setSelectedMemberId(null);
|
||||
toast.success("Сотрудник добавлен в отдел");
|
||||
setSelectedRoleId(null);
|
||||
setMemberModalMode("add");
|
||||
toast.success(
|
||||
memberModalMode === "add"
|
||||
? "Сотрудник добавлен в отдел"
|
||||
: "Роль назначена сотруднику",
|
||||
);
|
||||
} catch (submitError) {
|
||||
toast.error(getErrorMessage(submitError, "Не удалось добавить сотрудника"));
|
||||
toast.error(
|
||||
getErrorMessage(
|
||||
submitError,
|
||||
memberModalMode === "add"
|
||||
? "Не удалось добавить сотрудника"
|
||||
: "Не удалось назначить роль сотруднику",
|
||||
),
|
||||
);
|
||||
} finally {
|
||||
setPendingKey(null);
|
||||
}
|
||||
}, [department, loadCore, selectedMemberId]);
|
||||
}, [
|
||||
department,
|
||||
loadCore,
|
||||
memberModalMode,
|
||||
selectedMemberId,
|
||||
selectedRoleId,
|
||||
userPerms,
|
||||
]);
|
||||
|
||||
const submitDeleteDepartment = useCallback(async () => {
|
||||
if (!department) return false;
|
||||
@ -529,6 +600,7 @@ export function useDepartmentPage(departmentId: number): DepartmentPageControlle
|
||||
return {
|
||||
addMemberOpen,
|
||||
allEmployees,
|
||||
assignableEmployees,
|
||||
currentUserId,
|
||||
department,
|
||||
departmentDraft,
|
||||
@ -539,6 +611,7 @@ export function useDepartmentPage(departmentId: number): DepartmentPageControlle
|
||||
filteredMembers,
|
||||
headCandidates,
|
||||
loading,
|
||||
memberModalMode,
|
||||
members,
|
||||
pendingKey,
|
||||
roleDraft,
|
||||
@ -548,11 +621,13 @@ export function useDepartmentPage(departmentId: number): DepartmentPageControlle
|
||||
selectableEmployees,
|
||||
selectedHeadId,
|
||||
selectedMemberId,
|
||||
selectedRoleId,
|
||||
userPerms: userPerms || EMPTY_USER_PERMS,
|
||||
closeAddMember,
|
||||
closeDepartmentEditor,
|
||||
closeRoleEditor,
|
||||
openAddMember,
|
||||
openAssignRoleModal,
|
||||
openCreateRole,
|
||||
openDepartmentEditor,
|
||||
openEditRole,
|
||||
@ -560,8 +635,10 @@ export function useDepartmentPage(departmentId: number): DepartmentPageControlle
|
||||
saveDepartment,
|
||||
saveRole,
|
||||
setRoleDraft,
|
||||
setMemberModalMode,
|
||||
setSelectedHeadId,
|
||||
setSelectedMemberId,
|
||||
setSelectedRoleId,
|
||||
submitAddMember,
|
||||
submitDeleteDepartment,
|
||||
submitHeadChange,
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user