/** * ContactsList Component v2 * ADR-024: Chat Contacts with improved tabs * * New structure: * - Контакты (Contacts) - all users with filter toolbar * - AI Чат (Quick Chat) - AI agents for quick chat * - Задачи (Tasks) - rows from configured table with columns */ import { useState, useMemo, useRef, useEffect } from 'react'; import { useQuery } from '@tanstack/react-query'; import { Users, Bot, User, ListTodo, Search, Zap, X, ChevronRight, Loader2, Settings, Database, Table2, Hash, MoreVertical, MessageSquare, UserPlus, UserMinus, Calendar, Clock } from 'lucide-react'; import { cn } from '@/shared/utils/cn'; import { apiClient } from '@/shared/utils/apiClient'; import { AIAgent } from '../types'; export type ContactType = 'human' | 'agent-user'; export interface Contact { id: number; type: ContactType; name: string; email?: string; avatar?: string; icon?: string; description?: string; status?: 'online' | 'offline' | 'away'; agentTableId?: number; agentRowId?: number; lastMessage?: string; lastMessageTime?: Date; unreadCount?: number; } interface UserData { id: number; name: string; email?: string; avatar_url?: string; user_type?: 'human' | 'agent'; managed_by_agent_table_id?: number; managed_by_agent_row_id?: number; } export interface TasksSourceConfig { tableId: number; tableName: string; tableIcon?: string; displayColumn?: string; // Additional column mappings deadlineColumn?: string; statusColumn?: string; priorityColumn?: string; } export interface ContactsListProps { agents: AIAgent[]; onSelectAgent: (agent: AIAgent) => void; onSelectUser?: (userId: number) => void; onSelectTask?: (taskId: number, tableId: number) => void; onNewQuickChat?: () => void; onConfigureTasks?: () => void; onAddToChat?: (userId: number) => void; onRemoveFromChat?: (userId: number) => void; currentAgentId?: number; spaceId?: number; tasksSource?: TasksSourceConfig; chatParticipantIds?: number[]; className?: string; } type TabKey = 'contacts' | 'ai-chat' | 'tasks'; type ContactFilter = 'all' | 'humans' | 'agents'; // Contact Menu Component function ContactMenu({ contact, isInChat, onAddToChat, onRemoveFromChat, onStartChat }: { contact: Contact; isInChat: boolean; onAddToChat?: () => void; onRemoveFromChat?: () => void; onStartChat?: () => void; }) { const [isOpen, setIsOpen] = useState(false); const menuRef = useRef(null); useEffect(() => { const handleClickOutside = (e: MouseEvent) => { if (menuRef.current && !menuRef.current.contains(e.target as Node)) { setIsOpen(false); } }; if (isOpen) { document.addEventListener('mousedown', handleClickOutside); } return () => document.removeEventListener('mousedown', handleClickOutside); }, [isOpen]); return (
{isOpen && (
{isInChat ? ( ) : ( )}
)}
); } // Status Badge Component function StatusBadge({ status }: { status?: 'online' | 'offline' | 'away' }) { const config = { online: { color: 'bg-green-500', label: 'В сети' }, away: { color: 'bg-yellow-500', label: 'Отошёл' }, offline: { color: 'bg-gray-400', label: 'Не в сети' } }; const s = config[status || 'offline']; return ( {s.label} ); } export function ContactsList({ agents, onSelectAgent, onSelectUser, onSelectTask, onNewQuickChat, onConfigureTasks, onAddToChat, onRemoveFromChat, currentAgentId, spaceId, tasksSource, chatParticipantIds = [], className }: ContactsListProps) { const [activeTab, setActiveTab] = useState('contacts'); const [searchQuery, setSearchQuery] = useState(''); const [contactFilter, setContactFilter] = useState('all'); // Fetch ALL users const { data: users = [], isLoading: isLoadingUsers } = useQuery({ queryKey: ['chat-users-all', spaceId], queryFn: async () => { const response = await apiClient.get<{ success: boolean; data: UserData[]; }>('/users'); return response.success ? response.data : []; } }); // Fetch tasks from configured table const { data: taskRows = [], isLoading: isLoadingTasks } = useQuery({ queryKey: ['task-rows', tasksSource?.tableId], queryFn: async () => { if (!tasksSource?.tableId) return []; const response = await apiClient.get<{ success: boolean; data: { rows: Array<{ id: number; data: Record }> }; }>(`/tables/${tasksSource.tableId}/rows?limit=100`); return response.success ? response.data.rows : []; }, enabled: !!tasksSource?.tableId && activeTab === 'tasks' }); // Convert users to contacts const allContacts: Contact[] = useMemo(() => { return users.map(user => ({ id: user.id, type: user.managed_by_agent_table_id != null ? 'agent-user' as const : 'human' as const, name: user.name, email: user.email, avatar: user.avatar_url, agentTableId: user.managed_by_agent_table_id, agentRowId: user.managed_by_agent_row_id, status: 'offline' as const // TODO: real status })); }, [users]); const humanContacts = useMemo(() => allContacts.filter(c => c.type === 'human'), [allContacts]); const agentUserContacts = useMemo(() => allContacts.filter(c => c.type === 'agent-user'), [allContacts]); // Filter contacts const filteredContacts = useMemo(() => { let contacts = allContacts; if (contactFilter === 'humans') { contacts = humanContacts; } else if (contactFilter === 'agents') { contacts = agentUserContacts; } if (searchQuery.trim()) { const query = searchQuery.toLowerCase(); contacts = contacts.filter(c => c.name.toLowerCase().includes(query) || c.email?.toLowerCase().includes(query) ); } return contacts; }, [allContacts, humanContacts, agentUserContacts, contactFilter, searchQuery]); // Filter AI agents by search const filteredAgents = useMemo(() => { if (!searchQuery.trim()) return agents; const query = searchQuery.toLowerCase(); return agents.filter(a => a.name.toLowerCase().includes(query) || a.description?.toLowerCase().includes(query) ); }, [agents, searchQuery]); const handleContactClick = (contact: Contact) => { onSelectUser?.(contact.id); }; const handleTaskClick = (taskId: number) => { if (tasksSource) { onSelectTask?.(taskId, tasksSource.tableId); } }; const tabs = [ { key: 'contacts' as TabKey, label: 'Контакты', icon: , count: allContacts.length }, { key: 'ai-chat' as TabKey, label: 'AI Чат', icon: , count: agents.length }, { key: 'tasks' as TabKey, label: 'Задачи', icon: }, ]; return (
{/* Tabs */}
{tabs.map(tab => ( ))}
{/* Toolbar - Always visible */}
{/* Search */}
setSearchQuery(e.target.value)} placeholder={ activeTab === 'contacts' ? 'Поиск контактов...' : activeTab === 'ai-chat' ? 'Поиск агентов...' : 'Поиск задач...' } className="w-full pl-8 pr-8 py-1.5 text-sm rounded-lg bg-[var(--bg-tertiary)] text-[var(--text-primary)] placeholder:text-[var(--text-tertiary)] border-none focus:outline-none focus:ring-2 focus:ring-[var(--color-primary-500)]/30" /> {searchQuery && ( )}
{/* Filter for Contacts tab */} {activeTab === 'contacts' && ( )} {/* Settings for Tasks tab */} {activeTab === 'tasks' && onConfigureTasks && ( )}
{/* Content */}
{/* Contacts Tab */} {activeTab === 'contacts' && ( isLoadingUsers ? (
) : filteredContacts.length === 0 ? (

{searchQuery ? 'Ничего не найдено' : 'Нет контактов'}

) : (
{filteredContacts.map(contact => { const isInChat = chatParticipantIds.includes(contact.id); return (
{/* Avatar */} {/* Status Badge */} {/* Menu */} onAddToChat?.(contact.id)} onRemoveFromChat={() => onRemoveFromChat?.(contact.id)} onStartChat={() => handleContactClick(contact)} />
); })}
) )} {/* AI Chat Tab */} {activeTab === 'ai-chat' && ( filteredAgents.length === 0 ? (

{searchQuery ? 'Агенты не найдены' : 'Нет доступных агентов'}

) : (
{filteredAgents.map(agent => ( ))}
) )} {/* Tasks Tab */} {activeTab === 'tasks' && ( tasksSource ? (
{/* Source header */}
{tasksSource.tableIcon || '📋'} {tasksSource.tableName}
{isLoadingTasks ? (
) : taskRows.length === 0 ? (
Нет записей в таблице
) : ( taskRows.map(row => { const displayCol = tasksSource.displayColumn || 'name'; const title = String(row.data[displayCol] || row.data['title'] || row.data['name'] || `#${row.id}`); const deadline = tasksSource.deadlineColumn ? row.data[tasksSource.deadlineColumn] as string | null : null; const status = tasksSource.statusColumn ? row.data[tasksSource.statusColumn] as string | null : null; return ( ); }) )}
) : (

Источник не настроен

Выберите таблицу для отображения записей

{onConfigureTasks && ( )}
) )}
); }