Добавлены новые функции для управления категориями: реализованы мутации для создания, обновления и удаления категорий. Обновлены компоненты админ-панели для отображения и управления категориями, улучшен интерфейс и адаптивность. Добавлены новые кнопки и обработчики событий для взаимодействия с категориями.

This commit is contained in:
Bivekich
2025-07-19 17:09:40 +03:00
parent 965482b617
commit 8d57fcd748
12 changed files with 1733 additions and 67 deletions

View File

@ -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>
))
)}

View File

@ -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>
)}