refactor(frontend): use people picker in department modal
This commit is contained in:
parent
ad4f5cc37f
commit
a4910421c3
@ -2,7 +2,7 @@
|
||||
|
||||
import Link from "next/link";
|
||||
import { useParams, useRouter } from "next/navigation";
|
||||
import { useEffect, useRef, useState, type ReactNode, type RefObject } from "react";
|
||||
import { useEffect, useMemo, useRef, useState, type ReactNode, type RefObject } from "react";
|
||||
import {
|
||||
ArrowLeft,
|
||||
Building2,
|
||||
@ -12,6 +12,7 @@ import {
|
||||
Crown,
|
||||
PencilLine,
|
||||
Plus,
|
||||
Search,
|
||||
Trash2,
|
||||
UserPlus,
|
||||
Users,
|
||||
@ -23,7 +24,6 @@ import {
|
||||
getDepartmentMembersListClassName,
|
||||
} from "@/components/departments/layout";
|
||||
import {
|
||||
getDepartmentMemberModalEmployeeLabel,
|
||||
getDepartmentMemberModalHelperText,
|
||||
getDepartmentMemberModalItems,
|
||||
getDepartmentMemberModalPlaceholder,
|
||||
@ -36,7 +36,7 @@ import { SearchableSelectSingle } from "@/components/shared/SearchableSelect";
|
||||
import { Modal } from "@/components/ui/Modal";
|
||||
import { useDepartmentPage } from "@/hooks/useDepartmentPage";
|
||||
import { displayUserName, userProfileLink } from "@/lib/shared";
|
||||
import type { DepartmentMemberLink } from "@/types/api";
|
||||
import type { DepartmentMemberLink, User } from "@/types/api";
|
||||
|
||||
type DepartmentMemberModalMode = "add" | "assignRole";
|
||||
|
||||
@ -417,7 +417,7 @@ function AddMemberModal({
|
||||
canAssignRoles: boolean;
|
||||
directoryError: string | null;
|
||||
isOpen: boolean;
|
||||
items: Array<{ id: number; name: string }>;
|
||||
items: User[];
|
||||
loading: boolean;
|
||||
mode: DepartmentMemberModalMode;
|
||||
optionsLoading: boolean;
|
||||
@ -431,6 +431,12 @@ function AddMemberModal({
|
||||
selectedId: number | null;
|
||||
selectedRoleId: number | null;
|
||||
}) {
|
||||
const [query, setQuery] = useState("");
|
||||
const handleClose = () => {
|
||||
setQuery("");
|
||||
onClose();
|
||||
};
|
||||
|
||||
const helperText = getDepartmentMemberModalHelperText(
|
||||
mode,
|
||||
optionsLoading,
|
||||
@ -443,11 +449,26 @@ function AddMemberModal({
|
||||
selectedEmployeeId: selectedId,
|
||||
selectedRoleId,
|
||||
});
|
||||
const filteredItems = useMemo(() => {
|
||||
const normalizedQuery = query.trim().toLowerCase();
|
||||
if (!normalizedQuery) return items;
|
||||
|
||||
return items.filter((employee) => {
|
||||
const personName = displayUserName(employee).toLowerCase();
|
||||
const email = (employee.email || "").toLowerCase();
|
||||
const position = (employee.position?.name || "").toLowerCase();
|
||||
return (
|
||||
personName.includes(normalizedQuery) ||
|
||||
email.includes(normalizedQuery) ||
|
||||
position.includes(normalizedQuery)
|
||||
);
|
||||
});
|
||||
}, [items, query]);
|
||||
|
||||
return (
|
||||
<Modal
|
||||
isOpen={isOpen}
|
||||
onClose={onClose}
|
||||
onClose={handleClose}
|
||||
title={getDepartmentMemberModalTitle(mode)}
|
||||
size="md"
|
||||
className="overflow-visible"
|
||||
@ -455,7 +476,7 @@ function AddMemberModal({
|
||||
<div className="flex flex-wrap justify-end gap-2">
|
||||
<button
|
||||
type="button"
|
||||
onClick={onClose}
|
||||
onClick={handleClose}
|
||||
className="app-action-secondary rounded-lg px-4 py-2 text-sm"
|
||||
>
|
||||
Отмена
|
||||
@ -472,14 +493,87 @@ function AddMemberModal({
|
||||
}
|
||||
>
|
||||
<section className="app-surface-muted space-y-4 rounded-xl p-4">
|
||||
<SearchableSelectSingle
|
||||
label={getDepartmentMemberModalEmployeeLabel(mode)}
|
||||
items={items}
|
||||
selectedId={selectedId}
|
||||
onSelect={onSelect}
|
||||
placeholder={getDepartmentMemberModalPlaceholder(mode, optionsLoading)}
|
||||
disabled={selectDisabled}
|
||||
/>
|
||||
<div className="space-y-3">
|
||||
<div className="flex items-center gap-2 rounded-xl border border-[var(--border-subtle)] bg-[var(--surface-secondary)] px-3 py-2.5">
|
||||
<Search size={16} className="app-text-muted shrink-0" />
|
||||
<input
|
||||
value={query}
|
||||
onChange={(event) => setQuery(event.target.value)}
|
||||
placeholder={getDepartmentMemberModalPlaceholder(mode, optionsLoading)}
|
||||
disabled={selectDisabled}
|
||||
className="min-w-0 flex-1 bg-transparent text-sm text-[var(--foreground)] outline-none placeholder:text-[var(--muted-foreground)] disabled:cursor-not-allowed"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-between px-1 text-xs">
|
||||
<span className="app-text-muted">
|
||||
{optionsLoading ? "Загружаем сотрудников..." : `Найдено: ${filteredItems.length}`}
|
||||
</span>
|
||||
<span className="app-accent-text font-medium">
|
||||
Выбрано: {selectedId ? 1 : 0}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="app-surface max-h-72 space-y-2 overflow-y-auto rounded-2xl p-2">
|
||||
{!directoryError && !optionsLoading && filteredItems.length > 0 ? (
|
||||
filteredItems.map((employee) => {
|
||||
const isSelected = selectedId === employee.id;
|
||||
const personName = displayUserName(employee);
|
||||
const fallback = (
|
||||
employee.first_name?.[0] ||
|
||||
employee.last_name?.[0] ||
|
||||
employee.email?.[0] ||
|
||||
"?"
|
||||
).toUpperCase();
|
||||
|
||||
return (
|
||||
<button
|
||||
key={employee.id}
|
||||
type="button"
|
||||
onClick={() => onSelect(isSelected ? null : employee.id)}
|
||||
className={`flex w-full items-center gap-3 rounded-xl px-3 py-3 text-left transition ${
|
||||
isSelected
|
||||
? "app-selected"
|
||||
: "app-surface-muted hover:bg-[var(--surface-tertiary)]"
|
||||
}`}
|
||||
>
|
||||
<RequestAvatar
|
||||
alt={personName}
|
||||
fallback={fallback}
|
||||
size="lg"
|
||||
src={employee.avatar}
|
||||
/>
|
||||
<div className="min-w-0 flex-1">
|
||||
<div className="truncate text-sm font-medium text-[var(--foreground)]">
|
||||
{personName}
|
||||
</div>
|
||||
<div className="truncate text-xs text-[var(--muted-foreground)]">
|
||||
{employee.email || employee.position?.name || "Сотрудник"}
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className={`flex h-6 w-6 shrink-0 items-center justify-center rounded-full border transition ${
|
||||
isSelected
|
||||
? "border-[color:var(--accent-primary)] bg-[var(--accent-primary)] text-white"
|
||||
: "border-[color:var(--border-strong)] text-transparent"
|
||||
}`}
|
||||
aria-hidden="true"
|
||||
>
|
||||
<Check size={14} />
|
||||
</div>
|
||||
</button>
|
||||
);
|
||||
})
|
||||
) : !directoryError && !optionsLoading ? (
|
||||
<div className="app-surface-muted rounded-xl px-3 py-6 text-center">
|
||||
<p className="text-sm font-medium text-[var(--foreground)]">Ничего не найдено</p>
|
||||
<p className="app-text-muted mt-1 text-xs">
|
||||
Попробуй изменить запрос или очистить поиск.
|
||||
</p>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{directoryError ? (
|
||||
<div className="app-feedback-danger rounded-xl p-3 text-sm">
|
||||
|
||||
@ -35,7 +35,7 @@ type SavingKey =
|
||||
export type DepartmentPageController = {
|
||||
addMemberOpen: boolean;
|
||||
allEmployees: User[];
|
||||
assignableEmployees: Array<{ id: number; name: string }>;
|
||||
assignableEmployees: User[];
|
||||
currentUserId: number | null;
|
||||
department: Department | null;
|
||||
departmentDraft: { name: string; description: string };
|
||||
@ -54,7 +54,7 @@ export type DepartmentPageController = {
|
||||
roleEditorOpen: boolean;
|
||||
roleUsage: Record<number, number>;
|
||||
roles: DepartmentRole[];
|
||||
selectableEmployees: Array<{ id: number; name: string }>;
|
||||
selectableEmployees: User[];
|
||||
selectedHeadId: number | null;
|
||||
selectedMemberId: number | null;
|
||||
selectedRoleId: number | null;
|
||||
@ -272,19 +272,11 @@ export function useDepartmentPage(departmentId: number): DepartmentPageControlle
|
||||
members.filter((member) => member.is_active).map((member) => member.employee.id),
|
||||
);
|
||||
|
||||
return allEmployees
|
||||
.filter((employee) => !activeIds.has(employee.id))
|
||||
.map((employee) => ({
|
||||
id: employee.id,
|
||||
name: `${employee.last_name || ""} ${employee.first_name || ""}`.trim() || employee.email,
|
||||
}));
|
||||
return allEmployees.filter((employee) => !activeIds.has(employee.id));
|
||||
}, [allEmployees, members]);
|
||||
|
||||
const assignableEmployees = useMemo(() => {
|
||||
return allEmployees.map((employee) => ({
|
||||
id: employee.id,
|
||||
name: `${employee.last_name || ""} ${employee.first_name || ""}`.trim() || employee.email,
|
||||
}));
|
||||
return allEmployees;
|
||||
}, [allEmployees]);
|
||||
|
||||
const headCandidates = useMemo(() => {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user