Удалены страницы для управления товарами с лучшими ценами и топом продаж. Обновлены компоненты и резолверы для улучшения структуры данных и функциональности. Изменены типы GraphQL и обновлены запросы для повышения гибкости взаимодействия с API. Упрощена навигация в боковом меню, что улучшает пользовательский опыт.

This commit is contained in:
Bivekich
2025-07-11 02:42:44 +03:00
parent 013c05fb02
commit db29525da5
11 changed files with 1628 additions and 1037 deletions

View File

@ -0,0 +1,266 @@
"use client"
import React, { useState } from 'react'
import { Button } from '@/components/ui/button'
import { Input } from '@/components/ui/input'
import { Label } from '@/components/ui/label'
import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
DialogFooter,
} from '@/components/ui/dialog'
import { Search, ChevronRight, ChevronDown, Folder, FolderOpen } from 'lucide-react'
interface Category {
id: string
name: string
slug: string
level?: number
parentId?: string | null
children?: Category[]
_count?: {
products: number
}
}
interface CategorySelectorProps {
open: boolean
onOpenChange: (open: boolean) => void
categories: Category[]
onCategorySelect: (categoryId: string, categoryName: string) => void
title?: string
description?: string
}
export const CategorySelector = ({
open,
onOpenChange,
categories,
onCategorySelect,
title = "Выберите категорию",
description = "Выберите категорию для перемещения товаров"
}: CategorySelectorProps) => {
const [searchQuery, setSearchQuery] = useState('')
const [selectedCategoryId, setSelectedCategoryId] = useState<string | null>(null)
const [expandedCategories, setExpandedCategories] = useState<Set<string>>(new Set())
// Функция для построения дерева категорий
const buildCategoryTree = (categories: Category[]): Category[] => {
const categoryMap = new Map<string, Category>()
const rootCategories: Category[] = []
// Создаем карту всех категорий
categories.forEach(category => {
categoryMap.set(category.id, { ...category, children: [] })
})
// Строим дерево
categories.forEach(category => {
const categoryWithChildren = categoryMap.get(category.id)!
if (category.parentId) {
const parent = categoryMap.get(category.parentId)
if (parent) {
parent.children = parent.children || []
parent.children.push(categoryWithChildren)
}
} else {
rootCategories.push(categoryWithChildren)
}
})
return rootCategories
}
// Фильтрация категорий по поисковому запросу
const filterCategories = (categories: Category[], query: string): Category[] => {
if (!query) return categories
const filtered: Category[] = []
categories.forEach(category => {
const matchesQuery = category.name.toLowerCase().includes(query.toLowerCase())
const filteredChildren = category.children ? filterCategories(category.children, query) : []
if (matchesQuery || filteredChildren.length > 0) {
filtered.push({
...category,
children: filteredChildren
})
}
})
return filtered
}
const toggleExpanded = (categoryId: string) => {
const newExpanded = new Set(expandedCategories)
if (newExpanded.has(categoryId)) {
newExpanded.delete(categoryId)
} else {
newExpanded.add(categoryId)
}
setExpandedCategories(newExpanded)
}
const renderCategory = (category: Category, level = 0) => {
const hasChildren = category.children && category.children.length > 0
const isExpanded = expandedCategories.has(category.id)
const isSelected = selectedCategoryId === category.id
return (
<div key={category.id}>
<div
className={`flex items-center py-2 px-3 rounded cursor-pointer hover:bg-gray-50 ${
isSelected ? 'bg-blue-50 border border-blue-200' : ''
}`}
style={{ paddingLeft: `${level * 20 + 12}px` }}
onClick={() => setSelectedCategoryId(category.id)}
>
{hasChildren && (
<button
onClick={(e) => {
e.stopPropagation()
toggleExpanded(category.id)
}}
className="mr-2 p-1 hover:bg-gray-200 rounded"
>
{isExpanded ? (
<ChevronDown className="w-4 h-4" />
) : (
<ChevronRight className="w-4 h-4" />
)}
</button>
)}
{!hasChildren && <div className="w-6 mr-2" />}
<div className="mr-2">
{hasChildren ? (
isExpanded ? <FolderOpen className="w-4 h-4 text-blue-500" /> : <Folder className="w-4 h-4 text-blue-500" />
) : (
<Folder className="w-4 h-4 text-gray-400" />
)}
</div>
<div className="flex-1">
<span className={`text-sm ${isSelected ? 'font-medium text-blue-700' : 'text-gray-900'}`}>
{category.name}
</span>
{category._count && (
<span className="text-xs text-gray-500 ml-2">
({category._count.products} товаров)
</span>
)}
</div>
</div>
{hasChildren && isExpanded && (
<div>
{category.children!.map(child => renderCategory(child, level + 1))}
</div>
)}
</div>
)
}
const handleConfirm = () => {
if (selectedCategoryId) {
const selectedCategory = categories.find(cat => cat.id === selectedCategoryId)
if (selectedCategory) {
onCategorySelect(selectedCategoryId, selectedCategory.name)
onOpenChange(false)
setSelectedCategoryId(null)
setSearchQuery('')
}
}
}
const handleCancel = () => {
onOpenChange(false)
setSelectedCategoryId(null)
setSearchQuery('')
}
const categoryTree = buildCategoryTree(categories)
const filteredCategories = filterCategories(categoryTree, searchQuery)
// Автоматически разворачиваем категории при поиске
React.useEffect(() => {
if (searchQuery) {
const expandAll = (categories: Category[]) => {
categories.forEach(category => {
if (category.children && category.children.length > 0) {
setExpandedCategories(prev => new Set([...prev, category.id]))
expandAll(category.children)
}
})
}
expandAll(filteredCategories)
}
}, [searchQuery, filteredCategories])
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="max-w-2xl max-h-[80vh] overflow-hidden flex flex-col">
<DialogHeader>
<DialogTitle>{title}</DialogTitle>
{description && (
<p className="text-sm text-gray-600">{description}</p>
)}
</DialogHeader>
<div className="space-y-4 flex-1 overflow-hidden flex flex-col">
{/* Поиск */}
<div className="relative">
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400 w-4 h-4" />
<Input
placeholder="Поиск категорий..."
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
className="pl-10"
/>
</div>
{/* Список категорий */}
<div className="flex-1 overflow-y-auto border rounded-lg">
{filteredCategories.length > 0 ? (
<div className="p-2 space-y-1">
{filteredCategories.map(category => renderCategory(category))}
</div>
) : (
<div className="p-8 text-center text-gray-500">
{searchQuery ? 'Категории не найдены' : 'Нет доступных категорий'}
</div>
)}
</div>
{/* Выбранная категория */}
{selectedCategoryId && (
<div className="p-3 bg-blue-50 border border-blue-200 rounded-lg">
<Label className="text-sm font-medium text-blue-900">Выбранная категория:</Label>
<p className="text-sm text-blue-700 mt-1">
{categories.find(cat => cat.id === selectedCategoryId)?.name}
</p>
</div>
)}
</div>
<DialogFooter>
<Button variant="outline" onClick={handleCancel}>
Отмена
</Button>
<Button
onClick={handleConfirm}
disabled={!selectedCategoryId}
style={{ cursor: selectedCategoryId ? 'pointer' : 'not-allowed' }}
>
Переместить товары
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
)
}

View File

@ -29,6 +29,7 @@ interface Product {
isVisible: boolean
applyDiscounts: boolean
stock: number
brand?: string
categories: Category[]
images: ProductImage[]
characteristics: ProductCharacteristic[]
@ -188,6 +189,7 @@ export const ProductForm = ({
isVisible: true,
applyDiscounts: true,
stock: '0',
brand: '',
categoryIds: selectedCategoryId ? [selectedCategoryId] : [] as string[]
})
@ -250,6 +252,7 @@ export const ProductForm = ({
isVisible: product.isVisible ?? true,
applyDiscounts: product.applyDiscounts ?? true,
stock: product.stock?.toString() || '0',
brand: product.brand || '',
categoryIds: product.categories?.map(cat => cat.id) || []
})
// Очищаем изображения от лишних полей
@ -474,6 +477,11 @@ export const ProductForm = ({
return
}
if (!formData.brand.trim()) {
alert('Введите бренд товара')
return
}
try {
const dimensions = [formData.dimensionLength, formData.dimensionWidth, formData.dimensionHeight]
.filter(d => d.trim())
@ -493,6 +501,7 @@ export const ProductForm = ({
isVisible: formData.isVisible,
applyDiscounts: formData.applyDiscounts,
stock: parseInt(formData.stock) || 0,
brand: formData.brand.trim(),
categoryIds: formData.categoryIds
}
@ -658,8 +667,8 @@ export const ProductForm = ({
</div>
{/* Основная информация */}
<div className="grid grid-cols-2 gap-4">
<div className="col-span-2">
<div className="grid grid-cols-3 gap-4">
<div className="col-span-3">
<Label htmlFor="name">Наименование *</Label>
<Input
id="name"
@ -680,6 +689,18 @@ export const ProductForm = ({
/>
</div>
<div>
<Label htmlFor="brand">Бренд *</Label>
<Input
id="brand"
value={formData.brand}
onChange={(e) => handleInputChange('brand', e.target.value)}
placeholder="Введите бренд товара"
required
style={{ cursor: 'pointer' }}
/>
</div>
<div>
<Label htmlFor="slug">Адрес (Slug)</Label>
<Input

View File

@ -1,12 +1,13 @@
"use client"
import React, { useState } from 'react'
import { Loader2, Package, Plus, Edit, Trash2 } from 'lucide-react'
import { Loader2, Package, Plus, Edit, Trash2, FolderOpen } from 'lucide-react'
import { Button } from '@/components/ui/button'
import { Checkbox } from '@/components/ui/checkbox'
import { Switch } from '@/components/ui/switch'
import { useMutation } from '@apollo/client'
import { DELETE_PRODUCT, DELETE_PRODUCTS, UPDATE_PRODUCT_VISIBILITY, UPDATE_PRODUCTS_VISIBILITY } from '@/lib/graphql/mutations'
import { DELETE_PRODUCT, DELETE_PRODUCTS, UPDATE_PRODUCT_VISIBILITY, UPDATE_PRODUCTS_VISIBILITY, MOVE_PRODUCTS_TO_CATEGORY } from '@/lib/graphql/mutations'
import { CategorySelector } from './CategorySelector'
interface Product {
id: string
@ -20,22 +21,36 @@ interface Product {
categories: { id: string; name: string }[]
}
interface Category {
id: string
name: string
slug: string
level?: number
parentId?: string | null
_count?: {
products: number
}
}
interface ProductListProps {
products: Product[]
loading?: boolean
onProductEdit: (product: Product) => void
onProductCreated: () => void
categories?: Category[]
}
export const ProductList = ({ products, loading, onProductEdit, onProductCreated }: ProductListProps) => {
export const ProductList = ({ products, loading, onProductEdit, onProductCreated, categories = [] }: ProductListProps) => {
const [selectedProducts, setSelectedProducts] = useState<string[]>([])
const [selectAll, setSelectAll] = useState(false)
const [bulkLoading, setBulkLoading] = useState(false)
const [showCategorySelector, setShowCategorySelector] = useState(false)
const [deleteProduct] = useMutation(DELETE_PRODUCT)
const [deleteProducts] = useMutation(DELETE_PRODUCTS)
const [updateProductVisibility] = useMutation(UPDATE_PRODUCT_VISIBILITY)
const [updateProductsVisibility] = useMutation(UPDATE_PRODUCTS_VISIBILITY)
const [moveProductsToCategory] = useMutation(MOVE_PRODUCTS_TO_CATEGORY)
const handleSelectAll = (checked: boolean) => {
setSelectAll(checked)
@ -120,6 +135,30 @@ export const ProductList = ({ products, loading, onProductEdit, onProductCreated
}
}
const handleMoveToCategory = async (categoryId: string, categoryName: string) => {
if (selectedProducts.length === 0) return
setBulkLoading(true)
try {
const result = await moveProductsToCategory({
variables: {
productIds: selectedProducts,
categoryId
}
})
console.log('Результат перемещения товаров:', result)
alert(`Успешно перемещено ${result.data?.moveProductsToCategory?.count || selectedProducts.length} товаров в категорию "${categoryName}"`)
setSelectedProducts([])
setSelectAll(false)
onProductCreated() // Обновляем список
} catch (error) {
console.error('Ошибка перемещения товаров:', error)
alert('Не удалось переместить товары в категорию')
} finally {
setBulkLoading(false)
}
}
if (loading) {
return (
<div className="flex items-center justify-center py-12">
@ -174,6 +213,15 @@ export const ProductList = ({ products, loading, onProductEdit, onProductCreated
{bulkLoading ? <Loader2 className="w-4 h-4 mr-2 animate-spin" /> : null}
Скрыть с сайта
</Button>
<Button
variant="outline"
size="sm"
onClick={() => setShowCategorySelector(true)}
disabled={bulkLoading}
>
{bulkLoading ? <Loader2 className="w-4 h-4 mr-2 animate-spin" /> : <FolderOpen className="w-4 h-4 mr-2" />}
Переместить в категорию
</Button>
<Button
variant="outline"
size="sm"
@ -311,7 +359,15 @@ export const ProductList = ({ products, loading, onProductEdit, onProductCreated
))}
</div>
{/* Модальное окно выбора категории */}
<CategorySelector
open={showCategorySelector}
onOpenChange={setShowCategorySelector}
categories={categories}
onCategorySelect={handleMoveToCategory}
title="Переместить товары в категорию"
description={`Выберите категорию для перемещения ${selectedProducts.length} товаров`}
/>
</div>
)
}

View File

@ -39,16 +39,6 @@ const navigationItems = [
href: '/dashboard/homepage-products',
icon: Star,
},
{
title: 'Лучшие цены',
href: '/dashboard/best-price-products',
icon: Star,
},
{
title: 'Топ продаж',
href: '/dashboard/top-sales-products',
icon: Star,
},
{
title: 'Заказы',
href: '/dashboard/orders',