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

This commit is contained in:
Bivekich
2025-08-01 12:10:48 +03:00
parent 52881cf302
commit 50b02f97b7
7 changed files with 566 additions and 804 deletions

View File

@ -1,6 +1,6 @@
"use client";
import { useState, useRef, useEffect } from "react";
import { useState, useRef, useEffect, useCallback } from "react";
import Image from "next/image";
import { useMutation, useQuery } from "@apollo/client";
import { Button } from "@/components/ui/button";
@ -14,7 +14,7 @@ import {
SelectValue,
} from "@/components/ui/select";
import { Card } from "@/components/ui/card";
import { CREATE_PRODUCT, UPDATE_PRODUCT } from "@/graphql/mutations";
import { CREATE_PRODUCT, UPDATE_PRODUCT, CHECK_ARTICLE_UNIQUENESS } from "@/graphql/mutations";
import { GET_CATEGORIES } from "@/graphql/queries";
import { X, Star, Upload, RefreshCw } from "lucide-react";
import { toast } from "sonner";
@ -82,10 +82,20 @@ export function ProductForm({ product, onSave, onCancel }: ProductFormProps) {
const [uploadingImages, setUploadingImages] = useState<Set<number>>(
new Set()
);
const [articleValidation, setArticleValidation] = useState<{
isChecking: boolean;
isValid: boolean;
message: string;
}>({
isChecking: false,
isValid: true,
message: '',
});
const fileInputRef = useRef<HTMLInputElement>(null);
const [createProduct, { loading: creating }] = useMutation(CREATE_PRODUCT);
const [updateProduct, { loading: updating }] = useMutation(UPDATE_PRODUCT);
const [checkArticleUniqueness] = useMutation(CHECK_ARTICLE_UNIQUENESS);
// Загружаем категории
const { data: categoriesData } = useQuery(GET_CATEGORIES);
@ -127,6 +137,82 @@ export function ProductForm({ product, onSave, onCancel }: ProductFormProps) {
}));
};
// Функция проверки уникальности артикула
const checkArticleUniquenessFn = useCallback(async (article: string) => {
if (!article || article.length < 3) {
setArticleValidation({ isChecking: false, isValid: true, message: '' });
return;
}
setArticleValidation({ isChecking: true, isValid: true, message: '' });
try {
const result = await checkArticleUniqueness({
variables: {
article,
excludeId: product?.id || null,
},
});
// Безопасная проверка наличия данных
if (!result?.data?.checkArticleUniqueness) {
setArticleValidation({
isChecking: false,
isValid: true,
message: '',
});
return;
}
const { isUnique, existingProduct } = result.data.checkArticleUniqueness;
if (isUnique) {
setArticleValidation({
isChecking: false,
isValid: true,
message: '✅ Артикул доступен',
});
} else {
setArticleValidation({
isChecking: false,
isValid: false,
message: `❌ Артикул уже используется товаром "${existingProduct?.name || 'неизвестным'}"`,
});
}
} catch (error) {
console.error('Error checking article uniqueness:', error);
setArticleValidation({
isChecking: false,
isValid: true,
message: '',
});
}
}, [checkArticleUniqueness, product?.id]);
// Debounced проверка артикула
useEffect(() => {
const timeoutId = setTimeout(() => {
if (formData.article && !formData.autoGenerateArticle && formData.article.length >= 3) {
// Проверяем только если артикул изменился по сравнению с оригинальным
if (!product || formData.article !== product.article) {
checkArticleUniquenessFn(formData.article);
} else {
// Если артикул не изменился при редактировании - валидация успешна
setArticleValidation({
isChecking: false,
isValid: true,
message: '✅ Текущий артикул товара'
});
}
} else if (formData.article.length < 3) {
// Сбрасываем валидацию если артикул слишком короткий
setArticleValidation({ isChecking: false, isValid: true, message: '' });
}
}, 500);
return () => clearTimeout(timeoutId);
}, [formData.article, formData.autoGenerateArticle, product, checkArticleUniquenessFn]);
const handleImageUpload = async (files: FileList) => {
const newUploadingIndexes = new Set<number>();
const startIndex = formData.images.length;
@ -232,6 +318,12 @@ export function ProductForm({ product, onSave, onCancel }: ProductFormProps) {
return;
}
// Проверяем уникальность артикула
if (!articleValidation.isValid) {
toast.error("Артикул уже используется другим товаром");
return;
}
console.log("📝 ФОРМА ДАННЫЕ ПЕРЕД ОТПРАВКОЙ:", formData);
try {
@ -288,6 +380,8 @@ export function ProductForm({ product, onSave, onCancel }: ProductFormProps) {
}
};
return (
<form onSubmit={handleSubmit} className="space-y-3">
{/* Верхняя часть - 2 колонки */}
@ -340,8 +434,18 @@ export function ProductForm({ product, onSave, onCancel }: ProductFormProps) {
</Button>
)}
</div>
{formData.autoGenerateArticle && (
{formData.autoGenerateArticle ? (
<p className="text-white/60 text-xs mt-1">Автогенерация</p>
) : (
<div className="mt-1">
{articleValidation.isChecking ? (
<p className="text-blue-400 text-xs">🔄 Проверка уникальности...</p>
) : articleValidation.message ? (
<p className={`text-xs ${articleValidation.isValid ? 'text-green-400' : 'text-red-400'}`}>
{articleValidation.message}
</p>
) : null}
</div>
)}
</div>

View File

@ -186,6 +186,61 @@ export function WarehouseStatistics({ products }: WarehouseStatisticsProps) {
</div>
</div>
</div>
{/* Уведомления о низких остатках */}
{(lowStockProducts.length > 0 || outOfStockProducts.length > 0) && (
<div className="space-y-3">
<div className="flex items-center gap-2">
<AlertTriangle className="h-4 w-4 text-yellow-400" />
<h3 className="text-sm font-semibold text-white">Предупреждения</h3>
</div>
{outOfStockProducts.length > 0 && (
<Card className="bg-red-500/10 backdrop-blur border-red-400/30 p-3">
<div className="flex items-center gap-2 mb-2">
<AlertTriangle className="h-4 w-4 text-red-400" />
<span className="text-red-300 font-medium text-sm">Нет в наличии ({outOfStockProducts.length})</span>
</div>
<div className="space-y-1">
{outOfStockProducts.slice(0, 3).map(product => (
<div key={product.id} className="text-red-200 text-xs">
{product.name} (арт. {product.article})
</div>
))}
{outOfStockProducts.length > 3 && (
<div className="text-red-300 text-xs">
и ещё {outOfStockProducts.length - 3} товаров...
</div>
)}
</div>
</Card>
)}
{lowStockProducts.length > 0 && (
<Card className="bg-yellow-500/10 backdrop-blur border-yellow-400/30 p-3">
<div className="flex items-center gap-2 mb-2">
<AlertTriangle className="h-4 w-4 text-yellow-400" />
<span className="text-yellow-300 font-medium text-sm">Мало на складе ({lowStockProducts.length})</span>
</div>
<div className="space-y-1">
{lowStockProducts.slice(0, 3).map(product => {
const stock = product.stock || product.quantity || 0;
return (
<div key={product.id} className="text-yellow-200 text-xs">
{product.name} (арт. {product.article}) - {stock} шт.
</div>
);
})}
{lowStockProducts.length > 3 && (
<div className="text-yellow-300 text-xs">
и ещё {lowStockProducts.length - 3} товаров...
</div>
)}
</div>
</Card>
)}
</div>
)}
</div>
);
}