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

381 lines
14 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 } from 'react'
import { useMutation } from '@apollo/client'
import { Card, CardContent } from '@/components/ui/card'
import { Button } from '@/components/ui/button'
import { Badge } from '@/components/ui/badge'
import {
Package,
ChevronRight,
FolderOpen,
Layers,
Image as ImageIcon,
Download,
Loader2
} from 'lucide-react'
import { FETCH_CATEGORY_PRODUCTS } from '@/lib/graphql/queries'
import toast from 'react-hot-toast'
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 KrajaCategoriesProps {
categories: PartsIndexCategory[] | PartsAPICategory[]
onCategorySelect: (category: PartsIndexCategory | PartsAPICategory, group?: any) => void
type: 'partsindex' | 'partsapi'
}
export const KrajaCategories = ({ categories, onCategorySelect, type }: KrajaCategoriesProps) => {
const [expandedCategories, setExpandedCategories] = useState<Set<string>>(new Set())
const [fetchingCategories, setFetchingCategories] = useState<Set<string>>(new Set())
const [fetchCategoryProducts] = useMutation(FETCH_CATEGORY_PRODUCTS, {
onCompleted: (data) => {
if (data.fetchCategoryProducts.success) {
toast.success(`${data.fetchCategoryProducts.message}`)
} else {
toast.error(`${data.fetchCategoryProducts.message}`)
}
},
onError: (error) => {
toast.error(`${error.message}`)
}
})
const toggleCategory = (categoryId: string) => {
setExpandedCategories(prev => {
const newSet = new Set(prev)
if (newSet.has(categoryId)) {
newSet.delete(categoryId)
} else {
newSet.add(categoryId)
}
return newSet
})
}
const handleCategoryClick = (category: PartsIndexCategory | PartsAPICategory, group?: any) => {
onCategorySelect(category, group)
}
const handleFetchProducts = async (
category: PartsIndexCategory | PartsAPICategory,
group?: any,
fetchAll: boolean = false
) => {
const fetchKey = group ? `${category.id}_${group.id}` : category.id
setFetchingCategories(prev => new Set(prev).add(fetchKey))
try {
await fetchCategoryProducts({
variables: {
input: {
categoryId: category.id,
categoryName: category.name,
categoryType: type.toUpperCase(),
groupId: group?.id,
groupName: group?.name,
fetchAll,
limit: fetchAll ? 1000 : 100
}
}
})
} catch (error) {
console.error('Fetch error:', error)
} finally {
setFetchingCategories(prev => {
const newSet = new Set(prev)
newSet.delete(fetchKey)
return newSet
})
}
}
if (!categories || categories.length === 0) {
return (
<Card>
<CardContent className="p-8">
<div className="text-center text-gray-500">
<Package className="h-12 w-12 mx-auto mb-4 text-gray-300" />
<p>Категории не найдены</p>
</div>
</CardContent>
</Card>
)
}
if (type === 'partsindex') {
const partsIndexCategories = categories as PartsIndexCategory[]
return (
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
{partsIndexCategories.map((category) => (
<Card key={category.id} className="hover:shadow-md transition-shadow">
<CardContent className="p-4">
<div className="space-y-3">
{/* Заголовок категории */}
<div className="flex items-center gap-3">
<div className="w-12 h-12 bg-blue-50 rounded-lg flex items-center justify-center overflow-hidden">
{category.image ? (
<img
src={category.image}
alt={category.name}
className="w-full h-full object-cover"
/>
) : (
<Package className="h-6 w-6 text-blue-600" />
)}
</div>
<div className="flex-1">
<h3 className="font-semibold text-gray-900">{category.name}</h3>
<div className="flex items-center gap-2 mt-1">
<Badge variant="secondary" className="text-xs">
{category.groups?.length || 0} групп
</Badge>
</div>
</div>
<Button
variant="ghost"
size="sm"
onClick={() => toggleCategory(category.id)}
className="text-gray-400 hover:text-gray-600"
>
<ChevronRight
className={`h-4 w-4 transition-transform ${
expandedCategories.has(category.id) ? 'rotate-90' : ''
}`}
/>
</Button>
</div>
{/* Группы категории */}
{expandedCategories.has(category.id) && category.groups && (
<div className="space-y-2 mt-3 border-t pt-3">
{category.groups.map((group) => (
<div key={group.id} className="space-y-2">
<div className="flex items-center gap-2">
<Button
variant="ghost"
size="sm"
onClick={() => handleCategoryClick(category, group)}
className="flex-1 justify-start text-left hover:bg-blue-50"
>
<div className="flex items-center gap-2">
<div className="w-6 h-6 bg-gray-100 rounded flex items-center justify-center overflow-hidden">
{group.image ? (
<img
src={group.image}
alt={group.name}
className="w-full h-full object-cover"
/>
) : (
<FolderOpen className="h-3 w-3 text-gray-500" />
)}
</div>
<span className="text-sm text-gray-700">{group.name}</span>
{group.entityNames && (
<Badge variant="outline" className="text-xs ml-auto">
{group.entityNames.length} товаров
</Badge>
)}
</div>
</Button>
<Button
variant="outline"
size="sm"
onClick={() => handleFetchProducts(category, group, true)}
disabled={fetchingCategories.has(`${category.id}_${group.id}`)}
className="px-2"
title="Сохранить все товары группы"
>
{fetchingCategories.has(`${category.id}_${group.id}`) ? (
<Loader2 className="h-3 w-3 animate-spin" />
) : (
<Download className="h-3 w-3" />
)}
</Button>
</div>
{/* Подгруппы */}
{group.subgroups && group.subgroups.length > 0 && (
<div className="ml-6 space-y-1">
{group.subgroups.slice(0, 3).map((subgroup) => (
<Button
key={subgroup.id}
variant="ghost"
size="sm"
onClick={() => handleCategoryClick(category, subgroup)}
className="w-full justify-start text-left text-xs hover:bg-blue-50"
>
<div className="flex items-center gap-2">
<Layers className="h-3 w-3 text-gray-400" />
<span className="text-gray-600">{subgroup.name}</span>
{subgroup.entityNames && (
<Badge variant="outline" className="text-xs ml-auto">
{subgroup.entityNames.length}
</Badge>
)}
</div>
</Button>
))}
{group.subgroups.length > 3 && (
<div className="text-xs text-gray-500 ml-6">
и ещё {group.subgroups.length - 3} подгрупп...
</div>
)}
</div>
)}
</div>
))}
</div>
)}
{/* Кнопки действий */}
<div className="space-y-2 mt-3">
<Button
variant="outline"
size="sm"
onClick={() => handleCategoryClick(category)}
className="w-full"
>
<Package className="h-4 w-4 mr-2" />
Просмотреть товары
</Button>
<Button
variant="default"
size="sm"
onClick={() => handleFetchProducts(category, null, true)}
disabled={fetchingCategories.has(category.id)}
className="w-full"
>
{fetchingCategories.has(category.id) ? (
<Loader2 className="h-4 w-4 mr-2 animate-spin" />
) : (
<Download className="h-4 w-4 mr-2" />
)}
Сохранить все товары
</Button>
</div>
</div>
</CardContent>
</Card>
))}
</div>
)
}
// PartsAPI categories (tree structure)
const partsAPICategories = categories as PartsAPICategory[]
const renderPartsAPICategory = (category: PartsAPICategory, level: number = 0) => (
<div key={category.id} className={`${level > 0 ? 'ml-4' : ''}`}>
<Card className="mb-2 hover:shadow-md transition-shadow">
<CardContent className="p-3">
<div className="flex items-center justify-between">
<div className="flex items-center gap-2">
<div className="w-8 h-8 bg-green-50 rounded flex items-center justify-center">
<Package className="h-4 w-4 text-green-600" />
</div>
<div>
<h4 className="font-medium text-gray-900">{category.name}</h4>
<div className="flex items-center gap-2">
<Badge variant="outline" className="text-xs">
Уровень {category.level}
</Badge>
{category.children && category.children.length > 0 && (
<Badge variant="secondary" className="text-xs">
{category.children.length} подкатегорий
</Badge>
)}
</div>
</div>
</div>
<div className="flex items-center gap-2">
<Button
variant="outline"
size="sm"
onClick={() => handleCategoryClick(category)}
>
Просмотреть
</Button>
<Button
variant="default"
size="sm"
onClick={() => handleFetchProducts(category, null, true)}
disabled={fetchingCategories.has(category.id)}
title="Сохранить все товары категории"
>
{fetchingCategories.has(category.id) ? (
<Loader2 className="h-3 w-3 animate-spin" />
) : (
<Download className="h-3 w-3" />
)}
</Button>
{category.children && category.children.length > 0 && (
<Button
variant="ghost"
size="sm"
onClick={() => toggleCategory(category.id)}
>
<ChevronRight
className={`h-4 w-4 transition-transform ${
expandedCategories.has(category.id) ? 'rotate-90' : ''
}`}
/>
</Button>
)}
</div>
</div>
</CardContent>
</Card>
{/* Подкатегории */}
{expandedCategories.has(category.id) && category.children && (
<div className="ml-4 mt-2">
{category.children.map((child) => renderPartsAPICategory(child, level + 1))}
</div>
)}
</div>
)
return (
<div className="space-y-2">
{partsAPICategories.map((category) => renderPartsAPICategory(category))}
</div>
)
}