diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 6cbcbba..9475f65 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -236,7 +236,13 @@ model Product { article String description String? price Decimal @db.Decimal(12, 2) + pricePerSet Decimal? @db.Decimal(12, 2) quantity Int @default(0) + setQuantity Int? + ordered Int? + inTransit Int? + stock Int? + sold Int? type ProductType @default(PRODUCT) categoryId String? brand String? diff --git a/src/components/warehouse/product-card.tsx b/src/components/warehouse/product-card.tsx index 65d21c1..e5d99e9 100644 --- a/src/components/warehouse/product-card.tsx +++ b/src/components/warehouse/product-card.tsx @@ -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 ( {/* Изображение товара */} @@ -192,12 +204,44 @@ export function ProductCard({ product, onEdit, onDeleted }: ProductCardProps) { {/* Цена и количество */} -
-
- {formatPrice(product.price)} +
+
+
+
+ {formatPrice(product.price)} +
+ {product.pricePerSet && ( +
+ Комплект: {formatPrice(product.pricePerSet)} +
+ )} +
+
+
+ {getStockQuantity()} шт. +
+ {product.setQuantity && ( +
+ {product.setQuantity} компл. +
+ )} +
-
- {product.quantity} шт. + + {/* Учет движения */} +
+
+
{product.ordered || 0}
+
Заказано
+
+
+
{product.inTransit || 0}
+
В пути
+
+
+
{product.sold || 0}
+
Продано
+
diff --git a/src/components/warehouse/product-form.tsx b/src/components/warehouse/product-form.tsx index 02a69d5..ce6c007 100644 --- a/src/components/warehouse/product-form.tsx +++ b/src/components/warehouse/product-form.tsx @@ -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 { @@ -25,7 +25,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; @@ -49,9 +55,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 +92,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 +120,13 @@ export function ProductForm({ product, onSave, onCancel }: ProductFormProps) { })); }; + const handleGenerateNewArticle = () => { + setFormData(prev => ({ + ...prev, + article: generateArticle() + })); + }; + const handleImageUpload = async (files: FileList) => { const newUploadingIndexes = new Set(); const startIndex = formData.images.length; @@ -194,13 +232,21 @@ export function ProductForm({ product, onSave, onCancel }: ProductFormProps) { return; } + console.log("📝 ФОРМА ДАННЫЕ ПЕРЕД ОТПРАВКОЙ:", formData); + try { const input = { name: formData.name, 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" @@ -218,10 +264,12 @@ export function ProductForm({ product, onSave, onCancel }: ProductFormProps) { }; if (product) { - await updateProduct({ + console.log("📝 ОБНОВЛЕНИЕ ТОВАРА - ОТПРАВКА ЗАПРОСА:", input); + const result = await updateProduct({ variables: { id: product.id, input }, refetchQueries: ["GetMyProducts"], }); + console.log("📝 РЕЗУЛЬТАТ ОБНОВЛЕНИЯ ТОВАРА:", result); toast.success("Товар успешно обновлен"); } else { console.log("📝 СОЗДАНИЕ ТОВАРА - ОТПРАВКА ЗАПРОСА:", input); @@ -241,322 +289,341 @@ export function ProductForm({ product, onSave, onCancel }: ProductFormProps) { }; return ( -
- {/* Основная информация */} - -

Основная информация

-
-
- - handleInputChange("name", e.target.value)} - placeholder="iPhone 15 Pro Max" - className="glass-input text-white placeholder:text-white/40 h-10" - required - /> -
+ + {/* Верхняя часть - 2 колонки */} +
+ {/* Левая колонка */} +
+ {/* Основная информация */} + +

Основная информация

+
+
+ + handleInputChange("name", e.target.value)} + placeholder="iPhone 15 Pro Max" + className="glass-input text-white placeholder:text-white/40 h-8 text-sm" + required + /> +
-
- - handleInputChange("article", e.target.value)} - placeholder="IP15PM-256-BLU" - className="glass-input text-white placeholder:text-white/40 h-10" - required - /> -
-
- -
- -