Обновлены модели и компоненты для управления поставками и расходниками. Добавлены новые поля в модели SupplyOrder и соответствующие резолверы для поддержки логистики. Реализованы компоненты уведомлений для отображения статуса логистических заявок и поставок. Оптимизирован интерфейс для улучшения пользовательского опыта, добавлены логи для диагностики запросов. Обновлены GraphQL схемы и мутации для поддержки новых функциональных возможностей.
This commit is contained in:
@ -99,14 +99,44 @@ export function CreateFulfillmentConsumablesSupplyPage() {
|
||||
GET_MY_COUNTERPARTIES
|
||||
);
|
||||
|
||||
// ОТЛАДКА: Логируем состояние перед запросом товаров
|
||||
console.log("🔍 ДИАГНОСТИКА ЗАПРОСА ТОВАРОВ:", {
|
||||
selectedSupplier: selectedSupplier
|
||||
? {
|
||||
id: selectedSupplier.id,
|
||||
name: selectedSupplier.name || selectedSupplier.fullName,
|
||||
type: selectedSupplier.type,
|
||||
}
|
||||
: null,
|
||||
skipQuery: !selectedSupplier,
|
||||
productSearchQuery,
|
||||
});
|
||||
|
||||
// Загружаем товары для выбранного поставщика
|
||||
const { data: productsData, loading: productsLoading } = useQuery(
|
||||
GET_ALL_PRODUCTS,
|
||||
{
|
||||
skip: !selectedSupplier,
|
||||
variables: { search: productSearchQuery || null, category: null },
|
||||
}
|
||||
);
|
||||
const {
|
||||
data: productsData,
|
||||
loading: productsLoading,
|
||||
error: productsError,
|
||||
} = useQuery(GET_ALL_PRODUCTS, {
|
||||
skip: !selectedSupplier,
|
||||
variables: { search: productSearchQuery || null, category: null },
|
||||
onCompleted: (data) => {
|
||||
console.log("✅ GET_ALL_PRODUCTS COMPLETED:", {
|
||||
totalProducts: data?.allProducts?.length || 0,
|
||||
products:
|
||||
data?.allProducts?.map((p) => ({
|
||||
id: p.id,
|
||||
name: p.name,
|
||||
type: p.type,
|
||||
orgId: p.organization?.id,
|
||||
orgName: p.organization?.name,
|
||||
})) || [],
|
||||
});
|
||||
},
|
||||
onError: (error) => {
|
||||
console.error("❌ GET_ALL_PRODUCTS ERROR:", error);
|
||||
},
|
||||
});
|
||||
|
||||
// Мутация для создания заказа поставки расходников
|
||||
const [createSupplyOrder] = useMutation(CREATE_SUPPLY_ORDER);
|
||||
@ -117,9 +147,9 @@ export function CreateFulfillmentConsumablesSupplyPage() {
|
||||
).filter((org: FulfillmentConsumableSupplier) => org.type === "WHOLESALE");
|
||||
|
||||
// Фильтруем только логистические компании
|
||||
const logisticsPartners = (
|
||||
counterpartiesData?.myCounterparties || []
|
||||
).filter((org: FulfillmentConsumableSupplier) => org.type === "LOGIST");
|
||||
const logisticsPartners = (counterpartiesData?.myCounterparties || []).filter(
|
||||
(org: FulfillmentConsumableSupplier) => org.type === "LOGIST"
|
||||
);
|
||||
|
||||
// Фильтруем поставщиков по поисковому запросу
|
||||
const filteredSuppliers = consumableSuppliers.filter(
|
||||
@ -150,6 +180,7 @@ export function CreateFulfillmentConsumablesSupplyPage() {
|
||||
}
|
||||
: null,
|
||||
productsLoading,
|
||||
productsError: productsError?.message,
|
||||
allProductsCount: productsData?.allProducts?.length || 0,
|
||||
supplierProductsCount: supplierProducts.length,
|
||||
allProducts:
|
||||
@ -160,14 +191,20 @@ export function CreateFulfillmentConsumablesSupplyPage() {
|
||||
organizationName: p.organization.name,
|
||||
type: p.type || "NO_TYPE",
|
||||
})) || [],
|
||||
supplierProducts: supplierProducts.map((p) => ({
|
||||
supplierProductsDetails: supplierProducts.slice(0, 5).map((p) => ({
|
||||
id: p.id,
|
||||
name: p.name,
|
||||
organizationId: p.organization.id,
|
||||
organizationName: p.organization.name,
|
||||
})),
|
||||
});
|
||||
}, [selectedSupplier, productsData, productsLoading, supplierProducts]);
|
||||
}, [
|
||||
selectedSupplier,
|
||||
productsData,
|
||||
productsLoading,
|
||||
productsError,
|
||||
supplierProducts.length,
|
||||
]);
|
||||
|
||||
const formatCurrency = (amount: number) => {
|
||||
return new Intl.NumberFormat("ru-RU", {
|
||||
@ -198,10 +235,13 @@ export function CreateFulfillmentConsumablesSupplyPage() {
|
||||
|
||||
// 🔒 ВАЛИДАЦИЯ ОСТАТКОВ согласно правилам (раздел 6.2)
|
||||
if (quantity > 0) {
|
||||
const availableStock = (product.stock || product.quantity || 0) - (product.ordered || 0);
|
||||
|
||||
const availableStock =
|
||||
(product.stock || product.quantity || 0) - (product.ordered || 0);
|
||||
|
||||
if (quantity > availableStock) {
|
||||
toast.error(`❌ Недостаточно остатков!\nДоступно: ${availableStock} шт.\nЗапрашивается: ${quantity} шт.`);
|
||||
toast.error(
|
||||
`❌ Недостаточно остатков!\nДоступно: ${availableStock} шт.\nЗапрашивается: ${quantity} шт.`
|
||||
);
|
||||
return;
|
||||
}
|
||||
}
|
||||
@ -265,7 +305,15 @@ export function CreateFulfillmentConsumablesSupplyPage() {
|
||||
!deliveryDate ||
|
||||
!selectedLogistics
|
||||
) {
|
||||
toast.error("Заполните все обязательные поля");
|
||||
toast.error(
|
||||
"Заполните все обязательные поля: поставщик, расходники, дата доставки и логистика"
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
// Дополнительная проверка ID логистики
|
||||
if (!selectedLogistics.id) {
|
||||
toast.error("Выберите логистическую компанию");
|
||||
return;
|
||||
}
|
||||
|
||||
@ -279,7 +327,7 @@ export function CreateFulfillmentConsumablesSupplyPage() {
|
||||
deliveryDate: deliveryDate,
|
||||
// Для фулфилмента указываем себя как получателя (поставка на свой склад)
|
||||
fulfillmentCenterId: user?.organization?.id,
|
||||
logisticsPartnerId: selectedLogistics?.id,
|
||||
logisticsPartnerId: selectedLogistics.id,
|
||||
// 🏷️ КЛАССИФИКАЦИЯ согласно правилам (раздел 2.2)
|
||||
consumableType: "FULFILLMENT_CONSUMABLES", // Расходники фулфилмента
|
||||
items: selectedConsumables.map((consumable) => ({
|
||||
@ -574,15 +622,19 @@ export function CreateFulfillmentConsumablesSupplyPage() {
|
||||
<div className="aspect-square bg-white/5 rounded-lg overflow-hidden relative flex-shrink-0">
|
||||
{/* 🚫 ОВЕРЛЕЙ НЕДОСТУПНОСТИ */}
|
||||
{(() => {
|
||||
const totalStock = product.stock || product.quantity || 0;
|
||||
const totalStock =
|
||||
product.stock || product.quantity || 0;
|
||||
const orderedStock = product.ordered || 0;
|
||||
const availableStock = totalStock - orderedStock;
|
||||
|
||||
const availableStock =
|
||||
totalStock - orderedStock;
|
||||
|
||||
if (availableStock <= 0) {
|
||||
return (
|
||||
<div className="absolute inset-0 bg-black/60 backdrop-blur-sm flex items-center justify-center z-10">
|
||||
<div className="text-center">
|
||||
<div className="text-red-400 font-bold text-xs">НЕТ В НАЛИЧИИ</div>
|
||||
<div className="text-red-400 font-bold text-xs">
|
||||
НЕТ В НАЛИЧИИ
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
@ -636,10 +688,12 @@ export function CreateFulfillmentConsumablesSupplyPage() {
|
||||
)}
|
||||
{/* 🚨 ИНДИКАТОР НИЗКИХ ОСТАТКОВ согласно правилам (раздел 6.3) */}
|
||||
{(() => {
|
||||
const totalStock = product.stock || product.quantity || 0;
|
||||
const totalStock =
|
||||
product.stock || product.quantity || 0;
|
||||
const orderedStock = product.ordered || 0;
|
||||
const availableStock = totalStock - orderedStock;
|
||||
|
||||
const availableStock =
|
||||
totalStock - orderedStock;
|
||||
|
||||
if (availableStock <= 0) {
|
||||
return (
|
||||
<Badge className="bg-red-500/30 text-red-300 border-red-500/50 text-xs px-2 py-1 animate-pulse">
|
||||
@ -663,19 +717,26 @@ export function CreateFulfillmentConsumablesSupplyPage() {
|
||||
{/* 📊 АКТУАЛЬНЫЙ ОСТАТОК согласно правилам (раздел 6.4.2) */}
|
||||
<div className="text-right">
|
||||
{(() => {
|
||||
const totalStock = product.stock || product.quantity || 0;
|
||||
const orderedStock = product.ordered || 0;
|
||||
const availableStock = totalStock - orderedStock;
|
||||
|
||||
const totalStock =
|
||||
product.stock ||
|
||||
product.quantity ||
|
||||
0;
|
||||
const orderedStock =
|
||||
product.ordered || 0;
|
||||
const availableStock =
|
||||
totalStock - orderedStock;
|
||||
|
||||
return (
|
||||
<div className="flex flex-col items-end">
|
||||
<span className={`text-xs font-medium ${
|
||||
availableStock <= 0
|
||||
? 'text-red-400'
|
||||
: availableStock <= 10
|
||||
? 'text-yellow-400'
|
||||
: 'text-white/80'
|
||||
}`}>
|
||||
<span
|
||||
className={`text-xs font-medium ${
|
||||
availableStock <= 0
|
||||
? "text-red-400"
|
||||
: availableStock <= 10
|
||||
? "text-yellow-400"
|
||||
: "text-white/80"
|
||||
}`}
|
||||
>
|
||||
Доступно: {availableStock}
|
||||
</span>
|
||||
{orderedStock > 0 && (
|
||||
@ -693,10 +754,12 @@ export function CreateFulfillmentConsumablesSupplyPage() {
|
||||
{/* Управление количеством */}
|
||||
<div className="flex flex-col items-center space-y-2 mt-auto">
|
||||
{(() => {
|
||||
const totalStock = product.stock || product.quantity || 0;
|
||||
const totalStock =
|
||||
product.stock || product.quantity || 0;
|
||||
const orderedStock = product.ordered || 0;
|
||||
const availableStock = totalStock - orderedStock;
|
||||
|
||||
const availableStock =
|
||||
totalStock - orderedStock;
|
||||
|
||||
return (
|
||||
<div className="flex items-center space-x-2">
|
||||
<Button
|
||||
@ -713,81 +776,92 @@ export function CreateFulfillmentConsumablesSupplyPage() {
|
||||
>
|
||||
<Minus className="h-3 w-3" />
|
||||
</Button>
|
||||
<Input
|
||||
type="text"
|
||||
inputMode="numeric"
|
||||
pattern="[0-9]*"
|
||||
value={
|
||||
selectedQuantity === 0
|
||||
? ""
|
||||
: selectedQuantity.toString()
|
||||
}
|
||||
onChange={(e) => {
|
||||
let inputValue = e.target.value;
|
||||
<Input
|
||||
type="text"
|
||||
inputMode="numeric"
|
||||
pattern="[0-9]*"
|
||||
value={
|
||||
selectedQuantity === 0
|
||||
? ""
|
||||
: selectedQuantity.toString()
|
||||
}
|
||||
onChange={(e) => {
|
||||
let inputValue = e.target.value;
|
||||
|
||||
// Удаляем все нецифровые символы
|
||||
inputValue = inputValue.replace(
|
||||
/[^0-9]/g,
|
||||
""
|
||||
);
|
||||
// Удаляем все нецифровые символы
|
||||
inputValue = inputValue.replace(
|
||||
/[^0-9]/g,
|
||||
""
|
||||
);
|
||||
|
||||
// Удаляем ведущие нули
|
||||
inputValue = inputValue.replace(
|
||||
/^0+/,
|
||||
""
|
||||
);
|
||||
// Удаляем ведущие нули
|
||||
inputValue = inputValue.replace(
|
||||
/^0+/,
|
||||
""
|
||||
);
|
||||
|
||||
// Если строка пустая после удаления нулей, устанавливаем 0
|
||||
const numericValue =
|
||||
inputValue === ""
|
||||
? 0
|
||||
: parseInt(inputValue);
|
||||
// Если строка пустая после удаления нулей, устанавливаем 0
|
||||
const numericValue =
|
||||
inputValue === ""
|
||||
? 0
|
||||
: parseInt(inputValue);
|
||||
|
||||
// Ограничиваем значение максимумом доступного остатка
|
||||
const clampedValue = Math.min(
|
||||
numericValue,
|
||||
availableStock,
|
||||
99999
|
||||
);
|
||||
// Ограничиваем значение максимумом доступного остатка
|
||||
const clampedValue = Math.min(
|
||||
numericValue,
|
||||
availableStock,
|
||||
99999
|
||||
);
|
||||
|
||||
updateConsumableQuantity(
|
||||
product.id,
|
||||
clampedValue
|
||||
);
|
||||
}}
|
||||
onBlur={(e) => {
|
||||
// При потере фокуса, если поле пустое, устанавливаем 0
|
||||
if (e.target.value === "") {
|
||||
updateConsumableQuantity(
|
||||
product.id,
|
||||
0
|
||||
);
|
||||
}
|
||||
}}
|
||||
className="w-16 h-7 text-center text-sm bg-white/10 border-white/20 text-white rounded px-1 focus:ring-2 focus:ring-purple-400/50 focus:border-purple-400/50"
|
||||
placeholder="0"
|
||||
/>
|
||||
updateConsumableQuantity(
|
||||
product.id,
|
||||
clampedValue
|
||||
);
|
||||
}}
|
||||
onBlur={(e) => {
|
||||
// При потере фокуса, если поле пустое, устанавливаем 0
|
||||
if (e.target.value === "") {
|
||||
updateConsumableQuantity(
|
||||
product.id,
|
||||
0
|
||||
);
|
||||
}
|
||||
}}
|
||||
className="w-16 h-7 text-center text-sm bg-white/10 border-white/20 text-white rounded px-1 focus:ring-2 focus:ring-purple-400/50 focus:border-purple-400/50"
|
||||
placeholder="0"
|
||||
/>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() =>
|
||||
updateConsumableQuantity(
|
||||
product.id,
|
||||
Math.min(selectedQuantity + 1, availableStock, 99999)
|
||||
Math.min(
|
||||
selectedQuantity + 1,
|
||||
availableStock,
|
||||
99999
|
||||
)
|
||||
)
|
||||
}
|
||||
className={`h-6 w-6 p-0 rounded-full transition-all duration-300 ${
|
||||
selectedQuantity >= availableStock || availableStock <= 0
|
||||
? 'text-white/30 cursor-not-allowed'
|
||||
: 'text-white/60 hover:text-white hover:bg-white/20'
|
||||
selectedQuantity >=
|
||||
availableStock ||
|
||||
availableStock <= 0
|
||||
? "text-white/30 cursor-not-allowed"
|
||||
: "text-white/60 hover:text-white hover:bg-white/20"
|
||||
}`}
|
||||
disabled={selectedQuantity >= availableStock || availableStock <= 0}
|
||||
disabled={
|
||||
selectedQuantity >=
|
||||
availableStock ||
|
||||
availableStock <= 0
|
||||
}
|
||||
title={
|
||||
availableStock <= 0
|
||||
? 'Товар отсутствует на складе'
|
||||
: selectedQuantity >= availableStock
|
||||
? `Максимум доступно: ${availableStock}`
|
||||
: 'Увеличить количество'
|
||||
availableStock <= 0
|
||||
? "Товар отсутствует на складе"
|
||||
: selectedQuantity >=
|
||||
availableStock
|
||||
? `Максимум доступно: ${availableStock}`
|
||||
: "Увеличить количество"
|
||||
}
|
||||
>
|
||||
<Plus className="h-3 w-3" />
|
||||
@ -903,7 +977,9 @@ export function CreateFulfillmentConsumablesSupplyPage() {
|
||||
value={selectedLogistics?.id || ""}
|
||||
onChange={(e) => {
|
||||
const logisticsId = e.target.value;
|
||||
const logistics = logisticsPartners.find(p => p.id === logisticsId);
|
||||
const logistics = logisticsPartners.find(
|
||||
(p) => p.id === logisticsId
|
||||
);
|
||||
setSelectedLogistics(logistics || null);
|
||||
}}
|
||||
className="w-full bg-white/10 border border-white/20 rounded-md px-3 py-2 text-white text-sm focus:outline-none focus:ring-1 focus:ring-purple-500 focus:border-transparent appearance-none"
|
||||
@ -912,8 +988,8 @@ export function CreateFulfillmentConsumablesSupplyPage() {
|
||||
Выберите логистику
|
||||
</option>
|
||||
{logisticsPartners.map((partner) => (
|
||||
<option
|
||||
key={partner.id}
|
||||
<option
|
||||
key={partner.id}
|
||||
value={partner.id}
|
||||
className="bg-gray-800 text-white"
|
||||
>
|
||||
@ -922,8 +998,18 @@ export function CreateFulfillmentConsumablesSupplyPage() {
|
||||
))}
|
||||
</select>
|
||||
<div className="absolute inset-y-0 right-0 flex items-center px-2 pointer-events-none">
|
||||
<svg className="w-4 h-4 text-white/60" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 9l-7 7-7-7" />
|
||||
<svg
|
||||
className="w-4 h-4 text-white/60"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M19 9l-7 7-7-7"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
|
Reference in New Issue
Block a user