Files
protekauto-cms/src/components/kraja/KrajaCategoryItems.tsx
2025-07-29 18:55:09 +03:00

538 lines
19 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

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

'use client'
import { useState, useEffect } from 'react'
import { useQuery } from '@apollo/client'
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
import { Button } from '@/components/ui/button'
import { Input } from '@/components/ui/input'
import { Badge } from '@/components/ui/badge'
import {
Tabs,
TabsContent,
TabsList,
TabsTrigger,
} from '@/components/ui/tabs'
import {
Package,
Search,
Loader2,
AlertCircle,
Eye,
Filter,
Grid,
List
} from 'lucide-react'
import { GET_PARTSINDEX_CATALOG_ENTITIES, GET_PARTSAPI_ARTICLES, GET_CATEGORY_PRODUCTS } from '@/lib/graphql/queries'
interface PartsIndexCategory {
id: string
name: string
image?: string
groups?: Array<{
id: string
name: string
image?: string
subgroups?: Array<{
id: string
name: string
image?: string
entityNames?: Array<{
id: string
name: string
}>
}>
entityNames?: Array<{
id: string
name: string
}>
}>
}
interface PartsAPICategory {
id: string
name: string
level: number
parentId?: string
children?: PartsAPICategory[]
}
interface KrajaCategoryItemsProps {
category: PartsIndexCategory | PartsAPICategory
group?: any
categoryType: 'partsindex' | 'partsapi'
isViewingSavedData?: boolean
}
interface PartsIndexEntity {
id: string
name: string
image?: string
brand?: string
description?: string
price?: number
}
interface PartsAPIArticle {
supBrand: string
supId: number
productGroup: string
ptId: number
artSupBrand: string
artArticleNr: string
artId: string
}
export const KrajaCategoryItems = ({ category, group, categoryType, isViewingSavedData = false }: KrajaCategoryItemsProps) => {
const [searchQuery, setSearchQuery] = useState('')
const [viewMode, setViewMode] = useState<'grid' | 'list'>('grid')
const [currentPage, setCurrentPage] = useState(1)
const itemsPerPage = isViewingSavedData ? 100 : 20
// Для PartsIndex
const {
data: partsIndexData,
loading: partsIndexLoading,
error: partsIndexError,
refetch: refetchPartsIndex
} = useQuery(GET_PARTSINDEX_CATALOG_ENTITIES, {
variables: {
catalogId: categoryType === 'partsindex' ? category.id : undefined,
groupId: group?.id || undefined,
lang: 'ru',
limit: itemsPerPage,
page: currentPage,
q: searchQuery || undefined
},
skip: categoryType !== 'partsindex' || !category.id,
errorPolicy: 'all'
})
// Для PartsAPI - используем strId (нужно преобразовать id в число)
const {
data: partsAPIData,
loading: partsAPILoading,
error: partsAPIError,
refetch: refetchPartsAPI
} = useQuery(GET_PARTSAPI_ARTICLES, {
variables: {
strId: categoryType === 'partsapi' ? parseInt(category.id) : undefined,
carId: 9877,
carType: 'PC'
},
skip: categoryType !== 'partsapi' || !category.id || isViewingSavedData,
errorPolicy: 'all'
})
// Для просмотра сохраненных данных
const {
data: savedData,
loading: savedLoading,
error: savedError,
refetch: refetchSaved
} = useQuery(GET_CATEGORY_PRODUCTS, {
variables: {
categoryId: category.id,
categoryType: categoryType.toUpperCase(),
search: searchQuery || undefined,
limit: itemsPerPage,
offset: (currentPage - 1) * itemsPerPage
},
skip: !isViewingSavedData,
errorPolicy: 'all'
})
// Обновляем поиск с задержкой
useEffect(() => {
const timeoutId = setTimeout(() => {
if (isViewingSavedData) {
refetchSaved()
} else if (categoryType === 'partsindex') {
refetchPartsIndex()
} else {
refetchPartsAPI()
}
}, 500)
return () => clearTimeout(timeoutId)
}, [searchQuery, categoryType, isViewingSavedData, refetchPartsIndex, refetchPartsAPI, refetchSaved])
const isLoading = isViewingSavedData
? savedLoading
: (categoryType === 'partsindex' ? partsIndexLoading : partsAPILoading)
const error = isViewingSavedData
? savedError
: (categoryType === 'partsindex' ? partsIndexError : partsAPIError)
const items = isViewingSavedData
? savedData?.getCategoryProducts?.products || []
: (categoryType === 'partsindex'
? partsIndexData?.partsIndexCatalogEntities?.list || []
: partsAPIData?.partsAPIArticles || [])
const renderPartsIndexItem = (item: PartsIndexEntity) => (
<Card key={item.id} className={`hover:shadow-md transition-shadow ${viewMode === 'list' ? 'mb-2' : ''}`}>
<CardContent className={`${viewMode === 'grid' ? 'p-4' : 'p-3'}`}>
{viewMode === 'grid' ? (
<div className="space-y-3">
<div className="flex items-center gap-3">
<div className="w-12 h-12 bg-gray-100 rounded-lg flex items-center justify-center overflow-hidden">
{item.image ? (
<img
src={item.image}
alt={item.name}
className="w-full h-full object-cover"
/>
) : (
<Package className="h-6 w-6 text-gray-400" />
)}
</div>
<div className="flex-1">
<h4 className="font-medium text-gray-900 line-clamp-2">{item.name}</h4>
{item.brand && (
<Badge variant="outline" className="text-xs mt-1">
{item.brand}
</Badge>
)}
</div>
</div>
{item.description && (
<p className="text-sm text-gray-600 line-clamp-2">{item.description}</p>
)}
{item.price && (
<div className="text-lg font-semibold text-blue-600">
{item.price.toLocaleString('ru-RU')}
</div>
)}
<Button variant="outline" size="sm" className="w-full">
<Eye className="h-4 w-4 mr-2" />
Подробнее
</Button>
</div>
) : (
<div className="flex items-center gap-4">
<div className="w-10 h-10 bg-gray-100 rounded flex items-center justify-center overflow-hidden">
{item.image ? (
<img
src={item.image}
alt={item.name}
className="w-full h-full object-cover"
/>
) : (
<Package className="h-5 w-5 text-gray-400" />
)}
</div>
<div className="flex-1">
<h4 className="font-medium text-gray-900">{item.name}</h4>
<div className="flex items-center gap-2 mt-1">
{item.brand && (
<Badge variant="outline" className="text-xs">
{item.brand}
</Badge>
)}
{item.price && (
<span className="text-sm font-semibold text-blue-600">
{item.price.toLocaleString('ru-RU')}
</span>
)}
</div>
</div>
<Button variant="outline" size="sm">
<Eye className="h-4 w-4 mr-2" />
Подробнее
</Button>
</div>
)}
</CardContent>
</Card>
)
const renderSavedItem = (item: any) => (
<Card key={item.id} className={`hover:shadow-md transition-shadow ${viewMode === 'list' ? 'mb-2' : ''}`}>
<CardContent className={`${viewMode === 'grid' ? 'p-4' : 'p-3'}`}>
{viewMode === 'grid' ? (
<div className="space-y-3">
<div className="flex items-center gap-3">
<div className="w-12 h-12 bg-gray-100 rounded-lg flex items-center justify-center overflow-hidden">
{item.image_url ? (
<img
src={item.image_url}
alt={item.name}
className="w-full h-full object-cover"
/>
) : (
<Package className="h-6 w-6 text-gray-400" />
)}
</div>
<div className="flex-1">
<h4 className="font-medium text-gray-900 line-clamp-2">{item.name}</h4>
{item.brand && (
<Badge variant="outline" className="text-xs mt-1">
{item.brand}
</Badge>
)}
</div>
</div>
{item.description && (
<p className="text-sm text-gray-600 line-clamp-2">{item.description}</p>
)}
{item.price && (
<div className="text-lg font-semibold text-blue-600">
{parseFloat(item.price).toLocaleString('ru-RU')}
</div>
)}
<div className="text-xs text-gray-500">
Сохранено: {new Date(item.created_at).toLocaleDateString('ru-RU')}
</div>
<Button variant="outline" size="sm" className="w-full">
<Eye className="h-4 w-4 mr-2" />
Подробнее
</Button>
</div>
) : (
<div className="flex items-center gap-4">
<div className="w-10 h-10 bg-gray-100 rounded flex items-center justify-center overflow-hidden">
{item.image_url ? (
<img
src={item.image_url}
alt={item.name}
className="w-full h-full object-cover"
/>
) : (
<Package className="h-5 w-5 text-gray-400" />
)}
</div>
<div className="flex-1">
<h4 className="font-medium text-gray-900">{item.name}</h4>
<div className="flex items-center gap-2 mt-1">
{item.brand && (
<Badge variant="outline" className="text-xs">
{item.brand}
</Badge>
)}
{item.price && (
<span className="text-sm font-semibold text-blue-600">
{parseFloat(item.price).toLocaleString('ru-RU')}
</span>
)}
<span className="text-xs text-gray-500">
{new Date(item.created_at).toLocaleDateString('ru-RU')}
</span>
</div>
</div>
<Button variant="outline" size="sm">
<Eye className="h-4 w-4 mr-2" />
Подробнее
</Button>
</div>
)}
</CardContent>
</Card>
)
const renderPartsAPIItem = (item: PartsAPIArticle, index: number) => (
<Card key={`${item.artId}-${index}`} className={`hover:shadow-md transition-shadow ${viewMode === 'list' ? 'mb-2' : ''}`}>
<CardContent className={`${viewMode === 'grid' ? 'p-4' : 'p-3'}`}>
{viewMode === 'grid' ? (
<div className="space-y-3">
<div className="flex items-center gap-3">
<div className="w-12 h-12 bg-green-100 rounded-lg flex items-center justify-center">
<Package className="h-6 w-6 text-green-600" />
</div>
<div className="flex-1">
<h4 className="font-medium text-gray-900">{item.artArticleNr}</h4>
<div className="flex items-center gap-2 mt-1">
<Badge variant="outline" className="text-xs">
{item.artSupBrand}
</Badge>
</div>
</div>
</div>
<div className="space-y-1">
<p className="text-sm text-gray-600">Группа: {item.productGroup}</p>
<p className="text-xs text-gray-500">Поставщик: {item.supBrand}</p>
<p className="text-xs text-gray-500">ID: {item.artId}</p>
</div>
<Button variant="outline" size="sm" className="w-full">
<Eye className="h-4 w-4 mr-2" />
Подробнее
</Button>
</div>
) : (
<div className="flex items-center gap-4">
<div className="w-10 h-10 bg-green-100 rounded flex items-center justify-center">
<Package className="h-5 w-5 text-green-600" />
</div>
<div className="flex-1">
<h4 className="font-medium text-gray-900">{item.artArticleNr}</h4>
<div className="flex items-center gap-2 mt-1">
<Badge variant="outline" className="text-xs">
{item.artSupBrand}
</Badge>
<span className="text-xs text-gray-500">{item.productGroup}</span>
</div>
</div>
<Button variant="outline" size="sm">
<Eye className="h-4 w-4 mr-2" />
Подробнее
</Button>
</div>
)}
</CardContent>
</Card>
)
return (
<div className="space-y-6">
{/* Панель управления */}
<Card>
<CardContent className="p-4">
<div className="flex flex-col md:flex-row gap-4 items-start md:items-center justify-between">
{/* Поиск */}
<div className="relative flex-1 max-w-md">
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 h-4 w-4 text-gray-400" />
<Input
placeholder="Поиск товаров..."
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
className="pl-10"
/>
</div>
{/* Элементы управления */}
<div className="flex items-center gap-2">
<Badge variant="secondary" className="text-sm">
{items.length} товаров
</Badge>
<div className="flex items-center border rounded-md">
<Button
variant={viewMode === 'grid' ? 'default' : 'ghost'}
size="sm"
onClick={() => setViewMode('grid')}
className="rounded-r-none"
>
<Grid className="h-4 w-4" />
</Button>
<Button
variant={viewMode === 'list' ? 'default' : 'ghost'}
size="sm"
onClick={() => setViewMode('list')}
className="rounded-l-none"
>
<List className="h-4 w-4" />
</Button>
</div>
</div>
</div>
</CardContent>
</Card>
{/* Содержимое */}
{isLoading ? (
<Card>
<CardContent className="p-12">
<div className="flex items-center justify-center">
<Loader2 className="h-8 w-8 animate-spin text-gray-400 mr-3" />
<span className="text-gray-600">Загрузка товаров...</span>
</div>
</CardContent>
</Card>
) : error ? (
<Card>
<CardContent className="p-6">
<div className="flex items-center justify-center text-red-600">
<AlertCircle className="h-6 w-6 mr-2" />
<span>Ошибка загрузки: {error.message}</span>
</div>
</CardContent>
</Card>
) : items.length === 0 ? (
<Card>
<CardContent className="p-12">
<div className="text-center text-gray-500">
<Package className="h-12 w-12 mx-auto mb-4 text-gray-300" />
<p className="text-lg mb-2">Товары не найдены</p>
<p className="text-sm">Попробуйте изменить критерии поиска</p>
</div>
</CardContent>
</Card>
) : (
<div className={
viewMode === 'grid'
? 'grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-4'
: 'space-y-2'
}>
{isViewingSavedData
? items.map((item: any) => renderSavedItem(item))
: (categoryType === 'partsindex'
? items.map((item: PartsIndexEntity) => renderPartsIndexItem(item))
: items.map((item: PartsAPIArticle, index: number) => renderPartsAPIItem(item, index))
)
}
</div>
)}
{/* Пагинация и статистика */}
{isViewingSavedData && savedData?.getCategoryProducts && (
<Card>
<CardContent className="p-4">
<div className="flex items-center justify-between">
<div className="text-sm text-gray-600">
Показано {((currentPage - 1) * itemsPerPage) + 1}-{Math.min(currentPage * itemsPerPage, savedData.getCategoryProducts.total)} из {savedData.getCategoryProducts.total.toLocaleString()} сохраненных товаров
</div>
<div className="flex items-center gap-2">
<Button
variant="outline"
size="sm"
onClick={() => setCurrentPage(Math.max(1, currentPage - 1))}
disabled={currentPage === 1}
>
Предыдущая
</Button>
<span className="text-sm text-gray-600 px-2">
Страница {currentPage}
</span>
<Button
variant="outline"
size="sm"
onClick={() => setCurrentPage(currentPage + 1)}
disabled={items.length < itemsPerPage}
>
Следующая
</Button>
</div>
</div>
</CardContent>
</Card>
)}
{/* Пагинация для обычного просмотра */}
{!isViewingSavedData && items.length >= itemsPerPage && (
<Card>
<CardContent className="p-4">
<div className="flex items-center justify-center gap-2">
<Button
variant="outline"
onClick={() => setCurrentPage(Math.max(1, currentPage - 1))}
disabled={currentPage === 1}
>
Предыдущая
</Button>
<span className="text-sm text-gray-600 px-4">
Страница {currentPage}
</span>
<Button
variant="outline"
onClick={() => setCurrentPage(currentPage + 1)}
disabled={items.length < itemsPerPage}
>
Следующая
</Button>
</div>
</CardContent>
</Card>
)}
</div>
)
}