Добавлено отладочное логирование в компоненты создания и отображения товаров, обновлены типы продуктов в GraphQL запросах и резолверах. Оптимизирована логика обработки данных о товарах и расходниках, улучшено взаимодействие с пользователем через обновление интерфейса.

This commit is contained in:
Veronika Smirnova
2025-07-30 17:45:02 +03:00
parent 3e7ea13026
commit c3e84f0c49
6 changed files with 550 additions and 293 deletions

96
diagnostic-script.js Normal file
View File

@ -0,0 +1,96 @@
const { PrismaClient } = require("@prisma/client");
const prisma = new PrismaClient();
async function diagnoseDatabase() {
try {
console.log("🔍 ДИАГНОСТИКА БАЗЫ ДАННЫХ...\n");
// Проверяем пользователей
const users = await prisma.user.findMany({
include: {
organization: true,
},
});
console.log("👥 ПОЛЬЗОВАТЕЛИ:");
users.forEach((user) => {
console.log(` - ID: ${user.id}`);
console.log(` Телефон: ${user.phone}`);
console.log(` Организация: ${user.organization?.name || "НЕТ"}`);
console.log(` Тип организации: ${user.organization?.type || "НЕТ"}`);
console.log("");
});
// Проверяем организации
const organizations = await prisma.organization.findMany();
console.log("🏢 ОРГАНИЗАЦИИ:");
organizations.forEach((org) => {
console.log(` - ID: ${org.id}`);
console.log(` Название: ${org.name}`);
console.log(` Тип: ${org.type}`);
console.log("");
});
// Проверяем товары
const products = await prisma.product.findMany({
include: {
organization: true,
category: true,
},
orderBy: {
createdAt: "desc",
},
});
console.log("🛍️ ТОВАРЫ:");
if (products.length === 0) {
console.log(" НЕТ ТОВАРОВ В БАЗЕ ДАННЫХ");
} else {
products.forEach((product) => {
console.log(` - ID: ${product.id}`);
console.log(` Название: ${product.name}`);
console.log(` Артикул: ${product.article}`);
console.log(` Тип: ${product.type}`);
console.log(` Активен: ${product.isActive}`);
console.log(
` Организация: ${product.organization?.name || "НЕТ"} (${
product.organization?.type || "НЕТ"
})`
);
console.log(` Создан: ${product.createdAt}`);
console.log("");
});
}
// Проверяем товары поставщиков
const wholesaleProducts = await prisma.product.findMany({
where: {
organization: {
type: "WHOLESALE",
},
type: "PRODUCT",
},
include: {
organization: true,
},
});
console.log("🏪 ТОВАРЫ ПОСТАВЩИКОВ (WHOLESALE + PRODUCT):");
if (wholesaleProducts.length === 0) {
console.log(" НЕТ ТОВАРОВ ПОСТАВЩИКОВ");
} else {
wholesaleProducts.forEach((product) => {
console.log(
` - ${product.name} (${product.article}) - ${product.organization?.name}`
);
});
}
} catch (error) {
console.error("❌ ОШИБКА:", error);
} finally {
await prisma.$disconnect();
}
}
diagnoseDatabase();

View File

@ -1,6 +1,6 @@
"use client"; "use client";
import React, { useState } from "react"; import React, { useState, useEffect } from "react";
import { useRouter } from "next/navigation"; import { useRouter } from "next/navigation";
import { useQuery, useMutation } from "@apollo/client"; import { useQuery, useMutation } from "@apollo/client";
import { Sidebar } from "@/components/dashboard/sidebar"; import { Sidebar } from "@/components/dashboard/sidebar";
@ -54,6 +54,7 @@ interface FulfillmentConsumableProduct {
name: string; name: string;
description?: string; description?: string;
price: number; price: number;
type?: "PRODUCT" | "CONSUMABLE";
category?: { name: string }; category?: { name: string };
images: string[]; images: string[];
mainImage?: string; mainImage?: string;
@ -128,6 +129,36 @@ export function CreateFulfillmentConsumablesSupplyPage() {
) )
: []; : [];
// Отладочное логирование
React.useEffect(() => {
console.log("🛒 FULFILLMENT CONSUMABLES DEBUG:", {
selectedSupplier: selectedSupplier
? {
id: selectedSupplier.id,
name: selectedSupplier.name || selectedSupplier.fullName,
type: selectedSupplier.type,
}
: null,
productsLoading,
allProductsCount: productsData?.allProducts?.length || 0,
supplierProductsCount: supplierProducts.length,
allProducts:
productsData?.allProducts?.map((p) => ({
id: p.id,
name: p.name,
organizationId: p.organization.id,
organizationName: p.organization.name,
type: p.type || "NO_TYPE",
})) || [],
supplierProducts: supplierProducts.map((p) => ({
id: p.id,
name: p.name,
organizationId: p.organization.id,
organizationName: p.organization.name,
})),
});
}, [selectedSupplier, productsData, productsLoading, supplierProducts]);
const formatCurrency = (amount: number) => { const formatCurrency = (amount: number) => {
return new Intl.NumberFormat("ru-RU", { return new Intl.NumberFormat("ru-RU", {
style: "currency", style: "currency",
@ -363,7 +394,14 @@ export function CreateFulfillmentConsumablesSupplyPage() {
width: "calc((100% - 48px) / 7)", // 48px = 6 gaps * 8px each width: "calc((100% - 48px) / 7)", // 48px = 6 gaps * 8px each
animationDelay: `${index * 100}ms`, animationDelay: `${index * 100}ms`,
}} }}
onClick={() => setSelectedSupplier(supplier)} onClick={() => {
console.log("🔄 ВЫБРАН ПОСТАВЩИК:", {
id: supplier.id,
name: supplier.name || supplier.fullName,
type: supplier.type,
});
setSelectedSupplier(supplier);
}}
> >
<div className="flex flex-col items-center justify-center h-full p-2 space-y-1"> <div className="flex flex-col items-center justify-center h-full p-2 space-y-1">
<div className="relative"> <div className="relative">

View File

@ -1,181 +1,197 @@
"use client" "use client";
import { useState, useRef } from 'react' import { useState, useRef } from "react";
import Image from 'next/image' import Image from "next/image";
import { useMutation, useQuery } from '@apollo/client' import { useMutation, useQuery } from "@apollo/client";
import { Button } from '@/components/ui/button' import { Button } from "@/components/ui/button";
import { Input } from '@/components/ui/input' import { Input } from "@/components/ui/input";
import { Label } from '@/components/ui/label' import { Label } from "@/components/ui/label";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select' import {
import { Card } from '@/components/ui/card' Select,
import { CREATE_PRODUCT, UPDATE_PRODUCT } from '@/graphql/mutations' SelectContent,
import { GET_CATEGORIES } from '@/graphql/queries' SelectItem,
import { X, Star, Upload } from 'lucide-react' SelectTrigger,
import { toast } from 'sonner' SelectValue,
} from "@/components/ui/select";
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 { toast } from "sonner";
interface Product { interface Product {
id: string id: string;
name: string name: string;
article: string article: string;
description: string description: string;
price: number price: number;
quantity: number quantity: number;
type: 'PRODUCT' | 'CONSUMABLE' type: "PRODUCT" | "CONSUMABLE";
category: { id: string; name: string } | null category: { id: string; name: string } | null;
brand: string brand: string;
color: string color: string;
size: string size: string;
weight: number weight: number;
dimensions: string dimensions: string;
material: string material: string;
images: string[] images: string[];
mainImage: string mainImage: string;
isActive: boolean isActive: boolean;
} }
interface ProductFormProps { interface ProductFormProps {
product?: Product | null product?: Product | null;
onSave: () => void onSave: () => void;
onCancel: () => void onCancel: () => void;
} }
export function ProductForm({ product, onSave, onCancel }: ProductFormProps) { export function ProductForm({ product, onSave, onCancel }: ProductFormProps) {
const [formData, setFormData] = useState({ const [formData, setFormData] = useState({
name: product?.name || '', name: product?.name || "",
article: product?.article || '', article: product?.article || "",
description: product?.description || '', description: product?.description || "",
price: product?.price || 0, price: product?.price || 0,
quantity: product?.quantity || 0, quantity: product?.quantity || 0,
type: product?.type || 'PRODUCT' as 'PRODUCT' | 'CONSUMABLE', type: product?.type || ("PRODUCT" as "PRODUCT" | "CONSUMABLE"),
categoryId: product?.category?.id || 'none', categoryId: product?.category?.id || "none",
brand: product?.brand || '', brand: product?.brand || "",
color: product?.color || '', color: product?.color || "",
size: product?.size || '', size: product?.size || "",
weight: product?.weight || 0, weight: product?.weight || 0,
dimensions: product?.dimensions || '', dimensions: product?.dimensions || "",
material: product?.material || '', material: product?.material || "",
images: product?.images || [], images: product?.images || [],
mainImage: product?.mainImage || '', mainImage: product?.mainImage || "",
isActive: product?.isActive ?? true isActive: product?.isActive ?? true,
}) });
const [isUploading] = useState(false) const [isUploading] = useState(false);
const [uploadingImages, setUploadingImages] = useState<Set<number>>(new Set()) const [uploadingImages, setUploadingImages] = useState<Set<number>>(
const fileInputRef = useRef<HTMLInputElement>(null) new Set()
);
const fileInputRef = useRef<HTMLInputElement>(null);
const [createProduct, { loading: creating }] = useMutation(CREATE_PRODUCT) const [createProduct, { loading: creating }] = useMutation(CREATE_PRODUCT);
const [updateProduct, { loading: updating }] = useMutation(UPDATE_PRODUCT) const [updateProduct, { loading: updating }] = useMutation(UPDATE_PRODUCT);
// Загружаем категории // Загружаем категории
const { data: categoriesData } = useQuery(GET_CATEGORIES) const { data: categoriesData } = useQuery(GET_CATEGORIES);
const loading = creating || updating const loading = creating || updating;
const handleInputChange = (field: string, value: string | number | boolean) => { const handleInputChange = (
setFormData(prev => ({ field: string,
value: string | number | boolean
) => {
setFormData((prev) => ({
...prev, ...prev,
[field]: value [field]: value,
})) }));
} };
const handleImageUpload = async (files: FileList) => { const handleImageUpload = async (files: FileList) => {
const newUploadingIndexes = new Set<number>() const newUploadingIndexes = new Set<number>();
const startIndex = formData.images.length const startIndex = formData.images.length;
// Добавляем плейсхолдеры для загружаемых изображений // Добавляем плейсхолдеры для загружаемых изображений
const placeholders = Array.from(files).map((_, index) => { const placeholders = Array.from(files).map((_, index) => {
newUploadingIndexes.add(startIndex + index) newUploadingIndexes.add(startIndex + index);
return '' // Пустой URL как плейсхолдер return ""; // Пустой URL как плейсхолдер
}) });
setUploadingImages(prev => new Set([...prev, ...newUploadingIndexes])) setUploadingImages((prev) => new Set([...prev, ...newUploadingIndexes]));
setFormData(prev => ({ setFormData((prev) => ({
...prev, ...prev,
images: [...prev.images, ...placeholders] images: [...prev.images, ...placeholders],
})) }));
try { try {
// Загружаем каждое изображение // Загружаем каждое изображение
const uploadPromises = Array.from(files).map(async (file, index) => { const uploadPromises = Array.from(files).map(async (file, index) => {
const actualIndex = startIndex + index const actualIndex = startIndex + index;
const formData = new FormData() const formData = new FormData();
formData.append('file', file) formData.append("file", file);
formData.append('type', 'product') formData.append("type", "product");
const response = await fetch('/api/upload-file', { const response = await fetch("/api/upload-file", {
method: 'POST', method: "POST",
body: formData body: formData,
}) });
if (!response.ok) { if (!response.ok) {
throw new Error('Ошибка загрузки изображения') throw new Error("Ошибка загрузки изображения");
} }
const result = await response.json() const result = await response.json();
return { index: actualIndex, url: result.fileUrl } return { index: actualIndex, url: result.fileUrl };
}) });
const results = await Promise.all(uploadPromises) const results = await Promise.all(uploadPromises);
// Обновляем URLs загруженных изображений // Обновляем URLs загруженных изображений
setFormData(prev => { setFormData((prev) => {
const newImages = [...prev.images] const newImages = [...prev.images];
results.forEach(({ index, url }) => { results.forEach(({ index, url }) => {
newImages[index] = url newImages[index] = url;
}) });
return { return {
...prev, ...prev,
images: newImages, images: newImages,
mainImage: prev.mainImage || results[0]?.url || '' // Устанавливаем первое изображение как главное mainImage: prev.mainImage || results[0]?.url || "", // Устанавливаем первое изображение как главное
} };
}) });
toast.success('Изображения успешно загружены') toast.success("Изображения успешно загружены");
} catch (error) { } catch (error) {
console.error('Error uploading images:', error) console.error("Error uploading images:", error);
toast.error('Ошибка загрузки изображений') toast.error("Ошибка загрузки изображений");
// Удаляем неудачные плейсхолдеры // Удаляем неудачные плейсхолдеры
setFormData(prev => ({ setFormData((prev) => ({
...prev, ...prev,
images: prev.images.slice(0, startIndex) images: prev.images.slice(0, startIndex),
})) }));
} finally { } finally {
// Убираем индикаторы загрузки // Убираем индикаторы загрузки
setUploadingImages(prev => { setUploadingImages((prev) => {
const updated = new Set(prev) const updated = new Set(prev);
newUploadingIndexes.forEach(index => updated.delete(index)) newUploadingIndexes.forEach((index) => updated.delete(index));
return updated return updated;
}) });
} }
} };
const handleRemoveImage = (indexToRemove: number) => { const handleRemoveImage = (indexToRemove: number) => {
setFormData(prev => { setFormData((prev) => {
const newImages = prev.images.filter((_, index) => index !== indexToRemove) const newImages = prev.images.filter(
const removedImageUrl = prev.images[indexToRemove] (_, index) => index !== indexToRemove
);
const removedImageUrl = prev.images[indexToRemove];
return { return {
...prev, ...prev,
images: newImages, images: newImages,
mainImage: prev.mainImage === removedImageUrl ? (newImages[0] || '') : prev.mainImage mainImage:
} prev.mainImage === removedImageUrl
}) ? newImages[0] || ""
} : prev.mainImage,
};
});
};
const handleSetMainImage = (imageUrl: string) => { const handleSetMainImage = (imageUrl: string) => {
setFormData(prev => ({ setFormData((prev) => ({
...prev, ...prev,
mainImage: imageUrl mainImage: imageUrl,
})) }));
} };
const handleSubmit = async (e: React.FormEvent) => { const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault() e.preventDefault();
if (!formData.name || !formData.article || formData.price <= 0) { if (!formData.name || !formData.article || formData.price <= 0) {
toast.error('Пожалуйста, заполните все обязательные поля') toast.error("Пожалуйста, заполните все обязательные поля");
return return;
} }
try { try {
@ -186,36 +202,43 @@ export function ProductForm({ product, onSave, onCancel }: ProductFormProps) {
price: formData.price, price: formData.price,
quantity: formData.quantity, quantity: formData.quantity,
type: formData.type, type: formData.type,
categoryId: formData.categoryId && formData.categoryId !== 'none' ? formData.categoryId : undefined, categoryId:
formData.categoryId && formData.categoryId !== "none"
? formData.categoryId
: undefined,
brand: formData.brand || undefined, brand: formData.brand || undefined,
color: formData.color || undefined, color: formData.color || undefined,
size: formData.size || undefined, size: formData.size || undefined,
weight: formData.weight || undefined, weight: formData.weight || undefined,
dimensions: formData.dimensions || undefined, dimensions: formData.dimensions || undefined,
material: formData.material || undefined, material: formData.material || undefined,
images: formData.images.filter(img => img), // Убираем пустые строки images: formData.images.filter((img) => img), // Убираем пустые строки
mainImage: formData.mainImage || undefined, mainImage: formData.mainImage || undefined,
isActive: formData.isActive isActive: formData.isActive,
} };
if (product) { if (product) {
await updateProduct({ await updateProduct({
variables: { id: product.id, input } variables: { id: product.id, input },
}) refetchQueries: ["GetMyProducts"],
toast.success('Товар успешно обновлен') });
toast.success("Товар успешно обновлен");
} else { } else {
await createProduct({ console.log("📝 СОЗДАНИЕ ТОВАРА - ОТПРАВКА ЗАПРОСА:", input);
variables: { input } const result = await createProduct({
}) variables: { input },
toast.success('Товар успешно создан') refetchQueries: ["GetMyProducts"],
});
console.log("📝 РЕЗУЛЬТАТ СОЗДАНИЯ ТОВАРА:", result);
toast.success("Товар успешно создан");
} }
onSave() onSave();
} catch (error: unknown) { } catch (error: unknown) {
console.error('Error saving product:', error) console.error("Error saving product:", error);
toast.error((error as Error).message || 'Ошибка при сохранении товара') toast.error((error as Error).message || "Ошибка при сохранении товара");
} }
} };
return ( return (
<form onSubmit={handleSubmit} className="space-y-6"> <form onSubmit={handleSubmit} className="space-y-6">
@ -229,7 +252,7 @@ export function ProductForm({ product, onSave, onCancel }: ProductFormProps) {
</Label> </Label>
<Input <Input
value={formData.name} value={formData.name}
onChange={(e) => handleInputChange('name', e.target.value)} onChange={(e) => handleInputChange("name", e.target.value)}
placeholder="iPhone 15 Pro Max" placeholder="iPhone 15 Pro Max"
className="glass-input text-white placeholder:text-white/40 h-10" className="glass-input text-white placeholder:text-white/40 h-10"
required required
@ -242,7 +265,7 @@ export function ProductForm({ product, onSave, onCancel }: ProductFormProps) {
</Label> </Label>
<Input <Input
value={formData.article} value={formData.article}
onChange={(e) => handleInputChange('article', e.target.value)} onChange={(e) => handleInputChange("article", e.target.value)}
placeholder="IP15PM-256-BLU" placeholder="IP15PM-256-BLU"
className="glass-input text-white placeholder:text-white/40 h-10" className="glass-input text-white placeholder:text-white/40 h-10"
required required
@ -254,7 +277,7 @@ export function ProductForm({ product, onSave, onCancel }: ProductFormProps) {
<Label className="text-white/80 text-sm mb-2 block">Описание</Label> <Label className="text-white/80 text-sm mb-2 block">Описание</Label>
<textarea <textarea
value={formData.description} value={formData.description}
onChange={(e) => handleInputChange('description', e.target.value)} onChange={(e) => handleInputChange("description", e.target.value)}
placeholder="Подробное описание товара..." placeholder="Подробное описание товара..."
className="glass-input text-white placeholder:text-white/40 w-full resize-none" className="glass-input text-white placeholder:text-white/40 w-full resize-none"
rows={3} rows={3}
@ -267,7 +290,7 @@ export function ProductForm({ product, onSave, onCancel }: ProductFormProps) {
</Label> </Label>
<Select <Select
value={formData.type} value={formData.type}
onValueChange={(value) => handleInputChange('type', value)} onValueChange={(value) => handleInputChange("type", value)}
> >
<SelectTrigger className="glass-input text-white h-10"> <SelectTrigger className="glass-input text-white h-10">
<SelectValue placeholder="Выберите тип" /> <SelectValue placeholder="Выберите тип" />
@ -292,8 +315,10 @@ export function ProductForm({ product, onSave, onCancel }: ProductFormProps) {
type="number" type="number"
step="0.01" step="0.01"
min="0" min="0"
value={formData.price || ''} value={formData.price || ""}
onChange={(e) => handleInputChange('price', parseFloat(e.target.value) || 0)} onChange={(e) =>
handleInputChange("price", parseFloat(e.target.value) || 0)
}
placeholder="99999.99" placeholder="99999.99"
className="glass-input text-white placeholder:text-white/40 h-10" className="glass-input text-white placeholder:text-white/40 h-10"
required required
@ -301,12 +326,16 @@ export function ProductForm({ product, onSave, onCancel }: ProductFormProps) {
</div> </div>
<div> <div>
<Label className="text-white/80 text-sm mb-2 block">Количество</Label> <Label className="text-white/80 text-sm mb-2 block">
Количество
</Label>
<Input <Input
type="number" type="number"
min="0" min="0"
value={formData.quantity || ''} value={formData.quantity || ""}
onChange={(e) => handleInputChange('quantity', parseInt(e.target.value) || 0)} onChange={(e) =>
handleInputChange("quantity", parseInt(e.target.value) || 0)
}
placeholder="100" placeholder="100"
className="glass-input text-white placeholder:text-white/40 h-10" className="glass-input text-white placeholder:text-white/40 h-10"
/> />
@ -319,21 +348,29 @@ export function ProductForm({ product, onSave, onCancel }: ProductFormProps) {
<h3 className="text-white font-medium mb-4">Категоризация</h3> <h3 className="text-white font-medium mb-4">Категоризация</h3>
<div className="grid grid-cols-2 gap-4"> <div className="grid grid-cols-2 gap-4">
<div> <div>
<Label className="text-white/80 text-sm mb-2 block">Категория</Label> <Label className="text-white/80 text-sm mb-2 block">
Категория
</Label>
<Select <Select
value={formData.categoryId} value={formData.categoryId}
onValueChange={(value) => handleInputChange('categoryId', value)} onValueChange={(value) => handleInputChange("categoryId", value)}
> >
<SelectTrigger className="glass-input text-white h-10"> <SelectTrigger className="glass-input text-white h-10">
<SelectValue placeholder="Выберите категорию" /> <SelectValue placeholder="Выберите категорию" />
</SelectTrigger> </SelectTrigger>
<SelectContent className="glass-card"> <SelectContent className="glass-card">
<SelectItem value="none">Без категории</SelectItem> <SelectItem value="none">Без категории</SelectItem>
{categoriesData?.categories?.map((category: { id: string; name: string }) => ( {categoriesData?.categories?.map(
<SelectItem key={category.id} value={category.id} className="text-white"> (category: { id: string; name: string }) => (
{category.name} <SelectItem
</SelectItem> key={category.id}
))} value={category.id}
className="text-white"
>
{category.name}
</SelectItem>
)
)}
</SelectContent> </SelectContent>
</Select> </Select>
</div> </div>
@ -342,7 +379,7 @@ export function ProductForm({ product, onSave, onCancel }: ProductFormProps) {
<Label className="text-white/80 text-sm mb-2 block">Бренд</Label> <Label className="text-white/80 text-sm mb-2 block">Бренд</Label>
<Input <Input
value={formData.brand} value={formData.brand}
onChange={(e) => handleInputChange('brand', e.target.value)} onChange={(e) => handleInputChange("brand", e.target.value)}
placeholder="Apple" placeholder="Apple"
className="glass-input text-white placeholder:text-white/40 h-10" className="glass-input text-white placeholder:text-white/40 h-10"
/> />
@ -358,7 +395,7 @@ export function ProductForm({ product, onSave, onCancel }: ProductFormProps) {
<Label className="text-white/80 text-sm mb-2 block">Цвет</Label> <Label className="text-white/80 text-sm mb-2 block">Цвет</Label>
<Input <Input
value={formData.color} value={formData.color}
onChange={(e) => handleInputChange('color', e.target.value)} onChange={(e) => handleInputChange("color", e.target.value)}
placeholder="Синий" placeholder="Синий"
className="glass-input text-white placeholder:text-white/40 h-10" className="glass-input text-white placeholder:text-white/40 h-10"
/> />
@ -368,7 +405,7 @@ export function ProductForm({ product, onSave, onCancel }: ProductFormProps) {
<Label className="text-white/80 text-sm mb-2 block">Размер</Label> <Label className="text-white/80 text-sm mb-2 block">Размер</Label>
<Input <Input
value={formData.size} value={formData.size}
onChange={(e) => handleInputChange('size', e.target.value)} onChange={(e) => handleInputChange("size", e.target.value)}
placeholder="L, XL, 42" placeholder="L, XL, 42"
className="glass-input text-white placeholder:text-white/40 h-10" className="glass-input text-white placeholder:text-white/40 h-10"
/> />
@ -380,8 +417,10 @@ export function ProductForm({ product, onSave, onCancel }: ProductFormProps) {
type="number" type="number"
step="0.001" step="0.001"
min="0" min="0"
value={formData.weight || ''} value={formData.weight || ""}
onChange={(e) => handleInputChange('weight', parseFloat(e.target.value) || 0)} onChange={(e) =>
handleInputChange("weight", parseFloat(e.target.value) || 0)
}
placeholder="0.221" placeholder="0.221"
className="glass-input text-white placeholder:text-white/40 h-10" className="glass-input text-white placeholder:text-white/40 h-10"
/> />
@ -391,7 +430,7 @@ export function ProductForm({ product, onSave, onCancel }: ProductFormProps) {
<Label className="text-white/80 text-sm mb-2 block">Габариты</Label> <Label className="text-white/80 text-sm mb-2 block">Габариты</Label>
<Input <Input
value={formData.dimensions} value={formData.dimensions}
onChange={(e) => handleInputChange('dimensions', e.target.value)} onChange={(e) => handleInputChange("dimensions", e.target.value)}
placeholder="159.9 × 76.7 × 8.25 мм" placeholder="159.9 × 76.7 × 8.25 мм"
className="glass-input text-white placeholder:text-white/40 h-10" className="glass-input text-white placeholder:text-white/40 h-10"
/> />
@ -402,7 +441,7 @@ export function ProductForm({ product, onSave, onCancel }: ProductFormProps) {
<Label className="text-white/80 text-sm mb-2 block">Материал</Label> <Label className="text-white/80 text-sm mb-2 block">Материал</Label>
<Input <Input
value={formData.material} value={formData.material}
onChange={(e) => handleInputChange('material', e.target.value)} onChange={(e) => handleInputChange("material", e.target.value)}
placeholder="Титан, стекло" placeholder="Титан, стекло"
className="glass-input text-white placeholder:text-white/40 h-10" className="glass-input text-white placeholder:text-white/40 h-10"
/> />
@ -420,7 +459,9 @@ export function ProductForm({ product, onSave, onCancel }: ProductFormProps) {
type="file" type="file"
accept="image/*" accept="image/*"
multiple multiple
onChange={(e) => e.target.files && handleImageUpload(e.target.files)} onChange={(e) =>
e.target.files && handleImageUpload(e.target.files)
}
className="hidden" className="hidden"
/> />
<Button <Button
@ -431,7 +472,7 @@ export function ProductForm({ product, onSave, onCancel }: ProductFormProps) {
className="glass-secondary text-white hover:text-white cursor-pointer" className="glass-secondary text-white hover:text-white cursor-pointer"
> >
<Upload className="h-4 w-4 mr-2" /> <Upload className="h-4 w-4 mr-2" />
{isUploading ? 'Загрузка...' : 'Добавить изображения'} {isUploading ? "Загрузка..." : "Добавить изображения"}
</Button> </Button>
</div> </div>
@ -511,9 +552,13 @@ export function ProductForm({ product, onSave, onCancel }: ProductFormProps) {
disabled={loading || isUploading} disabled={loading || isUploading}
className="flex-1 bg-gradient-to-r from-purple-600 to-pink-600 hover:from-purple-500 hover:to-pink-500 text-white border-0 shadow-lg shadow-purple-500/25 hover:shadow-purple-500/40 transition-all duration-300" className="flex-1 bg-gradient-to-r from-purple-600 to-pink-600 hover:from-purple-500 hover:to-pink-500 text-white border-0 shadow-lg shadow-purple-500/25 hover:shadow-purple-500/40 transition-all duration-300"
> >
{loading ? 'Сохранение...' : (product ? 'Сохранить изменения' : 'Создать товар')} {loading
? "Сохранение..."
: product
? "Сохранить изменения"
: "Создать товар"}
</Button> </Button>
</div> </div>
</form> </form>
) );
} }

View File

@ -1,79 +1,106 @@
"use client" "use client";
import { useState } from 'react' import React, { useState } from "react";
import { useQuery } from '@apollo/client' import { useQuery } from "@apollo/client";
import { Card } from '@/components/ui/card' import { Card } from "@/components/ui/card";
import { Button } from '@/components/ui/button' import { Button } from "@/components/ui/button";
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogTrigger } from '@/components/ui/dialog' import {
import { Sidebar } from '@/components/dashboard/sidebar' Dialog,
import { useSidebar } from '@/hooks/useSidebar' DialogContent,
import { ProductForm } from './product-form' DialogHeader,
import { ProductCard } from './product-card' DialogTitle,
import { GET_MY_PRODUCTS } from '@/graphql/queries' DialogTrigger,
import { Plus, Search, Package } from 'lucide-react' } from "@/components/ui/dialog";
import { Input } from '@/components/ui/input' import { Sidebar } from "@/components/dashboard/sidebar";
import { useSidebar } from "@/hooks/useSidebar";
import { ProductForm } from "./product-form";
import { ProductCard } from "./product-card";
import { GET_MY_PRODUCTS } from "@/graphql/queries";
import { Plus, Search, Package } from "lucide-react";
import { Input } from "@/components/ui/input";
interface Product { interface Product {
id: string id: string;
name: string name: string;
article: string article: string;
description: string description: string;
price: number price: number;
quantity: number quantity: number;
type: 'PRODUCT' | 'CONSUMABLE' type: "PRODUCT" | "CONSUMABLE";
category: { id: string; name: string } | null category: { id: string; name: string } | null;
brand: string brand: string;
color: string color: string;
size: string size: string;
weight: number weight: number;
dimensions: string dimensions: string;
material: string material: string;
images: string[] images: string[];
mainImage: string mainImage: string;
isActive: boolean isActive: boolean;
createdAt: string createdAt: string;
updatedAt: string updatedAt: string;
} }
export function WarehouseDashboard() { export function WarehouseDashboard() {
const { getSidebarMargin } = useSidebar() const { getSidebarMargin } = useSidebar();
const [isDialogOpen, setIsDialogOpen] = useState(false) const [isDialogOpen, setIsDialogOpen] = useState(false);
const [editingProduct, setEditingProduct] = useState<Product | null>(null) const [editingProduct, setEditingProduct] = useState<Product | null>(null);
const [searchQuery, setSearchQuery] = useState('') const [searchQuery, setSearchQuery] = useState("");
const { data, loading, error, refetch } = useQuery(GET_MY_PRODUCTS, { const { data, loading, error, refetch } = useQuery(GET_MY_PRODUCTS, {
errorPolicy: 'all' errorPolicy: "all",
}) });
const products: Product[] = data?.myProducts || [] 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 => const filteredProducts = products.filter(
product.name.toLowerCase().includes(searchQuery.toLowerCase()) || (product) =>
product.article.toLowerCase().includes(searchQuery.toLowerCase()) || product.name.toLowerCase().includes(searchQuery.toLowerCase()) ||
product.category?.name?.toLowerCase().includes(searchQuery.toLowerCase()) || product.article.toLowerCase().includes(searchQuery.toLowerCase()) ||
product.brand?.toLowerCase().includes(searchQuery.toLowerCase()) product.category?.name
) ?.toLowerCase()
.includes(searchQuery.toLowerCase()) ||
product.brand?.toLowerCase().includes(searchQuery.toLowerCase())
);
const handleCreateProduct = () => { const handleCreateProduct = () => {
setEditingProduct(null) setEditingProduct(null);
setIsDialogOpen(true) setIsDialogOpen(true);
} };
const handleEditProduct = (product: Product) => { const handleEditProduct = (product: Product) => {
setEditingProduct(product) setEditingProduct(product);
setIsDialogOpen(true) setIsDialogOpen(true);
} };
const handleProductSaved = () => { const handleProductSaved = () => {
setIsDialogOpen(false) setIsDialogOpen(false);
setEditingProduct(null) setEditingProduct(null);
refetch() refetch();
} };
const handleProductDeleted = () => { const handleProductDeleted = () => {
refetch() refetch();
} };
if (error) { if (error) {
return ( return (
@ -85,9 +112,11 @@ export function WarehouseDashboard() {
<div className="flex items-center justify-center h-full"> <div className="flex items-center justify-center h-full">
<div className="text-center"> <div className="text-center">
<Package className="h-16 w-16 text-white/40 mx-auto mb-4" /> <Package className="h-16 w-16 text-white/40 mx-auto mb-4" />
<h3 className="text-lg font-medium text-white mb-2">Ошибка загрузки</h3> <h3 className="text-lg font-medium text-white mb-2">
Ошибка загрузки
</h3>
<p className="text-white/60 text-sm mb-4"> <p className="text-white/60 text-sm mb-4">
{error.message || 'Не удалось загрузить товары'} {error.message || "Не удалось загрузить товары"}
</p> </p>
<Button <Button
onClick={() => refetch()} onClick={() => refetch()}
@ -101,19 +130,23 @@ export function WarehouseDashboard() {
</div> </div>
</main> </main>
</div> </div>
) );
} }
return ( return (
<div className="h-screen flex overflow-hidden"> <div className="h-screen flex overflow-hidden">
<Sidebar /> <Sidebar />
<main className={`flex-1 ${getSidebarMargin()} px-6 py-4 overflow-hidden transition-all duration-300`}> <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="h-full w-full flex flex-col">
{/* Заголовок и поиск */} {/* Заголовок и поиск */}
<div className="flex items-center justify-between mb-4 flex-shrink-0"> <div className="flex items-center justify-between mb-4 flex-shrink-0">
<div> <div>
<h1 className="text-xl font-bold text-white mb-1">Склад товаров</h1> <h1 className="text-xl font-bold text-white mb-1">Мой склад</h1>
<p className="text-white/70 text-sm">Управление ассортиментом вашего склада</p> <p className="text-white/70 text-sm">
Управление товарами и расходниками
</p>
</div> </div>
<div className="flex gap-2"> <div className="flex gap-2">
@ -124,22 +157,24 @@ export function WarehouseDashboard() {
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" 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" /> <Plus className="w-4 h-4 mr-2" />
Добавить товар Добавить товар/расходник
</Button> </Button>
</DialogTrigger> </DialogTrigger>
<DialogContent className="glass-card max-w-4xl max-h-[90vh] overflow-y-auto"> <DialogContent className="glass-card max-w-4xl max-h-[90vh] overflow-y-auto">
<DialogHeader> <DialogHeader>
<DialogTitle className="text-white"> <DialogTitle className="text-white">
{editingProduct ? 'Редактировать товар' : 'Добавить новый товар'} {editingProduct
</DialogTitle> ? "Редактировать товар/расходник"
</DialogHeader> : "Добавить товар/расходник"}
<ProductForm </DialogTitle>
product={editingProduct} </DialogHeader>
onSave={handleProductSaved} <ProductForm
onCancel={() => setIsDialogOpen(false)} product={editingProduct}
/> onSave={handleProductSaved}
</DialogContent> onCancel={() => setIsDialogOpen(false)}
</Dialog> />
</DialogContent>
</Dialog>
</div> </div>
</div> </div>
@ -171,13 +206,12 @@ export function WarehouseDashboard() {
<div className="text-center"> <div className="text-center">
<Package className="h-16 w-16 text-white/40 mx-auto mb-4" /> <Package className="h-16 w-16 text-white/40 mx-auto mb-4" />
<h3 className="text-lg font-medium text-white mb-2"> <h3 className="text-lg font-medium text-white mb-2">
{searchQuery ? 'Товары не найдены' : 'Склад пуст'} {searchQuery ? "Товары не найдены" : "Склад пуст"}
</h3> </h3>
<p className="text-white/60 text-sm mb-4"> <p className="text-white/60 text-sm mb-4">
{searchQuery {searchQuery
? 'Попробуйте изменить критерии поиска' ? "Попробуйте изменить критерии поиска"
: 'Добавьте ваш первый товар на склад' : "Добавьте ваш первый товар на склад"}
}
</p> </p>
{!searchQuery && ( {!searchQuery && (
<Button <Button
@ -208,5 +242,5 @@ export function WarehouseDashboard() {
</div> </div>
</main> </main>
</div> </div>
) );
} }

View File

@ -464,6 +464,7 @@ export const GET_ALL_PRODUCTS = gql`
description description
price price
quantity quantity
type
category { category {
id id
name name

View File

@ -202,10 +202,6 @@ export const resolvers = {
JSON: JSONScalar, JSON: JSONScalar,
DateTime: DateTimeScalar, DateTime: DateTimeScalar,
Product: {
type: (parent: any) => parent.type || "PRODUCT",
},
Query: { Query: {
me: async (_: unknown, __: unknown, context: Context) => { me: async (_: unknown, __: unknown, context: Context) => {
if (!context.user) { if (!context.user) {
@ -781,11 +777,14 @@ export const resolvers = {
throw new GraphQLError("У пользователя нет организации"); throw new GraphQLError("У пользователя нет организации");
} }
// TypeScript assertion - мы знаем что organization не null после проверки выше
const organization = currentUser.organization;
// Получаем заказы поставок, созданные этим фулфилмент-центром для себя // Получаем заказы поставок, созданные этим фулфилмент-центром для себя
const fulfillmentSupplyOrders = await prisma.supplyOrder.findMany({ const fulfillmentSupplyOrders = await prisma.supplyOrder.findMany({
where: { where: {
organizationId: currentUser.organization.id, // Создали мы organizationId: organization.id, // Создали мы
fulfillmentCenterId: currentUser.organization.id, // Получатель - мы fulfillmentCenterId: organization.id, // Получатель - мы
status: { status: {
in: ["PENDING", "CONFIRMED", "IN_TRANSIT", "DELIVERED"], // Все статусы in: ["PENDING", "CONFIRMED", "IN_TRANSIT", "DELIVERED"], // Все статусы
}, },
@ -832,8 +831,8 @@ export const resolvers = {
imageUrl: null, imageUrl: null,
createdAt: order.createdAt, createdAt: order.createdAt,
updatedAt: order.updatedAt, updatedAt: order.updatedAt,
organizationId: currentUser.organization.id, organizationId: organization.id,
organization: currentUser.organization, organization: organization,
shippedQuantity: 0, shippedQuantity: 0,
})) }))
); );
@ -841,8 +840,8 @@ export const resolvers = {
// Логирование для отладки // Логирование для отладки
console.log("🔥🔥🔥 FULFILLMENT SUPPLIES RESOLVER CALLED 🔥🔥🔥"); console.log("🔥🔥🔥 FULFILLMENT SUPPLIES RESOLVER CALLED 🔥🔥🔥");
console.log("📊 Расходники фулфилмента:", { console.log("📊 Расходники фулфилмента:", {
organizationId: currentUser.organization.id, organizationId: organization.id,
organizationType: currentUser.organization.type, organizationType: organization.type,
fulfillmentOrdersCount: fulfillmentSupplyOrders.length, fulfillmentOrdersCount: fulfillmentSupplyOrders.length,
fulfillmentSuppliesCount: fulfillmentSupplies.length, fulfillmentSuppliesCount: fulfillmentSupplies.length,
fulfillmentOrders: fulfillmentSupplyOrders.map((o) => ({ fulfillmentOrders: fulfillmentSupplyOrders.map((o) => ({
@ -1035,8 +1034,14 @@ export const resolvers = {
}); });
}, },
// Мои товары (для поставщиков) // Мои товары и расходники (для поставщиков)
myProducts: async (_: unknown, __: unknown, context: Context) => { myProducts: async (_: unknown, __: unknown, context: Context) => {
console.log("🔍 MY_PRODUCTS RESOLVER - ВЫЗВАН:", {
hasUser: !!context.user,
userId: context.user?.id,
timestamp: new Date().toISOString(),
});
if (!context.user) { if (!context.user) {
throw new GraphQLError("Требуется авторизация", { throw new GraphQLError("Требуется авторизация", {
extensions: { code: "UNAUTHENTICATED" }, extensions: { code: "UNAUTHENTICATED" },
@ -1048,19 +1053,30 @@ export const resolvers = {
include: { organization: true }, include: { organization: true },
}); });
console.log("👤 ПОЛЬЗОВАТЕЛЬ НАЙДЕН:", {
userId: currentUser?.id,
hasOrganization: !!currentUser?.organization,
organizationType: currentUser?.organization?.type,
organizationName: currentUser?.organization?.name,
});
if (!currentUser?.organization) { if (!currentUser?.organization) {
throw new GraphQLError("У пользователя нет организации"); throw new GraphQLError("У пользователя нет организации");
} }
// Проверяем, что это поставщик // Проверяем, что это поставщик
if (currentUser.organization.type !== "WHOLESALE") { if (currentUser.organization.type !== "WHOLESALE") {
console.log("❌ ДОСТУП ЗАПРЕЩЕН - НЕ ПОСТАВЩИК:", {
actualType: currentUser.organization.type,
requiredType: "WHOLESALE",
});
throw new GraphQLError("Товары доступны только для поставщиков"); throw new GraphQLError("Товары доступны только для поставщиков");
} }
const products = await prisma.product.findMany({ const products = await prisma.product.findMany({
where: { where: {
organizationId: currentUser.organization.id, organizationId: currentUser.organization.id,
type: "PRODUCT", // Показываем только товары, исключаем расходники // Показываем и товары, и расходники поставщика
}, },
include: { include: {
category: true, category: true,
@ -1070,13 +1086,18 @@ export const resolvers = {
}); });
console.log("🔥 MY_PRODUCTS RESOLVER DEBUG:", { console.log("🔥 MY_PRODUCTS RESOLVER DEBUG:", {
userId: currentUser.id,
organizationId: currentUser.organization.id, organizationId: currentUser.organization.id,
organizationType: currentUser.organization.type, organizationType: currentUser.organization.type,
organizationName: currentUser.organization.name,
totalProducts: products.length, totalProducts: products.length,
productTypes: products.map((p) => ({ productTypes: products.map((p) => ({
id: p.id, id: p.id,
name: p.name, name: p.name,
article: p.article,
type: p.type, type: p.type,
isActive: p.isActive,
createdAt: p.createdAt,
})), })),
}); });
@ -1132,7 +1153,7 @@ export const resolvers = {
}); });
// Собираем все товары из доставленных заказов // Собираем все товары из доставленных заказов
const allProducts: any[] = []; const allProducts: unknown[] = [];
console.log("🔍 Резолвер warehouseProducts (доставленные заказы):", { console.log("🔍 Резолвер warehouseProducts (доставленные заказы):", {
currentUserId: currentUser.id, currentUserId: currentUser.id,
@ -1196,12 +1217,19 @@ export const resolvers = {
return allProducts; return allProducts;
}, },
// Все товары всех поставщиков для маркета // Все товары и расходники поставщиков для маркета
allProducts: async ( allProducts: async (
_: unknown, _: unknown,
args: { search?: string; category?: string }, args: { search?: string; category?: string },
context: Context context: Context
) => { ) => {
console.log("🛍️ ALL_PRODUCTS RESOLVER - ВЫЗВАН:", {
userId: context.user?.id,
search: args.search,
category: args.category,
timestamp: new Date().toISOString(),
});
if (!context.user) { if (!context.user) {
throw new GraphQLError("Требуется авторизация", { throw new GraphQLError("Требуется авторизация", {
extensions: { code: "UNAUTHENTICATED" }, extensions: { code: "UNAUTHENTICATED" },
@ -1210,7 +1238,7 @@ export const resolvers = {
const where: Record<string, unknown> = { const where: Record<string, unknown> = {
isActive: true, // Показываем только активные товары isActive: true, // Показываем только активные товары
type: "PRODUCT", // Показываем только товары, исключаем расходники // Показываем и товары, и расходники поставщиков
organization: { organization: {
type: "WHOLESALE", // Только товары поставщиков type: "WHOLESALE", // Только товары поставщиков
}, },
@ -1598,21 +1626,6 @@ export const resolvers = {
return scheduleRecords; return scheduleRecords;
}, },
// Получение всех категорий товаров
categories: async () => {
try {
const categories = await prisma.category.findMany({
orderBy: {
name: "asc",
},
});
return categories;
} catch (error) {
console.error("Ошибка получения категорий:", error);
throw new GraphQLError("Не удалось получить категории");
}
},
}, },
Mutation: { Mutation: {
@ -3537,8 +3550,6 @@ export const resolvers = {
where: { id: args.input.supplyId }, where: { id: args.input.supplyId },
data: { data: {
currentStock: existingSupply.currentStock - args.input.quantityUsed, currentStock: existingSupply.currentStock - args.input.quantityUsed,
usedStock:
(existingSupply.usedStock || 0) + args.input.quantityUsed,
updatedAt: new Date(), updatedAt: new Date(),
}, },
include: { organization: true }, include: { organization: true },
@ -3548,7 +3559,6 @@ export const resolvers = {
supplyName: updatedSupply.name, supplyName: updatedSupply.name,
quantityUsed: args.input.quantityUsed, quantityUsed: args.input.quantityUsed,
remainingStock: updatedSupply.currentStock, remainingStock: updatedSupply.currentStock,
totalUsed: updatedSupply.usedStock,
description: args.input.description, description: args.input.description,
}); });
@ -3723,7 +3733,7 @@ export const resolvers = {
try { try {
// Определяем начальный статус в зависимости от роли организации // Определяем начальный статус в зависимости от роли организации
let initialStatus = "PENDING"; let initialStatus: "PENDING" | "CONFIRMED" = "PENDING";
if (organizationRole === "SELLER") { if (organizationRole === "SELLER") {
initialStatus = "PENDING"; // Селлер создает заказ, ждет подтверждения поставщика initialStatus = "PENDING"; // Селлер создает заказ, ждет подтверждения поставщика
} else if (organizationRole === "FULFILLMENT") { } else if (organizationRole === "FULFILLMENT") {
@ -3779,7 +3789,10 @@ export const resolvers = {
const suppliesData = args.input.items.map((item) => { const suppliesData = args.input.items.map((item) => {
const product = products.find((p) => p.id === item.productId)!; const product = products.find((p) => p.id === item.productId)!;
const productWithCategory = supplyOrder.items.find( const productWithCategory = supplyOrder.items.find(
(orderItem) => orderItem.productId === item.productId (orderItem: {
productId: string;
product: { category?: { name: string } | null };
}) => orderItem.productId === item.productId
)?.product; )?.product;
return { return {
@ -3899,6 +3912,13 @@ export const resolvers = {
}, },
context: Context context: Context
) => { ) => {
console.log("🆕 CREATE_PRODUCT RESOLVER - ВЫЗВАН:", {
hasUser: !!context.user,
userId: context.user?.id,
inputData: args.input,
timestamp: new Date().toISOString(),
});
if (!context.user) { if (!context.user) {
throw new GraphQLError("Требуется авторизация", { throw new GraphQLError("Требуется авторизация", {
extensions: { code: "UNAUTHENTICATED" }, extensions: { code: "UNAUTHENTICATED" },
@ -3935,6 +3955,18 @@ export const resolvers = {
} }
try { try {
console.log("🛍️ СОЗДАНИЕ ТОВАРА - НАЧАЛО:", {
userId: currentUser.id,
organizationId: currentUser.organization.id,
organizationType: currentUser.organization.type,
productData: {
name: args.input.name,
article: args.input.article,
type: args.input.type || "PRODUCT",
isActive: args.input.isActive ?? true,
},
});
const product = await prisma.product.create({ const product = await prisma.product.create({
data: { data: {
name: args.input.name, name: args.input.name,
@ -3961,6 +3993,16 @@ export const resolvers = {
}, },
}); });
console.log("✅ ТОВАР УСПЕШНО СОЗДАН:", {
productId: product.id,
name: product.name,
article: product.article,
type: product.type,
isActive: product.isActive,
organizationId: product.organizationId,
createdAt: product.createdAt,
});
return { return {
success: true, success: true,
message: "Товар успешно создан", message: "Товар успешно создан",
@ -5494,6 +5536,7 @@ export const resolvers = {
}, },
Product: { Product: {
type: (parent: { type?: string | null }) => parent.type || "PRODUCT",
images: (parent: { images: unknown }) => { images: (parent: { images: unknown }) => {
// Если images это строка JSON, парсим её в массив // Если images это строка JSON, парсим её в массив
if (typeof parent.images === "string") { if (typeof parent.images === "string") {