Merge pull request 'Добавлено отладочное логирование в компоненты создания и отображения товаров, обновлены типы продуктов в GraphQL запросах и резолверах. Опт…' (#2) from testing into main

Reviewed-on: #2
This commit is contained in:
2025-07-30 17:46:06 +03:00
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";
import React, { useState } from "react";
import React, { useState, useEffect } from "react";
import { useRouter } from "next/navigation";
import { useQuery, useMutation } from "@apollo/client";
import { Sidebar } from "@/components/dashboard/sidebar";
@ -54,6 +54,7 @@ interface FulfillmentConsumableProduct {
name: string;
description?: string;
price: number;
type?: "PRODUCT" | "CONSUMABLE";
category?: { name: string };
images: 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) => {
return new Intl.NumberFormat("ru-RU", {
style: "currency",
@ -363,7 +394,14 @@ export function CreateFulfillmentConsumablesSupplyPage() {
width: "calc((100% - 48px) / 7)", // 48px = 6 gaps * 8px each
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="relative">

View File

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

View File

@ -1,79 +1,106 @@
"use client"
"use client";
import { 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 { GET_MY_PRODUCTS } from '@/graphql/queries'
import { Plus, Search, Package } from 'lucide-react'
import { Input } from '@/components/ui/input'
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 { GET_MY_PRODUCTS } from "@/graphql/queries";
import { Plus, Search, Package } from "lucide-react";
import { Input } from "@/components/ui/input";
interface Product {
id: string
name: string
article: string
description: string
price: number
quantity: 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
id: string;
name: string;
article: string;
description: string;
price: number;
quantity: 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 { getSidebarMargin } = useSidebar();
const [isDialogOpen, setIsDialogOpen] = useState(false);
const [editingProduct, setEditingProduct] = useState<Product | null>(null);
const [searchQuery, setSearchQuery] = useState("");
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) =>
product.name.toLowerCase().includes(searchQuery.toLowerCase()) ||
product.article.toLowerCase().includes(searchQuery.toLowerCase()) ||
product.category?.name?.toLowerCase().includes(searchQuery.toLowerCase()) ||
product.category?.name
?.toLowerCase()
.includes(searchQuery.toLowerCase()) ||
product.brand?.toLowerCase().includes(searchQuery.toLowerCase())
)
);
const handleCreateProduct = () => {
setEditingProduct(null)
setIsDialogOpen(true)
}
setEditingProduct(null);
setIsDialogOpen(true);
};
const handleEditProduct = (product: Product) => {
setEditingProduct(product)
setIsDialogOpen(true)
}
setEditingProduct(product);
setIsDialogOpen(true);
};
const handleProductSaved = () => {
setIsDialogOpen(false)
setEditingProduct(null)
refetch()
}
setIsDialogOpen(false);
setEditingProduct(null);
refetch();
};
const handleProductDeleted = () => {
refetch()
}
refetch();
};
if (error) {
return (
@ -85,9 +112,11 @@ export function WarehouseDashboard() {
<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>
<h3 className="text-lg font-medium text-white mb-2">
Ошибка загрузки
</h3>
<p className="text-white/60 text-sm mb-4">
{error.message || 'Не удалось загрузить товары'}
{error.message || "Не удалось загрузить товары"}
</p>
<Button
onClick={() => refetch()}
@ -101,19 +130,23 @@ export function WarehouseDashboard() {
</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`}>
<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>
<h1 className="text-xl font-bold text-white mb-1">Склад товаров</h1>
<p className="text-white/70 text-sm">Управление ассортиментом вашего склада</p>
<h1 className="text-xl font-bold text-white mb-1">Мой склад</h1>
<p className="text-white/70 text-sm">
Управление товарами и расходниками
</p>
</div>
<div className="flex gap-2">
@ -124,13 +157,15 @@ 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"
>
<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 ? 'Редактировать товар' : 'Добавить новый товар'}
{editingProduct
? "Редактировать товар/расходник"
: "Добавить товар/расходник"}
</DialogTitle>
</DialogHeader>
<ProductForm
@ -171,13 +206,12 @@ export function WarehouseDashboard() {
<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 ? 'Товары не найдены' : 'Склад пуст'}
{searchQuery ? "Товары не найдены" : "Склад пуст"}
</h3>
<p className="text-white/60 text-sm mb-4">
{searchQuery
? 'Попробуйте изменить критерии поиска'
: 'Добавьте ваш первый товар на склад'
}
? "Попробуйте изменить критерии поиска"
: "Добавьте ваш первый товар на склад"}
</p>
{!searchQuery && (
<Button
@ -208,5 +242,5 @@ export function WarehouseDashboard() {
</div>
</main>
</div>
)
);
}

View File

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

View File

@ -203,10 +203,6 @@ export const resolvers = {
JSON: JSONScalar,
DateTime: DateTimeScalar,
Product: {
type: (parent: any) => parent.type || "PRODUCT",
},
Query: {
me: async (_: unknown, __: unknown, context: Context) => {
if (!context.user) {
@ -782,11 +778,14 @@ export const resolvers = {
throw new GraphQLError("У пользователя нет организации");
}
// TypeScript assertion - мы знаем что organization не null после проверки выше
const organization = currentUser.organization;
// Получаем заказы поставок, созданные этим фулфилмент-центром для себя
const fulfillmentSupplyOrders = await prisma.supplyOrder.findMany({
where: {
organizationId: currentUser.organization.id, // Создали мы
fulfillmentCenterId: currentUser.organization.id, // Получатель - мы
organizationId: organization.id, // Создали мы
fulfillmentCenterId: organization.id, // Получатель - мы
status: {
in: ["PENDING", "CONFIRMED", "IN_TRANSIT", "DELIVERED"], // Все статусы
},
@ -833,8 +832,8 @@ export const resolvers = {
imageUrl: null,
createdAt: order.createdAt,
updatedAt: order.updatedAt,
organizationId: currentUser.organization.id,
organization: currentUser.organization,
organizationId: organization.id,
organization: organization,
shippedQuantity: 0,
}))
);
@ -842,8 +841,8 @@ export const resolvers = {
// Логирование для отладки
console.log("🔥🔥🔥 FULFILLMENT SUPPLIES RESOLVER CALLED 🔥🔥🔥");
console.log("📊 Расходники фулфилмента:", {
organizationId: currentUser.organization.id,
organizationType: currentUser.organization.type,
organizationId: organization.id,
organizationType: organization.type,
fulfillmentOrdersCount: fulfillmentSupplyOrders.length,
fulfillmentSuppliesCount: fulfillmentSupplies.length,
fulfillmentOrders: fulfillmentSupplyOrders.map((o) => ({
@ -1036,8 +1035,14 @@ export const resolvers = {
});
},
// Мои товары (для поставщиков)
// Мои товары и расходники (для поставщиков)
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) {
throw new GraphQLError("Требуется авторизация", {
extensions: { code: "UNAUTHENTICATED" },
@ -1049,19 +1054,30 @@ export const resolvers = {
include: { organization: true },
});
console.log("👤 ПОЛЬЗОВАТЕЛЬ НАЙДЕН:", {
userId: currentUser?.id,
hasOrganization: !!currentUser?.organization,
organizationType: currentUser?.organization?.type,
organizationName: currentUser?.organization?.name,
});
if (!currentUser?.organization) {
throw new GraphQLError("У пользователя нет организации");
}
// Проверяем, что это поставщик
if (currentUser.organization.type !== "WHOLESALE") {
console.log("❌ ДОСТУП ЗАПРЕЩЕН - НЕ ПОСТАВЩИК:", {
actualType: currentUser.organization.type,
requiredType: "WHOLESALE",
});
throw new GraphQLError("Товары доступны только для поставщиков");
}
const products = await prisma.product.findMany({
where: {
organizationId: currentUser.organization.id,
type: "PRODUCT", // Показываем только товары, исключаем расходники
// Показываем и товары, и расходники поставщика
},
include: {
category: true,
@ -1071,13 +1087,18 @@ export const resolvers = {
});
console.log("🔥 MY_PRODUCTS RESOLVER DEBUG:", {
userId: currentUser.id,
organizationId: currentUser.organization.id,
organizationType: currentUser.organization.type,
organizationName: currentUser.organization.name,
totalProducts: products.length,
productTypes: products.map((p) => ({
id: p.id,
name: p.name,
article: p.article,
type: p.type,
isActive: p.isActive,
createdAt: p.createdAt,
})),
});
@ -1133,7 +1154,7 @@ export const resolvers = {
});
// Собираем все товары из доставленных заказов
const allProducts: any[] = [];
const allProducts: unknown[] = [];
console.log("🔍 Резолвер warehouseProducts (доставленные заказы):", {
currentUserId: currentUser.id,
@ -1197,12 +1218,19 @@ export const resolvers = {
return allProducts;
},
// Все товары всех поставщиков для маркета
// Все товары и расходники поставщиков для маркета
allProducts: async (
_: unknown,
args: { search?: string; category?: string },
context: Context
) => {
console.log("🛍️ ALL_PRODUCTS RESOLVER - ВЫЗВАН:", {
userId: context.user?.id,
search: args.search,
category: args.category,
timestamp: new Date().toISOString(),
});
if (!context.user) {
throw new GraphQLError("Требуется авторизация", {
extensions: { code: "UNAUTHENTICATED" },
@ -1211,7 +1239,7 @@ export const resolvers = {
const where: Record<string, unknown> = {
isActive: true, // Показываем только активные товары
type: "PRODUCT", // Показываем только товары, исключаем расходники
// Показываем и товары, и расходники поставщиков
organization: {
type: "WHOLESALE", // Только товары поставщиков
},
@ -1599,21 +1627,6 @@ export const resolvers = {
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: {
@ -3538,8 +3551,6 @@ export const resolvers = {
where: { id: args.input.supplyId },
data: {
currentStock: existingSupply.currentStock - args.input.quantityUsed,
usedStock:
(existingSupply.usedStock || 0) + args.input.quantityUsed,
updatedAt: new Date(),
},
include: { organization: true },
@ -3549,7 +3560,6 @@ export const resolvers = {
supplyName: updatedSupply.name,
quantityUsed: args.input.quantityUsed,
remainingStock: updatedSupply.currentStock,
totalUsed: updatedSupply.usedStock,
description: args.input.description,
});
@ -3724,7 +3734,7 @@ export const resolvers = {
try {
// Определяем начальный статус в зависимости от роли организации
let initialStatus = "PENDING";
let initialStatus: "PENDING" | "CONFIRMED" = "PENDING";
if (organizationRole === "SELLER") {
initialStatus = "PENDING"; // Селлер создает заказ, ждет подтверждения поставщика
} else if (organizationRole === "FULFILLMENT") {
@ -3780,7 +3790,10 @@ export const resolvers = {
const suppliesData = args.input.items.map((item) => {
const product = products.find((p) => p.id === item.productId)!;
const productWithCategory = supplyOrder.items.find(
(orderItem) => orderItem.productId === item.productId
(orderItem: {
productId: string;
product: { category?: { name: string } | null };
}) => orderItem.productId === item.productId
)?.product;
return {
@ -3900,6 +3913,13 @@ export const resolvers = {
},
context: Context
) => {
console.log("🆕 CREATE_PRODUCT RESOLVER - ВЫЗВАН:", {
hasUser: !!context.user,
userId: context.user?.id,
inputData: args.input,
timestamp: new Date().toISOString(),
});
if (!context.user) {
throw new GraphQLError("Требуется авторизация", {
extensions: { code: "UNAUTHENTICATED" },
@ -3936,6 +3956,18 @@ export const resolvers = {
}
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({
data: {
name: args.input.name,
@ -3962,6 +3994,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 {
success: true,
message: "Товар успешно создан",
@ -5497,6 +5539,7 @@ export const resolvers = {
},
Product: {
type: (parent: { type?: string | null }) => parent.type || "PRODUCT",
images: (parent: { images: unknown }) => {
// Если images это строка JSON, парсим её в массив
if (typeof parent.images === "string") {