Добавлены новые функции для управления категориями: реализованы мутации для создания, обновления и удаления категорий. Обновлены компоненты админ-панели для отображения и управления категориями, улучшен интерфейс и адаптивность. Добавлены новые кнопки и обработчики событий для взаимодействия с категориями.
This commit is contained in:
@ -25,13 +25,15 @@ interface MessengerConversationsProps {
|
||||
loading: boolean
|
||||
selectedCounterparty: string | null
|
||||
onSelectCounterparty: (counterpartyId: string) => void
|
||||
compact?: boolean
|
||||
}
|
||||
|
||||
export function MessengerConversations({
|
||||
counterparties,
|
||||
loading,
|
||||
selectedCounterparty,
|
||||
onSelectCounterparty
|
||||
onSelectCounterparty,
|
||||
compact = false
|
||||
}: MessengerConversationsProps) {
|
||||
const [searchTerm, setSearchTerm] = useState('')
|
||||
|
||||
@ -129,22 +131,32 @@ export function MessengerConversations({
|
||||
return (
|
||||
<div className="flex flex-col h-full">
|
||||
{/* Заголовок */}
|
||||
<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">{counterparties.length} активных</p>
|
||||
{!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">{counterparties.length} активных</p>
|
||||
</div>
|
||||
</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">{counterparties.length}</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="Поиск по названию или ИНН..."
|
||||
placeholder={compact ? "Поиск..." : "Поиск по названию или ИНН..."}
|
||||
value={searchTerm}
|
||||
onChange={(e) => setSearchTerm(e.target.value)}
|
||||
className="glass-input text-white placeholder:text-white/40 pl-10 h-10"
|
||||
className={`glass-input text-white placeholder:text-white/40 pl-10 ${compact ? 'h-8 text-sm' : 'h-10'}`}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@ -164,41 +176,60 @@ export function MessengerConversations({
|
||||
<div
|
||||
key={org.id}
|
||||
onClick={() => onSelectCounterparty(org.id)}
|
||||
className={`p-3 rounded-lg cursor-pointer transition-all duration-200 ${
|
||||
className={`${compact ? 'p-2' : 'p-3'} rounded-lg cursor-pointer transition-all duration-200 ${
|
||||
selectedCounterparty === org.id
|
||||
? 'bg-white/20 border border-white/30'
|
||||
: 'bg-white/5 hover:bg-white/10 border border-white/10'
|
||||
}`}
|
||||
>
|
||||
<div className="flex items-start space-x-3">
|
||||
<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>
|
||||
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="flex items-center justify-between mb-1">
|
||||
<h4 className="text-white font-medium text-sm leading-tight truncate">
|
||||
{getOrganizationName(org)}
|
||||
</h4>
|
||||
<Badge className={`${getTypeColor(org.type)} text-xs flex-shrink-0 ml-2`}>
|
||||
{getTypeLabel(org.type)}
|
||||
</Badge>
|
||||
</div>
|
||||
|
||||
<p className="text-white/60 text-xs truncate">
|
||||
{getShortCompanyName(org.fullName || '')}
|
||||
</p>
|
||||
{compact ? (
|
||||
/* Компактный режим */
|
||||
<div className="flex items-center justify-center">
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
/* Обычный режим */
|
||||
<div className="flex items-start space-x-3">
|
||||
<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>
|
||||
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="flex items-center justify-between mb-1">
|
||||
<h4 className="text-white font-medium text-sm leading-tight truncate">
|
||||
{getOrganizationName(org)}
|
||||
</h4>
|
||||
<Badge className={`${getTypeColor(org.type)} text-xs flex-shrink-0 ml-2`}>
|
||||
{getTypeLabel(org.type)}
|
||||
</Badge>
|
||||
</div>
|
||||
|
||||
<p className="text-white/60 text-xs truncate">
|
||||
{getShortCompanyName(org.fullName || '')}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
))
|
||||
)}
|
||||
|
@ -1,14 +1,22 @@
|
||||
"use client"
|
||||
|
||||
import { useState } from 'react'
|
||||
import React, { useState, useRef, useCallback } from 'react'
|
||||
import { useQuery } from '@apollo/client'
|
||||
import { Card } from '@/components/ui/card'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Sidebar } from '@/components/dashboard/sidebar'
|
||||
import { MessengerConversations } from './messenger-conversations'
|
||||
import { MessengerChat } from './messenger-chat'
|
||||
import { MessengerEmptyState } from './messenger-empty-state'
|
||||
import { GET_MY_COUNTERPARTIES } from '@/graphql/queries'
|
||||
import { MessageCircle } from 'lucide-react'
|
||||
import {
|
||||
MessageCircle,
|
||||
PanelLeftOpen,
|
||||
PanelLeftClose,
|
||||
Maximize2,
|
||||
Minimize2,
|
||||
Settings
|
||||
} from 'lucide-react'
|
||||
|
||||
interface Organization {
|
||||
id: string
|
||||
@ -23,8 +31,14 @@ interface Organization {
|
||||
createdAt: string
|
||||
}
|
||||
|
||||
type LeftPanelSize = 'compact' | 'normal' | 'wide' | 'hidden'
|
||||
|
||||
export function MessengerDashboard() {
|
||||
const [selectedCounterparty, setSelectedCounterparty] = useState<string | null>(null)
|
||||
const [leftPanelSize, setLeftPanelSize] = useState<LeftPanelSize>('normal')
|
||||
const [isResizing, setIsResizing] = useState(false)
|
||||
const [leftPanelWidth, setLeftPanelWidth] = useState(350)
|
||||
const resizeRef = useRef<HTMLDivElement>(null)
|
||||
|
||||
const { data: counterpartiesData, loading: counterpartiesLoading } = useQuery(GET_MY_COUNTERPARTIES)
|
||||
const counterparties = counterpartiesData?.myCounterparties || []
|
||||
@ -35,6 +49,71 @@ export function MessengerDashboard() {
|
||||
|
||||
const selectedCounterpartyData = counterparties.find((cp: Organization) => cp.id === selectedCounterparty)
|
||||
|
||||
// Получение ширины для разных размеров панели
|
||||
const getPanelWidth = (size: LeftPanelSize) => {
|
||||
switch (size) {
|
||||
case 'hidden': return 0
|
||||
case 'compact': return 280
|
||||
case 'normal': return 350
|
||||
case 'wide': return 450
|
||||
default: return 350
|
||||
}
|
||||
}
|
||||
|
||||
const currentWidth = leftPanelSize === 'normal' ? leftPanelWidth : getPanelWidth(leftPanelSize)
|
||||
|
||||
// Обработка изменения размера панели
|
||||
const handleMouseDown = useCallback((e: React.MouseEvent) => {
|
||||
setIsResizing(true)
|
||||
e.preventDefault()
|
||||
}, [])
|
||||
|
||||
const handleMouseMove = useCallback((e: MouseEvent) => {
|
||||
if (!isResizing) return
|
||||
|
||||
const newWidth = Math.min(Math.max(280, e.clientX - 56 - 24), 600) // 56px sidebar + 24px padding
|
||||
setLeftPanelWidth(newWidth)
|
||||
setLeftPanelSize('normal')
|
||||
}, [isResizing])
|
||||
|
||||
const handleMouseUp = useCallback(() => {
|
||||
setIsResizing(false)
|
||||
}, [])
|
||||
|
||||
// Добавляем глобальные обработчики для изменения размера
|
||||
React.useEffect(() => {
|
||||
if (isResizing) {
|
||||
document.addEventListener('mousemove', handleMouseMove)
|
||||
document.addEventListener('mouseup', handleMouseUp)
|
||||
document.body.style.cursor = 'col-resize'
|
||||
document.body.style.userSelect = 'none'
|
||||
} else {
|
||||
document.removeEventListener('mousemove', handleMouseMove)
|
||||
document.removeEventListener('mouseup', handleMouseUp)
|
||||
document.body.style.cursor = ''
|
||||
document.body.style.userSelect = ''
|
||||
}
|
||||
|
||||
return () => {
|
||||
document.removeEventListener('mousemove', handleMouseMove)
|
||||
document.removeEventListener('mouseup', handleMouseUp)
|
||||
document.body.style.cursor = ''
|
||||
document.body.style.userSelect = ''
|
||||
}
|
||||
}, [isResizing, handleMouseMove, handleMouseUp])
|
||||
|
||||
// Переключение размеров панели
|
||||
const togglePanelSize = () => {
|
||||
const sizes: LeftPanelSize[] = ['compact', 'normal', 'wide']
|
||||
const currentIndex = sizes.indexOf(leftPanelSize)
|
||||
const nextIndex = (currentIndex + 1) % sizes.length
|
||||
setLeftPanelSize(sizes[nextIndex])
|
||||
}
|
||||
|
||||
const togglePanelVisibility = () => {
|
||||
setLeftPanelSize(leftPanelSize === 'hidden' ? 'normal' : 'hidden')
|
||||
}
|
||||
|
||||
// Если нет контрагентов, показываем заглушку
|
||||
if (!counterpartiesLoading && counterparties.length === 0) {
|
||||
return (
|
||||
@ -65,21 +144,88 @@ export function MessengerDashboard() {
|
||||
<Sidebar />
|
||||
<main className="flex-1 ml-56 px-6 py-4 overflow-hidden">
|
||||
<div className="h-full w-full flex flex-col">
|
||||
{/* Основной контент - сетка из 2 колонок */}
|
||||
<div className="flex-1 overflow-hidden">
|
||||
<div className="grid grid-cols-[350px_1fr] gap-4 h-full">
|
||||
{/* Левая колонка - список контрагентов */}
|
||||
<Card className="glass-card h-full overflow-hidden p-4">
|
||||
<MessengerConversations
|
||||
counterparties={counterparties}
|
||||
loading={counterpartiesLoading}
|
||||
selectedCounterparty={selectedCounterparty}
|
||||
onSelectCounterparty={handleSelectCounterparty}
|
||||
/>
|
||||
</Card>
|
||||
{/* Заголовок с управлением панелями */}
|
||||
<div className="flex items-center justify-between mb-4 flex-shrink-0">
|
||||
<div>
|
||||
<h1 className="text-xl font-bold text-white mb-1">Мессенджер</h1>
|
||||
<p className="text-white/70 text-sm">Общение с контрагентами</p>
|
||||
</div>
|
||||
|
||||
{/* Управление панелями */}
|
||||
<div className="flex items-center gap-2">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={togglePanelVisibility}
|
||||
className="bg-white/10 hover:bg-white/20 text-white border-white/20 h-8"
|
||||
title={leftPanelSize === 'hidden' ? 'Показать список' : 'Скрыть список'}
|
||||
>
|
||||
{leftPanelSize === 'hidden' ? (
|
||||
<PanelLeftOpen className="h-4 w-4" />
|
||||
) : (
|
||||
<PanelLeftClose className="h-4 w-4" />
|
||||
)}
|
||||
</Button>
|
||||
|
||||
{leftPanelSize !== 'hidden' && (
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={togglePanelSize}
|
||||
className="bg-white/10 hover:bg-white/20 text-white border-white/20 h-8"
|
||||
title="Изменить размер списка"
|
||||
>
|
||||
{leftPanelSize === 'compact' ? (
|
||||
<Minimize2 className="h-4 w-4" />
|
||||
) : leftPanelSize === 'wide' ? (
|
||||
<Maximize2 className="h-4 w-4" />
|
||||
) : (
|
||||
<Settings className="h-4 w-4" />
|
||||
)}
|
||||
<span className="ml-1 text-xs">
|
||||
{leftPanelSize === 'compact' ? 'Компакт' :
|
||||
leftPanelSize === 'wide' ? 'Широкий' : 'Обычный'}
|
||||
</span>
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Правая колонка - чат */}
|
||||
<Card className="glass-card h-full overflow-hidden">
|
||||
{/* Основной контент */}
|
||||
<div className="flex-1 overflow-hidden">
|
||||
<div className="flex gap-4 h-full">
|
||||
{/* Левая панель - список контрагентов */}
|
||||
{leftPanelSize !== 'hidden' && (
|
||||
<>
|
||||
<Card
|
||||
className="glass-card h-full overflow-hidden p-4 transition-all duration-200 ease-in-out"
|
||||
style={{ width: `${currentWidth}px` }}
|
||||
>
|
||||
<MessengerConversations
|
||||
counterparties={counterparties}
|
||||
loading={counterpartiesLoading}
|
||||
selectedCounterparty={selectedCounterparty}
|
||||
onSelectCounterparty={handleSelectCounterparty}
|
||||
compact={leftPanelSize === 'compact'}
|
||||
/>
|
||||
</Card>
|
||||
|
||||
{/* Разделитель для изменения размера */}
|
||||
{leftPanelSize === 'normal' && (
|
||||
<div
|
||||
ref={resizeRef}
|
||||
className="w-1 bg-white/10 hover:bg-white/20 cursor-col-resize transition-colors relative group"
|
||||
onMouseDown={handleMouseDown}
|
||||
>
|
||||
<div className="absolute inset-y-0 -inset-x-1 group-hover:bg-white/5 transition-colors" />
|
||||
<div className="absolute top-1/2 left-1/2 transform -translate-x-1/2 -translate-y-1/2 w-3 h-8 bg-white/20 rounded-full opacity-0 group-hover:opacity-100 transition-opacity" />
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* Правая панель - чат */}
|
||||
<Card className="glass-card h-full overflow-hidden flex-1">
|
||||
{selectedCounterparty && selectedCounterpartyData ? (
|
||||
<MessengerChat counterparty={selectedCounterpartyData} />
|
||||
) : (
|
||||
@ -92,6 +238,14 @@ export function MessengerDashboard() {
|
||||
<p className="text-white/40 text-sm">
|
||||
Начните беседу с одним из ваших контрагентов
|
||||
</p>
|
||||
{leftPanelSize === 'hidden' && (
|
||||
<Button
|
||||
onClick={togglePanelVisibility}
|
||||
className="mt-4 bg-purple-600 hover:bg-purple-700 text-white"
|
||||
>
|
||||
Показать список контрагентов
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
Reference in New Issue
Block a user