Files
sfera/src/components/warehouse/warehouse-dashboard.tsx
Bivekich 9da1fe9e78 Исправлена навигация сайдбара: убраны перезагрузки страницы и добавлен персистентный сайдбар
- Создан 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>
2025-08-07 19:23:51 +03:00

334 lines
15 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 { 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>
)
}