diff --git a/frontend/src/app/departments/[id]/page.tsx b/frontend/src/app/departments/[id]/page.tsx index 105e09b4..ad2d92e3 100644 --- a/frontend/src/app/departments/[id]/page.tsx +++ b/frontend/src/app/departments/[id]/page.tsx @@ -2,14 +2,15 @@ import Link from "next/link"; import { useParams } from "next/navigation"; -import { useState, type ReactNode } from "react"; +import { useEffect, useRef, useState, type ReactNode, type RefObject } from "react"; import { ArrowLeft, Building2, + Check, + ChevronDown, Crown, PencilLine, Plus, - Settings2, Trash2, UserPlus, Users, @@ -57,8 +58,12 @@ function DepartmentMemberRow({ pendingKey, roleOptions, onAssignRole, + onCreateRole, onRemoveMember, onSetHead, + onToggleRoleMenu, + roleMenuOpenForId, + roleMenuRef, }: { canAssignRoles: boolean; canChangeHead: boolean; @@ -69,8 +74,12 @@ function DepartmentMemberRow({ pendingKey: string | null; roleOptions: Array<{ id: number; name: string }>; onAssignRole: (employeeId: number, roleId: number | null) => Promise; + onCreateRole: () => void; onRemoveMember: (employeeId: number) => Promise; onSetHead: (employeeId: number) => Promise; + onToggleRoleMenu: (employeeId: number | null) => void; + roleMenuOpenForId: number | null; + roleMenuRef: RefObject; }) { const isHead = currentHeadId === member.employee.id; const isRemoving = pendingKey === `member-remove-${member.employee.id}`; @@ -78,6 +87,7 @@ function DepartmentMemberRow({ const subtitle = member.employee.position?.name || null; const managementMode = canAssignRoles || canChangeHead || canManage; const roleLabel = member.role?.name || "Без роли"; + const isRoleMenuOpen = roleMenuOpenForId === member.employee.id; const personName = displayUserName(member.employee); const profileLink = userProfileLink(member.employee, currentUserId); const fallback = ( @@ -132,16 +142,84 @@ function DepartmentMemberRow({ } return ( -
- +
+ - - {roleLabel} - + {canAssignRoles ? ( +
+ + + {isRoleMenuOpen ? ( +
+
+ Роль участника +
+
+ {roleOptions.map((role) => { + const isSelected = member.role?.id === role.id; + return ( + + ); + })} + + {member.role ? ( + + ) : null} + + +
+
+ ) : null} +
+ ) : member.role ? ( + + {roleLabel} + + ) : null} {isHead ? ( @@ -150,52 +228,29 @@ function DepartmentMemberRow({ ) : null} - {!member.is_active ? ( - - Неактивное участие - + {canChangeHead && !isHead ? ( + ) : null} - {managementMode ? ( - <> - {canAssignRoles ? ( -
- - void onAssignRole(member.employee.id, roleId) - } - placeholder="Без роли" - disabled={isRoleBusy} - /> -
- ) : null} - - {canChangeHead && !isHead ? ( - - ) : null} - - {canManage && !isHead ? ( - - ) : null} - + {canManage && !isHead ? ( + ) : null}
); @@ -472,6 +527,8 @@ export default function DepartmentDetailPage() { const departmentId = Number(params?.id); const h = useDepartmentPage(departmentId); const [managementMode, setManagementMode] = useState(false); + const [roleMenuOpenForId, setRoleMenuOpenForId] = useState(null); + const roleMenuRef = useRef(null); const activeMembersCount = h.members.filter((member) => member.is_active).length; const roleOptions = h.roles.map((role) => ({ id: role.id, name: role.name })); @@ -482,6 +539,29 @@ export default function DepartmentDetailPage() { const isManagementMode = canUseManagementMode && managementMode; + useEffect(() => { + if (roleMenuOpenForId === null) return; + + const handlePointerDown = (event: MouseEvent) => { + if (roleMenuRef.current && !roleMenuRef.current.contains(event.target as Node)) { + setRoleMenuOpenForId(null); + } + }; + + const handleEscape = (event: KeyboardEvent) => { + if (event.key === "Escape") { + setRoleMenuOpenForId(null); + } + }; + + document.addEventListener("mousedown", handlePointerDown); + document.addEventListener("keydown", handleEscape); + return () => { + document.removeEventListener("mousedown", handlePointerDown); + document.removeEventListener("keydown", handleEscape); + }; + }, [roleMenuOpenForId]); + return (
@@ -525,22 +605,27 @@ export default function DepartmentDetailPage() { {h.department.name}

- {h.department.description || - "Здесь можно посмотреть состав отдела и его внутренние роли."} + {h.department.description}

{canUseManagementMode ? ( ) : null} {isManagementMode && h.userPerms.can_manage ? ( @@ -588,8 +673,12 @@ export default function DepartmentDetailPage() { pendingKey={h.pendingKey} roleOptions={roleOptions} onAssignRole={h.submitMemberRole} + onCreateRole={h.openCreateRole} onRemoveMember={h.submitRemoveMember} onSetHead={h.submitQuickHeadChange} + onToggleRoleMenu={setRoleMenuOpenForId} + roleMenuOpenForId={roleMenuOpenForId} + roleMenuRef={roleMenuRef} /> )) ) : (