From d412d6634b985b1d4e389ac6e37296fe2586335a Mon Sep 17 00:00:00 2001 From: lizerk Date: Wed, 15 Apr 2026 02:32:01 +0300 Subject: [PATCH] =?UTF-8?q?=D1=84=D0=B8=D0=BA=D1=81=20=D0=BE=D1=82=D0=B4?= =?UTF-8?q?=D0=B5=D0=BB=D0=BE=D0=B2=20=D0=B8=20=D1=81=D0=BE=D1=82=D1=80?= =?UTF-8?q?=D1=83=D0=B4=D0=BD=D0=B8=D0=BA=D0=BE=D0=B2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/src/app/departments/page.tsx | 179 +++++++++++++++++++++- frontend/src/app/users/[id]/page.tsx | 11 +- frontend/src/hooks/useProfilePage.ts | 20 +-- frontend/src/lib/api/employees.ts | 5 + frontend/src/lib/users/userDetailUtils.ts | 35 ++++- 5 files changed, 224 insertions(+), 26 deletions(-) diff --git a/frontend/src/app/departments/page.tsx b/frontend/src/app/departments/page.tsx index af940b53..defa7556 100644 --- a/frontend/src/app/departments/page.tsx +++ b/frontend/src/app/departments/page.tsx @@ -1,17 +1,38 @@ "use client"; import { AppShell } from "../../components/AppShell"; +import { Modal } from "@/components/ui/Modal"; +import { useUser } from "@/contexts/UserContext"; import { apiClient } from "@/lib/api"; import { useEffect, useMemo, useState } from "react"; import Link from "next/link"; import type { Department } from "@/types/api"; -import { Building2, Search, Users } from "lucide-react"; +import { Building2, Plus, Search, Users } from "lucide-react"; + +const scopeTabs = [ + { value: "all", label: "Все" }, + { value: "mine", label: "Мои" }, +] as const; export default function DepartmentsPage() { + const { user } = useUser(); const [departments, setDepartments] = useState([]); const [search, setSearch] = useState(""); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); + const [scope, setScope] = useState<"all" | "mine">("all"); + const [createOpen, setCreateOpen] = useState(false); + const [createError, setCreateError] = useState(null); + const [creating, setCreating] = useState(false); + const [draft, setDraft] = useState({ name: "", description: "" }); + + const canCreateDepartment = Boolean( + user?.auth?.is_staff || user?.auth?.is_superuser, + ); + const myDepartmentIds = useMemo( + () => new Set((user?.departments || []).map((department) => department.id)), + [user?.departments], + ); useEffect(() => { async function loadDepartments() { @@ -33,7 +54,10 @@ export default function DepartmentsPage() { const filteredDepartments = useMemo(() => { const q = search.trim().toLowerCase(); - const sorted = [...departments].sort((a, b) => a.name.localeCompare(b.name, "ru")); + const visibleDepartments = scope === "mine" + ? departments.filter((department) => myDepartmentIds.has(department.id)) + : departments; + const sorted = [...visibleDepartments].sort((a, b) => a.name.localeCompare(b.name, "ru")); if (!q) return sorted; return sorted.filter((dep) => { @@ -42,7 +66,56 @@ export default function DepartmentsPage() { const head = dep.head ? `${dep.head.last_name} ${dep.head.first_name}`.toLowerCase() : ""; return name.includes(q) || desc.includes(q) || head.includes(q); }); - }, [departments, search]); + }, [departments, myDepartmentIds, scope, search]); + + const scopeCounts = useMemo( + () => ({ + all: departments.length, + mine: departments.filter((department) => myDepartmentIds.has(department.id)).length, + }), + [departments, myDepartmentIds], + ); + + async function handleCreateDepartment() { + const name = draft.name.trim(); + const description = draft.description.trim(); + + if (!name) { + setCreateError("Укажите название отдела"); + return; + } + + try { + setCreating(true); + setCreateError(null); + + const created = await apiClient.createDepartment({ + name, + description: description || undefined, + }) as Department; + + setDepartments((current) => [...current, created]); + setCreateOpen(false); + setDraft({ name: "", description: "" }); + } catch (err) { + console.error("Ошибка создания отдела:", err); + setCreateError("Не удалось создать отдел"); + } finally { + setCreating(false); + } + } + + function openCreateDepartment() { + setCreateError(null); + setDraft({ name: "", description: "" }); + setCreateOpen(true); + } + + function closeCreateDepartment() { + if (creating) return; + setCreateOpen(false); + setCreateError(null); + } return ( @@ -57,6 +130,19 @@ export default function DepartmentsPage() { ) : (
+
+

Отделы

+ {canCreateDepartment ? ( + + ) : null} +
+
+
+ {scopeTabs.map((tab) => ( + + ))} +
+
{filteredDepartments.length === 0 ? (
@@ -106,6 +214,71 @@ export default function DepartmentsPage() {
)} + + + + + + } + > +
+ {createError ? ( +

{createError}

+ ) : null} + +
+ + + setDraft((current) => ({ ...current, name: event.target.value })) + } + className="app-input w-full rounded-lg px-3 py-2 text-sm" + placeholder="Например, Отдел закупок" + disabled={creating} + /> +
+ +
+ +