
- Создан layout для (dashboard) группы с персистентным сайдбаром - Заменен window.location.href на router.push() в хуках авторизации - Перемещены все страницы в (dashboard) группу для единого layout - Удалены дублирующие <Sidebar /> компоненты из индивидуальных страниц - Исправлены все компоненты dashboard для использования getSidebarMargin() - Добавлена skeleton загрузка кнопок сайдбара для плавного UX - Исправлены критические синтаксические ошибки в JSX компонентах - Удалены дублирующие main теги и исправлена структура layout 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
334 lines
15 KiB
TypeScript
334 lines
15 KiB
TypeScript
'use client'
|
||
|
||
import { useQuery } from '@apollo/client'
|
||
import { Plus, Package, Grid3X3, List, Edit3, Trash2 } from 'lucide-react'
|
||
import React, { useState } from 'react'
|
||
|
||
import { Button } from '@/components/ui/button'
|
||
import { Card } from '@/components/ui/card'
|
||
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogTrigger } from '@/components/ui/dialog'
|
||
import { Input } from '@/components/ui/input'
|
||
import { GET_MY_PRODUCTS } from '@/graphql/queries'
|
||
import { useSidebar } from '@/hooks/useSidebar'
|
||
|
||
import { ProductCard } from './product-card'
|
||
import { ProductForm } from './product-form'
|
||
import { WarehouseStatistics } from './warehouse-statistics'
|
||
|
||
interface Product {
|
||
id: string
|
||
name: string
|
||
article: string
|
||
description: string
|
||
price: number
|
||
pricePerSet?: number
|
||
quantity: number
|
||
setQuantity?: number
|
||
ordered?: number
|
||
inTransit?: number
|
||
stock?: number
|
||
sold?: number
|
||
type: 'PRODUCT' | 'CONSUMABLE'
|
||
category: { id: string; name: string } | null
|
||
brand: string
|
||
color: string
|
||
size: string
|
||
weight: number
|
||
dimensions: string
|
||
material: string
|
||
images: string[]
|
||
mainImage: string
|
||
isActive: boolean
|
||
createdAt: string
|
||
updatedAt: string
|
||
}
|
||
|
||
export function WarehouseDashboard() {
|
||
const { getSidebarMargin } = useSidebar()
|
||
const [isDialogOpen, setIsDialogOpen] = useState(false)
|
||
const [editingProduct, setEditingProduct] = useState<Product | null>(null)
|
||
const [searchQuery, setSearchQuery] = useState('')
|
||
const [viewMode, setViewMode] = useState<'cards' | 'table'>('cards')
|
||
|
||
const { data, loading, error, refetch } = useQuery(GET_MY_PRODUCTS, {
|
||
errorPolicy: 'all',
|
||
})
|
||
|
||
const products: Product[] = data?.myProducts || []
|
||
|
||
// Фильтрация товаров по поисковому запросу
|
||
const filteredProducts = products.filter(
|
||
(product) =>
|
||
product.name.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
||
product.article.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
||
product.category?.name?.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
||
product.brand?.toLowerCase().includes(searchQuery.toLowerCase()),
|
||
)
|
||
|
||
const handleCreateProduct = () => {
|
||
setEditingProduct(null)
|
||
setIsDialogOpen(true)
|
||
}
|
||
|
||
const handleEditProduct = (product: Product) => {
|
||
setEditingProduct(product)
|
||
setIsDialogOpen(true)
|
||
}
|
||
|
||
const handleProductSaved = () => {
|
||
setIsDialogOpen(false)
|
||
setEditingProduct(null)
|
||
refetch()
|
||
}
|
||
|
||
const handleProductDeleted = () => {
|
||
refetch()
|
||
}
|
||
|
||
if (error) {
|
||
return (
|
||
<div className={`${getSidebarMargin()} px-6 py-4 overflow-hidden transition-all duration-300 h-full`}>
|
||
<main className="flex-1 ml-56 px-6 py-4 overflow-hidden">
|
||
<div className="h-full w-full flex flex-col">
|
||
<Card className="flex-1 bg-white/5 backdrop-blur border-white/10 p-6">
|
||
<div className="flex items-center justify-center h-full">
|
||
<div className="text-center">
|
||
<Package className="h-16 w-16 text-white/40 mx-auto mb-4" />
|
||
<h3 className="text-lg font-medium text-white mb-2">Ошибка загрузки</h3>
|
||
<p className="text-white/60 text-sm mb-4">{error.message || 'Не удалось загрузить товары'}</p>
|
||
<Button onClick={() => refetch()} className="bg-purple-600 hover:bg-purple-700 text-white">
|
||
Попробовать снова
|
||
</Button>
|
||
</div>
|
||
</div>
|
||
</Card>
|
||
</div>
|
||
|
||
</div>
|
||
)
|
||
}
|
||
|
||
return (
|
||
<div className={`${getSidebarMargin()} px-6 py-4 overflow-hidden transition-all duration-300 h-full`}>
|
||
|
||
<div className="h-full w-full flex flex-col">
|
||
{/* Поиск и управление */}
|
||
<div className="flex items-center justify-between mb-4 flex-shrink-0">
|
||
<div className="flex gap-4 items-center flex-1">
|
||
<div className="relative max-w-md">
|
||
<Input
|
||
type="text"
|
||
placeholder="Поиск по названию, артикулу, категории..."
|
||
value={searchQuery}
|
||
onChange={(e) => setSearchQuery(e.target.value)}
|
||
className="glass-input text-white placeholder:text-white/50 h-10"
|
||
/>
|
||
</div>
|
||
|
||
{/* Переключатель режимов отображения */}
|
||
<div className="flex border border-white/10 rounded-lg overflow-hidden">
|
||
<Button
|
||
onClick={() => setViewMode('cards')}
|
||
variant="ghost"
|
||
size="sm"
|
||
className={`px-3 h-10 rounded-none ${
|
||
viewMode === 'cards'
|
||
? 'bg-purple-500/20 text-white border-purple-400/30'
|
||
: 'text-white/70 hover:text-white hover:bg-white/5'
|
||
}`}
|
||
>
|
||
<Grid3X3 className="w-4 h-4" />
|
||
</Button>
|
||
<Button
|
||
onClick={() => setViewMode('table')}
|
||
variant="ghost"
|
||
size="sm"
|
||
className={`px-3 h-10 rounded-none border-l border-white/10 ${
|
||
viewMode === 'table'
|
||
? 'bg-purple-500/20 text-white border-purple-400/30'
|
||
: 'text-white/70 hover:text-white hover:bg-white/5'
|
||
}`}
|
||
>
|
||
<List className="w-4 h-4" />
|
||
</Button>
|
||
</div>
|
||
</div>
|
||
|
||
<Dialog open={isDialogOpen} onOpenChange={setIsDialogOpen}>
|
||
<DialogTrigger asChild>
|
||
<Button
|
||
onClick={handleCreateProduct}
|
||
className="bg-gradient-to-r from-purple-600 to-pink-600 hover:from-purple-700 hover:to-pink-700 text-white border-0 shadow-lg shadow-purple-500/25 transition-all duration-300"
|
||
>
|
||
<Plus className="w-4 h-4 mr-2" />
|
||
Добавить товар/расходник
|
||
</Button>
|
||
</DialogTrigger>
|
||
<DialogContent
|
||
className="glass-card !w-[90vw] !max-w-[90vw] max-h-[95vh]"
|
||
style={{ width: '90vw', maxWidth: '90vw' }}
|
||
>
|
||
<DialogHeader>
|
||
<DialogTitle className="text-white">
|
||
{editingProduct ? 'Редактировать товар/расходник' : 'Добавить товар/расходник'}
|
||
</DialogTitle>
|
||
</DialogHeader>
|
||
<ProductForm
|
||
product={editingProduct}
|
||
onSave={handleProductSaved}
|
||
onCancel={() => setIsDialogOpen(false)}
|
||
/>
|
||
</DialogContent>
|
||
</Dialog>
|
||
</div>
|
||
|
||
{/* Блок статистики */}
|
||
<Card className="bg-white/5 backdrop-blur border-white/10 p-4 mb-4">
|
||
<WarehouseStatistics products={filteredProducts} />
|
||
</Card>
|
||
|
||
{/* Основной контент */}
|
||
<Card className="flex-1 bg-white/5 backdrop-blur border-white/10 p-6 overflow-y-auto">
|
||
{loading ? (
|
||
<div className="flex items-center justify-center h-full">
|
||
<div className="text-center">
|
||
<div className="animate-spin rounded-full h-16 w-16 border-4 border-white border-t-transparent mx-auto mb-4"></div>
|
||
<p className="text-white/70">Загрузка товаров...</p>
|
||
</div>
|
||
</div>
|
||
) : filteredProducts.length === 0 ? (
|
||
<div className="flex items-center justify-center h-full">
|
||
<div className="text-center">
|
||
<Package className="h-16 w-16 text-white/40 mx-auto mb-4" />
|
||
<h3 className="text-lg font-medium text-white mb-2">
|
||
{searchQuery ? 'Товары не найдены' : 'Склад пуст'}
|
||
</h3>
|
||
<p className="text-white/60 text-sm mb-4">
|
||
{searchQuery ? 'Попробуйте изменить критерии поиска' : 'Добавьте ваш первый товар на склад'}
|
||
</p>
|
||
{!searchQuery && (
|
||
<Button onClick={handleCreateProduct} className="bg-purple-600 hover:bg-purple-700 text-white">
|
||
<Plus className="w-4 h-4 mr-2" />
|
||
Добавить товар
|
||
</Button>
|
||
)}
|
||
</div>
|
||
</div>
|
||
) : (
|
||
<div className="space-y-4">
|
||
{viewMode === 'cards' ? (
|
||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 2xl:grid-cols-5 gap-4">
|
||
{filteredProducts.map((product) => (
|
||
<ProductCard
|
||
key={product.id}
|
||
product={product}
|
||
onEdit={handleEditProduct}
|
||
onDelete={handleProductDeleted}
|
||
/>
|
||
))}
|
||
</div>
|
||
) : (
|
||
<div className="space-y-2">
|
||
<div className="grid grid-cols-12 gap-4 p-4 text-white/60 text-sm font-medium border-b border-white/10">
|
||
<div className="col-span-1">Фото</div>
|
||
<div className="col-span-2">Название</div>
|
||
<div className="col-span-1">Артикул</div>
|
||
<div className="col-span-1">Тип</div>
|
||
<div className="col-span-1">Категория</div>
|
||
<div className="col-span-1">Цена</div>
|
||
<div className="col-span-1">Остаток</div>
|
||
<div className="col-span-1">Заказано</div>
|
||
<div className="col-span-1">В пути</div>
|
||
<div className="col-span-1">Продано</div>
|
||
<div className="col-span-1">Действия</div>
|
||
</div>
|
||
{filteredProducts.map((product) => (
|
||
<div
|
||
key={product.id}
|
||
className="grid grid-cols-12 gap-4 p-4 hover:bg-white/5 rounded-lg transition-colors"
|
||
>
|
||
<div className="col-span-1">
|
||
{product.mainImage || product.images[0] ? (
|
||
<img
|
||
src={product.mainImage || product.images[0]}
|
||
alt={product.name}
|
||
className="w-12 h-12 object-contain rounded bg-white/10"
|
||
/>
|
||
) : (
|
||
<div className="w-12 h-12 bg-white/10 rounded flex items-center justify-center">
|
||
<Package className="h-6 w-6 text-white/40" />
|
||
</div>
|
||
)}
|
||
</div>
|
||
<div className="col-span-2 text-white text-sm">
|
||
<div className="font-medium truncate">{product.name}</div>
|
||
<div className="text-white/60 text-xs">{product.brand}</div>
|
||
</div>
|
||
<div className="col-span-1 text-white/70 text-sm font-mono">{product.article}</div>
|
||
<div className="col-span-1">
|
||
<span
|
||
className={`inline-block px-2 py-1 rounded text-xs ${
|
||
product.type === 'PRODUCT'
|
||
? 'bg-blue-500/20 text-blue-300 border border-blue-400/30'
|
||
: 'bg-orange-500/20 text-orange-300 border border-orange-400/30'
|
||
}`}
|
||
>
|
||
{product.type === 'PRODUCT' ? 'Товар' : 'Расходник'}
|
||
</span>
|
||
</div>
|
||
<div className="col-span-1 text-white/70 text-sm">{product.category?.name || 'Нет'}</div>
|
||
<div className="col-span-1 text-white text-sm font-medium">
|
||
{new Intl.NumberFormat('ru-RU', {
|
||
style: 'currency',
|
||
currency: 'RUB',
|
||
minimumFractionDigits: 0,
|
||
}).format(product.price)}
|
||
</div>
|
||
<div className="col-span-1 text-white text-sm">
|
||
<span
|
||
className={`${
|
||
(product.stock || product.quantity) === 0
|
||
? 'text-red-400'
|
||
: (product.stock || product.quantity) < 10
|
||
? 'text-yellow-400'
|
||
: 'text-green-400'
|
||
}`}
|
||
>
|
||
{product.stock || product.quantity || 0}
|
||
</span>
|
||
</div>
|
||
<div className="col-span-1 text-white/70 text-sm">{product.ordered || 0}</div>
|
||
<div className="col-span-1 text-white/70 text-sm">{product.inTransit || 0}</div>
|
||
<div className="col-span-1 text-white/70 text-sm">{product.sold || 0}</div>
|
||
<div className="col-span-1">
|
||
<div className="flex gap-1">
|
||
<Button
|
||
size="sm"
|
||
variant="outline"
|
||
onClick={() => handleEditProduct(product)}
|
||
className="p-1 h-7 w-7 bg-white/10 border-white/20 hover:bg-white/20"
|
||
>
|
||
<Edit3 className="h-3 w-3 text-white" />
|
||
</Button>
|
||
<Button
|
||
size="sm"
|
||
variant="outline"
|
||
className="p-1 h-7 w-7 bg-red-500/20 border-red-400/30 hover:bg-red-500/30"
|
||
>
|
||
<Trash2 className="h-3 w-3 text-white" />
|
||
</Button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
))}
|
||
</div>
|
||
)}
|
||
</div>
|
||
)}
|
||
</Card>
|
||
</div>
|
||
|
||
</div>
|
||
)
|
||
}
|