Compare commits

...

2 Commits

Author SHA1 Message Date
d45cdde42d Merge branch 'main' of https://gittea.biveki.ru/Sfera/sfera 2025-08-01 11:06:03 +03:00
0a3a2dae7b Реализована система управления складом поставщика согласно требованиям rules1.md
- Добавлена автогенерация артикулов СФ (SF-T для товаров, SF-C для расходников)
- Добавлены поля для комплектов: количество и цена за комплект
- Добавлены поля учета движения товаров: заказано, в пути, остаток, продано
- Создан компонент статистики склада с общими показателями и разбивкой по типам
- Реализовано переключение между карточным и табличным режимом отображения (5 карточек в ряду)
- Обновлены GraphQL схемы для поддержки новых полей
- Улучшен пользовательский интерфейс: убран заголовок, оптимизирована компоновка
- Синхронизированы поля между карточками и таблицей для единообразного отображения

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-08-01 11:05:44 +03:00
7 changed files with 560 additions and 96 deletions

View File

@ -18,7 +18,13 @@ interface Product {
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
@ -66,19 +72,25 @@ export function ProductCard({ product, onEdit, onDeleted }: ProductCardProps) {
}
const getStatusColor = () => {
const stock = product.stock || product.quantity || 0;
if (!product.isActive) return 'bg-gray-500/20 text-gray-300 border-gray-400/30'
if (product.quantity === 0) return 'bg-red-500/20 text-red-300 border-red-400/30'
if (product.quantity < 10) return 'bg-yellow-500/20 text-yellow-300 border-yellow-400/30'
if (stock === 0) return 'bg-red-500/20 text-red-300 border-red-400/30'
if (stock < 10) return 'bg-yellow-500/20 text-yellow-300 border-yellow-400/30'
return 'bg-green-500/20 text-green-300 border-green-400/30'
}
const getStatusText = () => {
const stock = product.stock || product.quantity || 0;
if (!product.isActive) return 'Неактивен'
if (product.quantity === 0) return 'Нет в наличии'
if (product.quantity < 10) return 'Мало на складе'
if (stock === 0) return 'Нет в наличии'
if (stock < 10) return 'Мало на складе'
return 'В наличии'
}
const getStockQuantity = () => {
return product.stock || product.quantity || 0;
}
return (
<Card className="glass-card group relative overflow-hidden transition-all duration-300 hover:scale-[1.02] hover:shadow-xl hover:shadow-purple-500/20">
{/* Изображение товара */}
@ -192,12 +204,44 @@ export function ProductCard({ product, onEdit, onDeleted }: ProductCardProps) {
</div>
{/* Цена и количество */}
<div className="flex items-center justify-between">
<div className="text-white font-semibold">
{formatPrice(product.price)}
<div className="space-y-2">
<div className="flex items-center justify-between">
<div>
<div className="text-white font-semibold">
{formatPrice(product.price)}
</div>
{product.pricePerSet && (
<div className="text-white/60 text-xs">
Комплект: {formatPrice(product.pricePerSet)}
</div>
)}
</div>
<div className="text-right">
<div className="text-white/70 text-sm">
{getStockQuantity()} шт.
</div>
{product.setQuantity && (
<div className="text-white/60 text-xs">
{product.setQuantity} компл.
</div>
)}
</div>
</div>
<div className="text-white/70 text-sm">
{product.quantity} шт.
{/* Учет движения */}
<div className="grid grid-cols-3 gap-2 text-xs">
<div className="text-center bg-yellow-500/10 rounded p-1">
<div className="text-yellow-300 font-medium">{product.ordered || 0}</div>
<div className="text-yellow-300/70">Заказано</div>
</div>
<div className="text-center bg-purple-500/10 rounded p-1">
<div className="text-purple-300 font-medium">{product.inTransit || 0}</div>
<div className="text-purple-300/70">В пути</div>
</div>
<div className="text-center bg-green-500/10 rounded p-1">
<div className="text-green-300 font-medium">{product.sold || 0}</div>
<div className="text-green-300/70">Продано</div>
</div>
</div>
</div>

View File

@ -1,6 +1,6 @@
"use client";
import { useState, useRef } from "react";
import { useState, useRef, useEffect } from "react";
import Image from "next/image";
import { useMutation, useQuery } from "@apollo/client";
import { Button } from "@/components/ui/button";
@ -16,7 +16,7 @@ import {
import { Card } from "@/components/ui/card";
import { CREATE_PRODUCT, UPDATE_PRODUCT } from "@/graphql/mutations";
import { GET_CATEGORIES } from "@/graphql/queries";
import { X, Star, Upload } from "lucide-react";
import { X, Star, Upload, RefreshCw } from "lucide-react";
import { toast } from "sonner";
interface Product {
@ -49,9 +49,16 @@ export function ProductForm({ product, onSave, onCancel }: ProductFormProps) {
const [formData, setFormData] = useState({
name: product?.name || "",
article: product?.article || "",
autoGenerateArticle: !product?.article, // Автогенерация только для новых товаров
description: product?.description || "",
price: product?.price || 0,
quantity: product?.quantity || 0,
setQuantity: product?.setQuantity || 0, // Количество комплектов
pricePerSet: product?.pricePerSet || 0, // Цена за комплект
ordered: product?.ordered || 0, // Заказано
inTransit: product?.inTransit || 0, // В пути
stock: product?.stock || 0, // Остаток
sold: product?.sold || 0, // Продано
type: product?.type || ("PRODUCT" as "PRODUCT" | "CONSUMABLE"),
categoryId: product?.category?.id || "none",
brand: product?.brand || "",
@ -79,6 +86,24 @@ export function ProductForm({ product, onSave, onCancel }: ProductFormProps) {
const loading = creating || updating;
// Генерация артикула СФ
const generateArticle = () => {
const prefix = formData.type === 'PRODUCT' ? 'SF-T' : 'SF-C'; // T=Товар, C=Расходник
const timestamp = Date.now().toString().slice(-6); // Последние 6 цифр timestamp
const random = Math.floor(Math.random() * 1000).toString().padStart(3, '0');
return `${prefix}-${timestamp}-${random}`;
};
// Автогенерация артикула при смене типа
useEffect(() => {
if (formData.autoGenerateArticle && formData.type) {
setFormData(prev => ({
...prev,
article: generateArticle()
}));
}
}, [formData.type, formData.autoGenerateArticle]);
const handleInputChange = (
field: string,
value: string | number | boolean
@ -89,6 +114,13 @@ export function ProductForm({ product, onSave, onCancel }: ProductFormProps) {
}));
};
const handleGenerateNewArticle = () => {
setFormData(prev => ({
...prev,
article: generateArticle()
}));
};
const handleImageUpload = async (files: FileList) => {
const newUploadingIndexes = new Set<number>();
const startIndex = formData.images.length;
@ -200,7 +232,13 @@ export function ProductForm({ product, onSave, onCancel }: ProductFormProps) {
article: formData.article,
description: formData.description || undefined,
price: formData.price,
pricePerSet: formData.pricePerSet || undefined,
quantity: formData.quantity,
setQuantity: formData.setQuantity || undefined,
ordered: formData.ordered || undefined,
inTransit: formData.inTransit || undefined,
stock: formData.stock || undefined,
sold: formData.sold || undefined,
type: formData.type,
categoryId:
formData.categoryId && formData.categoryId !== "none"
@ -241,7 +279,7 @@ export function ProductForm({ product, onSave, onCancel }: ProductFormProps) {
};
return (
<form onSubmit={handleSubmit} className="space-y-6">
<form onSubmit={handleSubmit} className="space-y-4 pb-4">
{/* Основная информация */}
<Card className="bg-white/5 backdrop-blur border-white/10 p-4">
<h3 className="text-white font-medium mb-4">Основная информация</h3>
@ -261,15 +299,38 @@ export function ProductForm({ product, onSave, onCancel }: ProductFormProps) {
<div>
<Label className="text-white/80 text-sm mb-2 block">
Артикул <span className="text-red-400">*</span>
Артикул СФ <span className="text-red-400">*</span>
</Label>
<Input
value={formData.article}
onChange={(e) => handleInputChange("article", e.target.value)}
placeholder="IP15PM-256-BLU"
className="glass-input text-white placeholder:text-white/40 h-10"
required
/>
<div className="flex gap-2">
<Input
value={formData.article}
onChange={(e) => {
handleInputChange("article", e.target.value);
handleInputChange("autoGenerateArticle", false); // Отключаем автогенерацию при ручном вводе
}}
placeholder="SF-T-123456-001"
className="glass-input text-white placeholder:text-white/40 h-10 flex-1"
required
readOnly={formData.autoGenerateArticle}
/>
{!product && (
<Button
type="button"
variant="outline"
size="sm"
onClick={handleGenerateNewArticle}
className="glass-secondary text-white hover:text-white px-3 h-10"
title="Генерировать новый артикул"
>
<RefreshCw className="h-4 w-4" />
</Button>
)}
</div>
{formData.autoGenerateArticle && (
<p className="text-white/60 text-xs mt-1">
Артикул генерируется автоматически
</p>
)}
</div>
</div>
@ -309,7 +370,7 @@ export function ProductForm({ product, onSave, onCancel }: ProductFormProps) {
<div className="grid grid-cols-2 gap-4 mt-4">
<div>
<Label className="text-white/80 text-sm mb-2 block">
Цена () <span className="text-red-400">*</span>
Цена за единицу () <span className="text-red-400">*</span>
</Label>
<Input
type="number"
@ -327,7 +388,26 @@ export function ProductForm({ product, onSave, onCancel }: ProductFormProps) {
<div>
<Label className="text-white/80 text-sm mb-2 block">
Количество
Цена за комплект ()
</Label>
<Input
type="number"
step="0.01"
min="0"
value={formData.pricePerSet || ""}
onChange={(e) =>
handleInputChange("pricePerSet", parseFloat(e.target.value) || 0)
}
placeholder="299999.99"
className="glass-input text-white placeholder:text-white/40 h-10"
/>
</div>
</div>
<div className="grid grid-cols-2 gap-4 mt-4">
<div>
<Label className="text-white/80 text-sm mb-2 block">
Количество предметов
</Label>
<Input
type="number"
@ -340,6 +420,22 @@ export function ProductForm({ product, onSave, onCancel }: ProductFormProps) {
className="glass-input text-white placeholder:text-white/40 h-10"
/>
</div>
<div>
<Label className="text-white/80 text-sm mb-2 block">
Количество комплектов
</Label>
<Input
type="number"
min="0"
value={formData.setQuantity || ""}
onChange={(e) =>
handleInputChange("setQuantity", parseInt(e.target.value) || 0)
}
placeholder="10"
className="glass-input text-white placeholder:text-white/40 h-10"
/>
</div>
</div>
</Card>

View File

@ -15,8 +15,9 @@ 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, Search, Package } from "lucide-react";
import { Plus, Package, Grid3X3, List, Edit3, Trash2 } from "lucide-react";
import { Input } from "@/components/ui/input";
interface Product {
@ -25,7 +26,13 @@ interface Product {
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;
@ -46,6 +53,7 @@ export function WarehouseDashboard() {
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",
@ -53,24 +61,6 @@ export function WarehouseDashboard() {
const products: Product[] = data?.myProducts || [];
// Отладочное логирование
React.useEffect(() => {
console.log("🏪 WAREHOUSE DASHBOARD DEBUG:", {
loading,
error: error?.message,
dataReceived: !!data,
productsCount: products.length,
products: products.map((p) => ({
id: p.id,
name: p.name,
article: p.article,
type: p.type,
isActive: p.isActive,
createdAt: p.createdAt,
})),
});
}, [data, loading, error, products]);
// Фильтрация товаров по поисковому запросу
const filteredProducts = products.filter(
(product) =>
@ -140,61 +130,85 @@ export function WarehouseDashboard() {
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>
<h1 className="text-xl font-bold text-white mb-1">Мой склад</h1>
<p className="text-white/70 text-sm">
Управление товарами и расходниками
</p>
<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>
<div className="flex gap-2">
<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 max-w-4xl max-h-[90vh] overflow-y-auto">
<DialogHeader>
<DialogTitle className="text-white">
{editingProduct
? "Редактировать товар/расходник"
: "Добавить товар/расходник"}
</DialogTitle>
</DialogHeader>
<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 max-w-4xl h-[90vh] flex flex-col">
<DialogHeader className="flex-shrink-0">
<DialogTitle className="text-white">
{editingProduct
? "Редактировать товар/расходник"
: "Добавить товар/расходник"}
</DialogTitle>
</DialogHeader>
<div className="flex-1 overflow-y-auto pr-2">
<ProductForm
product={editingProduct}
onSave={handleProductSaved}
onCancel={() => setIsDialogOpen(false)}
/>
</DialogContent>
</Dialog>
</div>
</div>
</DialogContent>
</Dialog>
</div>
{/* Поиск */}
<div className="mb-4 flex-shrink-0">
<div className="relative max-w-md">
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 h-4 w-4 text-white/50" />
<Input
type="text"
placeholder="Поиск по названию, артикулу, категории..."
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
className="glass-input text-white placeholder:text-white/50 pl-10 h-10"
/>
</div>
</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-hidden">
{loading ? (
<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>
@ -225,17 +239,106 @@ export function WarehouseDashboard() {
</div>
</div>
) : (
<div className="h-full overflow-y-auto">
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-4">
{filteredProducts.map((product) => (
<ProductCard
key={product.id}
product={product}
onEdit={handleEditProduct}
onDeleted={handleProductDeleted}
/>
))}
</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>

View File

@ -0,0 +1,191 @@
"use client";
import React from "react";
import { Card } from "@/components/ui/card";
import { Badge } from "@/components/ui/badge";
import {
Package,
ShoppingCart,
Truck,
CheckCircle,
AlertTriangle,
TrendingUp,
TrendingDown
} from "lucide-react";
interface Product {
id: string;
name: string;
article: string;
type: "PRODUCT" | "CONSUMABLE";
quantity: number;
ordered?: number;
inTransit?: number;
stock?: number;
sold?: number;
isActive: boolean;
}
interface WarehouseStatisticsProps {
products: Product[];
}
export function WarehouseStatistics({ products }: WarehouseStatisticsProps) {
console.log('📊 STATISTICS DEBUG:', { productsCount: products.length, products });
// Разделение товаров по типам
const goods = products.filter(p => p.type === 'PRODUCT');
const consumables = products.filter(p => p.type === 'CONSUMABLE');
// Общая статистика
const totalProducts = products.length;
const activeProducts = products.filter(p => p.isActive).length;
const totalStock = products.reduce((sum, p) => sum + (p.stock || p.quantity || 0), 0);
const totalOrdered = products.reduce((sum, p) => sum + (p.ordered || 0), 0);
const totalInTransit = products.reduce((sum, p) => sum + (p.inTransit || 0), 0);
const totalSold = products.reduce((sum, p) => sum + (p.sold || 0), 0);
// Статистика по товарам
const goodsStock = goods.reduce((sum, p) => sum + (p.stock || p.quantity || 0), 0);
const goodsOrdered = goods.reduce((sum, p) => sum + (p.ordered || 0), 0);
const goodsInTransit = goods.reduce((sum, p) => sum + (p.inTransit || 0), 0);
const goodsSold = goods.reduce((sum, p) => sum + (p.sold || 0), 0);
// Статистика по расходникам
const consumablesStock = consumables.reduce((sum, p) => sum + (p.stock || p.quantity || 0), 0);
const consumablesOrdered = consumables.reduce((sum, p) => sum + (p.ordered || 0), 0);
const consumablesInTransit = consumables.reduce((sum, p) => sum + (p.inTransit || 0), 0);
const consumablesSold = consumables.reduce((sum, p) => sum + (p.sold || 0), 0);
// Товары с низкими остатками
const lowStockProducts = products.filter(p => {
const stock = p.stock || p.quantity || 0;
return stock > 0 && stock < 10;
});
const outOfStockProducts = products.filter(p => {
const stock = p.stock || p.quantity || 0;
return stock === 0;
});
const StatCard = ({
icon: Icon,
title,
value,
subtitle,
trend,
color = "text-white"
}: {
icon: any;
title: string;
value: number;
subtitle?: string;
trend?: 'up' | 'down';
color?: string;
}) => (
<Card className="bg-white/5 backdrop-blur border-white/10 p-4">
<div className="flex items-center justify-between">
<div>
<div className="flex items-center gap-2 mb-1">
<Icon className={`h-4 w-4 ${color}`} />
<span className="text-white/70 text-sm">{title}</span>
</div>
<div className="text-2xl font-bold text-white">{value.toLocaleString()}</div>
{subtitle && (
<div className="text-white/60 text-xs mt-1">{subtitle}</div>
)}
</div>
{trend && (
<div className="text-right">
{trend === 'up' ? (
<TrendingUp className="h-4 w-4 text-green-400" />
) : (
<TrendingDown className="h-4 w-4 text-red-400" />
)}
</div>
)}
</div>
</Card>
);
return (
<div className="space-y-4">
{/* Общая статистика */}
<div className="grid grid-cols-2 md:grid-cols-4 gap-3">
<StatCard
icon={Package}
title="Всего позиций"
value={totalProducts}
subtitle={`Активных: ${activeProducts}`}
color="text-blue-400"
/>
<StatCard
icon={CheckCircle}
title="Остаток"
value={totalStock}
subtitle="штук на складе"
color="text-green-400"
/>
<StatCard
icon={ShoppingCart}
title="Заказано"
value={totalOrdered}
subtitle="штук в заказах"
color="text-yellow-400"
/>
<StatCard
icon={Truck}
title="В пути"
value={totalInTransit}
subtitle="штук доставляется"
color="text-purple-400"
/>
</div>
{/* Статистика по типам */}
<div className="grid grid-cols-1 lg:grid-cols-2 gap-4">
{/* Товары */}
<div>
<div className="flex items-center gap-2 mb-2">
<Package className="h-4 w-4 text-blue-400" />
<h3 className="text-sm font-semibold text-white">Товары</h3>
<Badge className="bg-blue-500/20 text-blue-300 border-blue-400/30 text-xs">
{goods.length}
</Badge>
</div>
<div className="grid grid-cols-2 gap-2">
<div className="bg-white/5 backdrop-blur border-white/10 p-2 rounded">
<div className="text-xs text-white/70">Остаток</div>
<div className="text-sm font-bold text-green-400">{goodsStock}</div>
</div>
<div className="bg-white/5 backdrop-blur border-white/10 p-2 rounded">
<div className="text-xs text-white/70">Продано</div>
<div className="text-sm font-bold text-emerald-400">{goodsSold}</div>
</div>
</div>
</div>
{/* Расходники */}
<div>
<div className="flex items-center gap-2 mb-2">
<Package className="h-4 w-4 text-orange-400" />
<h3 className="text-sm font-semibold text-white">Расходники</h3>
<Badge className="bg-orange-500/20 text-orange-300 border-orange-400/30 text-xs">
{consumables.length}
</Badge>
</div>
<div className="grid grid-cols-2 gap-2">
<div className="bg-white/5 backdrop-blur border-white/10 p-2 rounded">
<div className="text-xs text-white/70">Остаток</div>
<div className="text-sm font-bold text-green-400">{consumablesStock}</div>
</div>
<div className="bg-white/5 backdrop-blur border-white/10 p-2 rounded">
<div className="text-xs text-white/70">Продано</div>
<div className="text-sm font-bold text-emerald-400">{consumablesSold}</div>
</div>
</div>
</div>
</div>
</div>
);
}

View File

@ -803,7 +803,13 @@ export const CREATE_PRODUCT = gql`
article
description
price
pricePerSet
quantity
setQuantity
ordered
inTransit
stock
sold
type
category {
id
@ -836,7 +842,13 @@ export const UPDATE_PRODUCT = gql`
article
description
price
pricePerSet
quantity
setQuantity
ordered
inTransit
stock
sold
type
category {
id

View File

@ -134,7 +134,13 @@ export const GET_MY_PRODUCTS = gql`
article
description
price
pricePerSet
quantity
setQuantity
ordered
inTransit
stock
sold
type
category {
id

View File

@ -696,7 +696,13 @@ export const typeDefs = gql`
article: String!
description: String
price: Float!
pricePerSet: Float
quantity: Int!
setQuantity: Int
ordered: Int
inTransit: Int
stock: Int
sold: Int
type: ProductType
category: Category
brand: String
@ -718,7 +724,13 @@ export const typeDefs = gql`
article: String!
description: String
price: Float!
pricePerSet: Float
quantity: Int!
setQuantity: Int
ordered: Int
inTransit: Int
stock: Int
sold: Int
type: ProductType
categoryId: ID
brand: String