Добавлено отладочное логирование в компоненты создания и отображения товаров, обновлены типы продуктов в GraphQL запросах и резолверах. Оптимизирована логика обработки данных о товарах и расходниках, улучшено взаимодействие с пользователем через обновление интерфейса.
This commit is contained in:
96
diagnostic-script.js
Normal file
96
diagnostic-script.js
Normal 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();
|
@ -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">
|
||||||
|
@ -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>
|
||||||
)
|
);
|
||||||
}
|
}
|
@ -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>
|
||||||
)
|
);
|
||||||
}
|
}
|
@ -464,6 +464,7 @@ export const GET_ALL_PRODUCTS = gql`
|
|||||||
description
|
description
|
||||||
price
|
price
|
||||||
quantity
|
quantity
|
||||||
|
type
|
||||||
category {
|
category {
|
||||||
id
|
id
|
||||||
name
|
name
|
||||||
|
@ -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") {
|
||||||
|
Reference in New Issue
Block a user