Оптимизирована производительность React компонентов с помощью мемоизации
КРИТИЧНЫЕ КОМПОНЕНТЫ ОПТИМИЗИРОВАНЫ: • AdminDashboard (346 kB) - добавлены React.memo, useCallback, useMemo • SellerStatisticsDashboard (329 kB) - мемоизация кэша и callback функций • CreateSupplyPage (276 kB) - оптимизированы вычисления и обработчики • EmployeesDashboard (268 kB) - мемоизация списков и функций • SalesTab + AdvertisingTab - React.memo обертка ТЕХНИЧЕСКИЕ УЛУЧШЕНИЯ: ✅ React.memo() для предотвращения лишних рендеров ✅ useMemo() для тяжелых вычислений ✅ useCallback() для стабильных ссылок на функции ✅ Мемоизация фильтрации и сортировки списков ✅ Оптимизация пропсов в компонентах-контейнерах РЕЗУЛЬТАТЫ: • Все компоненты успешно компилируются • Линтер проходит без критических ошибок • Сохранена вся функциональность • Улучшена производительность рендеринга • Снижена нагрузка на React дерево 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@ -1,7 +1,8 @@
|
||||
"use client"
|
||||
'use client'
|
||||
|
||||
import { Building, Users, Target, Briefcase } from 'lucide-react'
|
||||
|
||||
import { Card } from '@/components/ui/card'
|
||||
import { Building, Users, Target, Briefcase } from 'lucide-react'
|
||||
|
||||
export function MarketBusiness() {
|
||||
return (
|
||||
@ -23,9 +24,7 @@ export function MarketBusiness() {
|
||||
<Building className="h-8 w-8 text-orange-400" />
|
||||
<h4 className="text-lg font-semibold text-white">Франшизы</h4>
|
||||
</div>
|
||||
<p className="text-white/60 text-sm">
|
||||
Готовые бизнес-решения и франшизы в сфере логистики и торговли
|
||||
</p>
|
||||
<p className="text-white/60 text-sm">Готовые бизнес-решения и франшизы в сфере логистики и торговли</p>
|
||||
</Card>
|
||||
|
||||
<Card className="bg-white/5 backdrop-blur border-white/10 p-6">
|
||||
@ -33,9 +32,7 @@ export function MarketBusiness() {
|
||||
<Users className="h-8 w-8 text-blue-400" />
|
||||
<h4 className="text-lg font-semibold text-white">Партнёрство</h4>
|
||||
</div>
|
||||
<p className="text-white/60 text-sm">
|
||||
Поиск бизнес-партнёров для совместных проектов и развития
|
||||
</p>
|
||||
<p className="text-white/60 text-sm">Поиск бизнес-партнёров для совместных проектов и развития</p>
|
||||
</Card>
|
||||
|
||||
<Card className="bg-white/5 backdrop-blur border-white/10 p-6">
|
||||
@ -43,9 +40,7 @@ export function MarketBusiness() {
|
||||
<Target className="h-8 w-8 text-green-400" />
|
||||
<h4 className="text-lg font-semibold text-white">Консалтинг</h4>
|
||||
</div>
|
||||
<p className="text-white/60 text-sm">
|
||||
Бизнес-консультации и стратегическое планирование развития
|
||||
</p>
|
||||
<p className="text-white/60 text-sm">Бизнес-консультации и стратегическое планирование развития</p>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
@ -54,11 +49,9 @@ export function MarketBusiness() {
|
||||
<Briefcase className="h-8 w-8 text-white/40" />
|
||||
</div>
|
||||
<p className="text-white/60 text-lg mb-2">Раздел в разработке</p>
|
||||
<p className="text-white/40 text-sm">
|
||||
Бизнес-функционал будет доступен в ближайших обновлениях
|
||||
</p>
|
||||
<p className="text-white/40 text-sm">Бизнес-функционал будет доступен в ближайших обновлениях</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -1,11 +1,12 @@
|
||||
"use client"
|
||||
'use client'
|
||||
|
||||
import { useQuery } from '@apollo/client'
|
||||
import { Card } from '@/components/ui/card'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { GET_CATEGORIES, GET_MY_CART } from '@/graphql/queries'
|
||||
import { Package2, ArrowRight, Sparkles, ShoppingCart, Heart } from 'lucide-react'
|
||||
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Card } from '@/components/ui/card'
|
||||
import { GET_CATEGORIES, GET_MY_CART } from '@/graphql/queries'
|
||||
|
||||
interface Category {
|
||||
id: string
|
||||
name: string
|
||||
@ -59,12 +60,8 @@ export function MarketCategories({ onSelectCategory, onShowCart, onShowFavorites
|
||||
<Package2 className="h-8 w-8 text-purple-400" />
|
||||
</div>
|
||||
<div>
|
||||
<h1 className="text-2xl font-bold text-white mb-1">
|
||||
Каталог товаров
|
||||
</h1>
|
||||
<p className="text-white/60">
|
||||
Выберите категорию для просмотра товаров от поставщиков
|
||||
</p>
|
||||
<h1 className="text-2xl font-bold text-white mb-1">Каталог товаров</h1>
|
||||
<p className="text-white/60">Выберите категорию для просмотра товаров от поставщиков</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -80,7 +77,7 @@ export function MarketCategories({ onSelectCategory, onShowCart, onShowFavorites
|
||||
Избранное
|
||||
</Button>
|
||||
)}
|
||||
|
||||
|
||||
{onShowCart && (
|
||||
<Button
|
||||
onClick={onShowCart}
|
||||
@ -99,18 +96,14 @@ export function MarketCategories({ onSelectCategory, onShowCart, onShowFavorites
|
||||
<div className="glass-card p-8">
|
||||
<div className="text-center">
|
||||
<Package2 className="h-16 w-16 text-white/20 mx-auto mb-4" />
|
||||
<h3 className="text-lg font-semibold text-white mb-2">
|
||||
Категории отсутствуют
|
||||
</h3>
|
||||
<p className="text-white/60">
|
||||
Пока нет доступных категорий товаров
|
||||
</p>
|
||||
<h3 className="text-lg font-semibold text-white mb-2">Категории отсутствуют</h3>
|
||||
<p className="text-white/60">Пока нет доступных категорий товаров</p>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-6">
|
||||
{/* Карточка "Все товары" */}
|
||||
<Card
|
||||
<Card
|
||||
onClick={() => onSelectCategory('', 'Все товары')}
|
||||
className="group relative overflow-hidden bg-gradient-to-br from-indigo-500/10 via-purple-500/10 to-pink-500/10 backdrop-blur border-white/10 hover:border-white/20 transition-all duration-300 cursor-pointer hover:scale-105"
|
||||
>
|
||||
@ -125,12 +118,10 @@ export function MarketCategories({ onSelectCategory, onShowCart, onShowFavorites
|
||||
<h3 className="text-lg font-semibold text-white group-hover:text-white transition-colors">
|
||||
Все товары
|
||||
</h3>
|
||||
<p className="text-white/60 text-sm">
|
||||
Просмотреть весь каталог
|
||||
</p>
|
||||
<p className="text-white/60 text-sm">Просмотреть весь каталог</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
{/* Эффект при наведении */}
|
||||
<div className="absolute inset-0 bg-gradient-to-r from-indigo-500/5 to-purple-500/5 opacity-0 group-hover:opacity-100 transition-opacity duration-300" />
|
||||
</Card>
|
||||
@ -145,27 +136,27 @@ export function MarketCategories({ onSelectCategory, onShowCart, onShowFavorites
|
||||
'from-yellow-500/10 via-orange-500/10 to-red-500/10',
|
||||
'from-pink-500/10 via-rose-500/10 to-purple-500/10',
|
||||
'from-indigo-500/10 via-blue-500/10 to-cyan-500/10',
|
||||
'from-teal-500/10 via-green-500/10 to-emerald-500/10'
|
||||
'from-teal-500/10 via-green-500/10 to-emerald-500/10',
|
||||
]
|
||||
|
||||
|
||||
const borderColors = [
|
||||
'border-purple-500/30',
|
||||
'border-blue-500/30',
|
||||
'border-blue-500/30',
|
||||
'border-green-500/30',
|
||||
'border-orange-500/30',
|
||||
'border-pink-500/30',
|
||||
'border-indigo-500/30',
|
||||
'border-teal-500/30'
|
||||
'border-teal-500/30',
|
||||
]
|
||||
|
||||
const iconColors = [
|
||||
'text-purple-400',
|
||||
'text-blue-400',
|
||||
'text-green-400',
|
||||
'text-green-400',
|
||||
'text-orange-400',
|
||||
'text-pink-400',
|
||||
'text-indigo-400',
|
||||
'text-teal-400'
|
||||
'text-teal-400',
|
||||
]
|
||||
|
||||
const bgColors = [
|
||||
@ -175,7 +166,7 @@ export function MarketCategories({ onSelectCategory, onShowCart, onShowFavorites
|
||||
'from-yellow-500/20 to-orange-500/20',
|
||||
'from-pink-500/20 to-rose-500/20',
|
||||
'from-indigo-500/20 to-blue-500/20',
|
||||
'from-teal-500/20 to-green-500/20'
|
||||
'from-teal-500/20 to-green-500/20',
|
||||
]
|
||||
|
||||
const gradient = gradients[index % gradients.length]
|
||||
@ -184,7 +175,7 @@ export function MarketCategories({ onSelectCategory, onShowCart, onShowFavorites
|
||||
const bgColor = bgColors[index % bgColors.length]
|
||||
|
||||
return (
|
||||
<Card
|
||||
<Card
|
||||
key={category.id}
|
||||
onClick={() => onSelectCategory(category.id, category.name)}
|
||||
className={`group relative overflow-hidden bg-gradient-to-br ${gradient} backdrop-blur border-white/10 hover:${borderColor} transition-all duration-300 cursor-pointer hover:scale-105`}
|
||||
@ -200,14 +191,14 @@ export function MarketCategories({ onSelectCategory, onShowCart, onShowFavorites
|
||||
<h3 className="text-lg font-semibold text-white group-hover:text-white transition-colors">
|
||||
{category.name}
|
||||
</h3>
|
||||
<p className="text-white/60 text-sm">
|
||||
Товары категории
|
||||
</p>
|
||||
<p className="text-white/60 text-sm">Товары категории</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
{/* Эффект при наведении */}
|
||||
<div className={`absolute inset-0 bg-gradient-to-r ${gradient} opacity-0 group-hover:opacity-50 transition-opacity duration-300`} />
|
||||
<div
|
||||
className={`absolute inset-0 bg-gradient-to-r ${gradient} opacity-0 group-hover:opacity-50 transition-opacity duration-300`}
|
||||
/>
|
||||
</Card>
|
||||
)
|
||||
})}
|
||||
@ -216,4 +207,4 @@ export function MarketCategories({ onSelectCategory, onShowCart, onShowFavorites
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -1,15 +1,8 @@
|
||||
"use client"
|
||||
'use client'
|
||||
|
||||
import React, { useState, useMemo } from 'react'
|
||||
import { useQuery, useMutation } from '@apollo/client'
|
||||
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { GlassInput } from '@/components/ui/input'
|
||||
import { Badge } from '@/components/ui/badge'
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'
|
||||
import {
|
||||
Users,
|
||||
import {
|
||||
Users,
|
||||
ArrowUpCircle,
|
||||
ArrowDownCircle,
|
||||
Search,
|
||||
@ -21,12 +14,25 @@ import {
|
||||
Phone,
|
||||
Mail,
|
||||
MapPin,
|
||||
X
|
||||
X,
|
||||
} from 'lucide-react'
|
||||
import { OrganizationCard } from './organization-card'
|
||||
import { OrganizationAvatar } from './organization-avatar'
|
||||
import { GET_MY_COUNTERPARTIES, GET_INCOMING_REQUESTS, GET_OUTGOING_REQUESTS, SEARCH_ORGANIZATIONS } from '@/graphql/queries'
|
||||
import React, { useState, useMemo } from 'react'
|
||||
|
||||
import { Badge } from '@/components/ui/badge'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { GlassInput } from '@/components/ui/input'
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'
|
||||
import { RESPOND_TO_COUNTERPARTY_REQUEST, CANCEL_COUNTERPARTY_REQUEST, REMOVE_COUNTERPARTY } from '@/graphql/mutations'
|
||||
import {
|
||||
GET_MY_COUNTERPARTIES,
|
||||
GET_INCOMING_REQUESTS,
|
||||
GET_OUTGOING_REQUESTS,
|
||||
SEARCH_ORGANIZATIONS,
|
||||
} from '@/graphql/queries'
|
||||
|
||||
import { OrganizationAvatar } from './organization-avatar'
|
||||
import { OrganizationCard } from './organization-card'
|
||||
|
||||
interface Organization {
|
||||
id: string
|
||||
@ -38,7 +44,7 @@ interface Organization {
|
||||
phones?: Array<{ value: string }>
|
||||
emails?: Array<{ value: string }>
|
||||
createdAt: string
|
||||
users?: Array<{ id: string, avatar?: string }>
|
||||
users?: Array<{ id: string; avatar?: string }>
|
||||
}
|
||||
|
||||
interface CounterpartyRequest {
|
||||
@ -70,9 +76,9 @@ export function MarketCounterparties() {
|
||||
{ query: SEARCH_ORGANIZATIONS, variables: { type: 'SELLER' } },
|
||||
{ query: SEARCH_ORGANIZATIONS, variables: { type: 'FULFILLMENT' } },
|
||||
{ query: SEARCH_ORGANIZATIONS, variables: { type: 'LOGIST' } },
|
||||
{ query: SEARCH_ORGANIZATIONS, variables: { type: 'WHOLESALE' } }
|
||||
{ query: SEARCH_ORGANIZATIONS, variables: { type: 'WHOLESALE' } },
|
||||
],
|
||||
awaitRefetchQueries: true
|
||||
awaitRefetchQueries: true,
|
||||
})
|
||||
|
||||
const [cancelRequest] = useMutation(CANCEL_COUNTERPARTY_REQUEST, {
|
||||
@ -81,9 +87,9 @@ export function MarketCounterparties() {
|
||||
{ query: SEARCH_ORGANIZATIONS, variables: { type: 'SELLER' } },
|
||||
{ query: SEARCH_ORGANIZATIONS, variables: { type: 'FULFILLMENT' } },
|
||||
{ query: SEARCH_ORGANIZATIONS, variables: { type: 'LOGIST' } },
|
||||
{ query: SEARCH_ORGANIZATIONS, variables: { type: 'WHOLESALE' } }
|
||||
{ query: SEARCH_ORGANIZATIONS, variables: { type: 'WHOLESALE' } },
|
||||
],
|
||||
awaitRefetchQueries: true
|
||||
awaitRefetchQueries: true,
|
||||
})
|
||||
|
||||
const [removeCounterparty] = useMutation(REMOVE_COUNTERPARTY, {
|
||||
@ -92,22 +98,23 @@ export function MarketCounterparties() {
|
||||
{ query: SEARCH_ORGANIZATIONS, variables: { type: 'SELLER' } },
|
||||
{ query: SEARCH_ORGANIZATIONS, variables: { type: 'FULFILLMENT' } },
|
||||
{ query: SEARCH_ORGANIZATIONS, variables: { type: 'LOGIST' } },
|
||||
{ query: SEARCH_ORGANIZATIONS, variables: { type: 'WHOLESALE' } }
|
||||
{ query: SEARCH_ORGANIZATIONS, variables: { type: 'WHOLESALE' } },
|
||||
],
|
||||
awaitRefetchQueries: true
|
||||
awaitRefetchQueries: true,
|
||||
})
|
||||
|
||||
// Фильтрация и сортировка контрагентов
|
||||
const filteredAndSortedCounterparties = useMemo(() => {
|
||||
const filtered = (counterpartiesData?.myCounterparties || []).filter((org: Organization) => {
|
||||
const matchesSearch = !searchQuery ||
|
||||
(org.name?.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
||||
org.fullName?.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
||||
org.inn.includes(searchQuery) ||
|
||||
org.address?.toLowerCase().includes(searchQuery.toLowerCase()))
|
||||
|
||||
const matchesSearch =
|
||||
!searchQuery ||
|
||||
org.name?.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
||||
org.fullName?.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
||||
org.inn.includes(searchQuery) ||
|
||||
org.address?.toLowerCase().includes(searchQuery.toLowerCase())
|
||||
|
||||
const matchesType = typeFilter === 'all' || org.type === typeFilter
|
||||
|
||||
|
||||
return matchesSearch && matchesType
|
||||
})
|
||||
|
||||
@ -150,7 +157,7 @@ export function MarketCounterparties() {
|
||||
const handleAcceptRequest = async (requestId: string) => {
|
||||
try {
|
||||
await respondToRequest({
|
||||
variables: { requestId, accept: true }
|
||||
variables: { requestId, accept: true },
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('Ошибка при принятии заявки:', error)
|
||||
@ -160,7 +167,7 @@ export function MarketCounterparties() {
|
||||
const handleRejectRequest = async (requestId: string) => {
|
||||
try {
|
||||
await respondToRequest({
|
||||
variables: { requestId, accept: false }
|
||||
variables: { requestId, accept: false },
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('Ошибка при отклонении заявки:', error)
|
||||
@ -170,7 +177,7 @@ export function MarketCounterparties() {
|
||||
const handleCancelRequest = async (requestId: string) => {
|
||||
try {
|
||||
await cancelRequest({
|
||||
variables: { requestId }
|
||||
variables: { requestId },
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('Ошибка при отмене заявки:', error)
|
||||
@ -180,7 +187,7 @@ export function MarketCounterparties() {
|
||||
const handleRemoveCounterparty = async (organizationId: string) => {
|
||||
try {
|
||||
await removeCounterparty({
|
||||
variables: { organizationId }
|
||||
variables: { organizationId },
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('Ошибка при удалении контрагента:', error)
|
||||
@ -191,7 +198,7 @@ export function MarketCounterparties() {
|
||||
if (!dateString) return ''
|
||||
try {
|
||||
let date: Date
|
||||
|
||||
|
||||
// Проверяем, является ли строка числом (Unix timestamp)
|
||||
if (/^\d+$/.test(dateString)) {
|
||||
// Если это Unix timestamp в миллисекундах
|
||||
@ -201,15 +208,15 @@ export function MarketCounterparties() {
|
||||
// Обычная строка даты
|
||||
date = new Date(dateString)
|
||||
}
|
||||
|
||||
|
||||
if (isNaN(date.getTime())) {
|
||||
return 'Неверная дата'
|
||||
}
|
||||
|
||||
|
||||
return date.toLocaleDateString('ru-RU', {
|
||||
year: 'numeric',
|
||||
month: 'long',
|
||||
day: 'numeric'
|
||||
day: 'numeric',
|
||||
})
|
||||
} catch {
|
||||
return 'Ошибка даты'
|
||||
@ -231,21 +238,31 @@ export function MarketCounterparties() {
|
||||
|
||||
const getTypeLabel = (type: string) => {
|
||||
switch (type) {
|
||||
case 'FULFILLMENT': return 'Фулфилмент'
|
||||
case 'SELLER': return 'Селлер'
|
||||
case 'LOGIST': return 'Логистика'
|
||||
case 'WHOLESALE': return 'Поставщик'
|
||||
default: return type
|
||||
case 'FULFILLMENT':
|
||||
return 'Фулфилмент'
|
||||
case 'SELLER':
|
||||
return 'Селлер'
|
||||
case 'LOGIST':
|
||||
return 'Логистика'
|
||||
case 'WHOLESALE':
|
||||
return 'Поставщик'
|
||||
default:
|
||||
return type
|
||||
}
|
||||
}
|
||||
|
||||
const getTypeBadgeStyles = (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'
|
||||
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'
|
||||
}
|
||||
}
|
||||
|
||||
@ -254,18 +271,27 @@ export function MarketCounterparties() {
|
||||
<div className="flex-1 overflow-hidden">
|
||||
<Tabs defaultValue="counterparties" className="h-full flex flex-col">
|
||||
<TabsList className="grid w-full grid-cols-3 bg-white/5 border-white/10">
|
||||
<TabsTrigger value="counterparties" className="data-[state=active]:bg-blue-500/20 data-[state=active]:text-blue-300">
|
||||
<TabsTrigger
|
||||
value="counterparties"
|
||||
className="data-[state=active]:bg-blue-500/20 data-[state=active]:text-blue-300"
|
||||
>
|
||||
<Users className="h-4 w-4 mr-2" />
|
||||
Контрагенты ({counterparties.length})
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="incoming" className={`data-[state=active]:bg-green-500/20 data-[state=active]:text-green-300 relative ${incomingRequests.length > 0 ? 'ring-2 ring-green-400/50 animate-pulse' : ''}`}>
|
||||
<TabsTrigger
|
||||
value="incoming"
|
||||
className={`data-[state=active]:bg-green-500/20 data-[state=active]:text-green-300 relative ${incomingRequests.length > 0 ? 'ring-2 ring-green-400/50 animate-pulse' : ''}`}
|
||||
>
|
||||
<ArrowDownCircle className="h-4 w-4 mr-2" />
|
||||
Входящие ({incomingRequests.length})
|
||||
{incomingRequests.length > 0 && (
|
||||
<div className="absolute -top-1 -right-1 w-3 h-3 bg-green-500 rounded-full"></div>
|
||||
)}
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="outgoing" className="data-[state=active]:bg-orange-500/20 data-[state=active]:text-orange-300">
|
||||
<TabsTrigger
|
||||
value="outgoing"
|
||||
className="data-[state=active]:bg-orange-500/20 data-[state=active]:text-orange-300"
|
||||
>
|
||||
<ArrowUpCircle className="h-4 w-4 mr-2" />
|
||||
Исходящие ({outgoingRequests.length})
|
||||
</TabsTrigger>
|
||||
@ -348,7 +374,7 @@ export function MarketCounterparties() {
|
||||
{['FULFILLMENT', 'SELLER', 'LOGIST', 'WHOLESALE'].map((type) => {
|
||||
const count = counterparties.filter((org: Organization) => org.type === type).length
|
||||
if (count === 0) return null
|
||||
|
||||
|
||||
return (
|
||||
<Button
|
||||
key={type}
|
||||
@ -356,8 +382,8 @@ export function MarketCounterparties() {
|
||||
size="sm"
|
||||
onClick={() => setTypeFilter(typeFilter === type ? 'all' : type)}
|
||||
className={`h-6 px-2 text-xs ${
|
||||
typeFilter === type
|
||||
? getTypeBadgeStyles(type) + ' border'
|
||||
typeFilter === type
|
||||
? getTypeBadgeStyles(type) + ' border'
|
||||
: 'text-white/50 hover:text-white hover:bg-white/10'
|
||||
}`}
|
||||
>
|
||||
@ -382,9 +408,7 @@ export function MarketCounterparties() {
|
||||
<>
|
||||
<Users className="h-12 w-12 text-white/20 mx-auto mb-4" />
|
||||
<p className="text-white/60">У вас пока нет контрагентов</p>
|
||||
<p className="text-white/40 text-sm mt-2">
|
||||
Перейдите на другие вкладки, чтобы найти партнеров
|
||||
</p>
|
||||
<p className="text-white/40 text-sm mt-2">Перейдите на другие вкладки, чтобы найти партнеров</p>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
@ -415,34 +439,34 @@ export function MarketCounterparties() {
|
||||
</Badge>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center text-white/60 text-sm">
|
||||
<Building className="h-3 w-3 mr-2 flex-shrink-0" />
|
||||
<span>ИНН: {organization.inn}</span>
|
||||
</div>
|
||||
|
||||
|
||||
{organization.address && (
|
||||
<div className="flex items-start text-white/60 text-sm">
|
||||
<MapPin className="h-3 w-3 mr-2 mt-0.5 flex-shrink-0" />
|
||||
<span className="line-clamp-2">{organization.address}</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
|
||||
{organization.phones && organization.phones.length > 0 && (
|
||||
<div className="flex items-center text-white/60 text-sm">
|
||||
<Phone className="h-3 w-3 mr-2 flex-shrink-0" />
|
||||
<span>{organization.phones[0].value}</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
|
||||
{organization.emails && organization.emails.length > 0 && (
|
||||
<div className="flex items-center text-white/60 text-sm">
|
||||
<Mail className="h-3 w-3 mr-2 flex-shrink-0" />
|
||||
<span className="truncate">{organization.emails[0].value}</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
|
||||
<div className="flex items-center text-white/40 text-xs pt-2">
|
||||
<Calendar className="h-3 w-3 mr-2 flex-shrink-0" />
|
||||
<span>Добавлен {formatDate(organization.createdAt)}</span>
|
||||
@ -450,7 +474,7 @@ export function MarketCounterparties() {
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
@ -497,7 +521,7 @@ export function MarketCounterparties() {
|
||||
</Badge>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div className="space-y-2">
|
||||
<p className="text-white/60 text-sm">ИНН: {request.sender.inn}</p>
|
||||
{request.sender.address && (
|
||||
@ -516,7 +540,7 @@ export function MarketCounterparties() {
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div className="flex space-x-2">
|
||||
<Button
|
||||
size="sm"
|
||||
@ -569,16 +593,24 @@ export function MarketCounterparties() {
|
||||
<Badge className={getTypeBadgeStyles(request.receiver.type)}>
|
||||
{getTypeLabel(request.receiver.type)}
|
||||
</Badge>
|
||||
<Badge className={
|
||||
request.status === 'PENDING' ? 'bg-yellow-500/20 text-yellow-300 border-yellow-500/30' :
|
||||
request.status === 'REJECTED' ? 'bg-red-500/20 text-red-300 border-red-500/30' :
|
||||
'bg-gray-500/20 text-gray-300 border-gray-500/30'
|
||||
}>
|
||||
{request.status === 'PENDING' ? 'Ожидает ответа' : request.status === 'REJECTED' ? 'Отклонено' : request.status}
|
||||
<Badge
|
||||
className={
|
||||
request.status === 'PENDING'
|
||||
? 'bg-yellow-500/20 text-yellow-300 border-yellow-500/30'
|
||||
: request.status === 'REJECTED'
|
||||
? 'bg-red-500/20 text-red-300 border-red-500/30'
|
||||
: 'bg-gray-500/20 text-gray-300 border-gray-500/30'
|
||||
}
|
||||
>
|
||||
{request.status === 'PENDING'
|
||||
? 'Ожидает ответа'
|
||||
: request.status === 'REJECTED'
|
||||
? 'Отклонено'
|
||||
: request.status}
|
||||
</Badge>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div className="space-y-2">
|
||||
<p className="text-white/60 text-sm">ИНН: {request.receiver.inn}</p>
|
||||
{request.receiver.address && (
|
||||
@ -597,7 +629,7 @@ export function MarketCounterparties() {
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
{request.status === 'PENDING' && (
|
||||
<Button
|
||||
size="sm"
|
||||
@ -618,4 +650,4 @@ export function MarketCounterparties() {
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -1,17 +1,20 @@
|
||||
"use client"
|
||||
'use client'
|
||||
|
||||
import { useState } from 'react'
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'
|
||||
import { Card } from '@/components/ui/card'
|
||||
|
||||
import { Sidebar } from '@/components/dashboard/sidebar'
|
||||
import { Card } from '@/components/ui/card'
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'
|
||||
import { useSidebar } from '@/hooks/useSidebar'
|
||||
import { MarketProducts } from './market-products'
|
||||
import { MarketCategories } from './market-categories'
|
||||
import { MarketRequests } from './market-requests'
|
||||
import { MarketInvestments } from './market-investments'
|
||||
import { MarketBusiness } from './market-business'
|
||||
|
||||
import { FavoritesDashboard } from '../favorites/favorites-dashboard'
|
||||
|
||||
import { MarketBusiness } from './market-business'
|
||||
import { MarketCategories } from './market-categories'
|
||||
import { MarketInvestments } from './market-investments'
|
||||
import { MarketProducts } from './market-products'
|
||||
import { MarketRequests } from './market-requests'
|
||||
|
||||
export function MarketDashboard() {
|
||||
const { getSidebarMargin } = useSidebar()
|
||||
const [productsView, setProductsView] = useState<'categories' | 'products' | 'cart' | 'favorites'>('categories')
|
||||
@ -44,8 +47,8 @@ export function MarketDashboard() {
|
||||
<div className="h-full w-full flex flex-col">
|
||||
{/* Основной контент с табами */}
|
||||
<div className="flex-1 overflow-hidden">
|
||||
<Tabs
|
||||
defaultValue="investments"
|
||||
<Tabs
|
||||
defaultValue="investments"
|
||||
className="h-full flex flex-col"
|
||||
onValueChange={(value) => {
|
||||
if (value === 'products') {
|
||||
@ -56,26 +59,26 @@ export function MarketDashboard() {
|
||||
}}
|
||||
>
|
||||
<TabsList className="grid w-full grid-cols-4 bg-white/5 backdrop-blur border-white/10 flex-shrink-0">
|
||||
<TabsTrigger
|
||||
value="investments"
|
||||
<TabsTrigger
|
||||
value="investments"
|
||||
className="data-[state=active]:bg-white/20 data-[state=active]:text-white text-white/70"
|
||||
>
|
||||
Инвестиции
|
||||
</TabsTrigger>
|
||||
<TabsTrigger
|
||||
value="business"
|
||||
<TabsTrigger
|
||||
value="business"
|
||||
className="data-[state=active]:bg-white/20 data-[state=active]:text-white text-white/70"
|
||||
>
|
||||
Бизнес
|
||||
</TabsTrigger>
|
||||
<TabsTrigger
|
||||
value="products"
|
||||
<TabsTrigger
|
||||
value="products"
|
||||
className="data-[state=active]:bg-white/20 data-[state=active]:text-white text-white/70"
|
||||
>
|
||||
Товары
|
||||
</TabsTrigger>
|
||||
<TabsTrigger
|
||||
value="requests"
|
||||
<TabsTrigger
|
||||
value="requests"
|
||||
className="data-[state=active]:bg-white/20 data-[state=active]:text-white text-white/70"
|
||||
>
|
||||
Заявки
|
||||
@ -103,9 +106,13 @@ export function MarketDashboard() {
|
||||
<TabsContent value="products" className="flex-1 overflow-hidden mt-6">
|
||||
<Card className="glass-card h-full overflow-hidden p-0">
|
||||
{productsView === 'categories' ? (
|
||||
<MarketCategories onSelectCategory={handleSelectCategory} onShowCart={handleShowCart} onShowFavorites={handleShowFavorites} />
|
||||
<MarketCategories
|
||||
onSelectCategory={handleSelectCategory}
|
||||
onShowCart={handleShowCart}
|
||||
onShowFavorites={handleShowFavorites}
|
||||
/>
|
||||
) : productsView === 'products' ? (
|
||||
<MarketProducts
|
||||
<MarketProducts
|
||||
selectedCategoryId={selectedCategory?.id}
|
||||
selectedCategoryName={selectedCategory?.name}
|
||||
onBackToCategories={handleBackToCategories}
|
||||
@ -123,4 +130,4 @@ export function MarketDashboard() {
|
||||
</main>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -1,13 +1,15 @@
|
||||
"use client"
|
||||
'use client'
|
||||
|
||||
import { useState } from 'react'
|
||||
import { useQuery, useMutation } from '@apollo/client'
|
||||
import { Search, Package } from 'lucide-react'
|
||||
import { useState } from 'react'
|
||||
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Input } from '@/components/ui/input'
|
||||
import { Search, Package } from 'lucide-react'
|
||||
import { OrganizationCard } from './organization-card'
|
||||
import { SEARCH_ORGANIZATIONS, GET_INCOMING_REQUESTS, GET_OUTGOING_REQUESTS } from '@/graphql/queries'
|
||||
import { SEND_COUNTERPARTY_REQUEST } from '@/graphql/mutations'
|
||||
import { SEARCH_ORGANIZATIONS, GET_INCOMING_REQUESTS, GET_OUTGOING_REQUESTS } from '@/graphql/queries'
|
||||
|
||||
import { OrganizationCard } from './organization-card'
|
||||
|
||||
interface Organization {
|
||||
id: string
|
||||
@ -19,7 +21,7 @@ interface Organization {
|
||||
phones?: Array<{ value: string }>
|
||||
emails?: Array<{ value: string }>
|
||||
createdAt: string
|
||||
users?: Array<{ id: string, avatar?: string }>
|
||||
users?: Array<{ id: string; avatar?: string }>
|
||||
isCounterparty?: boolean
|
||||
isCurrentUser?: boolean
|
||||
hasOutgoingRequest?: boolean
|
||||
@ -30,7 +32,7 @@ export function MarketFulfillment() {
|
||||
const [searchTerm, setSearchTerm] = useState('')
|
||||
|
||||
const { data, loading, refetch } = useQuery(SEARCH_ORGANIZATIONS, {
|
||||
variables: { type: 'FULFILLMENT', search: searchTerm || null }
|
||||
variables: { type: 'FULFILLMENT', search: searchTerm || null },
|
||||
})
|
||||
|
||||
const [sendRequest, { loading: sendingRequest }] = useMutation(SEND_COUNTERPARTY_REQUEST, {
|
||||
@ -40,9 +42,9 @@ export function MarketFulfillment() {
|
||||
{ query: SEARCH_ORGANIZATIONS, variables: { type: 'LOGIST' } },
|
||||
{ query: SEARCH_ORGANIZATIONS, variables: { type: 'WHOLESALE' } },
|
||||
{ query: GET_OUTGOING_REQUESTS },
|
||||
{ query: GET_INCOMING_REQUESTS }
|
||||
{ query: GET_INCOMING_REQUESTS },
|
||||
],
|
||||
awaitRefetchQueries: true
|
||||
awaitRefetchQueries: true,
|
||||
})
|
||||
|
||||
const handleSearch = () => {
|
||||
@ -54,8 +56,8 @@ export function MarketFulfillment() {
|
||||
await sendRequest({
|
||||
variables: {
|
||||
organizationId: organizationId,
|
||||
message: message || 'Заявка на добавление в контрагенты'
|
||||
}
|
||||
message: message || 'Заявка на добавление в контрагенты',
|
||||
},
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('Ошибка отправки заявки:', error)
|
||||
@ -78,7 +80,7 @@ export function MarketFulfillment() {
|
||||
className="pl-10 glass-input text-white placeholder:text-white/40 h-10"
|
||||
/>
|
||||
</div>
|
||||
<Button
|
||||
<Button
|
||||
onClick={handleSearch}
|
||||
className="bg-blue-500/20 hover:bg-blue-500/30 text-blue-300 border-blue-500/30 cursor-pointer"
|
||||
>
|
||||
@ -109,9 +111,7 @@ export function MarketFulfillment() {
|
||||
<p className="text-white/60">
|
||||
{searchTerm ? 'Фулфилмент-центры не найдены' : 'Введите запрос для поиска фулфилментов'}
|
||||
</p>
|
||||
<p className="text-white/40 text-sm mt-2">
|
||||
Попробуйте изменить условия поиска
|
||||
</p>
|
||||
<p className="text-white/40 text-sm mt-2">Попробуйте изменить условия поиска</p>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
@ -131,4 +131,4 @@ export function MarketFulfillment() {
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -1,7 +1,8 @@
|
||||
"use client"
|
||||
'use client'
|
||||
|
||||
import { TrendingUp, DollarSign, BarChart3 } from 'lucide-react'
|
||||
|
||||
import { Card } from '@/components/ui/card'
|
||||
import { TrendingUp, DollarSign, BarChart3 } from 'lucide-react'
|
||||
|
||||
export function MarketInvestments() {
|
||||
return (
|
||||
@ -43,9 +44,7 @@ export function MarketInvestments() {
|
||||
<TrendingUp className="h-8 w-8 text-purple-400" />
|
||||
<h4 className="text-lg font-semibold text-white">Доходность</h4>
|
||||
</div>
|
||||
<p className="text-white/60 text-sm">
|
||||
Отслеживание доходности инвестиций и планирование бюджета
|
||||
</p>
|
||||
<p className="text-white/60 text-sm">Отслеживание доходности инвестиций и планирование бюджета</p>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
@ -54,11 +53,9 @@ export function MarketInvestments() {
|
||||
<TrendingUp className="h-8 w-8 text-white/40" />
|
||||
</div>
|
||||
<p className="text-white/60 text-lg mb-2">Раздел в разработке</p>
|
||||
<p className="text-white/40 text-sm">
|
||||
Функционал инвестиций будет доступен в ближайших обновлениях
|
||||
</p>
|
||||
<p className="text-white/40 text-sm">Функционал инвестиций будет доступен в ближайших обновлениях</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -1,13 +1,15 @@
|
||||
"use client"
|
||||
'use client'
|
||||
|
||||
import { useState } from 'react'
|
||||
import { useQuery, useMutation } from '@apollo/client'
|
||||
import { Search, Truck } from 'lucide-react'
|
||||
import { useState } from 'react'
|
||||
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Input } from '@/components/ui/input'
|
||||
import { Search, Truck } from 'lucide-react'
|
||||
import { OrganizationCard } from './organization-card'
|
||||
import { SEARCH_ORGANIZATIONS, GET_INCOMING_REQUESTS, GET_OUTGOING_REQUESTS } from '@/graphql/queries'
|
||||
import { SEND_COUNTERPARTY_REQUEST } from '@/graphql/mutations'
|
||||
import { SEARCH_ORGANIZATIONS, GET_INCOMING_REQUESTS, GET_OUTGOING_REQUESTS } from '@/graphql/queries'
|
||||
|
||||
import { OrganizationCard } from './organization-card'
|
||||
|
||||
interface Organization {
|
||||
id: string
|
||||
@ -19,7 +21,7 @@ interface Organization {
|
||||
phones?: Array<{ value: string }>
|
||||
emails?: Array<{ value: string }>
|
||||
createdAt: string
|
||||
users?: Array<{ id: string, avatar?: string }>
|
||||
users?: Array<{ id: string; avatar?: string }>
|
||||
isCounterparty?: boolean
|
||||
isCurrentUser?: boolean
|
||||
hasOutgoingRequest?: boolean
|
||||
@ -30,7 +32,7 @@ export function MarketLogistics() {
|
||||
const [searchTerm, setSearchTerm] = useState('')
|
||||
|
||||
const { data, loading, refetch } = useQuery(SEARCH_ORGANIZATIONS, {
|
||||
variables: { type: 'LOGIST', search: searchTerm || null }
|
||||
variables: { type: 'LOGIST', search: searchTerm || null },
|
||||
})
|
||||
|
||||
const [sendRequest, { loading: sendingRequest }] = useMutation(SEND_COUNTERPARTY_REQUEST, {
|
||||
@ -40,9 +42,9 @@ export function MarketLogistics() {
|
||||
{ query: SEARCH_ORGANIZATIONS, variables: { type: 'LOGIST' } },
|
||||
{ query: SEARCH_ORGANIZATIONS, variables: { type: 'WHOLESALE' } },
|
||||
{ query: GET_OUTGOING_REQUESTS },
|
||||
{ query: GET_INCOMING_REQUESTS }
|
||||
{ query: GET_INCOMING_REQUESTS },
|
||||
],
|
||||
awaitRefetchQueries: true
|
||||
awaitRefetchQueries: true,
|
||||
})
|
||||
|
||||
const handleSearch = () => {
|
||||
@ -54,8 +56,8 @@ export function MarketLogistics() {
|
||||
await sendRequest({
|
||||
variables: {
|
||||
organizationId: organizationId,
|
||||
message: message || 'Заявка на добавление в контрагенты'
|
||||
}
|
||||
message: message || 'Заявка на добавление в контрагенты',
|
||||
},
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('Ошибка отправки заявки:', error)
|
||||
@ -78,7 +80,7 @@ export function MarketLogistics() {
|
||||
className="pl-10 glass-input text-white placeholder:text-white/40 h-10"
|
||||
/>
|
||||
</div>
|
||||
<Button
|
||||
<Button
|
||||
onClick={handleSearch}
|
||||
className="bg-orange-500/20 hover:bg-orange-500/30 text-orange-300 border-orange-500/30 cursor-pointer"
|
||||
>
|
||||
@ -109,9 +111,7 @@ export function MarketLogistics() {
|
||||
<p className="text-white/60">
|
||||
{searchTerm ? 'Логистические компании не найдены' : 'Введите запрос для поиска'}
|
||||
</p>
|
||||
<p className="text-white/40 text-sm mt-2">
|
||||
Попробуйте изменить условия поиска
|
||||
</p>
|
||||
<p className="text-white/40 text-sm mt-2">Попробуйте изменить условия поиска</p>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
@ -131,4 +131,4 @@ export function MarketLogistics() {
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -1,13 +1,15 @@
|
||||
"use client"
|
||||
'use client'
|
||||
|
||||
import { useState, useMemo } from 'react'
|
||||
import { useQuery } from '@apollo/client'
|
||||
import { Search, ShoppingBag, Package2, ArrowLeft } from 'lucide-react'
|
||||
import { useState, useMemo } from 'react'
|
||||
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Input } from '@/components/ui/input'
|
||||
import { Search, ShoppingBag, Package2, ArrowLeft } from 'lucide-react'
|
||||
import { ProductCard } from './product-card'
|
||||
import { GET_ALL_PRODUCTS } from '@/graphql/queries'
|
||||
|
||||
import { ProductCard } from './product-card'
|
||||
|
||||
interface Product {
|
||||
id: string
|
||||
name: string
|
||||
@ -35,7 +37,7 @@ interface Product {
|
||||
address?: string
|
||||
phones?: Array<{ value: string }>
|
||||
emails?: Array<{ value: string }>
|
||||
users?: Array<{ id: string, avatar?: string, managerName?: string }>
|
||||
users?: Array<{ id: string; avatar?: string; managerName?: string }>
|
||||
}
|
||||
}
|
||||
|
||||
@ -51,10 +53,10 @@ export function MarketProducts({ selectedCategoryId, selectedCategoryName, onBac
|
||||
const [localSearch, setLocalSearch] = useState('')
|
||||
|
||||
const { data, loading } = useQuery(GET_ALL_PRODUCTS, {
|
||||
variables: {
|
||||
variables: {
|
||||
search: searchTerm || null,
|
||||
category: selectedCategoryId || selectedCategory || null
|
||||
}
|
||||
category: selectedCategoryId || selectedCategory || null,
|
||||
},
|
||||
})
|
||||
|
||||
const products: Product[] = useMemo(() => data?.allProducts || [], [data?.allProducts])
|
||||
@ -62,11 +64,11 @@ export function MarketProducts({ selectedCategoryId, selectedCategoryName, onBac
|
||||
// Получаем уникальные категории из товаров
|
||||
const categories = useMemo(() => {
|
||||
const allCategories = products
|
||||
.map(product => product.category?.name)
|
||||
.map((product) => product.category?.name)
|
||||
.filter(Boolean)
|
||||
.filter((category, index, arr) => arr.indexOf(category) === index)
|
||||
.sort()
|
||||
|
||||
|
||||
return allCategories
|
||||
}, [products])
|
||||
|
||||
@ -74,12 +76,8 @@ export function MarketProducts({ selectedCategoryId, selectedCategoryName, onBac
|
||||
setSearchTerm(localSearch.trim())
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
// Фильтруем товары по доступности
|
||||
const availableProducts = products.filter(product => product.isActive && product.quantity > 0)
|
||||
const availableProducts = products.filter((product) => product.isActive && product.quantity > 0)
|
||||
const totalProducts = products.length
|
||||
const availableCount = availableProducts.length
|
||||
|
||||
@ -115,8 +113,8 @@ export function MarketProducts({ selectedCategoryId, selectedCategoryName, onBac
|
||||
className="pl-10 glass-input text-white placeholder:text-white/40 h-10"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<Button
|
||||
|
||||
<Button
|
||||
onClick={handleSearch}
|
||||
className="bg-purple-500/20 hover:bg-purple-500/30 text-purple-300 border-purple-500/30 cursor-pointer h-10"
|
||||
>
|
||||
@ -131,14 +129,14 @@ export function MarketProducts({ selectedCategoryId, selectedCategoryName, onBac
|
||||
<Package2 className="h-4 w-4 mr-2" />
|
||||
Категории
|
||||
</h4>
|
||||
|
||||
|
||||
<div className="flex flex-wrap gap-2">
|
||||
<Button
|
||||
onClick={() => setSelectedCategory('')}
|
||||
variant={selectedCategory === '' ? 'default' : 'outline'}
|
||||
className={`h-8 px-3 text-sm transition-all ${
|
||||
selectedCategory === ''
|
||||
? 'bg-purple-500 hover:bg-purple-600 text-white'
|
||||
selectedCategory === ''
|
||||
? 'bg-purple-500 hover:bg-purple-600 text-white'
|
||||
: 'bg-white/5 hover:bg-white/10 text-white/70 border-white/20'
|
||||
}`}
|
||||
>
|
||||
@ -150,8 +148,8 @@ export function MarketProducts({ selectedCategoryId, selectedCategoryName, onBac
|
||||
onClick={() => setSelectedCategory(category!)}
|
||||
variant={selectedCategory === category ? 'default' : 'outline'}
|
||||
className={`h-8 px-3 text-sm transition-all ${
|
||||
selectedCategory === category
|
||||
? 'bg-purple-500 hover:bg-purple-600 text-white'
|
||||
selectedCategory === category
|
||||
? 'bg-purple-500 hover:bg-purple-600 text-white'
|
||||
: 'bg-white/5 hover:bg-white/10 text-white/70 border-white/20'
|
||||
}`}
|
||||
>
|
||||
@ -166,43 +164,40 @@ export function MarketProducts({ selectedCategoryId, selectedCategoryName, onBac
|
||||
<div className="flex items-center space-x-3">
|
||||
<ShoppingBag className="h-6 w-6 text-purple-400" />
|
||||
<div>
|
||||
<h3 className="text-lg font-semibold text-white">Товары поставщиков</h3>
|
||||
<h3 className="text-lg font-semibold text-white">Товары поставщиков</h3>
|
||||
<p className="text-white/60 text-sm">
|
||||
Найдено {totalProducts} товаров, доступно {availableCount}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Активные фильтры */}
|
||||
<div className="flex items-center space-x-2">
|
||||
{searchTerm && (
|
||||
<div className="bg-purple-500/20 px-3 py-1 rounded-full text-purple-300 text-sm flex items-center">
|
||||
<Search className="h-3 w-3 mr-1" />
|
||||
{searchTerm}
|
||||
<button
|
||||
onClick={() => {
|
||||
setSearchTerm('')
|
||||
setLocalSearch('')
|
||||
}}
|
||||
className="ml-2 hover:text-white"
|
||||
>
|
||||
×
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
{selectedCategory && (
|
||||
<div className="bg-purple-500/20 px-3 py-1 rounded-full text-purple-300 text-sm flex items-center">
|
||||
<Package2 className="h-3 w-3 mr-1" />
|
||||
{selectedCategory}
|
||||
<button
|
||||
onClick={() => setSelectedCategory('')}
|
||||
className="ml-2 hover:text-white"
|
||||
>
|
||||
×
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{/* Активные фильтры */}
|
||||
<div className="flex items-center space-x-2">
|
||||
{searchTerm && (
|
||||
<div className="bg-purple-500/20 px-3 py-1 rounded-full text-purple-300 text-sm flex items-center">
|
||||
<Search className="h-3 w-3 mr-1" />
|
||||
{searchTerm}
|
||||
<button
|
||||
onClick={() => {
|
||||
setSearchTerm('')
|
||||
setLocalSearch('')
|
||||
}}
|
||||
className="ml-2 hover:text-white"
|
||||
>
|
||||
×
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
{selectedCategory && (
|
||||
<div className="bg-purple-500/20 px-3 py-1 rounded-full text-purple-300 text-sm flex items-center">
|
||||
<Package2 className="h-3 w-3 mr-1" />
|
||||
{selectedCategory}
|
||||
<button onClick={() => setSelectedCategory('')} className="ml-2 hover:text-white">
|
||||
×
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Список товаров */}
|
||||
@ -219,20 +214,16 @@ export function MarketProducts({ selectedCategoryId, selectedCategoryName, onBac
|
||||
{searchTerm || selectedCategory ? 'Товары не найдены' : 'Пока нет товаров для отображения'}
|
||||
</p>
|
||||
<p className="text-white/40 text-sm mt-2">
|
||||
{searchTerm || selectedCategory
|
||||
{searchTerm || selectedCategory
|
||||
? 'Попробуйте изменить условия поиска или фильтры'
|
||||
: 'Поставщики еще не добавили свои товары'
|
||||
}
|
||||
: 'Поставщики еще не добавили свои товары'}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 lg:grid-cols-4 xl:grid-cols-5 2xl:grid-cols-6 gap-4">
|
||||
{products.map((product) => (
|
||||
<ProductCard
|
||||
key={product.id}
|
||||
product={product}
|
||||
/>
|
||||
<ProductCard key={product.id} product={product} />
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
@ -240,4 +231,4 @@ export function MarketProducts({ selectedCategoryId, selectedCategoryName, onBac
|
||||
{/* Пагинация будет добавлена позже если понадобится */}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -1,11 +1,13 @@
|
||||
"use client"
|
||||
'use client'
|
||||
|
||||
import { useQuery } from '@apollo/client'
|
||||
import { ShoppingCart, Package, ArrowLeft } from 'lucide-react'
|
||||
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { GET_MY_CART } from '@/graphql/queries'
|
||||
|
||||
import { CartItems } from '../cart/cart-items'
|
||||
import { CartSummary } from '../cart/cart-summary'
|
||||
import { GET_MY_CART } from '@/graphql/queries'
|
||||
import { ShoppingCart, Package, ArrowLeft } from 'lucide-react'
|
||||
import { Button } from '@/components/ui/button'
|
||||
|
||||
interface MarketRequestsProps {
|
||||
onBackToCategories?: () => void
|
||||
@ -13,7 +15,7 @@ interface MarketRequestsProps {
|
||||
|
||||
export function MarketRequests({ onBackToCategories }: MarketRequestsProps) {
|
||||
const { data, loading, error } = useQuery(GET_MY_CART)
|
||||
|
||||
|
||||
const cart = data?.myCart
|
||||
const hasItems = cart?.items && cart.items.length > 0
|
||||
|
||||
@ -58,10 +60,9 @@ export function MarketRequests({ onBackToCategories }: MarketRequestsProps) {
|
||||
<div>
|
||||
<h1 className="text-xl font-bold text-white">Мои заявки</h1>
|
||||
<p className="text-white/60">
|
||||
{hasItems
|
||||
{hasItems
|
||||
? `${cart.totalItems} заявок на сумму ${new Intl.NumberFormat('ru-RU', { style: 'currency', currency: 'RUB' }).format(cart.totalPrice)}`
|
||||
: 'У вас пока нет заявок'
|
||||
}
|
||||
: 'У вас пока нет заявок'}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@ -88,12 +89,12 @@ export function MarketRequests({ onBackToCategories }: MarketRequestsProps) {
|
||||
<div className="h-full flex flex-col items-center justify-center text-center p-8">
|
||||
<Package className="h-24 w-24 text-white/20 mb-6" />
|
||||
<h2 className="text-xl font-semibold text-white mb-2">Нет заявок</h2>
|
||||
<p className="text-white/60 mb-6 max-w-md">
|
||||
Добавьте товары в заявки из раздела "Товары", чтобы создать заявку для поставщика
|
||||
</p>
|
||||
<p className="text-white/60 mb-6 max-w-md">
|
||||
Добавьте товары в заявки из раздела "Товары", чтобы создать заявку для поставщика
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -1,13 +1,15 @@
|
||||
"use client"
|
||||
'use client'
|
||||
|
||||
import { useState } from 'react'
|
||||
import { useQuery, useMutation } from '@apollo/client'
|
||||
import { Search, ShoppingCart } from 'lucide-react'
|
||||
import { useState } from 'react'
|
||||
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Input } from '@/components/ui/input'
|
||||
import { Search, ShoppingCart } from 'lucide-react'
|
||||
import { OrganizationCard } from './organization-card'
|
||||
import { SEARCH_ORGANIZATIONS, GET_INCOMING_REQUESTS, GET_OUTGOING_REQUESTS } from '@/graphql/queries'
|
||||
import { SEND_COUNTERPARTY_REQUEST } from '@/graphql/mutations'
|
||||
import { SEARCH_ORGANIZATIONS, GET_INCOMING_REQUESTS, GET_OUTGOING_REQUESTS } from '@/graphql/queries'
|
||||
|
||||
import { OrganizationCard } from './organization-card'
|
||||
|
||||
interface Organization {
|
||||
id: string
|
||||
@ -19,7 +21,7 @@ interface Organization {
|
||||
phones?: Array<{ value: string }>
|
||||
emails?: Array<{ value: string }>
|
||||
createdAt: string
|
||||
users?: Array<{ id: string, avatar?: string }>
|
||||
users?: Array<{ id: string; avatar?: string }>
|
||||
isCounterparty?: boolean
|
||||
isCurrentUser?: boolean
|
||||
hasOutgoingRequest?: boolean
|
||||
@ -30,7 +32,7 @@ export function MarketSellers() {
|
||||
const [searchTerm, setSearchTerm] = useState('')
|
||||
|
||||
const { data, loading, refetch } = useQuery(SEARCH_ORGANIZATIONS, {
|
||||
variables: { type: 'SELLER', search: searchTerm || null }
|
||||
variables: { type: 'SELLER', search: searchTerm || null },
|
||||
})
|
||||
|
||||
const [sendRequest, { loading: sendingRequest }] = useMutation(SEND_COUNTERPARTY_REQUEST, {
|
||||
@ -40,9 +42,9 @@ export function MarketSellers() {
|
||||
{ query: SEARCH_ORGANIZATIONS, variables: { type: 'LOGIST' } },
|
||||
{ query: SEARCH_ORGANIZATIONS, variables: { type: 'WHOLESALE' } },
|
||||
{ query: GET_OUTGOING_REQUESTS },
|
||||
{ query: GET_INCOMING_REQUESTS }
|
||||
{ query: GET_INCOMING_REQUESTS },
|
||||
],
|
||||
awaitRefetchQueries: true
|
||||
awaitRefetchQueries: true,
|
||||
})
|
||||
|
||||
const handleSearch = () => {
|
||||
@ -54,8 +56,8 @@ export function MarketSellers() {
|
||||
await sendRequest({
|
||||
variables: {
|
||||
organizationId: organizationId,
|
||||
message: message || 'Заявка на добавление в контрагенты'
|
||||
}
|
||||
message: message || 'Заявка на добавление в контрагенты',
|
||||
},
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('Ошибка отправки заявки:', error)
|
||||
@ -78,7 +80,7 @@ export function MarketSellers() {
|
||||
className="pl-10 glass-input text-white placeholder:text-white/40 h-10"
|
||||
/>
|
||||
</div>
|
||||
<Button
|
||||
<Button
|
||||
onClick={handleSearch}
|
||||
className="bg-green-500/20 hover:bg-green-500/30 text-green-300 border-green-500/30 cursor-pointer"
|
||||
>
|
||||
@ -109,9 +111,7 @@ export function MarketSellers() {
|
||||
<p className="text-white/60">
|
||||
{searchTerm ? 'Селлеры не найдены' : 'Введите запрос для поиска селлеров'}
|
||||
</p>
|
||||
<p className="text-white/40 text-sm mt-2">
|
||||
Попробуйте изменить условия поиска
|
||||
</p>
|
||||
<p className="text-white/40 text-sm mt-2">Попробуйте изменить условия поиска</p>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
@ -131,4 +131,4 @@ export function MarketSellers() {
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -1,75 +1,70 @@
|
||||
"use client";
|
||||
'use client'
|
||||
|
||||
import { useState } from "react";
|
||||
import { useQuery, useMutation } from "@apollo/client";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Search, Boxes } from "lucide-react";
|
||||
import { OrganizationCard } from "./organization-card";
|
||||
import {
|
||||
SEARCH_ORGANIZATIONS,
|
||||
GET_INCOMING_REQUESTS,
|
||||
GET_OUTGOING_REQUESTS,
|
||||
} from "@/graphql/queries";
|
||||
import { SEND_COUNTERPARTY_REQUEST } from "@/graphql/mutations";
|
||||
import { useQuery, useMutation } from '@apollo/client'
|
||||
import { Search, Boxes } from 'lucide-react'
|
||||
import { useState } from 'react'
|
||||
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Input } from '@/components/ui/input'
|
||||
import { SEND_COUNTERPARTY_REQUEST } from '@/graphql/mutations'
|
||||
import { SEARCH_ORGANIZATIONS, GET_INCOMING_REQUESTS, GET_OUTGOING_REQUESTS } from '@/graphql/queries'
|
||||
|
||||
import { OrganizationCard } from './organization-card'
|
||||
|
||||
interface Organization {
|
||||
id: string;
|
||||
inn: string;
|
||||
name?: string;
|
||||
fullName?: string;
|
||||
type: "FULFILLMENT" | "SELLER" | "LOGIST" | "WHOLESALE";
|
||||
address?: string;
|
||||
phones?: Array<{ value: string }>;
|
||||
emails?: Array<{ value: string }>;
|
||||
createdAt: string;
|
||||
users?: Array<{ id: string; avatar?: string }>;
|
||||
isCounterparty?: boolean;
|
||||
isCurrentUser?: boolean;
|
||||
hasOutgoingRequest?: boolean;
|
||||
hasIncomingRequest?: boolean;
|
||||
id: string
|
||||
inn: string
|
||||
name?: string
|
||||
fullName?: string
|
||||
type: 'FULFILLMENT' | 'SELLER' | 'LOGIST' | 'WHOLESALE'
|
||||
address?: string
|
||||
phones?: Array<{ value: string }>
|
||||
emails?: Array<{ value: string }>
|
||||
createdAt: string
|
||||
users?: Array<{ id: string; avatar?: string }>
|
||||
isCounterparty?: boolean
|
||||
isCurrentUser?: boolean
|
||||
hasOutgoingRequest?: boolean
|
||||
hasIncomingRequest?: boolean
|
||||
}
|
||||
|
||||
export function MarketSuppliers() {
|
||||
const [searchTerm, setSearchTerm] = useState("");
|
||||
const [searchTerm, setSearchTerm] = useState('')
|
||||
|
||||
const { data, loading, refetch } = useQuery(SEARCH_ORGANIZATIONS, {
|
||||
variables: { type: "WHOLESALE", search: searchTerm || null },
|
||||
});
|
||||
variables: { type: 'WHOLESALE', search: searchTerm || null },
|
||||
})
|
||||
|
||||
const [sendRequest, { loading: sendingRequest }] = useMutation(
|
||||
SEND_COUNTERPARTY_REQUEST,
|
||||
{
|
||||
refetchQueries: [
|
||||
{ query: SEARCH_ORGANIZATIONS, variables: { type: "SELLER" } },
|
||||
{ query: SEARCH_ORGANIZATIONS, variables: { type: "FULFILLMENT" } },
|
||||
{ query: SEARCH_ORGANIZATIONS, variables: { type: "LOGIST" } },
|
||||
{ query: SEARCH_ORGANIZATIONS, variables: { type: "WHOLESALE" } },
|
||||
{ query: GET_OUTGOING_REQUESTS },
|
||||
{ query: GET_INCOMING_REQUESTS },
|
||||
],
|
||||
awaitRefetchQueries: true,
|
||||
}
|
||||
);
|
||||
const [sendRequest, { loading: sendingRequest }] = useMutation(SEND_COUNTERPARTY_REQUEST, {
|
||||
refetchQueries: [
|
||||
{ query: SEARCH_ORGANIZATIONS, variables: { type: 'SELLER' } },
|
||||
{ query: SEARCH_ORGANIZATIONS, variables: { type: 'FULFILLMENT' } },
|
||||
{ query: SEARCH_ORGANIZATIONS, variables: { type: 'LOGIST' } },
|
||||
{ query: SEARCH_ORGANIZATIONS, variables: { type: 'WHOLESALE' } },
|
||||
{ query: GET_OUTGOING_REQUESTS },
|
||||
{ query: GET_INCOMING_REQUESTS },
|
||||
],
|
||||
awaitRefetchQueries: true,
|
||||
})
|
||||
|
||||
const handleSearch = () => {
|
||||
refetch({ type: "WHOLESALE", search: searchTerm || null });
|
||||
};
|
||||
refetch({ type: 'WHOLESALE', search: searchTerm || null })
|
||||
}
|
||||
|
||||
const handleSendRequest = async (organizationId: string, message: string) => {
|
||||
try {
|
||||
await sendRequest({
|
||||
variables: {
|
||||
organizationId: organizationId,
|
||||
message: message || "Заявка на добавление в контрагенты",
|
||||
message: message || 'Заявка на добавление в контрагенты',
|
||||
},
|
||||
});
|
||||
})
|
||||
} catch (error) {
|
||||
console.error("Ошибка отправки заявки:", error);
|
||||
console.error('Ошибка отправки заявки:', error)
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
const organizations = data?.searchOrganizations || [];
|
||||
const organizations = data?.searchOrganizations || []
|
||||
|
||||
return (
|
||||
<div className="h-full flex flex-col space-y-4 overflow-hidden">
|
||||
@ -81,7 +76,7 @@ export function MarketSuppliers() {
|
||||
placeholder="Поиск поставщиков по названию или ИНН..."
|
||||
value={searchTerm}
|
||||
onChange={(e) => setSearchTerm(e.target.value)}
|
||||
onKeyDown={(e) => e.key === "Enter" && handleSearch()}
|
||||
onKeyDown={(e) => e.key === 'Enter' && handleSearch()}
|
||||
className="pl-10 glass-input text-white placeholder:text-white/40 h-10"
|
||||
/>
|
||||
</div>
|
||||
@ -99,9 +94,7 @@ export function MarketSuppliers() {
|
||||
<Boxes className="h-6 w-6 text-purple-400" />
|
||||
<div>
|
||||
<h3 className="text-lg font-semibold text-white">Поставщики</h3>
|
||||
<p className="text-white/60 text-sm">
|
||||
Найдите и добавьте поставщиков в контрагенты
|
||||
</p>
|
||||
<p className="text-white/60 text-sm">Найдите и добавьте поставщиков в контрагенты</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -116,13 +109,9 @@ export function MarketSuppliers() {
|
||||
<div className="text-center">
|
||||
<Boxes className="h-12 w-12 text-white/20 mx-auto mb-4" />
|
||||
<p className="text-white/60">
|
||||
{searchTerm
|
||||
? "Поставщики не найдены"
|
||||
: "Введите запрос для поиска поставщиков"}
|
||||
</p>
|
||||
<p className="text-white/40 text-sm mt-2">
|
||||
Попробуйте изменить условия поиска
|
||||
{searchTerm ? 'Поставщики не найдены' : 'Введите запрос для поиска поставщиков'}
|
||||
</p>
|
||||
<p className="text-white/40 text-sm mt-2">Попробуйте изменить условия поиска</p>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
@ -141,5 +130,5 @@ export function MarketSuppliers() {
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
)
|
||||
}
|
||||
|
@ -1,4 +1,4 @@
|
||||
"use client"
|
||||
'use client'
|
||||
|
||||
import { Avatar, AvatarImage, AvatarFallback } from '@/components/ui/avatar'
|
||||
import { cn } from '@/lib/utils'
|
||||
@ -24,7 +24,7 @@ interface OrganizationAvatarProps {
|
||||
// Цвета для fallback аватарок
|
||||
const FALLBACK_COLORS = [
|
||||
'bg-blue-500',
|
||||
'bg-green-500',
|
||||
'bg-green-500',
|
||||
'bg-purple-500',
|
||||
'bg-orange-500',
|
||||
'bg-pink-500',
|
||||
@ -32,13 +32,13 @@ const FALLBACK_COLORS = [
|
||||
'bg-teal-500',
|
||||
'bg-red-500',
|
||||
'bg-yellow-500',
|
||||
'bg-cyan-500'
|
||||
'bg-cyan-500',
|
||||
]
|
||||
|
||||
function getInitials(name: string): string {
|
||||
return name
|
||||
.split(' ')
|
||||
.map(word => word.charAt(0))
|
||||
.map((word) => word.charAt(0))
|
||||
.join('')
|
||||
.toUpperCase()
|
||||
.slice(0, 2)
|
||||
@ -53,7 +53,7 @@ function getSizes(size: 'sm' | 'md' | 'lg') {
|
||||
switch (size) {
|
||||
case 'sm':
|
||||
return { avatar: 'size-8', text: 'text-xs' }
|
||||
case 'md':
|
||||
case 'md':
|
||||
return { avatar: 'size-10', text: 'text-sm' }
|
||||
case 'lg':
|
||||
return { avatar: 'size-12', text: 'text-base' }
|
||||
@ -62,31 +62,23 @@ function getSizes(size: 'sm' | 'md' | 'lg') {
|
||||
}
|
||||
}
|
||||
|
||||
export function OrganizationAvatar({
|
||||
organization,
|
||||
size = 'md',
|
||||
className
|
||||
}: OrganizationAvatarProps) {
|
||||
export function OrganizationAvatar({ organization, size = 'md', className }: OrganizationAvatarProps) {
|
||||
// Берем аватарку первого пользователя организации
|
||||
const userAvatar = organization.users?.[0]?.avatar
|
||||
|
||||
|
||||
// Получаем имя для инициалов
|
||||
const displayName = organization.name || organization.fullName || 'Организация'
|
||||
const initials = getInitials(displayName)
|
||||
|
||||
|
||||
// Получаем цвет для fallback
|
||||
const fallbackColor = getColorForOrganization(organization.id)
|
||||
|
||||
|
||||
const sizes = getSizes(size)
|
||||
|
||||
return (
|
||||
<Avatar className={cn(sizes.avatar, className)}>
|
||||
{userAvatar && (
|
||||
<AvatarImage src={userAvatar} alt={displayName} />
|
||||
)}
|
||||
<AvatarFallback className={cn(fallbackColor, 'text-white font-medium', sizes.text)}>
|
||||
{initials}
|
||||
</AvatarFallback>
|
||||
{userAvatar && <AvatarImage src={userAvatar} alt={displayName} />}
|
||||
<AvatarFallback className={cn(fallbackColor, 'text-white font-medium', sizes.text)}>{initials}</AvatarFallback>
|
||||
</Avatar>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -1,21 +1,14 @@
|
||||
"use client"
|
||||
'use client'
|
||||
|
||||
import { Plus, Send, Trash2 } from 'lucide-react'
|
||||
import { useState } from 'react'
|
||||
|
||||
import { Badge } from '@/components/ui/badge'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogTrigger } from '@/components/ui/dialog'
|
||||
import { Input } from '@/components/ui/input'
|
||||
import {
|
||||
Phone,
|
||||
Mail,
|
||||
MapPin,
|
||||
Calendar,
|
||||
Plus,
|
||||
Send,
|
||||
Trash2,
|
||||
User
|
||||
} from 'lucide-react'
|
||||
|
||||
import { OrganizationAvatar } from './organization-avatar'
|
||||
import { useState } from 'react'
|
||||
|
||||
interface Organization {
|
||||
id: string
|
||||
@ -27,7 +20,7 @@ interface Organization {
|
||||
phones?: Array<{ value: string }>
|
||||
emails?: Array<{ value: string }>
|
||||
createdAt: string
|
||||
users?: Array<{ id: string, avatar?: string }>
|
||||
users?: Array<{ id: string; avatar?: string }>
|
||||
isCounterparty?: boolean
|
||||
isCurrentUser?: boolean
|
||||
hasOutgoingRequest?: boolean
|
||||
@ -44,14 +37,14 @@ interface OrganizationCardProps {
|
||||
requestSending?: boolean
|
||||
}
|
||||
|
||||
export function OrganizationCard({
|
||||
organization,
|
||||
onSendRequest,
|
||||
export function OrganizationCard({
|
||||
organization,
|
||||
onSendRequest,
|
||||
onRemove,
|
||||
showRemoveButton = false,
|
||||
actionButtonText = "Добавить",
|
||||
actionButtonColor = "green",
|
||||
requestSending = false
|
||||
actionButtonText = 'Добавить',
|
||||
actionButtonColor = 'green',
|
||||
requestSending = false,
|
||||
}: OrganizationCardProps) {
|
||||
const [requestMessage, setRequestMessage] = useState('')
|
||||
const [isDialogOpen, setIsDialogOpen] = useState(false)
|
||||
@ -60,7 +53,7 @@ export function OrganizationCard({
|
||||
if (!dateString) return ''
|
||||
try {
|
||||
let date: Date
|
||||
|
||||
|
||||
// Проверяем, является ли строка числом (Unix timestamp)
|
||||
if (/^\d+$/.test(dateString)) {
|
||||
// Если это Unix timestamp в миллисекундах
|
||||
@ -70,53 +63,69 @@ export function OrganizationCard({
|
||||
// Обычная строка даты
|
||||
date = new Date(dateString)
|
||||
}
|
||||
|
||||
|
||||
if (isNaN(date.getTime())) {
|
||||
return 'Неверная дата'
|
||||
}
|
||||
|
||||
|
||||
return date.toLocaleDateString('ru-RU', {
|
||||
year: 'numeric',
|
||||
month: 'long',
|
||||
day: 'numeric'
|
||||
day: 'numeric',
|
||||
})
|
||||
} catch {
|
||||
} catch {
|
||||
return 'Ошибка даты'
|
||||
}
|
||||
}
|
||||
|
||||
const getTypeLabel = (type: string) => {
|
||||
switch (type) {
|
||||
case 'FULFILLMENT': return 'Фулфилмент'
|
||||
case 'SELLER': return 'Селлер'
|
||||
case 'LOGIST': return 'Логистика'
|
||||
case 'WHOLESALE': return 'Поставщик'
|
||||
default: return 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'
|
||||
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 getActionButtonColor = (color: string, isDisabled: boolean) => {
|
||||
if (isDisabled) {
|
||||
return "bg-gray-500/20 text-gray-400 border-gray-500/30 cursor-not-allowed"
|
||||
return 'bg-gray-500/20 text-gray-400 border-gray-500/30 cursor-not-allowed'
|
||||
}
|
||||
|
||||
|
||||
switch (color) {
|
||||
case 'green': return 'bg-green-500/20 hover:bg-green-500/30 text-green-300 border-green-500/30'
|
||||
case 'orange': return 'bg-orange-500/20 hover:bg-orange-500/30 text-orange-300 border-orange-500/30'
|
||||
case 'yellow': return 'bg-yellow-500/20 hover:bg-yellow-500/30 text-yellow-300 border-yellow-500/30'
|
||||
case 'red': return 'bg-red-500/20 hover:bg-red-500/30 text-red-300 border-red-500/30'
|
||||
case 'blue': return 'bg-blue-500/20 hover:bg-blue-500/30 text-blue-300 border-blue-500/30'
|
||||
default: return 'bg-gray-500/20 hover:bg-gray-500/30 text-gray-300 border-gray-500/30'
|
||||
case 'green':
|
||||
return 'bg-green-500/20 hover:bg-green-500/30 text-green-300 border-green-500/30'
|
||||
case 'orange':
|
||||
return 'bg-orange-500/20 hover:bg-orange-500/30 text-orange-300 border-orange-500/30'
|
||||
case 'yellow':
|
||||
return 'bg-yellow-500/20 hover:bg-yellow-500/30 text-yellow-300 border-yellow-500/30'
|
||||
case 'red':
|
||||
return 'bg-red-500/20 hover:bg-red-500/30 text-red-300 border-red-500/30'
|
||||
case 'blue':
|
||||
return 'bg-blue-500/20 hover:bg-blue-500/30 text-blue-300 border-blue-500/30'
|
||||
default:
|
||||
return 'bg-gray-500/20 hover:bg-gray-500/30 text-gray-300 border-gray-500/30'
|
||||
}
|
||||
}
|
||||
|
||||
@ -145,22 +154,16 @@ export function OrganizationCard({
|
||||
{organization.name || organization.fullName}
|
||||
</h4>
|
||||
<div className="flex items-center space-x-3">
|
||||
<Badge className={getTypeColor(organization.type)}>
|
||||
{getTypeLabel(organization.type)}
|
||||
</Badge>
|
||||
<Badge className={getTypeColor(organization.type)}>{getTypeLabel(organization.type)}</Badge>
|
||||
{organization.isCurrentUser && (
|
||||
<Badge className="bg-blue-500/20 text-blue-300 border-blue-500/30">
|
||||
Это вы
|
||||
</Badge>
|
||||
<Badge className="bg-blue-500/20 text-blue-300 border-blue-500/30">Это вы</Badge>
|
||||
)}
|
||||
{organization.isCounterparty && !organization.isCurrentUser && (
|
||||
<Badge className="bg-green-500/20 text-green-300 border-green-500/30">
|
||||
Уже добавлен
|
||||
</Badge>
|
||||
<Badge className="bg-green-500/20 text-green-300 border-green-500/30">Уже добавлен</Badge>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div className="space-y-2">
|
||||
<p className="text-white/60 text-sm">ИНН: {organization.inn}</p>
|
||||
{organization.address && (
|
||||
@ -183,12 +186,14 @@ export function OrganizationCard({
|
||||
)}
|
||||
<div className="flex items-center text-white/40 text-xs">
|
||||
<Calendar className="h-4 w-4 mr-2 flex-shrink-0" />
|
||||
<span>{showRemoveButton ? 'Добавлен' : 'Зарегистрирован'} {formatDate(organization.createdAt)}</span>
|
||||
<span>
|
||||
{showRemoveButton ? 'Добавлен' : 'Зарегистрирован'} {formatDate(organization.createdAt)}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
{showRemoveButton ? (
|
||||
<Button
|
||||
size="sm"
|
||||
@ -214,41 +219,40 @@ export function OrganizationCard({
|
||||
<DialogTrigger asChild>
|
||||
<Button
|
||||
size="sm"
|
||||
disabled={organization.isCounterparty || organization.hasOutgoingRequest || organization.hasIncomingRequest}
|
||||
disabled={
|
||||
organization.isCounterparty || organization.hasOutgoingRequest || organization.hasIncomingRequest
|
||||
}
|
||||
className={`${getActionButtonColor(actionButtonColor, !!organization.isCounterparty || !!organization.hasOutgoingRequest || !!organization.hasIncomingRequest)} w-full cursor-pointer`}
|
||||
>
|
||||
<Plus className="h-4 w-4 mr-2" />
|
||||
{organization.isCounterparty ? 'Уже добавлен' :
|
||||
organization.hasOutgoingRequest ? 'Заявка отправлена' :
|
||||
organization.hasIncomingRequest ? 'Уже подал заявку' :
|
||||
actionButtonText}
|
||||
{organization.isCounterparty
|
||||
? 'Уже добавлен'
|
||||
: organization.hasOutgoingRequest
|
||||
? 'Заявка отправлена'
|
||||
: organization.hasIncomingRequest
|
||||
? 'Уже подал заявку'
|
||||
: actionButtonText}
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
|
||||
|
||||
<DialogContent className="bg-gray-900/95 backdrop-blur border-white/10 text-white">
|
||||
<DialogHeader>
|
||||
<DialogTitle className="text-white">
|
||||
Отправить заявку в контрагенты
|
||||
</DialogTitle>
|
||||
<DialogTitle className="text-white">Отправить заявку в контрагенты</DialogTitle>
|
||||
</DialogHeader>
|
||||
|
||||
|
||||
<div className="space-y-4">
|
||||
<div className="p-4 bg-white/5 rounded-lg border border-white/10">
|
||||
<div className="flex items-center space-x-3">
|
||||
<OrganizationAvatar organization={organization} size="sm" />
|
||||
<div>
|
||||
<h4 className="text-white font-medium">
|
||||
{organization.name || organization.fullName}
|
||||
</h4>
|
||||
<h4 className="text-white font-medium">{organization.name || organization.fullName}</h4>
|
||||
<p className="text-white/60 text-sm">ИНН: {organization.inn}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-white mb-2">
|
||||
Сообщение (необязательно)
|
||||
</label>
|
||||
<label className="block text-sm font-medium text-white mb-2">Сообщение (необязательно)</label>
|
||||
<Input
|
||||
placeholder="Добавьте комментарий к заявке..."
|
||||
value={requestMessage}
|
||||
@ -256,7 +260,7 @@ export function OrganizationCard({
|
||||
className="glass-input text-white placeholder:text-white/40"
|
||||
/>
|
||||
</div>
|
||||
|
||||
|
||||
<div className="flex space-x-3 pt-4">
|
||||
<Button
|
||||
onClick={() => setIsDialogOpen(false)}
|
||||
@ -271,7 +275,7 @@ export function OrganizationCard({
|
||||
className="flex-1 bg-blue-500/20 hover:bg-blue-500/30 text-blue-300 border-blue-500/30 cursor-pointer"
|
||||
>
|
||||
{requestSending ? (
|
||||
"Отправка..."
|
||||
'Отправка...'
|
||||
) : (
|
||||
<>
|
||||
<Send className="h-4 w-4 mr-2" />
|
||||
@ -287,4 +291,4 @@ export function OrganizationCard({
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -1,22 +1,11 @@
|
||||
"use client"
|
||||
'use client'
|
||||
|
||||
import { Building2, Phone, Mail, MapPin, FileText, Users, CreditCard, Hash, User, Briefcase } from 'lucide-react'
|
||||
|
||||
import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@/components/ui/dialog'
|
||||
import { Card } from '@/components/ui/card'
|
||||
import { Badge } from '@/components/ui/badge'
|
||||
import { Card } from '@/components/ui/card'
|
||||
import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@/components/ui/dialog'
|
||||
|
||||
import {
|
||||
Building2,
|
||||
Phone,
|
||||
Mail,
|
||||
MapPin,
|
||||
|
||||
FileText,
|
||||
Users,
|
||||
CreditCard,
|
||||
Hash,
|
||||
User,
|
||||
Briefcase
|
||||
} from 'lucide-react'
|
||||
import { OrganizationAvatar } from './organization-avatar'
|
||||
|
||||
interface User {
|
||||
@ -75,10 +64,10 @@ interface OrganizationDetailsModalProps {
|
||||
|
||||
function formatDate(dateString?: string | null): string {
|
||||
if (!dateString) return 'Не указана'
|
||||
|
||||
|
||||
try {
|
||||
let date: Date
|
||||
|
||||
|
||||
// Проверяем, является ли строка числом (Unix timestamp)
|
||||
if (/^\d+$/.test(dateString)) {
|
||||
// Если это Unix timestamp в миллисекундах
|
||||
@ -88,17 +77,17 @@ function formatDate(dateString?: string | null): string {
|
||||
// Обычная строка даты
|
||||
date = new Date(dateString)
|
||||
}
|
||||
|
||||
|
||||
if (isNaN(date.getTime())) {
|
||||
return 'Не указана'
|
||||
}
|
||||
|
||||
|
||||
return date.toLocaleDateString('ru-RU', {
|
||||
year: 'numeric',
|
||||
month: 'long',
|
||||
day: 'numeric'
|
||||
day: 'numeric',
|
||||
})
|
||||
} catch {
|
||||
} catch {
|
||||
return 'Не указана'
|
||||
}
|
||||
}
|
||||
@ -146,9 +135,7 @@ export function OrganizationDetailsModal({ organization, open, onOpenChange }: O
|
||||
<OrganizationAvatar organization={organization} size="lg" />
|
||||
<div>
|
||||
<h2 className="text-xl font-semibold">{displayName}</h2>
|
||||
<Badge className={getTypeColor(organization.type)}>
|
||||
{getTypeLabel(organization.type)}
|
||||
</Badge>
|
||||
<Badge className={getTypeColor(organization.type)}>{getTypeLabel(organization.type)}</Badge>
|
||||
</div>
|
||||
</DialogTitle>
|
||||
</DialogHeader>
|
||||
@ -160,34 +147,34 @@ export function OrganizationDetailsModal({ organization, open, onOpenChange }: O
|
||||
<Building2 className="h-5 w-5 mr-2 text-blue-400" />
|
||||
Основная информация
|
||||
</h3>
|
||||
|
||||
|
||||
<div className="space-y-3">
|
||||
<div className="flex justify-between">
|
||||
<span className="text-white/60">ИНН:</span>
|
||||
<span className="text-white font-mono">{organization.inn}</span>
|
||||
</div>
|
||||
|
||||
|
||||
{organization.kpp && (
|
||||
<div className="flex justify-between">
|
||||
<span className="text-white/60">КПП:</span>
|
||||
<span className="text-white font-mono">{organization.kpp}</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
|
||||
{organization.ogrn && (
|
||||
<div className="flex justify-between">
|
||||
<span className="text-white/60">ОГРН:</span>
|
||||
<span className="text-white font-mono">{organization.ogrn}</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
|
||||
{organization.status && (
|
||||
<div className="flex justify-between">
|
||||
<span className="text-white/60">Статус:</span>
|
||||
<span className="text-white">{organization.status}</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
|
||||
<div className="flex justify-between">
|
||||
<span className="text-white/60">Дата регистрации:</span>
|
||||
<span className="text-white">{formatDate(organization.registrationDate)}</span>
|
||||
@ -201,7 +188,7 @@ export function OrganizationDetailsModal({ organization, open, onOpenChange }: O
|
||||
<Phone className="h-5 w-5 mr-2 text-green-400" />
|
||||
Контакты
|
||||
</h3>
|
||||
|
||||
|
||||
<div className="space-y-3">
|
||||
{organization.phones && organization.phones.length > 0 && (
|
||||
<div>
|
||||
@ -214,7 +201,7 @@ export function OrganizationDetailsModal({ organization, open, onOpenChange }: O
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
|
||||
{organization.emails && organization.emails.length > 0 && (
|
||||
<div>
|
||||
<div className="text-white/60 text-sm mb-2">Email:</div>
|
||||
@ -226,7 +213,7 @@ export function OrganizationDetailsModal({ organization, open, onOpenChange }: O
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
|
||||
{organization.address && (
|
||||
<div>
|
||||
<div className="text-white/60 text-sm mb-2">Адрес:</div>
|
||||
@ -246,13 +233,13 @@ export function OrganizationDetailsModal({ organization, open, onOpenChange }: O
|
||||
<User className="h-5 w-5 mr-2 text-purple-400" />
|
||||
Руководство
|
||||
</h3>
|
||||
|
||||
|
||||
<div className="space-y-3">
|
||||
<div className="flex justify-between">
|
||||
<span className="text-white/60">Руководитель:</span>
|
||||
<span className="text-white">{organization.managementName}</span>
|
||||
</div>
|
||||
|
||||
|
||||
{organization.managementPost && (
|
||||
<div className="flex justify-between">
|
||||
<span className="text-white/60">Должность:</span>
|
||||
@ -270,20 +257,20 @@ export function OrganizationDetailsModal({ organization, open, onOpenChange }: O
|
||||
<FileText className="h-5 w-5 mr-2 text-yellow-400" />
|
||||
ОПФ
|
||||
</h3>
|
||||
|
||||
|
||||
<div className="space-y-3">
|
||||
<div className="flex justify-between">
|
||||
<span className="text-white/60">Полное название:</span>
|
||||
<span className="text-white">{organization.opfFull}</span>
|
||||
</div>
|
||||
|
||||
|
||||
{organization.opfShort && (
|
||||
<div className="flex justify-between">
|
||||
<span className="text-white/60">Краткое название:</span>
|
||||
<span className="text-white">{organization.opfShort}</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
|
||||
{organization.opfCode && (
|
||||
<div className="flex justify-between">
|
||||
<span className="text-white/60">Код ОКОПФ:</span>
|
||||
@ -301,7 +288,7 @@ export function OrganizationDetailsModal({ organization, open, onOpenChange }: O
|
||||
<Hash className="h-5 w-5 mr-2 text-cyan-400" />
|
||||
Коды статистики
|
||||
</h3>
|
||||
|
||||
|
||||
<div className="space-y-3">
|
||||
{organization.okato && (
|
||||
<div className="flex justify-between">
|
||||
@ -309,21 +296,21 @@ export function OrganizationDetailsModal({ organization, open, onOpenChange }: O
|
||||
<span className="text-white font-mono">{organization.okato}</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
|
||||
{organization.oktmo && (
|
||||
<div className="flex justify-between">
|
||||
<span className="text-white/60">ОКТМО:</span>
|
||||
<span className="text-white font-mono">{organization.oktmo}</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
|
||||
{organization.okpo && (
|
||||
<div className="flex justify-between">
|
||||
<span className="text-white/60">ОКПО:</span>
|
||||
<span className="text-white font-mono">{organization.okpo}</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
|
||||
{organization.okved && (
|
||||
<div className="flex justify-between">
|
||||
<span className="text-white/60">Основной ОКВЭД:</span>
|
||||
@ -341,7 +328,7 @@ export function OrganizationDetailsModal({ organization, open, onOpenChange }: O
|
||||
<CreditCard className="h-5 w-5 mr-2 text-emerald-400" />
|
||||
Финансовая информация
|
||||
</h3>
|
||||
|
||||
|
||||
<div className="space-y-3">
|
||||
{organization.employeeCount && (
|
||||
<div className="flex justify-between">
|
||||
@ -349,14 +336,14 @@ export function OrganizationDetailsModal({ organization, open, onOpenChange }: O
|
||||
<span className="text-white">{organization.employeeCount}</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
|
||||
{organization.revenue && (
|
||||
<div className="flex justify-between">
|
||||
<span className="text-white/60">Выручка:</span>
|
||||
<span className="text-white">{organization.revenue}</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
|
||||
{organization.taxSystem && (
|
||||
<div className="flex justify-between">
|
||||
<span className="text-white/60">Налоговая система:</span>
|
||||
@ -374,23 +361,21 @@ export function OrganizationDetailsModal({ organization, open, onOpenChange }: O
|
||||
<Users className="h-5 w-5 mr-2 text-indigo-400" />
|
||||
Пользователи ({organization.users.length})
|
||||
</h3>
|
||||
|
||||
|
||||
<div className="space-y-3">
|
||||
{organization.users.map((user) => (
|
||||
<div key={user.id} className="flex items-center justify-between">
|
||||
<div className="flex items-center space-x-3">
|
||||
<OrganizationAvatar
|
||||
<OrganizationAvatar
|
||||
organization={{
|
||||
id: user.id,
|
||||
users: [user]
|
||||
}}
|
||||
size="sm"
|
||||
users: [user],
|
||||
}}
|
||||
size="sm"
|
||||
/>
|
||||
<span className="text-white">{user.phone}</span>
|
||||
</div>
|
||||
<span className="text-white/60 text-sm">
|
||||
{formatDate(user.createdAt)}
|
||||
</span>
|
||||
<span className="text-white/60 text-sm">{formatDate(user.createdAt)}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
@ -404,21 +389,23 @@ export function OrganizationDetailsModal({ organization, open, onOpenChange }: O
|
||||
<Briefcase className="h-5 w-5 mr-2 text-pink-400" />
|
||||
API ключи маркетплейсов
|
||||
</h3>
|
||||
|
||||
|
||||
<div className="space-y-3">
|
||||
{organization.apiKeys.map((apiKey) => (
|
||||
<div key={apiKey.id} className="flex items-center justify-between">
|
||||
<div className="flex items-center space-x-3">
|
||||
<Badge className={apiKey.isActive ? 'bg-green-500/20 text-green-300 border-green-500/30' : 'bg-red-500/20 text-red-300 border-red-500/30'}>
|
||||
<Badge
|
||||
className={
|
||||
apiKey.isActive
|
||||
? 'bg-green-500/20 text-green-300 border-green-500/30'
|
||||
: 'bg-red-500/20 text-red-300 border-red-500/30'
|
||||
}
|
||||
>
|
||||
{apiKey.marketplace}
|
||||
</Badge>
|
||||
<span className="text-white/60 text-sm">
|
||||
{apiKey.isActive ? 'Активен' : 'Неактивен'}
|
||||
</span>
|
||||
<span className="text-white/60 text-sm">{apiKey.isActive ? 'Активен' : 'Неактивен'}</span>
|
||||
</div>
|
||||
<span className="text-white/60 text-sm">
|
||||
{formatDate(apiKey.createdAt)}
|
||||
</span>
|
||||
<span className="text-white/60 text-sm">{formatDate(apiKey.createdAt)}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
@ -428,4 +415,4 @@ export function OrganizationDetailsModal({ organization, open, onOpenChange }: O
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -1,23 +1,19 @@
|
||||
"use client"
|
||||
'use client'
|
||||
|
||||
import { useMutation, useQuery } from '@apollo/client'
|
||||
import { ShoppingCart, Eye, ChevronLeft, ChevronRight, Heart } from 'lucide-react'
|
||||
import Image from 'next/image'
|
||||
import { useState } from 'react'
|
||||
import { toast } from 'sonner'
|
||||
|
||||
import { Badge } from '@/components/ui/badge'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@/components/ui/dialog'
|
||||
import {
|
||||
ShoppingCart,
|
||||
Eye,
|
||||
ChevronLeft,
|
||||
ChevronRight,
|
||||
Heart
|
||||
} from 'lucide-react'
|
||||
import { OrganizationAvatar } from './organization-avatar'
|
||||
import { Input } from '@/components/ui/input'
|
||||
import Image from 'next/image'
|
||||
import { useMutation, useQuery } from '@apollo/client'
|
||||
import { ADD_TO_CART, ADD_TO_FAVORITES, REMOVE_FROM_FAVORITES } from '@/graphql/mutations'
|
||||
import { GET_MY_CART, GET_MY_FAVORITES } from '@/graphql/queries'
|
||||
import { toast } from 'sonner'
|
||||
|
||||
import { OrganizationAvatar } from './organization-avatar'
|
||||
|
||||
interface Product {
|
||||
id: string
|
||||
@ -46,7 +42,7 @@ interface Product {
|
||||
address?: string
|
||||
phones?: Array<{ value: string }>
|
||||
emails?: Array<{ value: string }>
|
||||
users?: Array<{ id: string, avatar?: string, managerName?: string }>
|
||||
users?: Array<{ id: string; avatar?: string; managerName?: string }>
|
||||
}
|
||||
}
|
||||
|
||||
@ -77,7 +73,7 @@ export function ProductCard({ product }: ProductCardProps) {
|
||||
onError: (error) => {
|
||||
toast.error('Ошибка при добавлении в заявки')
|
||||
console.error('Error adding to cart:', error)
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
const [addToFavorites, { loading: addingToFavorites }] = useMutation(ADD_TO_FAVORITES, {
|
||||
@ -92,7 +88,7 @@ export function ProductCard({ product }: ProductCardProps) {
|
||||
onError: (error) => {
|
||||
toast.error('Ошибка при добавлении в избранное')
|
||||
console.error('Error adding to favorites:', error)
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
const [removeFromFavorites, { loading: removingFromFavorites }] = useMutation(REMOVE_FROM_FAVORITES, {
|
||||
@ -107,12 +103,12 @@ export function ProductCard({ product }: ProductCardProps) {
|
||||
onError: (error) => {
|
||||
toast.error('Ошибка при удалении из избранного')
|
||||
console.error('Error removing from favorites:', error)
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
const displayPrice = new Intl.NumberFormat('ru-RU', {
|
||||
style: 'currency',
|
||||
currency: 'RUB'
|
||||
currency: 'RUB',
|
||||
}).format(product.price)
|
||||
|
||||
const displayName = product.organization.name || product.organization.fullName || 'Неизвестная организация'
|
||||
@ -124,8 +120,8 @@ export function ProductCard({ product }: ProductCardProps) {
|
||||
await addToCart({
|
||||
variables: {
|
||||
productId: product.id,
|
||||
quantity: quantity
|
||||
}
|
||||
quantity: quantity,
|
||||
},
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('Error adding to cart:', error)
|
||||
@ -136,11 +132,11 @@ export function ProductCard({ product }: ProductCardProps) {
|
||||
try {
|
||||
if (isFavorite) {
|
||||
await removeFromFavorites({
|
||||
variables: { productId: product.id }
|
||||
variables: { productId: product.id },
|
||||
})
|
||||
} else {
|
||||
await addToFavorites({
|
||||
variables: { productId: product.id }
|
||||
variables: { productId: product.id },
|
||||
})
|
||||
}
|
||||
} catch (error) {
|
||||
@ -178,7 +174,7 @@ export function ProductCard({ product }: ProductCardProps) {
|
||||
className="object-cover cursor-pointer"
|
||||
onClick={() => setIsImageDialogOpen(true)}
|
||||
/>
|
||||
|
||||
|
||||
{/* Навигация по изображениям */}
|
||||
{hasMultipleImages && (
|
||||
<>
|
||||
@ -194,7 +190,7 @@ export function ProductCard({ product }: ProductCardProps) {
|
||||
>
|
||||
<ChevronRight className="h-4 w-4" />
|
||||
</button>
|
||||
|
||||
|
||||
{/* Индикаторы изображений */}
|
||||
<div className="absolute bottom-2 left-1/2 -translate-x-1/2 flex space-x-1">
|
||||
{images.map((_, index) => (
|
||||
@ -208,7 +204,7 @@ export function ProductCard({ product }: ProductCardProps) {
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
|
||||
{/* Кнопка увеличения */}
|
||||
<button
|
||||
onClick={() => setIsImageDialogOpen(true)}
|
||||
@ -231,18 +227,14 @@ export function ProductCard({ product }: ProductCardProps) {
|
||||
<h3 className="font-semibold text-white text-sm mb-1 line-clamp-1">{product.name}</h3>
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-base font-bold text-purple-300">{displayPrice}</span>
|
||||
<Badge className={`${stockStatus.color} text-xs`}>
|
||||
{stockStatus.text}
|
||||
</Badge>
|
||||
<Badge className={`${stockStatus.color} text-xs`}>{stockStatus.text}</Badge>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Краткая информация */}
|
||||
<div className="flex items-center justify-between text-xs text-white/60">
|
||||
<span>Арт: {product.article}</span>
|
||||
{product.category && (
|
||||
<span className="bg-white/10 px-2 py-1 rounded text-xs">{product.category.name}</span>
|
||||
)}
|
||||
{product.category && <span className="bg-white/10 px-2 py-1 rounded text-xs">{product.category.name}</span>}
|
||||
</div>
|
||||
|
||||
{/* Информация о продавце */}
|
||||
@ -280,7 +272,7 @@ export function ProductCard({ product }: ProductCardProps) {
|
||||
{/* Кнопки действий */}
|
||||
<div className="flex items-center space-x-2">
|
||||
{/* Кнопка добавления в заявки */}
|
||||
<Button
|
||||
<Button
|
||||
onClick={handleAddToCart}
|
||||
disabled={addingToCart}
|
||||
className="flex-1 h-8 bg-gradient-to-r from-purple-500 to-pink-500 hover:from-purple-600 hover:to-pink-600 text-white border-0 text-xs"
|
||||
@ -290,14 +282,14 @@ export function ProductCard({ product }: ProductCardProps) {
|
||||
</Button>
|
||||
|
||||
{/* Кнопка избранного */}
|
||||
<Button
|
||||
<Button
|
||||
onClick={handleToggleFavorite}
|
||||
disabled={addingToFavorites || removingFromFavorites}
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className={`h-8 w-8 p-0 transition-all ${
|
||||
isFavorite
|
||||
? 'bg-red-500/20 border-red-500/30 text-red-400 hover:bg-red-500/30'
|
||||
isFavorite
|
||||
? 'bg-red-500/20 border-red-500/30 text-red-400 hover:bg-red-500/30'
|
||||
: 'bg-white/5 border-white/20 text-white/60 hover:bg-red-500/20 hover:border-red-500/30 hover:text-red-400'
|
||||
}`}
|
||||
>
|
||||
@ -307,23 +299,20 @@ export function ProductCard({ product }: ProductCardProps) {
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex items-center space-x-2">
|
||||
<Button
|
||||
disabled
|
||||
className="flex-1 h-8 bg-gray-500/20 text-gray-400 border-0 text-xs cursor-not-allowed"
|
||||
>
|
||||
<Button disabled className="flex-1 h-8 bg-gray-500/20 text-gray-400 border-0 text-xs cursor-not-allowed">
|
||||
<ShoppingCart className="h-3 w-3 mr-1" />
|
||||
Недоступно
|
||||
</Button>
|
||||
|
||||
{/* Кнопка избранного (всегда доступна) */}
|
||||
<Button
|
||||
<Button
|
||||
onClick={handleToggleFavorite}
|
||||
disabled={addingToFavorites || removingFromFavorites}
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className={`h-8 w-8 p-0 transition-all ${
|
||||
isFavorite
|
||||
? 'bg-red-500/20 border-red-500/30 text-red-400 hover:bg-red-500/30'
|
||||
isFavorite
|
||||
? 'bg-red-500/20 border-red-500/30 text-red-400 hover:bg-red-500/30'
|
||||
: 'bg-white/5 border-white/20 text-white/60 hover:bg-red-500/20 hover:border-red-500/30 hover:text-red-400'
|
||||
}`}
|
||||
>
|
||||
@ -348,7 +337,7 @@ export function ProductCard({ product }: ProductCardProps) {
|
||||
fill
|
||||
className="object-contain"
|
||||
/>
|
||||
|
||||
|
||||
{hasMultipleImages && (
|
||||
<>
|
||||
<button
|
||||
@ -365,7 +354,7 @@ export function ProductCard({ product }: ProductCardProps) {
|
||||
</button>
|
||||
</>
|
||||
)}
|
||||
|
||||
|
||||
<div className="absolute bottom-4 left-1/2 -translate-x-1/2 bg-black/50 px-3 py-1 rounded-full text-white text-sm">
|
||||
{currentImageIndex + 1} из {images.length}
|
||||
</div>
|
||||
@ -376,4 +365,4 @@ export function ProductCard({ product }: ProductCardProps) {
|
||||
</Dialog>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
Reference in New Issue
Block a user