@ -1,319 +1,117 @@
import React , { useState } from 'react' ;
import { INITIAL_CONVERSATIONS , ConversationItem } from '../../mock/mockData' ;
import { mockApi } from '../../mock/mockApi' ;
import { Badge , Button , MaturityBadge , useToast } from '../../shared/ui' ;
import React , { useMemo , useState } from 'react' ;
import {
Send ,
Search ,
CheckCheck ,
Sparkles ,
Info ,
ArrowLeft , AtSign , Bot , CheckCheck , ChevronRight , Globe2 , Hash , LockKeyhole ,
Mail , MessageCircleMore , Paperclip , Plus , Search , Send , Settings2 , ShieldCheck , UsersRound ,
} from 'lucide-react' ;
import { Button , useToast } from '../../shared/ui' ;
type Section = 'messengers' | 'email' | 'channels' | 'settings' ;
type Message = { id : string ; mine? : boolean ; text : string ; time : string } ;
type Conversation = { id : string ; name : string ; preview : string ; time : string ; unread : number ; avatar : string ; messages : Message [ ] } ;
const messengerAccounts = [
{ id : 'telegram' , label : 'Telegram' , short : 'TG' , unread : 7 , color : 'bg-sky-500' } ,
{ id : 'max' , label : 'MAX' , short : 'M' , unread : 3 , color : 'bg-blue-600' } ,
{ id : 'whatsapp' , label : 'WhatsApp' , short : 'WA' , unread : 12 , color : 'bg-emerald-500' } ,
{ id : 'vk' , label : 'VK Мессенджер' , short : 'VK' , unread : 0 , color : 'bg-indigo-500' } ,
] ;
const emailAccounts = [
{ id : 'work' , label : 'Рабочая почта' , address : 'office@krona.systems' , unread : 9 , color : 'bg-krona-green' } ,
{ id : 'orders' , label : 'Заказы' , address : 'orders@krona.systems' , unread : 4 , color : 'bg-krona-gold' } ,
{ id : 'personal' , label : 'Личная почта' , address : 'owner@ochenstarik.ru' , unread : 1 , color : 'bg-violet-500' } ,
] ;
const conversations : Conversation [ ] = [
{ id : 'supplier' , name : 'Анна • Фуд-Сервис' , preview : 'Подтверждаем поставку на завтра' , time : '10:42' , unread : 3 , avatar : 'АФ' , messages : [
{ id : '1' , text : 'Добрый день! Получили вашу заявку на продукты.' , time : '10:35' } ,
{ id : '2' , mine : true , text : 'Анна, подтвердите доставку до 11:00.' , time : '10:38' } ,
{ id : '3' , text : 'Подтверждаем поставку. Машина будет к 10:30.' , time : '10:42' } ,
] } ,
{ id : 'client' , name : 'Мария Соколова' , preview : 'Можно перенести доставку на 19:00?' , time : '09:18' , unread : 2 , avatar : 'М С ' , messages : [
{ id : '1' , text : 'Здравствуйте! Можно перенести доставку на 19:00?' , time : '09:18' } ,
{ id : '2' , mine : true , text : 'Да, конечно. Уже изменили время доставки.' , time : '09:20' } ,
] } ,
{ id : 'team' , name : 'Алексей • Администратор' , preview : 'Закуп на завтра заполнен' , time : 'Вчера' , unread : 0 , avatar : 'А А ' , messages : [
{ id : '1' , text : 'Закуп заполнен, остатки по рыбе тоже указал.' , time : '18:05' } ,
{ id : '2' , mine : true , text : 'Принял, спасибо.' , time : '18:07' } ,
] } ,
] ;
const mailItems = [
{ id : 'm1' , from : 'Фуд-Сервис' , subject : 'Счёт и подтверждение поставки №1842' , preview : 'В о вложении направляем счёт на согласованную поставку…' , time : '10:31' } ,
{ id : 'm2' , from : 'Krona • Отчёты' , subject : 'Еженедельный отчёт по организациям' , preview : 'З а неделю подключено 12 новых пользователей…' , time : '08:00' } ,
{ id : 'm3' , from : 'Иван Петров' , subject : 'График смен на следующую неделю' , preview : 'Проверьте обновлённый график…' , time : 'Вчера' } ,
] ;
const workChannels = [
{ id : 'general' , label : 'Общий чат' , unread : 5 , icon : UsersRound } ,
{ id : 'purchases' , label : 'Закупки' , unread : 4 , icon : Hash } ,
{ id : 'managers' , label : 'Руководители' , unread : 0 , icon : LockKeyhole } ,
{ id : 'support' , label : 'Поддержка' , unread : 2 , icon : Hash } ,
] ;
const sections : Array < { id : Section ; label : string ; icon : React.ElementType ; count? : number } > = [
{ id : 'messengers' , label : 'Мессенджеры' , icon : MessageCircleMore , count : 22 } ,
{ id : 'email' , label : 'Почта' , icon : Mail , count : 14 } ,
{ id : 'channels' , label : 'Каналы' , icon : UsersRound , count : 11 } ,
{ id : 'settings' , label : 'Настройки' , icon : Settings2 } ,
] ;
function Count ( { value } : { value : number } ) {
return value ? < span className = "min-w-5 h-5 px-1.5 rounded-full bg-krona-gold text-white text-[11px] font-bold inline-flex items-center justify-center" > { value } < / span > : null ;
}
function AddButton ( { label } : { label : string } ) {
return < button className = "h-12 px-3 rounded-xl border border-dashed border-krona-border text-krona-secondary flex items-center gap-2 text-sm shrink-0" > < Plus className = "w-4 h-4" / > < span className = "hidden sm:inline" > { label } < / span > < / button > ;
}
function ConversationList ( { activeId , onSelect } : { activeId : string ; onSelect : ( id : string ) = > void } ) {
return < div className = "h-full min-h-0 flex flex-col" >
< div className = "p-3 border-b border-krona-border" > < label className = "relative block" > < Search className = "absolute left-3 top-3 w-4 h-4 text-krona-secondary" / > < input className = "w-full h-10 pl-9 pr-3 rounded-xl bg-krona-workspace border border-krona-border text-sm" placeholder = "Поиск диалогов" / > < / label > < / div >
< div className = "overflow-y-auto divide-y divide-krona-border/60" > { conversations . map ( ( item ) = > < button key = { item . id } onClick = { ( ) = > onSelect ( item . id ) } className = { ` w-full min-h-[76px] p-3 text-left flex gap-3 ${ activeId === item . id ? 'bg-krona-green/10' : 'hover:bg-krona-workspace' } ` } >
< span className = "w-11 h-11 rounded-full bg-krona-workspace border border-krona-border flex items-center justify-center text-xs font-bold shrink-0" > { item . avatar } < / span >
< span className = "min-w-0 flex-1" > < span className = "flex justify-between gap-2" > < strong className = "text-sm truncate" > { item . name } < / strong > < span className = "text-[11px] text-krona-secondary" > { item . time } < / span > < / span > < span className = "mt-1 flex items-center justify-between gap-2" > < span className = "text-xs text-krona-secondary truncate" > { item . preview } < / span > < Count value = { item . unread } / > < / span > < / span >
< / button > ) } < / div >
< / div > ;
}
function ConversationView ( { conversation , onBack } : { conversation : Conversation ; onBack : ( ) = > void } ) {
const { showToast } = useToast ( ) ;
const [ text , setText ] = useState ( '' ) ;
const [ sent , setSent ] = useState < Message [ ] > ( [ ] ) ;
const submit = ( event : React.FormEvent ) = > { event . preventDefault ( ) ; if ( ! text . trim ( ) ) return ; setSent ( ( old ) = > [ . . . old , { id : String ( Date . now ( ) ) , mine : true , text : text.trim ( ) , time : 'Сейчас' } ] ) ; setText ( '' ) ; showToast ( { title : 'Сообщение отправлено' , message : conversation.name , type : 'success' } ) ; } ;
return < div className = "h-full min-h-0 flex flex-col" >
< div className = "min-h-16 px-3 sm:px-4 border-b border-krona-border flex items-center gap-3" > < button onClick = { onBack } className = "md:hidden w-11 h-11 -ml-2 rounded-xl flex items-center justify-center" aria-label = "Назад" > < ArrowLeft className = "w-5 h-5" / > < / button > < span className = "w-10 h-10 rounded-full bg-krona-workspace border border-krona-border flex items-center justify-center text-xs font-bold" > { conversation . avatar } < / span > < span className = "min-w-0" > < strong className = "block text-sm truncate" > { conversation . name } < / strong > < span className = "text-xs text-emerald-600" > в с е т и < / span > < / span > < / div >
< div className = "flex-1 overflow-y-auto p-3 sm:p-5 space-y-3 bg-krona-workspace/40" > { [ . . . conversation . messages , . . . sent ] . map ( ( message ) = > < div key = { message . id } className = { ` flex ${ message . mine ? 'justify-end' : '' } ` } > < div className = { ` max-w-[86%] sm:max-w-[70%] px-3.5 py-2.5 rounded-2xl text-sm ${ message . mine ? 'bg-krona-green text-white rounded-br-md' : 'bg-krona-surface border border-krona-border rounded-bl-md' } ` } > < p > { message . text } < / p > < span className = "mt-1 flex items-center justify-end gap-1 text-[10px] opacity-70" > { message . time } { message . mine && < CheckCheck className = "w-3 h-3" / > } < / span > < / div > < / div > ) } < / div >
< form onSubmit = { submit } className = "p-2.5 sm:p-3 border-t border-krona-border flex items-end gap-2" > < button type = "button" className = "w-11 h-11 rounded-xl flex items-center justify-center" aria-label = "Прикрепить файл" > < Paperclip className = "w-5 h-5" / > < / button > < textarea rows = { 1 } value = { text } onChange = { ( e ) = > setText ( e . target . value ) } placeholder = "Сообщение" className = "min-h-11 max-h-28 flex-1 resize-none rounded-xl bg-krona-workspace border border-krona-border px-3 py-2.5 text-sm" / > < button type = "submit" className = "w-11 h-11 rounded-xl bg-krona-green text-white flex items-center justify-center" aria-label = "Отправить" > < Send className = "w-5 h-5" / > < / button > < / form >
< / div > ;
}
function Messengers() {
const [ account , setAccount ] = useState ( 'telegram' ) ; const [ activeId , setActiveId ] = useState ( conversations [ 0 ] . id ) ; const [ mobileChat , setMobileChat ] = useState ( false ) ;
const active = conversations . find ( ( item ) = > item . id === activeId ) ? ? conversations [ 0 ] ;
return < div className = "h-full min-h-0 flex flex-col" > < div className = "p-2.5 border-b border-krona-border flex gap-2 overflow-x-auto" > { messengerAccounts . map ( ( item ) = > < button key = { item . id } onClick = { ( ) = > setAccount ( item . id ) } title = { item . label } className = { ` relative h-12 min-w-12 px-2 rounded-xl border flex items-center justify-center gap-2 shrink-0 ${ account === item . id ? 'border-krona-green bg-krona-green/10' : 'border-krona-border' } ` } > < span className = { ` w-8 h-8 rounded-lg ${ item . color } text-white text-[10px] font-bold flex items-center justify-center ` } > { item . short } < / span > < span className = "hidden lg:inline text-sm" > { item . label } < / span > { item . unread > 0 && < span className = "absolute -top-1 -right-1" > < Count value = { item . unread } / > < / span > } < / button > ) } < AddButton label = "Подключить мессенджер" / > < / div > < div className = "flex-1 min-h-0 grid md:grid-cols-[320px_minmax(0,1fr)]" > < div className = { ` ${ mobileChat ? 'hidden' : 'block' } md:block border-r border-krona-border min-h-0 ` } > < ConversationList activeId = { activeId } onSelect = { ( id ) = > { setActiveId ( id ) ; setMobileChat ( true ) ; } } / > < / div > < div className = { ` ${ mobileChat ? 'block' : 'hidden' } md:block min-h-0 ` } > < ConversationView conversation = { active } onBack = { ( ) = > setMobileChat ( false ) } / > < / div > < / div > < / div > ;
}
function Email() {
const [ account , setAccount ] = useState ( emailAccounts [ 0 ] . id ) ; const [ mailId , setMailId ] = useState ( mailItems [ 0 ] . id ) ; const [ mobileMail , setMobileMail ] = useState ( false ) ; const mail = mailItems . find ( ( item ) = > item . id === mailId ) ? ? mailItems [ 0 ] ;
return < div className = "h-full min-h-0 flex flex-col" > < div className = "p-2.5 border-b border-krona-border flex gap-2 overflow-x-auto" > { emailAccounts . map ( ( item ) = > < button key = { item . id } onClick = { ( ) = > setAccount ( item . id ) } className = { ` h-12 px-3 rounded-xl border flex items-center gap-2 shrink-0 ${ account === item . id ? 'border-krona-green bg-krona-green/10' : 'border-krona-border' } ` } > < span className = { ` w-8 h-8 rounded-lg ${ item . color } text-white flex items-center justify-center ` } > < AtSign className = "w-4 h-4" / > < / span > < span className = "text-left" > < strong className = "block text-xs" > { item . label } < / strong > < span className = "hidden sm:block text-[10px] text-krona-secondary" > { item . address } < / span > < / span > < Count value = { item . unread } / > < / button > ) } < AddButton label = "Подключить почту" / > < / div > < div className = "flex-1 min-h-0 grid md:grid-cols-[300px_minmax(0,1fr)]" > < div className = { ` ${ mobileMail ? 'hidden' : 'block' } md:block border-r border-krona-border overflow-y-auto ` } > < div className = "p-3 border-b border-krona-border" > < Button className = "w-full" > < Plus className = "w-4 h-4 mr-2" / > Н о в о е п и с ь м о < / Button > < / div > { mailItems . map ( ( item ) = > < button key = { item . id } onClick = { ( ) = > { setMailId ( item . id ) ; setMobileMail ( true ) ; } } className = { ` w-full p-3 text-left border-b border-krona-border min-h-[92px] ${ mailId === item . id ? 'bg-krona-green/10' : '' } ` } > < span className = "flex justify-between gap-2" > < strong className = "text-sm truncate" > { item . from } < / strong > < span className = "text-[11px] text-krona-secondary" > { item . time } < / span > < / span > < span className = "block text-xs font-semibold mt-1 truncate" > { item . subject } < / span > < span className = "block text-xs text-krona-secondary mt-1 truncate" > { item . preview } < / span > < / button > ) } < / div > < article className = { ` ${ mobileMail ? 'flex' : 'hidden' } md:flex flex-col ` } > < header className = "p-3 sm:p-5 border-b border-krona-border" > < button onClick = { ( ) = > setMobileMail ( false ) } className = "md:hidden w-11 h-11 -ml-2 inline-flex items-center justify-center" > < ArrowLeft className = "w-5 h-5" / > < / button > < h3 className = "text-base sm:text-lg font-bold" > { mail . subject } < / h3 > < p className = "text-xs text-krona-secondary mt-1" > О т : { mail . from } • С е г о д н я , { mail . time } < / p > < / header > < div className = "flex-1 overflow-y-auto p-4 sm:p-6 text-sm leading-6" > < p > Д о б р ы й д е н ь ! < / p > < p className = "mt-4" > Н а п р а в л я е м д о к у м е н т ы и п о д т в е р ж д а е м с о г л а с о в а н н ы е у с л о в и я . П р о в е р ь т е в л о ж е н и е и с о о б щ и т е , е с л и п о т р е б у е т с я к о р р е к т и р о в к а . < / p > < / div > < div className = "p-3 border-t border-krona-border" > < Button > < Mail className = "w-4 h-4 mr-2" / > О т в е т и т ь < / Button > < / div > < / article > < / div > < / div > ;
}
function Channels() {
const [ channel , setChannel ] = useState ( 'general' ) ; const selected = workChannels . find ( ( item ) = > item . id === channel ) ? ? workChannels [ 0 ] ; const chat = useMemo < Conversation > ( ( ) = > ( { . . . conversations [ 2 ] , id : selected.id , name : selected.label , avatar : selected.label.slice ( 0 , 2 ) . toUpperCase ( ) } ) , [ selected ] ) ;
return < div className = "h-full min-h-0 flex flex-col md:grid md:grid-cols-[260px_minmax(0,1fr)]" > < aside className = "border-r border-krona-border p-2.5 flex md:flex-col gap-2 overflow-x-auto shrink-0" > { workChannels . map ( ( item ) = > { const Icon = item . icon ; return < button key = { item . id } onClick = { ( ) = > setChannel ( item . id ) } className = { ` h-12 px-3 rounded-xl flex items-center gap-2 shrink-0 text-sm ${ channel === item . id ? 'bg-krona-green text-white' : 'bg-krona-workspace' } ` } > < Icon className = "w-4 h-4" / > { item . label } < Count value = { item . unread } / > < / button > ; } ) } < AddButton label = "Создать канал" / > < / aside > < div className = "flex-1 min-h-0" > < ConversationView conversation = { chat } onBack = { ( ) = > undefined } / > < / div > < / div > ;
}
function Settings() {
const groups = [
[ MessageCircleMore , 'Мессенджеры' , 'Telegram, MAX, WhatsApp, VK и другие подключения' ] , [ Mail , 'Почтовые ящики' , 'Адреса организации, общие и личные ящики' ] , [ UsersRound , 'Каналы организации' , 'Общие, закрытые и проектные рабочие чаты' ] , [ ShieldCheck , 'Доступ сотрудников' , 'Роли, разрешения и видимость переписки' ] , [ Globe2 , 'VPN и сетевой шлюз' , 'Маршрутизация подключений и защищённый доступ' ] , [ Bot , 'Автоматизация и ИИ' , 'Распределение сообщений, ответы и правила обработки' ] ,
] as const ;
return < div className = "h-full overflow-y-auto p-3 sm:p-5" > < h3 className = "text-lg font-bold" > Н а с т р о й к и к о м м у н и к а ц и й < / h3 > < p className = "text-sm text-krona-secondary mt-1" > П о д к л ю ч е н и я и п р а в и л а в с е й о р г а н и з а ц и и с о б р а н ы в о д н о м м е с т е . < / p > < div className = "mt-5 max-w-5xl divide-y divide-krona-border border-y border-krona-border" > { groups . map ( ( [ Icon , title , text ] ) = > < button key = { title } className = "w-full min-h-[82px] py-3 flex items-center gap-3 text-left" > < span className = "w-11 h-11 rounded-xl bg-krona-workspace border border-krona-border flex items-center justify-center shrink-0" > < Icon className = "w-5 h-5 text-krona-green" / > < / span > < span className = "flex-1" > < strong className = "block text-sm" > { title } < / strong > < span className = "block text-xs text-krona-secondary mt-1" > { text } < / span > < / span > < ChevronRight className = "w-5 h-5 text-krona-secondary" / > < / button > ) } < / div > < / div > ;
}
export const CommunicationsModule : React.FC = ( ) = > {
const { showToast } = useToast ( ) ;
const [ conversations , setConversations ] = useState < ConversationItem [ ] > ( INITIAL_CONVERSATIONS ) ;
const [ activeConvId , setActiveConvId ] = useState < string > ( INITIAL_CONVERSATIONS [ 0 ] . id ) ;
const [ replyText , setReplyText ] = useState ( '' ) ;
const [ channelFilter , setChannelFilter ] = useState < string > ( 'all' ) ;
const [ isTyping , setIsTyping ] = useState ( false ) ;
const [ showCrmDrawer , setShowCrmDrawer ] = useState ( true ) ;
const activeConv = conversations . find ( ( c ) = > c . id === activeConvId ) || conversations [ 0 ] ;
const handleSend = async ( e : React.FormEvent ) = > {
e . preventDefault ( ) ;
if ( ! replyText . trim ( ) ) return ;
const userText = replyText ;
setReplyText ( '' ) ;
// Immediately post agent message
await mockApi . sendMessage ( activeConvId , userText ) ;
const updated = await mockApi . getMessages ( ) ;
setConversations ( updated ) ;
// Trigger simulated customer auto-reply
setIsTyping ( true ) ;
setTimeout ( async ( ) = > {
setIsTyping ( false ) ;
const simulatedReplies = [
'Спасибо за быстрый ответ! Будем ждать вас.' ,
'Отлично, подтверждаю заказ. Оплату проведу при получении.' ,
'Спасибо большое! Подскажите ещё, есть ли у вас десерты без с а х а р а ?' ,
'Супер, всё понятно, спасибо вашей поддержке!' ,
] ;
const autoText = simulatedReplies [ Math . floor ( Math . random ( ) * simulatedReplies . length ) ] ;
const currentConv = conversations . find ( c = > c . id === activeConvId ) ;
if ( currentConv ) {
const replyMsg = {
id : ` m-reply- ${ Date . now ( ) } ` ,
sender : 'client' as const ,
senderName : currentConv.clientName ,
text : autoText ,
time : new Date ( ) . toLocaleTimeString ( 'ru-RU' , { hour : '2-digit' , minute : '2-digit' } ) ,
} ;
setConversations ( prev = > prev . map ( c = > {
if ( c . id === activeConvId ) {
return {
. . . c ,
lastMessage : autoText ,
time : 'Только что' ,
messages : [ . . . c . messages , replyMsg ] ,
} ;
}
return c ;
} ) ) ;
showToast ( {
title : ` Новый ответ от ${ currentConv . clientName } ` ,
message : autoText ,
type : 'info' ,
} ) ;
}
} , 750 ) ;
} ;
const getChannelBadge = ( channel : ConversationItem [ 'channel' ] ) = > {
switch ( channel ) {
case 'telegram' :
return < Badge variant = "gold" size = "sm" > Telegram Bot < / Badge > ;
case 'whatsapp' :
return < Badge variant = "green" size = "sm" > WhatsApp < / Badge > ;
case 'max' :
return < Badge variant = "info" size = "sm" > MAX < / Badge > ;
case 'internal' :
return < Badge variant = "warning" size = "sm" > К о м а н д н ы й ч а т < / Badge > ;
default :
return < Badge variant = "neutral" size = "sm" > Ч а т < / Badge > ;
}
} ;
const filteredConversations = conversations . filter ( ( c ) = > {
if ( channelFilter === 'all' ) return true ;
return c . channel === channelFilter ;
} ) ;
return (
< div className = "space-y-4 animate-fade-in" >
{ /* Top Header with MaturityBadge */ }
< div className = "flex flex-col sm:flex-row sm:items-center justify-between gap-3 bg-krona-surface p-3.5 rounded-xl border border-krona-border shadow-krona-soft" >
< div >
< div className = "flex items-center gap-2" >
< h2 className = "text-sm font-bold text-krona-primary" >
Е д и н ы й ц е н т р к о м м у н и к а ц и й & О м н и к а н а л ь н ы й Inbox
< / h2 >
< MaturityBadge level = "target" size = "sm" / >
< / div >
< p className = "text-xs text-krona-secondary mt-0.5" >
Telegram , WhatsApp , MAX Messenger , Web - ч а т и в н у т р е н н и е к о м а н д н ы е к а н а л ы в о д н о м о к н е
< / p >
< / div >
< div className = "flex items-center gap-2 text-xs" >
< Button
variant = "secondary"
size = "sm"
onClick = { ( ) = > setShowCrmDrawer ( ! showCrmDrawer ) }
>
< Info className = "w-3.5 h-3.5 mr-1" / >
{ showCrmDrawer ? 'Скрыть CRM-карточку' : 'Показать CRM-карточку' }
< / Button >
< Badge variant = "green" size = "md" >
Онлайн : 4 к а н а л а а к т и в н ы
< / Badge >
< / div >
< / div >
< div className = "h-[calc(100vh-13rem)] flex flex-col md:flex-row gap-3 bg-krona-surface border border-krona-border rounded-xl shadow-krona-soft overflow-hidden" >
{ /* Left: Conversations List */ }
< div className = "w-full md:w-80 lg:w-96 border-r border-krona-border flex flex-col bg-krona-workspace/50" >
< div className = "p-3 border-b border-krona-border space-y-2" >
< div className = "flex items-center justify-between" >
< span className = "text-xs font-bold text-krona-primary uppercase tracking-wider" >
Д и а л о г и ( { filteredConversations . length } )
< / span >
< / div >
{ /* Channels Filter */ }
< div className = "flex items-center gap-1 overflow-x-auto text-[11px] pb-1" >
{ [
{ id : 'all' , label : 'В с е ' } ,
{ id : 'telegram' , label : 'Telegram' } ,
{ id : 'whatsapp' , label : 'WhatsApp' } ,
{ id : 'max' , label : 'MAX' } ,
{ id : 'internal' , label : 'Команда' } ,
] . map ( ( ch ) = > (
< button
key = { ch . id }
onClick = { ( ) = > setChannelFilter ( ch . id ) }
className = { ` px-2 py-0.5 rounded transition-colors whitespace-nowrap ${
channelFilter === ch . id
? 'bg-[#2F684C] text-white font-medium shadow-sm'
: 'bg-krona-surface text-krona-secondary hover:text-krona-primary border border-krona-border/60'
} ` }
>
{ ch . label }
< / button >
) ) }
< / div >
< div className = "relative" >
< Search className = "w-3.5 h-3.5 absolute left-2.5 top-2.5 text-krona-secondary" / >
< input
type = "text"
placeholder = "Поиск диалогов..."
className = "w-full pl-8 pr-3 py-1.5 text-xs bg-krona-surface border border-krona-border rounded-lg text-krona-primary placeholder:text-krona-secondary focus:outline-none"
/ >
< / div >
< / div >
< div className = "flex-1 overflow-y-auto divide-y divide-krona-border/40" >
{ filteredConversations . map ( ( conv ) = > (
< div
key = { conv . id }
onClick = { ( ) = > setActiveConvId ( conv . id ) }
className = { ` p-3 cursor-pointer transition-colors ${
conv . id === activeConvId
? 'bg-krona-surface font-medium border-l-2 border-krona-gold'
: 'hover:bg-krona-surface/60 text-krona-secondary'
} ` }
>
< div className = "flex items-center justify-between mb-1" >
< span className = "font-semibold text-xs text-krona-primary truncate" >
{ conv . clientName }
< / span >
< span className = "text-[10px] text-krona-secondary" > { conv . time } < / span >
< / div >
< p className = "text-xs text-krona-secondary truncate mb-1.5" >
{ conv . lastMessage }
< / p >
< div className = "flex items-center justify-between" >
{ getChannelBadge ( conv . channel ) }
{ conv . unreadCount > 0 && (
< span className = "w-4 h-4 rounded-full bg-krona-gold text-white text-[10px] font-bold flex items-center justify-center" >
{ conv . unreadCount }
< / span >
) }
< / div >
< / div >
) ) }
< / div >
< / div >
{ /* Center: Active Conversation Thread */ }
< div className = "flex-1 flex flex-col min-w-0 bg-krona-surface" >
{ /* Chat header */ }
< div className = "p-3.5 border-b border-krona-border flex items-center justify-between bg-krona-surface/80" >
< div className = "flex items-center gap-2.5" >
< div className = "w-8 h-8 rounded-full bg-krona-workspace border border-krona-border flex items-center justify-center font-bold text-xs text-krona-primary" >
{ activeConv . avatar }
< / div >
< div >
< h4 className = "text-xs font-bold text-krona-primary" > { activeConv . clientName } < / h4 >
< span className = "text-[10px] text-krona-secondary" > К а н а л с в я з и : { activeConv . channel } < / span >
< / div >
< / div >
{ getChannelBadge ( activeConv . channel ) }
< / div >
{ /* Message bubbles */ }
< div className = "flex-1 overflow-y-auto p-4 space-y-3" >
{ activeConv . messages . map ( ( msg ) = > (
< div
key = { msg . id }
className = { ` flex flex-col ${ msg . sender === 'agent' ? 'items-end' : 'items-start' } ` }
>
< span className = "text-[10px] text-krona-secondary mb-0.5 px-1" > { msg . senderName } < / span >
< div
className = { ` p-3 rounded-xl max-w-[75%] text-xs leading-relaxed shadow-krona-soft ${
msg . sender === 'agent'
? 'bg-[#2F684C] text-[#F8F6F0] rounded-br-none'
: 'bg-krona-workspace border border-krona-border text-krona-primary rounded-bl-none'
} ` }
>
< p > { msg . text } < / p >
< div className = "flex items-center justify-end gap-1 mt-1 text-[9px] opacity-70" >
< span > { msg . time } < / span >
{ msg . sender === 'agent' && < CheckCheck className = "w-3 h-3" / > }
< / div >
< / div >
< / div >
) ) }
{ isTyping && (
< div className = "flex items-center gap-2 text-xs text-krona-secondary bg-krona-workspace p-2 rounded-lg w-48 animate-pulse" >
< span className = "w-2 h-2 rounded-full bg-krona-gold animate-ping" > < / span >
< span > С о б е с е д н и к п е ч а т а е т . . . < / span >
< / div >
) }
< / div >
{ /* Reply form */ }
< div className = "p-3 border-t border-krona-border bg-krona-workspace/50" >
< form onSubmit = { handleSend } className = "flex items-center gap-2" >
< input
type = "text"
value = { replyText }
onChange = { ( e ) = > setReplyText ( e . target . value ) }
placeholder = "Напишите ответ клиенту или коллегам (симуляция авто-ответа)..."
className = "flex-1 bg-krona-surface border border-krona-border rounded-lg px-3 py-2 text-xs text-krona-primary placeholder:text-krona-secondary focus:outline-none focus:border-krona-green"
/ >
< Button type = "submit" variant = "primary" size = "sm" className = "px-3" >
< Send className = "w-3.5 h-3.5 mr-1" / >
О т п р а в и т ь
< / Button >
< / form >
< / div >
< / div >
{ /* Right CRM Context Drawer */ }
{ showCrmDrawer && (
< div className = "hidden lg:flex w-72 border-l border-krona-border flex-col bg-krona-surface p-3.5 space-y-4 text-xs" >
< div className = "flex items-center justify-between pb-2 border-b border-krona-border" >
< span className = "font-bold text-krona-primary uppercase text-[11px]" > CRM К о н т е к с т < / span >
< Badge variant = "gold" size = "sm" > VIP К л и е н т < / Badge >
< / div >
< div className = "space-y-2" >
< div >
< span className = "text-[10px] text-krona-secondary" > Ф И О к л и е н т а : < / span >
< div className = "font-bold text-krona-primary" > { activeConv . clientName } < / div >
< / div >
< div >
< span className = "text-[10px] text-krona-secondary" > Т е л е ф о н : < / span >
< div className = "font-mono text-krona-primary" > + 7 ( 913 ) 482 - 19 - 02 < / div >
< / div >
< div >
< span className = "text-[10px] text-krona-secondary" > LTV ( О б щ и й о б ъ е м з а к а з о в ) : < / span >
< div className = "font-bold text-krona-green font-mono" > 48 200 ₽ ( 14 ч е к о в ) < / div >
< / div >
< div >
< span className = "text-[10px] text-krona-secondary" > Л ю б и м ы е п о з и ц и и : < / span >
< div className = "text-krona-secondary text-[11px]" > Ф и л а д е л ь ф и я П р е м и у м , Т о м Я м < / div >
< / div >
< / div >
< div className = "p-2.5 bg-krona-workspace rounded-lg border border-krona-border space-y-1" >
< div className = "font-semibold text-krona-primary flex items-center gap-1" >
< Sparkles className = "w-3 h-3 text-krona-gold" / >
AI П о д с к а з к а о п е р а т о р у :
< / div >
< p className = "text-[11px] text-krona-secondary" >
Г о с т ь ч а с т о з а к а з ы в а е т п о п я т н и ц а м в 19 :30. П р е д л о ж и т е с е т О с е н н и й с о с к и д к о й п о с т о я н н о г о к л и е н т а .
< / p >
< / div >
< div className = "pt-2 border-t border-krona-border space-y-2" >
< Button variant = "outline" size = "sm" className = "w-full text-xs" >
О т к р ы т ь с д е л к у в CRM →
< / Button >
< / div >
< / div >
) }
< / div >
< / div >
) ;
const [ section , setSection ] = useState < Section > ( 'messengers' ) ;
return < div className = "h-[calc(100dvh-10.5rem)] md:h-[calc(100vh-8.5rem)] min-h-[560px] flex flex-col bg-krona-surface border border-krona-border rounded-2xl overflow-hidden shadow-krona-soft animate-fade-in" > < header className = "px-3 sm:px-5 pt-3 sm:pt-4 border-b border-krona-border shrink-0" > < div className = "flex items-center justify-between gap-3 mb-3" > < div > < h1 className = "text-base sm:text-lg font-bold" > К о м м у н и к а ц и и < / h1 > < p className = "hidden sm:block text-xs text-krona-secondary mt-0.5" > Л и ч н ы е м е с с е н д ж е р ы , п о ч т а и р а б о ч и е к а н а л ы в о д н о м о к н е < / p > < / div > < span className = "text-xs text-emerald-600 flex items-center gap-1.5" > < span className = "w-2 h-2 rounded-full bg-emerald-500" / > П о д к л ю ч е н о < / span > < / div > < nav className = "flex gap-1 overflow-x-auto -mb-px" aria-label = "Разделы коммуникаций" > { sections . map ( ( item ) = > { const Icon = item . icon ; return < button key = { item . id } onClick = { ( ) = > setSection ( item . id ) } className = { ` h-12 px-3 sm:px-4 flex items-center gap-2 border-b-2 text-sm font-medium whitespace-nowrap ${ section === item . id ? 'border-krona-green text-krona-green' : 'border-transparent text-krona-secondary' } ` } > < Icon className = "w-4 h-4" / > { item . label } { item . count ? < Count value = { item . count } / > : null } < / button > ; } ) } < / nav > < / header > < main className = "flex-1 min-h-0" > { section === 'messengers' && < Messengers / > } { section === 'email' && < Email / > } { section === 'channels' && < Channels / > } { section === 'settings' && < Settings / > } < / main > < / div > ;
} ;