Files
sfera/src/components/warehouse/warehouse-dashboard.tsx

348 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 React, { useState } from "react";
import { useQuery } from "@apollo/client";
import { Card } from "@/components/ui/card";
import { Button } from "@/components/ui/button";
import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "@/components/ui/dialog";
import { Sidebar } from "@/components/dashboard/sidebar";
import { useSidebar } from "@/hooks/useSidebar";
import { ProductForm } from "./product-form";
import { ProductCard } from "./product-card";
import { WarehouseStatistics } from "./warehouse-statistics";
import { GET_MY_PRODUCTS } from "@/graphql/queries";
import { Plus, Package, Grid3X3, List, Edit3, Trash2 } from "lucide-react";
import { Input } from "@/components/ui/input";
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="h-screen flex overflow-hidden">
<Sidebar />
<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>
</main>
</div>
);
}
return (
<div className="h-screen flex overflow-hidden">
<Sidebar />
<main
className={`flex-1 ${getSidebarMargin()} px-6 py-4 overflow-hidden transition-all duration-300`}
>
<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>
</main>
</div>
);
}