Files
sfera/src/components/messenger/messenger-conversations.tsx

336 lines
13 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"use client"
import { Badge } from '@/components/ui/badge'
import { Input } from '@/components/ui/input'
import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar'
import { Users, Search } from 'lucide-react'
import { useState } from 'react'
interface Organization {
id: string
inn: string
name?: string
fullName?: string
managementName?: string
type: 'FULFILLMENT' | 'SELLER' | 'LOGIST' | 'WHOLESALE'
address?: string
phones?: Array<{ value: string }>
emails?: Array<{ value: string }>
users?: Array<{ id: string, avatar?: string, managerName?: string }>
createdAt: string
}
interface Conversation {
id: string
counterparty: Organization
lastMessage?: {
id: string
content?: string
type?: string
senderId: string
isRead: boolean
createdAt: string
}
unreadCount: number
updatedAt: string
}
interface MessengerConversationsProps {
conversations?: Conversation[]
counterparties: Organization[]
loading: boolean
selectedCounterparty: string | null
onSelectCounterparty: (counterpartyId: string) => void
onRefresh?: () => void
compact?: boolean
}
export function MessengerConversations({
conversations = [],
counterparties,
loading,
selectedCounterparty,
onSelectCounterparty,
onRefresh,
compact = false
}: MessengerConversationsProps) {
const [searchTerm, setSearchTerm] = useState('')
const getOrganizationName = (org: Organization) => {
return org.name || org.fullName || 'Организация'
}
const getManagerName = (org: Organization) => {
return org.users?.[0]?.managerName || 'Управляющий'
}
const getInitials = (org: Organization) => {
const name = getOrganizationName(org)
return name.charAt(0).toUpperCase()
}
const getShortCompanyName = (fullName: string) => {
if (!fullName) return 'Полное название не указано'
// Словарь для замены полных форм на сокращенные
const legalFormReplacements: { [key: string]: string } = {
'Общество с ограниченной ответственностью': 'ООО',
'Открытое акционерное общество': 'ОАО',
'Закрытое акционерное общество': 'ЗАО',
'Публичное акционерное общество': 'ПАО',
'Непубличное акционерное общество': 'НАО',
'Акционерное общество': 'АО',
'Индивидуальный предприниматель': 'ИП',
'Товарищество с ограниченной ответственностью': 'ТОО',
'Частное предприятие': 'ЧП',
'Субъект предпринимательской деятельности': 'СПД'
}
let result = fullName
// Заменяем полные формы на сокращенные
for (const [fullForm, shortForm] of Object.entries(legalFormReplacements)) {
const regex = new RegExp(`^${fullForm}\\s+`, 'i')
if (regex.test(result)) {
result = result.replace(regex, `${shortForm} `)
break
}
}
return result
}
const getTypeLabel = (type: string) => {
switch (type) {
case 'FULFILLMENT':
return 'Фулфилмент'
case 'SELLER':
return 'Селлер'
case 'LOGIST':
return 'Логистика'
case 'WHOLESALE':
return 'Оптовик'
default:
return type
}
}
const getTypeColor = (type: string) => {
switch (type) {
case 'FULFILLMENT':
return 'bg-blue-500/20 text-blue-300 border-blue-500/30'
case 'SELLER':
return 'bg-green-500/20 text-green-300 border-green-500/30'
case 'LOGIST':
return 'bg-orange-500/20 text-orange-300 border-orange-500/30'
case 'WHOLESALE':
return 'bg-purple-500/20 text-purple-300 border-purple-500/30'
default:
return 'bg-gray-500/20 text-gray-300 border-gray-500/30'
}
}
// Объединяем чаты и контрагентов, приоритет у чатов
const allCounterparties = new Map<string, { org: Organization; conversation?: Conversation }>()
// Сначала добавляем из чатов
conversations.forEach(conv => {
allCounterparties.set(conv.counterparty.id, {
org: conv.counterparty,
conversation: conv
})
})
// Затем добавляем остальных контрагентов, если их еще нет
counterparties.forEach(org => {
if (!allCounterparties.has(org.id)) {
allCounterparties.set(org.id, { org })
}
})
const filteredCounterparties = Array.from(allCounterparties.values()).filter(({ org }) => {
if (!searchTerm) return true
const name = getOrganizationName(org).toLowerCase()
const managerName = getManagerName(org).toLowerCase()
const inn = org.inn.toLowerCase()
const search = searchTerm.toLowerCase()
return name.includes(search) || inn.includes(search) || managerName.includes(search)
}).sort((a, b) => {
// Сортируем: сначала с активными чатами, потом по времени последнего сообщения
if (a.conversation && !b.conversation) return -1
if (!a.conversation && b.conversation) return 1
if (a.conversation && b.conversation) {
return new Date(b.conversation.updatedAt).getTime() - new Date(a.conversation.updatedAt).getTime()
}
return 0
})
if (loading) {
return (
<div className="flex items-center justify-center p-8">
<div className="text-white/60">Загрузка...</div>
</div>
)
}
return (
<div className="flex flex-col h-full">
{/* Заголовок */}
{!compact && (
<div className="flex items-center space-x-3 mb-4">
<Users className="h-5 w-5 text-blue-400" />
<div>
<h3 className="text-lg font-semibold text-white">Контрагенты</h3>
<p className="text-white/60 text-sm">
{conversations.length > 0 ? `${conversations.length} активных чатов` : `${counterparties.length} контрагентов`}
</p>
</div>
</div>
)}
{/* Компактный заголовок */}
{compact && (
<div className="flex items-center justify-center mb-3">
<Users className="h-4 w-4 text-blue-400 mr-2" />
<span className="text-white font-medium text-sm">{allCounterparties.size}</span>
</div>
)}
{/* Поиск */}
<div className="relative mb-4">
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 h-4 w-4 text-white/40" />
<Input
placeholder={compact ? "Поиск..." : "Поиск по названию или ИНН..."}
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
className={`glass-input text-white placeholder:text-white/40 pl-10 ${compact ? 'h-8 text-sm' : 'h-10'}`}
/>
</div>
{/* Список контрагентов */}
<div className="flex-1 overflow-auto space-y-2">
{filteredCounterparties.length === 0 ? (
<div className="text-center py-8">
<div className="w-12 h-12 bg-white/10 rounded-full flex items-center justify-center mx-auto mb-3">
<Users className="h-6 w-6 text-white/40" />
</div>
<p className="text-white/60 text-sm">
{searchTerm ? 'Ничего не найдено' : 'Контрагенты не найдены'}
</p>
</div>
) : (
filteredCounterparties.map(({ org, conversation }) => (
<div
key={org.id}
onClick={() => {
onSelectCounterparty(org.id)
// Если есть непрочитанные сообщения и функция обновления, вызываем её
if (conversation?.unreadCount && conversation.unreadCount > 0 && onRefresh) {
setTimeout(() => onRefresh(), 100) // Небольшая задержка для UI
}
}}
className={`${compact ? 'p-2' : 'p-3'} rounded-lg cursor-pointer transition-all duration-200 relative ${
selectedCounterparty === org.id
? 'bg-white/20 border border-white/30'
: 'bg-white/5 hover:bg-white/10 border border-white/10'
}`}
>
{compact ? (
/* Компактный режим */
<div className="flex items-center justify-center relative">
<Avatar className="h-8 w-8">
{org.users?.[0]?.avatar ? (
<AvatarImage
src={org.users[0].avatar}
alt="Аватар организации"
className="w-full h-full object-cover"
/>
) : null}
<AvatarFallback className="bg-purple-500 text-white text-xs">
{getInitials(org)}
</AvatarFallback>
</Avatar>
{/* Индикатор непрочитанных сообщений для компактного режима */}
{conversation?.unreadCount && conversation.unreadCount > 0 && (
<div className="absolute -top-1 -right-1 w-5 h-5 bg-red-500 text-white text-xs rounded-full flex items-center justify-center font-bold">
{conversation.unreadCount > 9 ? '9+' : conversation.unreadCount}
</div>
)}
</div>
) : (
/* Обычный режим */
<div className="flex items-start space-x-3">
<div className="relative">
<Avatar className="h-10 w-10 flex-shrink-0">
{org.users?.[0]?.avatar ? (
<AvatarImage
src={org.users[0].avatar}
alt="Аватар организации"
className="w-full h-full object-cover"
/>
) : null}
<AvatarFallback className="bg-purple-500 text-white text-sm">
{getInitials(org)}
</AvatarFallback>
</Avatar>
{/* Индикатор непрочитанных сообщений */}
{conversation?.unreadCount && conversation.unreadCount > 0 && (
<div className="absolute -top-1 -right-1 w-5 h-5 bg-red-500 text-white text-xs rounded-full flex items-center justify-center font-bold">
{conversation.unreadCount > 9 ? '9+' : conversation.unreadCount}
</div>
)}
</div>
<div className="flex-1 min-w-0">
<div className="flex items-center justify-between mb-1">
<h4 className={`font-medium text-sm leading-tight truncate ${
conversation?.unreadCount && conversation.unreadCount > 0
? 'text-white'
: 'text-white/80'
}`}>
{getOrganizationName(org)}
</h4>
<div className="flex items-center space-x-2">
{conversation?.unreadCount && conversation.unreadCount > 0 && (
<Badge className="bg-red-500/20 text-red-300 border-red-500/30 text-xs">
{conversation.unreadCount}
</Badge>
)}
<Badge className={`${getTypeColor(org.type)} text-xs flex-shrink-0`}>
{getTypeLabel(org.type)}
</Badge>
</div>
</div>
{conversation?.lastMessage ? (
<p className={`text-xs truncate ${
conversation.unreadCount && conversation.unreadCount > 0
? 'text-white/80'
: 'text-white/60'
}`}>
{conversation.lastMessage.type === 'TEXT'
? conversation.lastMessage.content
: conversation.lastMessage.type === 'VOICE'
? '🎵 Голосовое сообщение'
: conversation.lastMessage.type === 'IMAGE'
? '🖼️ Изображение'
: conversation.lastMessage.type === 'FILE'
? '📄 Файл'
: 'Сообщение'
}
</p>
) : (
<p className="text-white/60 text-xs truncate">
{getShortCompanyName(org.fullName || '')}
</p>
)}
</div>
</div>
)}
</div>
))
)}
</div>
</div>
)
}