добавлена иконка чатов в список сотрудников

This commit is contained in:
lizerk 2026-03-16 18:29:26 +03:00
parent f28eda6929
commit 3dc8541508

View File

@ -4,20 +4,25 @@ import { AppShell } from "../../components/AppShell";
import { apiClient } from "@/lib/api";
import { useEffect, useRef, useState } from "react";
import Link from "next/link";
import { useRouter } from "next/navigation";
import { MessageCircle } from "lucide-react";
import { useUser } from "@/contexts/UserContext";
import type { User } from "@/types/api";
export default function EmployeesPage() {
const router = useRouter();
const { user: currentUser } = useUser();
const [employees, setEmployees] = useState<User[]>([]);
const [loading, setLoading] = useState(true);
const [loadingMore, setLoadingMore] = useState(false);
const [error, setError] = useState<string | null>(null);
const [page, setPage] = useState(1);
const [hasMore, setHasMore] = useState(true);
const [showOnlyActive, setShowOnlyActive] = useState(false);
const [creatingChatFor, setCreatingChatFor] = useState<number | null>(null);
const loaderRef = useRef<HTMLDivElement | null>(null);
const isFetchingRef = useRef(false);
const loadEmployees = async (pageToLoad: number, append: boolean, activeOnly: boolean) => {
const loadEmployees = async (pageToLoad: number, append: boolean) => {
if (isFetchingRef.current) return;
try {
@ -32,7 +37,7 @@ export default function EmployeesPage() {
const response = await apiClient.getEmployees({
page: pageToLoad,
limit: 20,
...(activeOnly && { is_active: true })
is_active: true
});
const nextChunk = response.results || [];
@ -50,8 +55,49 @@ export default function EmployeesPage() {
};
useEffect(() => {
loadEmployees(1, false, showOnlyActive);
}, [showOnlyActive]);
loadEmployees(1, false);
}, []);
// Создание чата с сотрудником
const handleStartChat = async (employee: User, e: React.MouseEvent) => {
e.preventDefault();
e.stopPropagation();
if (!currentUser || creatingChatFor === employee.id) return;
if (currentUser.id === employee.id) return; // Не создаем чат с собой
setCreatingChatFor(employee.id);
try {
// Ищем существующий приватный чат с этим пользователем
const chatsResponse = await apiClient.getChats();
const allChats = chatsResponse.results || chatsResponse;
const existingChat = allChats.find((chat: any) => {
if (chat.type !== 'private') return false;
const memberIds: number[] = chat.member_ids || [];
return memberIds.length === 2 &&
memberIds.includes(currentUser.id) &&
memberIds.includes(employee.id);
});
if (existingChat) {
router.push(`/messages/${existingChat.id}`);
} else {
const chat = await apiClient.createChat({
type: 'private',
name: 'Диалог',
participants: [employee.id]
});
router.push(`/messages/${chat.id}`);
}
} catch (err: any) {
console.error("Ошибка создания чата:", err);
alert("Не удалось открыть чат");
} finally {
setCreatingChatFor(null);
}
};
useEffect(() => {
if (!hasMore || loading) return;
@ -64,7 +110,7 @@ export default function EmployeesPage() {
const first = entries[0];
if (!first?.isIntersecting) return;
if (isFetchingRef.current || loadingMore || !hasMore) return;
loadEmployees(page + 1, true, showOnlyActive);
loadEmployees(page + 1, true);
},
{
root: null,
@ -111,52 +157,46 @@ export default function EmployeesPage() {
return (
<AppShell>
<div className="mb-4 flex items-center gap-3">
<button
onClick={() => setShowOnlyActive(false)}
className={`rounded-xl px-4 py-2 text-sm font-medium transition ${
!showOnlyActive
? 'bg-sky-400 text-white shadow-sm'
: 'bg-gray-100 text-gray-600 hover:bg-gray-200'
}`}
>
Все сотрудники
</button>
<button
onClick={() => setShowOnlyActive(true)}
className={`rounded-xl px-4 py-2 text-sm font-medium transition ${
showOnlyActive
? 'bg-sky-400 text-white shadow-sm'
: 'bg-gray-100 text-gray-600 hover:bg-gray-200'
}`}
>
Только активные
</button>
</div>
<div className="space-y-3">
{sortedEmployees.map((employee) => {
const fullName = `${employee.last_name} ${employee.first_name} ${employee.patronymic || ''}`.trim();
const initials = `${employee.last_name?.[0] || ''}${employee.first_name?.[0] || ''}`;
const position = employee.position?.name || 'Сотрудник';
const isCurrentUser = currentUser?.id === employee.id;
return (
<Link key={employee.id} href={`/users/${employee.id}`} className="flex items-center gap-4 rounded-2xl bg-white p-4 shadow-sm ring-1 ring-gray-100 transition hover:bg-gray-50">
<div className="flex h-12 w-12 items-center justify-center overflow-hidden rounded-full bg-sky-400 text-sm font-semibold text-white">
{employee.avatar ? (
<img src={employee.avatar} alt={fullName} className="h-full w-full object-cover" />
) : (
initials
)}
</div>
<div className="flex-1">
<p className="text-sm font-semibold text-gray-900">{fullName}</p>
<p className="text-xs text-gray-500">{position}</p>
{employee.email && (
<p className="mt-1 text-xs text-gray-500">{employee.email}</p>
)}
</div>
</Link>
<div key={employee.id} className="flex items-center gap-4 rounded-2xl bg-white p-4 shadow-sm ring-1 ring-gray-100">
<Link href={`/users/${employee.id}`} className="flex flex-1 items-center gap-4 transition hover:opacity-80">
<div className="flex h-12 w-12 items-center justify-center overflow-hidden rounded-full bg-sky-400 text-sm font-semibold text-white">
{employee.avatar ? (
<img src={employee.avatar} alt={fullName} className="h-full w-full object-cover" />
) : (
initials
)}
</div>
<div className="flex-1">
<p className="text-sm font-semibold text-gray-900">{fullName}</p>
<p className="text-xs text-gray-500">{position}</p>
{employee.email && (
<p className="mt-1 text-xs text-gray-500">{employee.email}</p>
)}
</div>
</Link>
{!isCurrentUser && (
<button
onClick={(e) => handleStartChat(employee, e)}
disabled={creatingChatFor === employee.id}
className="flex h-10 w-10 items-center justify-center rounded-full bg-sky-100 text-sky-600 transition hover:bg-sky-200 disabled:opacity-50"
title="Написать сообщение"
>
{creatingChatFor === employee.id ? (
<div className="h-4 w-4 animate-spin rounded-full border-2 border-sky-600 border-t-transparent" />
) : (
<MessageCircle size={18} />
)}
</button>
)}
</div>
);
})}