Добавлены новые поля в модель продукта и форму для управления складом: цена за комплект, количество комплектов, а также поля для учета движения товаров (заказано, в пути, остаток, продано). Обновлены GraphQL резолверы для поддержки новых полей. Оптимизирован интерфейс формы для улучшения пользовательского опыта.
This commit is contained in:
@ -236,7 +236,13 @@ model Product {
|
|||||||
article String
|
article String
|
||||||
description String?
|
description String?
|
||||||
price Decimal @db.Decimal(12, 2)
|
price Decimal @db.Decimal(12, 2)
|
||||||
|
pricePerSet Decimal? @db.Decimal(12, 2)
|
||||||
quantity Int @default(0)
|
quantity Int @default(0)
|
||||||
|
setQuantity Int?
|
||||||
|
ordered Int?
|
||||||
|
inTransit Int?
|
||||||
|
stock Int?
|
||||||
|
sold Int?
|
||||||
type ProductType @default(PRODUCT)
|
type ProductType @default(PRODUCT)
|
||||||
categoryId String?
|
categoryId String?
|
||||||
brand String?
|
brand String?
|
||||||
|
@ -25,7 +25,13 @@ interface Product {
|
|||||||
article: string;
|
article: string;
|
||||||
description: string;
|
description: string;
|
||||||
price: number;
|
price: number;
|
||||||
|
pricePerSet?: number;
|
||||||
quantity: number;
|
quantity: number;
|
||||||
|
setQuantity?: number;
|
||||||
|
ordered?: number;
|
||||||
|
inTransit?: number;
|
||||||
|
stock?: number;
|
||||||
|
sold?: number;
|
||||||
type: "PRODUCT" | "CONSUMABLE";
|
type: "PRODUCT" | "CONSUMABLE";
|
||||||
category: { id: string; name: string } | null;
|
category: { id: string; name: string } | null;
|
||||||
brand: string;
|
brand: string;
|
||||||
@ -226,6 +232,8 @@ export function ProductForm({ product, onSave, onCancel }: ProductFormProps) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
console.log("📝 ФОРМА ДАННЫЕ ПЕРЕД ОТПРАВКОЙ:", formData);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const input = {
|
const input = {
|
||||||
name: formData.name,
|
name: formData.name,
|
||||||
@ -256,10 +264,12 @@ export function ProductForm({ product, onSave, onCancel }: ProductFormProps) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
if (product) {
|
if (product) {
|
||||||
await updateProduct({
|
console.log("📝 ОБНОВЛЕНИЕ ТОВАРА - ОТПРАВКА ЗАПРОСА:", input);
|
||||||
|
const result = await updateProduct({
|
||||||
variables: { id: product.id, input },
|
variables: { id: product.id, input },
|
||||||
refetchQueries: ["GetMyProducts"],
|
refetchQueries: ["GetMyProducts"],
|
||||||
});
|
});
|
||||||
|
console.log("📝 РЕЗУЛЬТАТ ОБНОВЛЕНИЯ ТОВАРА:", result);
|
||||||
toast.success("Товар успешно обновлен");
|
toast.success("Товар успешно обновлен");
|
||||||
} else {
|
} else {
|
||||||
console.log("📝 СОЗДАНИЕ ТОВАРА - ОТПРАВКА ЗАПРОСА:", input);
|
console.log("📝 СОЗДАНИЕ ТОВАРА - ОТПРАВКА ЗАПРОСА:", input);
|
||||||
@ -279,380 +289,341 @@ export function ProductForm({ product, onSave, onCancel }: ProductFormProps) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<form onSubmit={handleSubmit} className="space-y-4 pb-4">
|
<form onSubmit={handleSubmit} className="space-y-3">
|
||||||
{/* Основная информация */}
|
{/* Верхняя часть - 2 колонки */}
|
||||||
<Card className="bg-white/5 backdrop-blur border-white/10 p-4">
|
<div className="grid grid-cols-2 gap-4">
|
||||||
<h3 className="text-white font-medium mb-4">Основная информация</h3>
|
{/* Левая колонка */}
|
||||||
<div className="grid grid-cols-2 gap-4">
|
<div className="space-y-3">
|
||||||
<div>
|
{/* Основная информация */}
|
||||||
<Label className="text-white/80 text-sm mb-2 block">
|
<Card className="bg-white/5 backdrop-blur border-white/10 p-3">
|
||||||
Название товара <span className="text-red-400">*</span>
|
<h3 className="text-white font-medium mb-3 text-sm">Основная информация</h3>
|
||||||
</Label>
|
<div className="space-y-3">
|
||||||
<Input
|
<div>
|
||||||
value={formData.name}
|
<Label className="text-white/80 text-xs mb-1 block">
|
||||||
onChange={(e) => handleInputChange("name", e.target.value)}
|
Название товара <span className="text-red-400">*</span>
|
||||||
placeholder="iPhone 15 Pro Max"
|
</Label>
|
||||||
className="glass-input text-white placeholder:text-white/40 h-10"
|
<Input
|
||||||
required
|
value={formData.name}
|
||||||
/>
|
onChange={(e) => handleInputChange("name", e.target.value)}
|
||||||
</div>
|
placeholder="iPhone 15 Pro Max"
|
||||||
|
className="glass-input text-white placeholder:text-white/40 h-8 text-sm"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<Label className="text-white/80 text-sm mb-2 block">
|
<Label className="text-white/80 text-xs mb-1 block">
|
||||||
Артикул СФ <span className="text-red-400">*</span>
|
Артикул СФ <span className="text-red-400">*</span>
|
||||||
</Label>
|
</Label>
|
||||||
<div className="flex gap-2">
|
<div className="flex gap-2">
|
||||||
<Input
|
<Input
|
||||||
value={formData.article}
|
value={formData.article}
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
handleInputChange("article", e.target.value);
|
handleInputChange("article", e.target.value);
|
||||||
handleInputChange("autoGenerateArticle", false); // Отключаем автогенерацию при ручном вводе
|
handleInputChange("autoGenerateArticle", false);
|
||||||
}}
|
}}
|
||||||
placeholder="SF-T-123456-001"
|
placeholder="SF-T-123456-001"
|
||||||
className="glass-input text-white placeholder:text-white/40 h-10 flex-1"
|
className="glass-input text-white placeholder:text-white/40 h-8 text-sm flex-1"
|
||||||
required
|
required
|
||||||
readOnly={formData.autoGenerateArticle}
|
readOnly={formData.autoGenerateArticle}
|
||||||
/>
|
/>
|
||||||
{!product && (
|
{!product && (
|
||||||
<Button
|
<Button
|
||||||
type="button"
|
type="button"
|
||||||
variant="outline"
|
variant="outline"
|
||||||
size="sm"
|
size="sm"
|
||||||
onClick={handleGenerateNewArticle}
|
onClick={handleGenerateNewArticle}
|
||||||
className="glass-secondary text-white hover:text-white px-3 h-10"
|
className="glass-secondary text-white hover:text-white px-2 h-8"
|
||||||
title="Генерировать новый артикул"
|
title="Генерировать новый артикул"
|
||||||
>
|
|
||||||
<RefreshCw className="h-4 w-4" />
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
{formData.autoGenerateArticle && (
|
|
||||||
<p className="text-white/60 text-xs mt-1">
|
|
||||||
Артикул генерируется автоматически
|
|
||||||
</p>
|
|
||||||
)}
|
|
||||||
</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"
|
|
||||||
step="0.01"
|
|
||||||
min="0"
|
|
||||||
value={formData.pricePerSet || ""}
|
|
||||||
onChange={(e) =>
|
|
||||||
handleInputChange("pricePerSet", parseFloat(e.target.value) || 0)
|
|
||||||
}
|
|
||||||
placeholder="299999.99"
|
|
||||||
className="glass-input text-white placeholder:text-white/40 h-10"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="grid grid-cols-2 gap-4 mt-4">
|
|
||||||
<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>
|
|
||||||
<Label className="text-white/80 text-sm mb-2 block">
|
|
||||||
Количество комплектов
|
|
||||||
</Label>
|
|
||||||
<Input
|
|
||||||
type="number"
|
|
||||||
min="0"
|
|
||||||
value={formData.setQuantity || ""}
|
|
||||||
onChange={(e) =>
|
|
||||||
handleInputChange("setQuantity", parseInt(e.target.value) || 0)
|
|
||||||
}
|
|
||||||
placeholder="10"
|
|
||||||
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}
|
<RefreshCw className="h-3 w-3" />
|
||||||
</SelectItem>
|
</Button>
|
||||||
)
|
)}
|
||||||
)}
|
</div>
|
||||||
</SelectContent>
|
{formData.autoGenerateArticle && (
|
||||||
</Select>
|
<p className="text-white/60 text-xs mt-1">Автогенерация</p>
|
||||||
</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>
|
||||||
))}
|
|
||||||
</div>
|
<div className="flex gap-2 relative">
|
||||||
)}
|
<div className="flex-1">
|
||||||
</Card>
|
<Label className="text-white/80 text-xs mb-1 block">
|
||||||
|
Тип <span className="text-red-400">*</span>
|
||||||
|
</Label>
|
||||||
|
<div className="flex border border-white/10 rounded-lg overflow-hidden h-8">
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
onClick={() => handleInputChange("type", "PRODUCT")}
|
||||||
|
className={`flex-1 h-full rounded-none text-xs border-0 ${
|
||||||
|
formData.type === 'PRODUCT'
|
||||||
|
? 'bg-blue-500/20 text-blue-300 hover:bg-blue-500/30'
|
||||||
|
: 'bg-transparent text-white/70 hover:text-white hover:bg-white/5'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
Товар
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
onClick={() => handleInputChange("type", "CONSUMABLE")}
|
||||||
|
className={`flex-1 h-full rounded-none border-l border-white/10 text-xs border-t-0 border-r-0 border-b-0 ${
|
||||||
|
formData.type === 'CONSUMABLE'
|
||||||
|
? 'bg-orange-500/20 text-orange-300 hover:bg-orange-500/30'
|
||||||
|
: 'bg-transparent text-white/70 hover:text-white hover:bg-white/5'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
Расходник
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex-1">
|
||||||
|
<Label className="text-white/80 text-xs mb-1 block">Категория</Label>
|
||||||
|
<Select
|
||||||
|
value={formData.categoryId}
|
||||||
|
onValueChange={(value) => handleInputChange("categoryId", value)}
|
||||||
|
>
|
||||||
|
<SelectTrigger className="glass-input text-white h-8 text-sm">
|
||||||
|
<SelectValue placeholder="Категория" />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent className="glass-card" side="bottom" align="end">
|
||||||
|
<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>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<Label className="text-white/80 text-xs mb-1 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 text-sm h-16"
|
||||||
|
style={{
|
||||||
|
background: 'rgba(255, 255, 255, 0.05)',
|
||||||
|
border: '1px solid rgba(255, 255, 255, 0.1)',
|
||||||
|
borderRadius: '0.5rem',
|
||||||
|
padding: '0.5rem'
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
{/* Ценообразование */}
|
||||||
|
<Card className="bg-white/5 backdrop-blur border-white/10 p-3">
|
||||||
|
<h3 className="text-white font-medium mb-3 text-sm">Ценообразование</h3>
|
||||||
|
<div className="grid grid-cols-2 gap-2">
|
||||||
|
<div>
|
||||||
|
<Label className="text-white/80 text-xs mb-1 block">Кол-во предметов</Label>
|
||||||
|
<Input
|
||||||
|
type="number"
|
||||||
|
value={formData.quantity || ""}
|
||||||
|
onChange={(e) => handleInputChange("quantity", parseInt(e.target.value) || 0)}
|
||||||
|
className="glass-input text-white h-8 text-sm"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<Label className="text-white/80 text-xs mb-1 block">
|
||||||
|
Цена за ед. (₽) <span className="text-red-400">*</span>
|
||||||
|
</Label>
|
||||||
|
<Input
|
||||||
|
type="number"
|
||||||
|
step="0.01"
|
||||||
|
value={formData.price || ""}
|
||||||
|
onChange={(e) => handleInputChange("price", parseFloat(e.target.value) || 0)}
|
||||||
|
className="glass-input text-white h-8 text-sm"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<Label className="text-white/80 text-xs mb-1 block">Кол-во комплектов</Label>
|
||||||
|
<Input
|
||||||
|
type="number"
|
||||||
|
value={formData.setQuantity || ""}
|
||||||
|
onChange={(e) => handleInputChange("setQuantity", parseInt(e.target.value) || 0)}
|
||||||
|
className="glass-input text-white h-8 text-sm"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<Label className="text-white/80 text-xs mb-1 block">Цена за компл. (₽)</Label>
|
||||||
|
<Input
|
||||||
|
type="number"
|
||||||
|
step="0.01"
|
||||||
|
value={formData.pricePerSet || ""}
|
||||||
|
onChange={(e) => handleInputChange("pricePerSet", parseFloat(e.target.value) || 0)}
|
||||||
|
className="glass-input text-white h-8 text-sm"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Правая колонка */}
|
||||||
|
<div className="space-y-3">
|
||||||
|
{/* Характеристики */}
|
||||||
|
<Card className="bg-white/5 backdrop-blur border-white/10 p-3">
|
||||||
|
<h3 className="text-white font-medium mb-3 text-sm">Характеристики</h3>
|
||||||
|
<div className="grid grid-cols-2 gap-2">
|
||||||
|
<div>
|
||||||
|
<Label className="text-white/80 text-xs mb-1 block">Бренд</Label>
|
||||||
|
<Input
|
||||||
|
value={formData.brand}
|
||||||
|
onChange={(e) => handleInputChange("brand", e.target.value)}
|
||||||
|
className="glass-input text-white h-8 text-sm"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<Label className="text-white/80 text-xs mb-1 block">Цвет</Label>
|
||||||
|
<Input
|
||||||
|
value={formData.color}
|
||||||
|
onChange={(e) => handleInputChange("color", e.target.value)}
|
||||||
|
className="glass-input text-white h-8 text-sm"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<Label className="text-white/80 text-xs mb-1 block">Размер</Label>
|
||||||
|
<Input
|
||||||
|
value={formData.size}
|
||||||
|
onChange={(e) => handleInputChange("size", e.target.value)}
|
||||||
|
className="glass-input text-white h-8 text-sm"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<Label className="text-white/80 text-xs mb-1 block">Вес (кг)</Label>
|
||||||
|
<Input
|
||||||
|
type="number"
|
||||||
|
step="0.001"
|
||||||
|
value={formData.weight || ""}
|
||||||
|
onChange={(e) => handleInputChange("weight", parseFloat(e.target.value) || 0)}
|
||||||
|
className="glass-input text-white h-8 text-sm"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="col-span-2">
|
||||||
|
<Label className="text-white/80 text-xs mb-1 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 h-8 text-sm"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="col-span-2">
|
||||||
|
<Label className="text-white/80 text-xs mb-1 block">Материал</Label>
|
||||||
|
<Input
|
||||||
|
value={formData.material}
|
||||||
|
onChange={(e) => handleInputChange("material", e.target.value)}
|
||||||
|
placeholder="Титан, стекло"
|
||||||
|
className="glass-input text-white h-8 text-sm"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
{/* Изображения */}
|
||||||
|
<Card className="bg-white/5 backdrop-blur border-white/10 p-3">
|
||||||
|
<div className="flex items-center justify-between mb-3">
|
||||||
|
<h3 className="text-white font-medium text-sm">Изображения</h3>
|
||||||
|
<input
|
||||||
|
ref={fileInputRef}
|
||||||
|
type="file"
|
||||||
|
accept="image/*"
|
||||||
|
multiple
|
||||||
|
onChange={(e) => e.target.files && handleImageUpload(e.target.files)}
|
||||||
|
className="hidden"
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
onClick={() => fileInputRef.current?.click()}
|
||||||
|
disabled={isUploading}
|
||||||
|
className="glass-secondary text-white hover:text-white h-7 px-2 text-xs"
|
||||||
|
>
|
||||||
|
<Upload className="h-3 w-3 mr-1" />
|
||||||
|
Загрузить
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{formData.images.length > 0 && (
|
||||||
|
<div className="grid grid-cols-4 gap-2">
|
||||||
|
{formData.images.slice(0, 8).map((imageUrl, index) => (
|
||||||
|
<div key={index} className="relative group">
|
||||||
|
{uploadingImages.has(index) ? (
|
||||||
|
<div className="aspect-square bg-white/10 rounded flex items-center justify-center">
|
||||||
|
<div className="animate-spin rounded-full h-4 w-4 border border-white border-t-transparent"></div>
|
||||||
|
</div>
|
||||||
|
) : imageUrl ? (
|
||||||
|
<>
|
||||||
|
<Image
|
||||||
|
src={imageUrl}
|
||||||
|
alt={`${index + 1}`}
|
||||||
|
width={60}
|
||||||
|
height={60}
|
||||||
|
className="w-full aspect-square object-contain rounded bg-white/5"
|
||||||
|
/>
|
||||||
|
{formData.mainImage === imageUrl && (
|
||||||
|
<Star className="absolute top-1 left-1 h-3 w-3 text-yellow-400 fill-current" />
|
||||||
|
)}
|
||||||
|
<div className="absolute top-1 right-1 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-0 h-4 w-4 bg-white/20 border-white/30 hover:bg-white/30"
|
||||||
|
>
|
||||||
|
<Star className="h-2 w-2 text-white" />
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
size="sm"
|
||||||
|
variant="outline"
|
||||||
|
onClick={() => handleRemoveImage(index)}
|
||||||
|
className="p-0 h-4 w-4 bg-red-500/20 border-red-400/30 hover:bg-red-500/30"
|
||||||
|
>
|
||||||
|
<X className="h-2 w-2 text-white" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<div className="aspect-square bg-white/10 rounded flex items-center justify-center">
|
||||||
|
<div className="text-white/40 text-xs">Нет</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* Кнопки управления */}
|
{/* Кнопки управления */}
|
||||||
<div className="flex gap-3 pt-4">
|
<div className="flex gap-3 pt-2">
|
||||||
<Button
|
<Button
|
||||||
type="button"
|
type="button"
|
||||||
variant="outline"
|
variant="outline"
|
||||||
onClick={onCancel}
|
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"
|
className="flex-1 border-purple-400/30 text-purple-200 hover:bg-purple-500/10 hover:border-purple-300 transition-all duration-300 h-9"
|
||||||
>
|
>
|
||||||
Отмена
|
Отмена
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
type="submit"
|
type="submit"
|
||||||
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 h-9"
|
||||||
>
|
>
|
||||||
{loading
|
{loading ? "Сохранение..." : product ? "Сохранить изменения" : "Создать товар"}
|
||||||
? "Сохранение..."
|
|
||||||
: product
|
|
||||||
? "Сохранить изменения"
|
|
||||||
: "Создать товар"}
|
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
@ -182,21 +182,19 @@ export function WarehouseDashboard() {
|
|||||||
Добавить товар/расходник
|
Добавить товар/расходник
|
||||||
</Button>
|
</Button>
|
||||||
</DialogTrigger>
|
</DialogTrigger>
|
||||||
<DialogContent className="glass-card max-w-4xl h-[90vh] flex flex-col">
|
<DialogContent className="glass-card !w-[90vw] !max-w-[90vw] max-h-[95vh]" style={{ width: '90vw', maxWidth: '90vw' }}>
|
||||||
<DialogHeader className="flex-shrink-0">
|
<DialogHeader>
|
||||||
<DialogTitle className="text-white">
|
<DialogTitle className="text-white">
|
||||||
{editingProduct
|
{editingProduct
|
||||||
? "Редактировать товар/расходник"
|
? "Редактировать товар/расходник"
|
||||||
: "Добавить товар/расходник"}
|
: "Добавить товар/расходник"}
|
||||||
</DialogTitle>
|
</DialogTitle>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
<div className="flex-1 overflow-y-auto pr-2">
|
<ProductForm
|
||||||
<ProductForm
|
product={editingProduct}
|
||||||
product={editingProduct}
|
onSave={handleProductSaved}
|
||||||
onSave={handleProductSaved}
|
onCancel={() => setIsDialogOpen(false)}
|
||||||
onCancel={() => setIsDialogOpen(false)}
|
/>
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
</div>
|
</div>
|
||||||
|
@ -4134,7 +4134,13 @@ export const resolvers = {
|
|||||||
article: string;
|
article: string;
|
||||||
description?: string;
|
description?: string;
|
||||||
price: number;
|
price: number;
|
||||||
|
pricePerSet?: number;
|
||||||
quantity: number;
|
quantity: number;
|
||||||
|
setQuantity?: number;
|
||||||
|
ordered?: number;
|
||||||
|
inTransit?: number;
|
||||||
|
stock?: number;
|
||||||
|
sold?: number;
|
||||||
type?: "PRODUCT" | "CONSUMABLE";
|
type?: "PRODUCT" | "CONSUMABLE";
|
||||||
categoryId?: string;
|
categoryId?: string;
|
||||||
brand?: string;
|
brand?: string;
|
||||||
@ -4211,7 +4217,13 @@ export const resolvers = {
|
|||||||
article: args.input.article,
|
article: args.input.article,
|
||||||
description: args.input.description,
|
description: args.input.description,
|
||||||
price: args.input.price,
|
price: args.input.price,
|
||||||
|
pricePerSet: args.input.pricePerSet,
|
||||||
quantity: args.input.quantity,
|
quantity: args.input.quantity,
|
||||||
|
setQuantity: args.input.setQuantity,
|
||||||
|
ordered: args.input.ordered,
|
||||||
|
inTransit: args.input.inTransit,
|
||||||
|
stock: args.input.stock,
|
||||||
|
sold: args.input.sold,
|
||||||
type: args.input.type || "PRODUCT",
|
type: args.input.type || "PRODUCT",
|
||||||
categoryId: args.input.categoryId,
|
categoryId: args.input.categoryId,
|
||||||
brand: args.input.brand,
|
brand: args.input.brand,
|
||||||
@ -4265,7 +4277,13 @@ export const resolvers = {
|
|||||||
article: string;
|
article: string;
|
||||||
description?: string;
|
description?: string;
|
||||||
price: number;
|
price: number;
|
||||||
|
pricePerSet?: number;
|
||||||
quantity: number;
|
quantity: number;
|
||||||
|
setQuantity?: number;
|
||||||
|
ordered?: number;
|
||||||
|
inTransit?: number;
|
||||||
|
stock?: number;
|
||||||
|
sold?: number;
|
||||||
type?: "PRODUCT" | "CONSUMABLE";
|
type?: "PRODUCT" | "CONSUMABLE";
|
||||||
categoryId?: string;
|
categoryId?: string;
|
||||||
brand?: string;
|
brand?: string;
|
||||||
@ -4334,7 +4352,13 @@ export const resolvers = {
|
|||||||
article: args.input.article,
|
article: args.input.article,
|
||||||
description: args.input.description,
|
description: args.input.description,
|
||||||
price: args.input.price,
|
price: args.input.price,
|
||||||
|
pricePerSet: args.input.pricePerSet,
|
||||||
quantity: args.input.quantity,
|
quantity: args.input.quantity,
|
||||||
|
setQuantity: args.input.setQuantity,
|
||||||
|
ordered: args.input.ordered,
|
||||||
|
inTransit: args.input.inTransit,
|
||||||
|
stock: args.input.stock,
|
||||||
|
sold: args.input.sold,
|
||||||
...(args.input.type && { type: args.input.type }),
|
...(args.input.type && { type: args.input.type }),
|
||||||
categoryId: args.input.categoryId,
|
categoryId: args.input.categoryId,
|
||||||
brand: args.input.brand,
|
brand: args.input.brand,
|
||||||
|
Reference in New Issue
Block a user