Files
sfera/src/components/warehouse/product-form.tsx

565 lines
20 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"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";
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;
}
interface ProductFormProps {
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 || "",
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 || "",
weight: product?.weight || 0,
dimensions: product?.dimensions || "",
material: product?.material || "",
images: product?.images || [],
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 [createProduct, { loading: creating }] = useMutation(CREATE_PRODUCT);
const [updateProduct, { loading: updating }] = useMutation(UPDATE_PRODUCT);
// Загружаем категории
const { data: categoriesData } = useQuery(GET_CATEGORIES);
const loading = creating || updating;
const handleInputChange = (
field: string,
value: string | number | boolean
) => {
setFormData((prev) => ({
...prev,
[field]: value,
}));
};
const handleImageUpload = async (files: FileList) => {
const newUploadingIndexes = new Set<number>();
const startIndex = formData.images.length;
// Добавляем плейсхолдеры для загружаемых изображений
const placeholders = Array.from(files).map((_, index) => {
newUploadingIndexes.add(startIndex + index);
return ""; // Пустой URL как плейсхолдер
});
setUploadingImages((prev) => new Set([...prev, ...newUploadingIndexes]));
setFormData((prev) => ({
...prev,
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 response = await fetch("/api/upload-file", {
method: "POST",
body: formData,
});
if (!response.ok) {
throw new Error("Ошибка загрузки изображения");
}
const result = await response.json();
return { index: actualIndex, url: result.fileUrl };
});
const results = await Promise.all(uploadPromises);
// Обновляем URLs загруженных изображений
setFormData((prev) => {
const newImages = [...prev.images];
results.forEach(({ index, url }) => {
newImages[index] = url;
});
return {
...prev,
images: newImages,
mainImage: prev.mainImage || results[0]?.url || "", // Устанавливаем первое изображение как главное
};
});
toast.success("Изображения успешно загружены");
} catch (error) {
console.error("Error uploading images:", error);
toast.error("Ошибка загрузки изображений");
// Удаляем неудачные плейсхолдеры
setFormData((prev) => ({
...prev,
images: prev.images.slice(0, startIndex),
}));
} finally {
// Убираем индикаторы загрузки
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];
return {
...prev,
images: newImages,
mainImage:
prev.mainImage === removedImageUrl
? newImages[0] || ""
: prev.mainImage,
};
});
};
const handleSetMainImage = (imageUrl: string) => {
setFormData((prev) => ({
...prev,
mainImage: imageUrl,
}));
};
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
if (!formData.name || !formData.article || formData.price <= 0) {
toast.error("Пожалуйста, заполните все обязательные поля");
return;
}
try {
const input = {
name: formData.name,
article: formData.article,
description: formData.description || undefined,
price: formData.price,
quantity: formData.quantity,
type: formData.type,
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), // Убираем пустые строки
mainImage: formData.mainImage || undefined,
isActive: formData.isActive,
};
if (product) {
await updateProduct({
variables: { id: product.id, input },
refetchQueries: ["GetMyProducts"],
});
toast.success("Товар успешно обновлен");
} else {
console.log("📝 СОЗДАНИЕ ТОВАРА - ОТПРАВКА ЗАПРОСА:", input);
const result = await createProduct({
variables: { input },
refetchQueries: ["GetMyProducts"],
});
console.log("📝 РЕЗУЛЬТАТ СОЗДАНИЯ ТОВАРА:", result);
toast.success("Товар успешно создан");
}
onSave();
} catch (error: unknown) {
console.error("Error saving product:", error);
toast.error((error as Error).message || "Ошибка при сохранении товара");
}
};
return (
<form onSubmit={handleSubmit} className="space-y-6">
{/* Основная информация */}
<Card className="bg-white/5 backdrop-blur border-white/10 p-4">
<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">
Название товара <span className="text-red-400">*</span>
</Label>
<Input
value={formData.name}
onChange={(e) => handleInputChange("name", e.target.value)}
placeholder="iPhone 15 Pro Max"
className="glass-input text-white placeholder:text-white/40 h-10"
required
/>
</div>
<div>
<Label className="text-white/80 text-sm mb-2 block">
Артикул <span className="text-red-400">*</span>
</Label>
<Input
value={formData.article}
onChange={(e) => handleInputChange("article", e.target.value)}
placeholder="IP15PM-256-BLU"
className="glass-input text-white placeholder:text-white/40 h-10"
required
/>
</div>
</div>
<div className="mt-4">
<Label className="text-white/80 text-sm mb-2 block">Описание</Label>
<textarea
value={formData.description}
onChange={(e) => handleInputChange("description", e.target.value)}
placeholder="Подробное описание товара..."
className="glass-input text-white placeholder:text-white/40 w-full resize-none"
rows={3}
/>
</div>
<div className="mt-4">
<Label className="text-white/80 text-sm mb-2 block">
Тип <span className="text-red-400">*</span>
</Label>
<Select
value={formData.type}
onValueChange={(value) => handleInputChange("type", value)}
>
<SelectTrigger className="glass-input text-white h-10">
<SelectValue placeholder="Выберите тип" />
</SelectTrigger>
<SelectContent className="glass-card">
<SelectItem value="PRODUCT" className="text-white">
Товар
</SelectItem>
<SelectItem value="CONSUMABLE" className="text-white">
Расходник
</SelectItem>
</SelectContent>
</Select>
</div>
<div className="grid grid-cols-2 gap-4 mt-4">
<div>
<Label className="text-white/80 text-sm mb-2 block">
Цена () <span className="text-red-400">*</span>
</Label>
<Input
type="number"
step="0.01"
min="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
/>
</div>
<div>
<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)
}
placeholder="100"
className="glass-input text-white placeholder:text-white/40 h-10"
/>
</div>
</div>
</Card>
{/* Категоризация */}
<Card className="bg-white/5 backdrop-blur border-white/10 p-4">
<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>
<Select
value={formData.categoryId}
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"
>
{category.name}
</SelectItem>
)
)}
</SelectContent>
</Select>
</div>
<div>
<Label className="text-white/80 text-sm mb-2 block">Бренд</Label>
<Input
value={formData.brand}
onChange={(e) => handleInputChange("brand", e.target.value)}
placeholder="Apple"
className="glass-input text-white placeholder:text-white/40 h-10"
/>
</div>
</div>
</Card>
{/* Характеристики */}
<Card className="bg-white/5 backdrop-blur border-white/10 p-4">
<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>
<Input
value={formData.color}
onChange={(e) => handleInputChange("color", e.target.value)}
placeholder="Синий"
className="glass-input text-white placeholder:text-white/40 h-10"
/>
</div>
<div>
<Label className="text-white/80 text-sm mb-2 block">Размер</Label>
<Input
value={formData.size}
onChange={(e) => handleInputChange("size", e.target.value)}
placeholder="L, XL, 42"
className="glass-input text-white placeholder:text-white/40 h-10"
/>
</div>
<div>
<Label className="text-white/80 text-sm mb-2 block">Вес (кг)</Label>
<Input
type="number"
step="0.001"
min="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"
/>
</div>
<div>
<Label className="text-white/80 text-sm mb-2 block">Габариты</Label>
<Input
value={formData.dimensions}
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"
/>
</div>
</div>
<div className="mt-4">
<Label className="text-white/80 text-sm mb-2 block">Материал</Label>
<Input
value={formData.material}
onChange={(e) => handleInputChange("material", e.target.value)}
placeholder="Титан, стекло"
className="glass-input text-white placeholder:text-white/40 h-10"
/>
</div>
</Card>
{/* Изображения */}
<Card className="bg-white/5 backdrop-blur border-white/10 p-4">
<h3 className="text-white font-medium mb-4">Изображения товара</h3>
{/* Кнопка загрузки */}
<div className="mb-4">
<input
ref={fileInputRef}
type="file"
accept="image/*"
multiple
onChange={(e) =>
e.target.files && handleImageUpload(e.target.files)
}
className="hidden"
/>
<Button
type="button"
variant="outline"
onClick={() => fileInputRef.current?.click()}
disabled={isUploading}
className="glass-secondary text-white hover:text-white cursor-pointer"
>
<Upload className="h-4 w-4 mr-2" />
{isUploading ? "Загрузка..." : "Добавить изображения"}
</Button>
</div>
{/* Галерея изображений */}
{formData.images.length > 0 && (
<div className="grid grid-cols-3 gap-4">
{formData.images.map((imageUrl, index) => (
<div key={index} className="relative group">
{uploadingImages.has(index) ? (
<div className="aspect-square bg-white/10 rounded-lg flex items-center justify-center">
<div className="animate-spin rounded-full h-8 w-8 border-2 border-white border-t-transparent"></div>
</div>
) : imageUrl ? (
<>
<Image
src={imageUrl}
alt={`Товар ${index + 1}`}
width={200}
height={150}
className="w-full aspect-square object-contain rounded-lg bg-white/5"
/>
{/* Индикатор главного изображения */}
{formData.mainImage === imageUrl && (
<div className="absolute top-2 left-2">
<Star className="h-5 w-5 text-yellow-400 fill-current" />
</div>
)}
{/* Кнопки управления */}
<div className="absolute top-2 right-2 opacity-0 group-hover:opacity-100 transition-opacity flex gap-1">
{formData.mainImage !== imageUrl && (
<Button
type="button"
size="sm"
variant="outline"
onClick={() => handleSetMainImage(imageUrl)}
className="p-1 h-7 w-7 bg-white/20 border-white/30 hover:bg-white/30"
>
<Star className="h-3 w-3 text-white" />
</Button>
)}
<Button
type="button"
size="sm"
variant="outline"
onClick={() => handleRemoveImage(index)}
className="p-1 h-7 w-7 bg-red-500/20 border-red-400/30 hover:bg-red-500/30"
>
<X className="h-3 w-3 text-white" />
</Button>
</div>
</>
) : (
<div className="aspect-square bg-white/10 rounded-lg flex items-center justify-center">
<div className="text-white/40 text-sm">Нет изображения</div>
</div>
)}
</div>
))}
</div>
)}
</Card>
{/* Кнопки управления */}
<div className="flex gap-3 pt-4">
<Button
type="button"
variant="outline"
onClick={onCancel}
className="flex-1 border-purple-400/30 text-purple-200 hover:bg-purple-500/10 hover:border-purple-300 transition-all duration-300"
>
Отмена
</Button>
<Button
type="submit"
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
? "Сохранить изменения"
: "Создать товар"}
</Button>
</div>
</form>
);
}