Добавлены новые модели и мутации для управления товарами дня, лучшими ценами и топом продаж. Обновлены типы GraphQL и резолверы для обработки запросов, что улучшает функциональность и структуру данных. В боковое меню добавлены новые элементы для навигации по товарам. Это повышает удобство работы с приложением и расширяет возможности взаимодействия с API.
This commit is contained in:
390
src/app/dashboard/best-price-products/page.tsx
Normal file
390
src/app/dashboard/best-price-products/page.tsx
Normal file
@ -0,0 +1,390 @@
|
||||
"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 { Label } from '@/components/ui/label'
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from '@/components/ui/dialog'
|
||||
import {
|
||||
Plus,
|
||||
Search,
|
||||
Edit,
|
||||
Trash2,
|
||||
Package,
|
||||
Star
|
||||
} from 'lucide-react'
|
||||
import { GET_BEST_PRICE_PRODUCTS, GET_PRODUCTS } from '@/lib/graphql/queries'
|
||||
import { CREATE_BEST_PRICE_PRODUCT, UPDATE_BEST_PRICE_PRODUCT, DELETE_BEST_PRICE_PRODUCT } from '@/lib/graphql/mutations'
|
||||
import toast from 'react-hot-toast'
|
||||
|
||||
interface BestPriceProduct {
|
||||
id: string
|
||||
productId: string
|
||||
discount: number
|
||||
isActive: boolean
|
||||
sortOrder: number
|
||||
product: {
|
||||
id: string
|
||||
name: string
|
||||
article?: string
|
||||
brand?: string
|
||||
retailPrice?: number
|
||||
images: { url: string; alt?: string }[]
|
||||
}
|
||||
}
|
||||
|
||||
interface Product {
|
||||
id: string
|
||||
name: string
|
||||
article?: string
|
||||
brand?: string
|
||||
retailPrice?: number
|
||||
images: { url: string; alt?: string }[]
|
||||
}
|
||||
|
||||
export default function BestPriceProductsPage() {
|
||||
const [showProductSelector, setShowProductSelector] = useState(false)
|
||||
const [editingBestPriceProduct, setEditingBestPriceProduct] = useState<BestPriceProduct | null>(null)
|
||||
const [searchQuery, setSearchQuery] = useState('')
|
||||
const [discount, setDiscount] = useState<number>(0)
|
||||
|
||||
const { data: bestPriceProductsData, loading: bestPriceProductsLoading, refetch: refetchBestPriceProducts } = useQuery(GET_BEST_PRICE_PRODUCTS)
|
||||
|
||||
const { data: productsData, loading: productsLoading } = useQuery(GET_PRODUCTS, {
|
||||
variables: {
|
||||
search: searchQuery || undefined,
|
||||
limit: 50
|
||||
},
|
||||
skip: !showProductSelector
|
||||
})
|
||||
|
||||
const [createBestPriceProduct, { loading: creating }] = useMutation(CREATE_BEST_PRICE_PRODUCT)
|
||||
const [updateBestPriceProduct, { loading: updating }] = useMutation(UPDATE_BEST_PRICE_PRODUCT)
|
||||
const [deleteBestPriceProduct, { loading: deleting }] = useMutation(DELETE_BEST_PRICE_PRODUCT)
|
||||
|
||||
const bestPriceProducts: BestPriceProduct[] = bestPriceProductsData?.bestPriceProducts || []
|
||||
const products: Product[] = productsData?.products || []
|
||||
|
||||
const handleAddProduct = async (productId: string) => {
|
||||
if (!discount || discount <= 0) {
|
||||
toast.error('Укажите размер скидки больше 0%')
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
await createBestPriceProduct({
|
||||
variables: {
|
||||
input: {
|
||||
productId,
|
||||
discount,
|
||||
isActive: true,
|
||||
sortOrder: bestPriceProducts.length
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
toast.success('Товар добавлен в лучшие цены!')
|
||||
setShowProductSelector(false)
|
||||
setDiscount(0)
|
||||
refetchBestPriceProducts()
|
||||
} catch (error) {
|
||||
console.error('Ошибка добавления товара:', error)
|
||||
toast.error('Не удалось добавить товар')
|
||||
}
|
||||
}
|
||||
|
||||
const handleEditProduct = (bestPriceProduct: BestPriceProduct) => {
|
||||
setEditingBestPriceProduct(bestPriceProduct)
|
||||
setDiscount(bestPriceProduct.discount || 0)
|
||||
}
|
||||
|
||||
const handleUpdateProduct = async () => {
|
||||
if (!editingBestPriceProduct) return
|
||||
|
||||
if (!discount || discount <= 0) {
|
||||
toast.error('Укажите размер скидки больше 0%')
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
await updateBestPriceProduct({
|
||||
variables: {
|
||||
id: editingBestPriceProduct.id,
|
||||
input: {
|
||||
discount,
|
||||
isActive: editingBestPriceProduct.isActive
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
toast.success('Товар обновлен!')
|
||||
setEditingBestPriceProduct(null)
|
||||
setDiscount(0)
|
||||
refetchBestPriceProducts()
|
||||
} catch (error) {
|
||||
console.error('Ошибка обновления товара:', error)
|
||||
toast.error('Не удалось обновить товар')
|
||||
}
|
||||
}
|
||||
|
||||
const handleDeleteProduct = async (id: string) => {
|
||||
if (!confirm('Удалить товар из списка товаров с лучшей ценой?')) return
|
||||
|
||||
try {
|
||||
await deleteBestPriceProduct({
|
||||
variables: { id }
|
||||
})
|
||||
|
||||
toast.success('Товар удален!')
|
||||
refetchBestPriceProducts()
|
||||
} catch (error) {
|
||||
console.error('Ошибка удаления товара:', error)
|
||||
toast.error('Не удалось удалить товар')
|
||||
}
|
||||
}
|
||||
|
||||
const formatPrice = (price?: number) => {
|
||||
if (!price) return '—'
|
||||
return `${price.toLocaleString('ru-RU')} ₽`
|
||||
}
|
||||
|
||||
const calculateDiscountedPrice = (price?: number, discount?: number) => {
|
||||
if (!price || !discount) return price
|
||||
return price * (1 - discount / 100)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="p-6 max-w-7xl mx-auto">
|
||||
<div className="mb-6">
|
||||
<h1 className="text-3xl font-bold text-gray-900 mb-2">Товары с лучшей ценой</h1>
|
||||
<p className="text-gray-600">Управление товарами с лучшими ценами, которые показываются на главной странице сайта</p>
|
||||
</div>
|
||||
|
||||
{/* Товары с лучшей ценой */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<div className="flex items-center justify-between">
|
||||
<CardTitle className="flex items-center">
|
||||
<Star className="w-5 h-5 mr-2 text-yellow-500" />
|
||||
Товары с лучшей ценой
|
||||
</CardTitle>
|
||||
<Button
|
||||
onClick={() => setShowProductSelector(true)}
|
||||
className="flex items-center"
|
||||
>
|
||||
<Plus className="w-4 h-4 mr-2" />
|
||||
Добавить товар
|
||||
</Button>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
{bestPriceProductsLoading ? (
|
||||
<div className="text-center py-8 text-gray-500">Загрузка товаров...</div>
|
||||
) : bestPriceProducts.length === 0 ? (
|
||||
<div className="text-center py-8 text-gray-500">
|
||||
Товары с лучшей ценой не добавлены
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-4">
|
||||
{bestPriceProducts.map((bestPriceProduct) => (
|
||||
<div key={bestPriceProduct.id} className="border rounded-lg p-4 flex items-center justify-between">
|
||||
<div className="flex items-center space-x-4">
|
||||
{/* Изображение товара */}
|
||||
<div className="w-16 h-16 bg-gray-100 rounded border flex items-center justify-center">
|
||||
{bestPriceProduct.product.images?.[0] ? (
|
||||
<img
|
||||
src={bestPriceProduct.product.images[0].url}
|
||||
alt={bestPriceProduct.product.name}
|
||||
className="w-full h-full object-cover rounded"
|
||||
/>
|
||||
) : (
|
||||
<span className="text-xs text-gray-400">Нет фото</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Информация о товаре */}
|
||||
<div className="flex-1">
|
||||
<h3 className="font-medium text-gray-900">{bestPriceProduct.product.name}</h3>
|
||||
<div className="text-sm text-gray-500 space-x-4">
|
||||
{bestPriceProduct.product.article && (
|
||||
<span>Артикул: {bestPriceProduct.product.article}</span>
|
||||
)}
|
||||
{bestPriceProduct.product.brand && (
|
||||
<span>Бренд: {bestPriceProduct.product.brand}</span>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex items-center space-x-2 mt-1">
|
||||
<span className="text-lg font-medium text-green-600">
|
||||
от {formatPrice(calculateDiscountedPrice(bestPriceProduct.product.retailPrice, bestPriceProduct.discount))}
|
||||
</span>
|
||||
<span className="text-sm text-gray-500 line-through">
|
||||
{formatPrice(bestPriceProduct.product.retailPrice)}
|
||||
</span>
|
||||
<span className="bg-red-100 text-red-800 text-xs px-2 py-1 rounded">
|
||||
-{bestPriceProduct.discount}%
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Действия */}
|
||||
<div className="flex items-center space-x-2">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => handleEditProduct(bestPriceProduct)}
|
||||
style={{ cursor: 'pointer' }}
|
||||
>
|
||||
<Edit className="w-4 h-4" />
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => handleDeleteProduct(bestPriceProduct.id)}
|
||||
disabled={deleting}
|
||||
style={{ cursor: 'pointer' }}
|
||||
>
|
||||
<Trash2 className="w-4 h-4" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Модальное окно выбора товара */}
|
||||
<Dialog open={showProductSelector} onOpenChange={setShowProductSelector}>
|
||||
<DialogContent className="max-w-4xl max-h-[80vh] overflow-y-auto">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Добавить товар с лучшей ценой</DialogTitle>
|
||||
</DialogHeader>
|
||||
|
||||
<div className="space-y-4">
|
||||
{/* Поиск товаров */}
|
||||
<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>
|
||||
<Label htmlFor="discount">Скидка (%) *</Label>
|
||||
<Input
|
||||
id="discount"
|
||||
type="number"
|
||||
min="1"
|
||||
max="99"
|
||||
value={discount}
|
||||
onChange={(e) => setDiscount(Number(e.target.value))}
|
||||
placeholder="Введите размер скидки"
|
||||
className="w-32"
|
||||
required
|
||||
/>
|
||||
<p className="text-sm text-gray-500 mt-1">Обязательное поле для товаров с лучшей ценой</p>
|
||||
</div>
|
||||
|
||||
{/* Список товаров */}
|
||||
{productsLoading ? (
|
||||
<div className="text-center py-8 text-gray-500">Загрузка товаров...</div>
|
||||
) : (
|
||||
<div className="space-y-2 max-h-96 overflow-y-auto">
|
||||
{products.map((product) => (
|
||||
<div key={product.id} className="border rounded-lg p-3 flex items-center justify-between hover:bg-gray-50">
|
||||
<div className="flex items-center space-x-3">
|
||||
<div className="w-12 h-12 bg-gray-100 rounded border flex items-center justify-center">
|
||||
{product.images?.[0] ? (
|
||||
<img
|
||||
src={product.images[0].url}
|
||||
alt={product.name}
|
||||
className="w-full h-full object-cover rounded"
|
||||
/>
|
||||
) : (
|
||||
<span className="text-xs text-gray-400">Нет фото</span>
|
||||
)}
|
||||
</div>
|
||||
<div>
|
||||
<h4 className="font-medium text-gray-900">{product.name}</h4>
|
||||
<div className="text-sm text-gray-500">
|
||||
{product.article && <span>Артикул: {product.article}</span>}
|
||||
{product.brand && <span className="ml-2">Бренд: {product.brand}</span>}
|
||||
<span className="ml-2">Цена: {formatPrice(product.retailPrice)}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<Button
|
||||
size="sm"
|
||||
onClick={() => handleAddProduct(product.id)}
|
||||
disabled={creating || bestPriceProducts.some(bp => bp.productId === product.id)}
|
||||
style={{ cursor: 'pointer' }}
|
||||
>
|
||||
{bestPriceProducts.some(bp => bp.productId === product.id) ? 'Уже добавлен' : 'Добавить'}
|
||||
</Button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
{/* Модальное окно редактирования */}
|
||||
<Dialog open={!!editingBestPriceProduct} onOpenChange={() => setEditingBestPriceProduct(null)}>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>Редактировать товар с лучшей ценой</DialogTitle>
|
||||
</DialogHeader>
|
||||
|
||||
{editingBestPriceProduct && (
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<h4 className="font-medium">{editingBestPriceProduct.product.name}</h4>
|
||||
<p className="text-sm text-gray-500">
|
||||
{editingBestPriceProduct.product.article && `Артикул: ${editingBestPriceProduct.product.article}`}
|
||||
{editingBestPriceProduct.product.brand && ` • Бренд: ${editingBestPriceProduct.product.brand}`}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Label htmlFor="edit-discount">Скидка (%) *</Label>
|
||||
<Input
|
||||
id="edit-discount"
|
||||
type="number"
|
||||
min="1"
|
||||
max="99"
|
||||
value={discount}
|
||||
onChange={(e) => setDiscount(Number(e.target.value))}
|
||||
placeholder="Введите размер скидки"
|
||||
className="w-32"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center space-x-2 pt-4">
|
||||
<Button onClick={handleUpdateProduct} disabled={updating} style={{ cursor: 'pointer' }}>
|
||||
{updating ? 'Сохранение...' : 'Сохранить'}
|
||||
</Button>
|
||||
<Button variant="outline" onClick={() => setEditingBestPriceProduct(null)} style={{ cursor: 'pointer' }}>
|
||||
Отмена
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</div>
|
||||
)
|
||||
}
|
418
src/app/dashboard/homepage-products/page.tsx
Normal file
418
src/app/dashboard/homepage-products/page.tsx
Normal file
@ -0,0 +1,418 @@
|
||||
"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 { Label } from '@/components/ui/label'
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from '@/components/ui/dialog'
|
||||
import {
|
||||
Calendar,
|
||||
Plus,
|
||||
Search,
|
||||
Edit,
|
||||
Trash2,
|
||||
Package
|
||||
} from 'lucide-react'
|
||||
import { format } from 'date-fns'
|
||||
import { ru } from 'date-fns/locale'
|
||||
import { GET_DAILY_PRODUCTS, GET_PRODUCTS } from '@/lib/graphql/queries'
|
||||
import { CREATE_DAILY_PRODUCT, UPDATE_DAILY_PRODUCT, DELETE_DAILY_PRODUCT } from '@/lib/graphql/mutations'
|
||||
import toast from 'react-hot-toast'
|
||||
|
||||
interface DailyProduct {
|
||||
id: string
|
||||
productId: string
|
||||
displayDate: string
|
||||
discount?: number
|
||||
isActive: boolean
|
||||
sortOrder: number
|
||||
product: {
|
||||
id: string
|
||||
name: string
|
||||
article?: string
|
||||
brand?: string
|
||||
retailPrice?: number
|
||||
images: { url: string; alt?: string }[]
|
||||
}
|
||||
}
|
||||
|
||||
interface Product {
|
||||
id: string
|
||||
name: string
|
||||
article?: string
|
||||
brand?: string
|
||||
retailPrice?: number
|
||||
images: { url: string; alt?: string }[]
|
||||
}
|
||||
|
||||
export default function HomepageProductsPage() {
|
||||
const [selectedDate, setSelectedDate] = useState<string>(format(new Date(), 'yyyy-MM-dd'))
|
||||
const [showProductSelector, setShowProductSelector] = useState(false)
|
||||
const [editingDailyProduct, setEditingDailyProduct] = useState<DailyProduct | null>(null)
|
||||
const [searchQuery, setSearchQuery] = useState('')
|
||||
const [discount, setDiscount] = useState<number>(0)
|
||||
|
||||
const { data: dailyProductsData, loading: dailyProductsLoading, refetch: refetchDailyProducts } = useQuery(GET_DAILY_PRODUCTS, {
|
||||
variables: { displayDate: selectedDate }
|
||||
})
|
||||
|
||||
const { data: productsData, loading: productsLoading } = useQuery(GET_PRODUCTS, {
|
||||
variables: {
|
||||
search: searchQuery || undefined,
|
||||
limit: 50
|
||||
},
|
||||
skip: !showProductSelector
|
||||
})
|
||||
|
||||
const [createDailyProduct, { loading: creating }] = useMutation(CREATE_DAILY_PRODUCT)
|
||||
const [updateDailyProduct, { loading: updating }] = useMutation(UPDATE_DAILY_PRODUCT)
|
||||
const [deleteDailyProduct, { loading: deleting }] = useMutation(DELETE_DAILY_PRODUCT)
|
||||
|
||||
const dailyProducts: DailyProduct[] = dailyProductsData?.dailyProducts || []
|
||||
const products: Product[] = productsData?.products || []
|
||||
|
||||
const handleAddProduct = async (productId: string) => {
|
||||
try {
|
||||
await createDailyProduct({
|
||||
variables: {
|
||||
input: {
|
||||
productId,
|
||||
displayDate: selectedDate,
|
||||
discount: discount || null,
|
||||
isActive: true,
|
||||
sortOrder: dailyProducts.length
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
toast.success('Товар добавлен!')
|
||||
setShowProductSelector(false)
|
||||
setDiscount(0)
|
||||
refetchDailyProducts()
|
||||
} catch (error) {
|
||||
console.error('Ошибка добавления товара:', error)
|
||||
toast.error('Не удалось добавить товар')
|
||||
}
|
||||
}
|
||||
|
||||
const handleEditProduct = (dailyProduct: DailyProduct) => {
|
||||
setEditingDailyProduct(dailyProduct)
|
||||
setDiscount(dailyProduct.discount || 0)
|
||||
}
|
||||
|
||||
const handleUpdateProduct = async () => {
|
||||
if (!editingDailyProduct) return
|
||||
|
||||
try {
|
||||
await updateDailyProduct({
|
||||
variables: {
|
||||
id: editingDailyProduct.id,
|
||||
input: {
|
||||
discount: discount || null,
|
||||
isActive: editingDailyProduct.isActive
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
toast.success('Товар обновлен!')
|
||||
setEditingDailyProduct(null)
|
||||
setDiscount(0)
|
||||
refetchDailyProducts()
|
||||
} catch (error) {
|
||||
console.error('Ошибка обновления товара:', error)
|
||||
toast.error('Не удалось обновить товар')
|
||||
}
|
||||
}
|
||||
|
||||
const handleDeleteProduct = async (id: string) => {
|
||||
if (!confirm('Удалить товар из списка товаров дня?')) return
|
||||
|
||||
try {
|
||||
await deleteDailyProduct({
|
||||
variables: { id }
|
||||
})
|
||||
|
||||
toast.success('Товар удален!')
|
||||
refetchDailyProducts()
|
||||
} catch (error) {
|
||||
console.error('Ошибка удаления товара:', error)
|
||||
toast.error('Не удалось удалить товар')
|
||||
}
|
||||
}
|
||||
|
||||
const formatPrice = (price?: number) => {
|
||||
if (!price) return '—'
|
||||
return `${price.toLocaleString('ru-RU')} ₽`
|
||||
}
|
||||
|
||||
const calculateDiscountedPrice = (price?: number, discount?: number) => {
|
||||
if (!price || !discount) return price
|
||||
return price * (1 - discount / 100)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="p-6 max-w-7xl mx-auto">
|
||||
<div className="mb-6">
|
||||
<h1 className="text-3xl font-bold text-gray-900 mb-2">Товары главной страницы</h1>
|
||||
<p className="text-gray-600">Управление товарами дня, которые показываются на главной странице сайта</p>
|
||||
</div>
|
||||
|
||||
{/* Выбор даты */}
|
||||
<Card className="mb-6">
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center">
|
||||
<Calendar className="w-5 h-5 mr-2" />
|
||||
Выбор даты показа
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="flex items-center space-x-4">
|
||||
<div>
|
||||
<Label htmlFor="date">Дата показа товаров</Label>
|
||||
<Input
|
||||
id="date"
|
||||
type="date"
|
||||
value={selectedDate}
|
||||
onChange={(e) => setSelectedDate(e.target.value)}
|
||||
className="w-48"
|
||||
/>
|
||||
</div>
|
||||
<div className="pt-6">
|
||||
<p className="text-sm text-gray-500">
|
||||
Выбранная дата: {format(new Date(selectedDate), 'dd MMMM yyyy', { locale: ru })}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Товары дня */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<div className="flex items-center justify-between">
|
||||
<CardTitle className="flex items-center">
|
||||
<Package className="w-5 h-5 mr-2" />
|
||||
Товары дня на {format(new Date(selectedDate), 'dd.MM.yyyy')}
|
||||
</CardTitle>
|
||||
<Button
|
||||
onClick={() => setShowProductSelector(true)}
|
||||
className="flex items-center"
|
||||
>
|
||||
<Plus className="w-4 h-4 mr-2" />
|
||||
Добавить товар
|
||||
</Button>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
{dailyProductsLoading ? (
|
||||
<div className="text-center py-8 text-gray-500">Загрузка товаров...</div>
|
||||
) : dailyProducts.length === 0 ? (
|
||||
<div className="text-center py-8 text-gray-500">
|
||||
На выбранную дату товары не добавлены
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-4">
|
||||
{dailyProducts.map((dailyProduct) => (
|
||||
<div key={dailyProduct.id} className="border rounded-lg p-4 flex items-center justify-between">
|
||||
<div className="flex items-center space-x-4">
|
||||
{/* Изображение товара */}
|
||||
<div className="w-16 h-16 bg-gray-100 rounded border flex items-center justify-center">
|
||||
{dailyProduct.product.images?.[0] ? (
|
||||
<img
|
||||
src={dailyProduct.product.images[0].url}
|
||||
alt={dailyProduct.product.name}
|
||||
className="w-full h-full object-cover rounded"
|
||||
/>
|
||||
) : (
|
||||
<span className="text-xs text-gray-400">Нет фото</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Информация о товаре */}
|
||||
<div className="flex-1">
|
||||
<h3 className="font-medium text-gray-900">{dailyProduct.product.name}</h3>
|
||||
<div className="text-sm text-gray-500 space-x-4">
|
||||
{dailyProduct.product.article && (
|
||||
<span>Артикул: {dailyProduct.product.article}</span>
|
||||
)}
|
||||
{dailyProduct.product.brand && (
|
||||
<span>Бренд: {dailyProduct.product.brand}</span>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex items-center space-x-2 mt-1">
|
||||
{dailyProduct.discount ? (
|
||||
<>
|
||||
<span className="text-lg font-medium text-green-600">
|
||||
от {formatPrice(calculateDiscountedPrice(dailyProduct.product.retailPrice, dailyProduct.discount))}
|
||||
</span>
|
||||
<span className="text-sm text-gray-500 line-through">
|
||||
{formatPrice(dailyProduct.product.retailPrice)}
|
||||
</span>
|
||||
<span className="bg-red-100 text-red-800 text-xs px-2 py-1 rounded">
|
||||
-{dailyProduct.discount}%
|
||||
</span>
|
||||
</>
|
||||
) : (
|
||||
<span className="text-lg font-medium">
|
||||
от {formatPrice(dailyProduct.product.retailPrice)}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Действия */}
|
||||
<div className="flex items-center space-x-2">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => handleEditProduct(dailyProduct)}
|
||||
>
|
||||
<Edit className="w-4 h-4" />
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => handleDeleteProduct(dailyProduct.id)}
|
||||
disabled={deleting}
|
||||
>
|
||||
<Trash2 className="w-4 h-4" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Модальное окно выбора товара */}
|
||||
<Dialog open={showProductSelector} onOpenChange={setShowProductSelector}>
|
||||
<DialogContent className="max-w-4xl max-h-[80vh] overflow-y-auto">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Добавить товар дня</DialogTitle>
|
||||
</DialogHeader>
|
||||
|
||||
<div className="space-y-4">
|
||||
{/* Поиск товаров */}
|
||||
<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>
|
||||
<Label htmlFor="discount">Скидка (%)</Label>
|
||||
<Input
|
||||
id="discount"
|
||||
type="number"
|
||||
min="0"
|
||||
max="99"
|
||||
value={discount}
|
||||
onChange={(e) => setDiscount(Number(e.target.value))}
|
||||
placeholder="Введите размер скидки"
|
||||
className="w-32"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Список товаров */}
|
||||
{productsLoading ? (
|
||||
<div className="text-center py-8 text-gray-500">Загрузка товаров...</div>
|
||||
) : (
|
||||
<div className="space-y-2 max-h-96 overflow-y-auto">
|
||||
{products.map((product) => (
|
||||
<div key={product.id} className="border rounded-lg p-3 flex items-center justify-between hover:bg-gray-50">
|
||||
<div className="flex items-center space-x-3">
|
||||
<div className="w-12 h-12 bg-gray-100 rounded border flex items-center justify-center">
|
||||
{product.images?.[0] ? (
|
||||
<img
|
||||
src={product.images[0].url}
|
||||
alt={product.name}
|
||||
className="w-full h-full object-cover rounded"
|
||||
/>
|
||||
) : (
|
||||
<span className="text-xs text-gray-400">Нет фото</span>
|
||||
)}
|
||||
</div>
|
||||
<div>
|
||||
<h4 className="font-medium text-gray-900">{product.name}</h4>
|
||||
<div className="text-sm text-gray-500">
|
||||
{product.article && <span>Артикул: {product.article}</span>}
|
||||
{product.brand && <span className="ml-2">Бренд: {product.brand}</span>}
|
||||
<span className="ml-2">Цена: {formatPrice(product.retailPrice)}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<Button
|
||||
size="sm"
|
||||
onClick={() => handleAddProduct(product.id)}
|
||||
disabled={creating || dailyProducts.some(dp => dp.productId === product.id)}
|
||||
>
|
||||
{dailyProducts.some(dp => dp.productId === product.id) ? 'Уже добавлен' : 'Добавить'}
|
||||
</Button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
{/* Модальное окно редактирования */}
|
||||
<Dialog open={!!editingDailyProduct} onOpenChange={() => setEditingDailyProduct(null)}>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>Редактировать товар дня</DialogTitle>
|
||||
</DialogHeader>
|
||||
|
||||
{editingDailyProduct && (
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<h4 className="font-medium">{editingDailyProduct.product.name}</h4>
|
||||
<p className="text-sm text-gray-500">
|
||||
{editingDailyProduct.product.article && `Артикул: ${editingDailyProduct.product.article}`}
|
||||
{editingDailyProduct.product.brand && ` • Бренд: ${editingDailyProduct.product.brand}`}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Label htmlFor="edit-discount">Скидка (%)</Label>
|
||||
<Input
|
||||
id="edit-discount"
|
||||
type="number"
|
||||
min="0"
|
||||
max="99"
|
||||
value={discount}
|
||||
onChange={(e) => setDiscount(Number(e.target.value))}
|
||||
placeholder="Введите размер скидки"
|
||||
className="w-32"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center space-x-2 pt-4">
|
||||
<Button onClick={handleUpdateProduct} disabled={updating}>
|
||||
{updating ? 'Сохранение...' : 'Сохранить'}
|
||||
</Button>
|
||||
<Button variant="outline" onClick={() => setEditingDailyProduct(null)}>
|
||||
Отмена
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</div>
|
||||
)
|
||||
}
|
436
src/app/dashboard/top-sales-products/page.tsx
Normal file
436
src/app/dashboard/top-sales-products/page.tsx
Normal file
@ -0,0 +1,436 @@
|
||||
'use client'
|
||||
|
||||
import React, { useState } from 'react'
|
||||
import { useQuery, useMutation } from '@apollo/client'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Input } from '@/components/ui/input'
|
||||
import { Switch } from '@/components/ui/switch'
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
|
||||
import { Badge } from '@/components/ui/badge'
|
||||
import { Trash2, Plus, Search, Edit, ChevronUp, ChevronDown } from 'lucide-react'
|
||||
import { toast } from 'sonner'
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
} from '@/components/ui/dialog'
|
||||
import { Label } from '@/components/ui/label'
|
||||
import { GET_TOP_SALES_PRODUCTS, GET_PRODUCTS } from '@/lib/graphql/queries'
|
||||
import {
|
||||
CREATE_TOP_SALES_PRODUCT,
|
||||
UPDATE_TOP_SALES_PRODUCT,
|
||||
DELETE_TOP_SALES_PRODUCT,
|
||||
} from '@/lib/graphql/mutations'
|
||||
|
||||
interface Product {
|
||||
id: string
|
||||
name: string
|
||||
article?: string
|
||||
brand?: string
|
||||
retailPrice?: number
|
||||
images: { url: string; alt?: string }[]
|
||||
}
|
||||
|
||||
interface TopSalesProduct {
|
||||
id: string
|
||||
productId: string
|
||||
isActive: boolean
|
||||
sortOrder: number
|
||||
product: Product
|
||||
createdAt: string
|
||||
updatedAt: string
|
||||
}
|
||||
|
||||
interface TopSalesProductInput {
|
||||
productId: string
|
||||
isActive?: boolean
|
||||
sortOrder?: number
|
||||
}
|
||||
|
||||
interface TopSalesProductUpdateInput {
|
||||
isActive?: boolean
|
||||
sortOrder?: number
|
||||
}
|
||||
|
||||
export default function TopSalesProductsPage() {
|
||||
const [searchTerm, setSearchTerm] = useState('')
|
||||
const [selectedProduct, setSelectedProduct] = useState<Product | null>(null)
|
||||
const [editingItem, setEditingItem] = useState<TopSalesProduct | null>(null)
|
||||
const [isAddDialogOpen, setIsAddDialogOpen] = useState(false)
|
||||
const [isEditDialogOpen, setIsEditDialogOpen] = useState(false)
|
||||
|
||||
// Загружаем топ продаж
|
||||
const { data: topSalesData, loading: topSalesLoading, refetch: refetchTopSales } = useQuery<{
|
||||
topSalesProducts: TopSalesProduct[]
|
||||
}>(GET_TOP_SALES_PRODUCTS)
|
||||
|
||||
// Загружаем все товары для поиска
|
||||
const { data: productsData, loading: productsLoading } = useQuery<{
|
||||
products: Product[]
|
||||
}>(GET_PRODUCTS, {
|
||||
variables: { limit: 100 }
|
||||
})
|
||||
|
||||
// Мутации
|
||||
const [createTopSalesProduct] = useMutation<
|
||||
{ createTopSalesProduct: TopSalesProduct },
|
||||
{ input: TopSalesProductInput }
|
||||
>(CREATE_TOP_SALES_PRODUCT, {
|
||||
onCompleted: () => {
|
||||
toast.success('Товар добавлен в топ продаж')
|
||||
refetchTopSales()
|
||||
setIsAddDialogOpen(false)
|
||||
setSelectedProduct(null)
|
||||
},
|
||||
onError: (error) => {
|
||||
toast.error(`Ошибка: ${error.message}`)
|
||||
}
|
||||
})
|
||||
|
||||
const [updateTopSalesProduct] = useMutation<
|
||||
{ updateTopSalesProduct: TopSalesProduct },
|
||||
{ id: string; input: TopSalesProductUpdateInput }
|
||||
>(UPDATE_TOP_SALES_PRODUCT, {
|
||||
onCompleted: () => {
|
||||
toast.success('Товар обновлен')
|
||||
refetchTopSales()
|
||||
setIsEditDialogOpen(false)
|
||||
setEditingItem(null)
|
||||
},
|
||||
onError: (error) => {
|
||||
toast.error(`Ошибка: ${error.message}`)
|
||||
}
|
||||
})
|
||||
|
||||
const [deleteTopSalesProduct] = useMutation<
|
||||
{ deleteTopSalesProduct: boolean },
|
||||
{ id: string }
|
||||
>(DELETE_TOP_SALES_PRODUCT, {
|
||||
onCompleted: () => {
|
||||
toast.success('Товар удален из топ продаж')
|
||||
refetchTopSales()
|
||||
},
|
||||
onError: (error) => {
|
||||
toast.error(`Ошибка: ${error.message}`)
|
||||
}
|
||||
})
|
||||
|
||||
// Фильтрация товаров для поиска
|
||||
const filteredProducts = productsData?.products?.filter(product =>
|
||||
product.name.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
||||
product.article?.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
||||
product.brand?.toLowerCase().includes(searchTerm.toLowerCase())
|
||||
) || []
|
||||
|
||||
// Обработчики
|
||||
const handleAddProduct = () => {
|
||||
if (!selectedProduct) {
|
||||
toast.error('Выберите товар')
|
||||
return
|
||||
}
|
||||
|
||||
createTopSalesProduct({
|
||||
variables: {
|
||||
input: {
|
||||
productId: selectedProduct.id,
|
||||
isActive: true,
|
||||
sortOrder: 0
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const handleUpdateProduct = (isActive: boolean, sortOrder: number) => {
|
||||
if (!editingItem) return
|
||||
|
||||
updateTopSalesProduct({
|
||||
variables: {
|
||||
id: editingItem.id,
|
||||
input: {
|
||||
isActive,
|
||||
sortOrder
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const handleDeleteProduct = (id: string) => {
|
||||
if (confirm('Вы уверены, что хотите удалить этот товар из топ продаж?')) {
|
||||
deleteTopSalesProduct({
|
||||
variables: { id }
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const handleToggleActive = (item: TopSalesProduct) => {
|
||||
updateTopSalesProduct({
|
||||
variables: {
|
||||
id: item.id,
|
||||
input: {
|
||||
isActive: !item.isActive,
|
||||
sortOrder: item.sortOrder
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const handleSortOrderChange = (item: TopSalesProduct, direction: 'up' | 'down') => {
|
||||
const newSortOrder = direction === 'up' ? item.sortOrder - 1 : item.sortOrder + 1
|
||||
updateTopSalesProduct({
|
||||
variables: {
|
||||
id: item.id,
|
||||
input: {
|
||||
isActive: item.isActive,
|
||||
sortOrder: Math.max(0, newSortOrder)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
if (topSalesLoading) {
|
||||
return <div className="p-6">Загрузка...</div>
|
||||
}
|
||||
|
||||
const topSalesProducts = topSalesData?.topSalesProducts || []
|
||||
|
||||
return (
|
||||
<div className="p-6 space-y-6">
|
||||
<div className="flex justify-between items-center">
|
||||
<h1 className="text-3xl font-bold">Топ продаж</h1>
|
||||
|
||||
<Dialog open={isAddDialogOpen} onOpenChange={setIsAddDialogOpen}>
|
||||
<DialogTrigger asChild>
|
||||
<Button>
|
||||
<Plus className="w-4 h-4 mr-2" />
|
||||
Добавить товар
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent className="max-w-2xl">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Добавить товар в топ продаж</DialogTitle>
|
||||
<DialogDescription>
|
||||
Найдите и выберите товар для добавления в топ продаж
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
<div className="space-y-4">
|
||||
<div className="relative">
|
||||
<Search className="absolute left-3 top-3 h-4 w-4 text-gray-400" />
|
||||
<Input
|
||||
placeholder="Поиск товаров..."
|
||||
value={searchTerm}
|
||||
onChange={(e) => setSearchTerm(e.target.value)}
|
||||
className="pl-10"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{productsLoading ? (
|
||||
<div>Загрузка товаров...</div>
|
||||
) : (
|
||||
<div className="max-h-96 overflow-y-auto space-y-2">
|
||||
{filteredProducts.map((product) => (
|
||||
<div
|
||||
key={product.id}
|
||||
className={`p-3 border rounded cursor-pointer transition-colors ${
|
||||
selectedProduct?.id === product.id
|
||||
? 'border-blue-500 bg-blue-50'
|
||||
: 'border-gray-200 hover:border-gray-300'
|
||||
}`}
|
||||
onClick={() => setSelectedProduct(product)}
|
||||
>
|
||||
<div className="flex items-center space-x-3">
|
||||
{product.images?.[0] && (
|
||||
<img
|
||||
src={product.images[0].url}
|
||||
alt={product.images[0].alt || product.name}
|
||||
className="w-12 h-12 object-cover rounded"
|
||||
/>
|
||||
)}
|
||||
<div className="flex-1">
|
||||
<h3 className="font-medium">{product.name}</h3>
|
||||
<p className="text-sm text-gray-500">
|
||||
{product.brand} • {product.article}
|
||||
</p>
|
||||
{product.retailPrice && (
|
||||
<p className="text-sm font-medium">
|
||||
{product.retailPrice.toLocaleString('ru-RU')} ₽
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<DialogFooter>
|
||||
<Button variant="outline" onClick={() => setIsAddDialogOpen(false)}>
|
||||
Отмена
|
||||
</Button>
|
||||
<Button onClick={handleAddProduct} disabled={!selectedProduct}>
|
||||
Добавить в топ продаж
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</div>
|
||||
|
||||
{/* Список топ продаж */}
|
||||
<div className="space-y-4">
|
||||
{topSalesProducts.length === 0 ? (
|
||||
<Card>
|
||||
<CardContent className="p-6 text-center">
|
||||
<p className="text-gray-500">Нет товаров в топ продаж</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
) : (
|
||||
[...topSalesProducts]
|
||||
.sort((a, b) => a.sortOrder - b.sortOrder)
|
||||
.map((item) => (
|
||||
<Card key={item.id}>
|
||||
<CardContent className="p-6">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center space-x-4">
|
||||
{item.product.images?.[0] && (
|
||||
<img
|
||||
src={item.product.images[0].url}
|
||||
alt={item.product.images[0].alt || item.product.name}
|
||||
className="w-16 h-16 object-cover rounded"
|
||||
/>
|
||||
)}
|
||||
<div>
|
||||
<h3 className="font-medium text-lg">{item.product.name}</h3>
|
||||
<p className="text-gray-600">
|
||||
{item.product.brand} • {item.product.article}
|
||||
</p>
|
||||
{item.product.retailPrice && (
|
||||
<p className="font-medium">
|
||||
{item.product.retailPrice.toLocaleString('ru-RU')} ₽
|
||||
</p>
|
||||
)}
|
||||
<div className="flex items-center space-x-2 mt-2">
|
||||
<Badge variant={item.isActive ? 'default' : 'secondary'}>
|
||||
{item.isActive ? 'Активен' : 'Неактивен'}
|
||||
</Badge>
|
||||
<Badge variant="outline">Порядок: {item.sortOrder}</Badge>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center space-x-2">
|
||||
{/* Управление порядком */}
|
||||
<div className="flex flex-col">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => handleSortOrderChange(item, 'up')}
|
||||
disabled={item.sortOrder === 0}
|
||||
>
|
||||
<ChevronUp className="w-4 h-4" />
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => handleSortOrderChange(item, 'down')}
|
||||
>
|
||||
<ChevronDown className="w-4 h-4" />
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* Переключатель активности */}
|
||||
<div className="flex items-center space-x-2">
|
||||
<Label htmlFor={`active-${item.id}`}>Активен</Label>
|
||||
<Switch
|
||||
id={`active-${item.id}`}
|
||||
checked={item.isActive}
|
||||
onCheckedChange={() => handleToggleActive(item)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Кнопка редактирования */}
|
||||
<Dialog open={isEditDialogOpen} onOpenChange={setIsEditDialogOpen}>
|
||||
<DialogTrigger asChild>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => setEditingItem(item)}
|
||||
>
|
||||
<Edit className="w-4 h-4" />
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>Редактировать товар</DialogTitle>
|
||||
</DialogHeader>
|
||||
|
||||
{editingItem && (
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-center space-x-2">
|
||||
<Label htmlFor="edit-active">Активен</Label>
|
||||
<Switch
|
||||
id="edit-active"
|
||||
checked={editingItem.isActive}
|
||||
onCheckedChange={(checked) =>
|
||||
setEditingItem({ ...editingItem, isActive: checked })
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Label htmlFor="edit-sort-order">Порядок сортировки</Label>
|
||||
<Input
|
||||
id="edit-sort-order"
|
||||
type="number"
|
||||
min="0"
|
||||
value={editingItem.sortOrder}
|
||||
onChange={(e) =>
|
||||
setEditingItem({
|
||||
...editingItem,
|
||||
sortOrder: parseInt(e.target.value) || 0
|
||||
})
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<DialogFooter>
|
||||
<Button variant="outline" onClick={() => setIsEditDialogOpen(false)}>
|
||||
Отмена
|
||||
</Button>
|
||||
<Button
|
||||
onClick={() => {
|
||||
if (editingItem) {
|
||||
handleUpdateProduct(editingItem.isActive, editingItem.sortOrder)
|
||||
}
|
||||
}}
|
||||
>
|
||||
Сохранить
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
{/* Кнопка удаления */}
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => handleDeleteProduct(item.id)}
|
||||
>
|
||||
<Trash2 className="w-4 h-4" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
Reference in New Issue
Block a user