Обновлены категории товаров с "Упаковка" на "Расходники" в различных компонентах и моделях. Добавлены уведомления о непринятых поставках и обновлены соответствующие GraphQL запросы и резолверы для поддержки новых данных. Оптимизирована логика отображения и обработки данных в интерфейсе.

This commit is contained in:
Veronika Smirnova
2025-07-28 13:19:19 +03:00
parent a1d1fcdd43
commit ac67b1e1ec
17 changed files with 1542 additions and 512 deletions

View File

@ -7,7 +7,11 @@ import { Input } from "@/components/ui/input";
import { Badge } from "@/components/ui/badge";
import { Label } from "@/components/ui/label";
import { PhoneInput } from "@/components/ui/phone-input";
import { formatPhoneInput, isValidPhone, formatNameInput } from "@/lib/input-masks";
import {
formatPhoneInput,
isValidPhone,
formatNameInput,
} from "@/lib/input-masks";
import {
Select,
SelectContent,
@ -51,7 +55,10 @@ import {
GET_COUNTERPARTY_SUPPLIES,
GET_SUPPLY_SUPPLIERS,
} from "@/graphql/queries";
import { CREATE_WILDBERRIES_SUPPLY, CREATE_SUPPLY_SUPPLIER } from "@/graphql/mutations";
import {
CREATE_WILDBERRIES_SUPPLY,
CREATE_SUPPLY_SUPPLIER,
} from "@/graphql/mutations";
import { toast } from "sonner";
import { format } from "date-fns";
import { ru } from "date-fns/locale";
@ -187,7 +194,8 @@ export function DirectSupplyCreation({
// Загружаем контрагентов-фулфилментов
const { data: counterpartiesData } = useQuery(GET_MY_COUNTERPARTIES);
const { data: suppliersData, refetch: refetchSuppliers } = useQuery(GET_SUPPLY_SUPPLIERS);
const { data: suppliersData, refetch: refetchSuppliers } =
useQuery(GET_SUPPLY_SUPPLIERS);
// Мутации
const [createSupply, { loading: creatingSupply }] = useMutation(
@ -207,17 +215,17 @@ export function DirectSupplyCreation({
},
}
);
const [createSupplierMutation, { loading: creatingSupplier }] = useMutation(
CREATE_SUPPLY_SUPPLIER,
{
onCompleted: (data) => {
if (data.createSupplySupplier.success) {
toast.success("Поставщик добавлен успешно!");
// Обновляем список поставщиков из БД
refetchSuppliers();
// Очищаем форму
setNewSupplier({
name: "",
@ -236,7 +244,10 @@ export function DirectSupplyCreation({
});
setShowSupplierModal(false);
} else {
toast.error(data.createSupplySupplier.message || "Ошибка при добавлении поставщика");
toast.error(
data.createSupplySupplier.message ||
"Ошибка при добавлении поставщика"
);
}
},
onError: (error) => {
@ -260,11 +271,11 @@ export function DirectSupplyCreation({
supplierVendorCode: "SUPPLIER-001",
mediaFiles: ["/api/placeholder/400/400"],
dimensions: {
length: 30, // 30 см
width: 25, // 25 см
height: 5, // 5 см
weightBrutto: 0.3, // 300г
isValid: true
length: 30, // 30 см
width: 25, // 25 см
height: 5, // 5 см
weightBrutto: 0.3, // 300г
isValid: true,
},
sizes: [
{
@ -289,11 +300,11 @@ export function DirectSupplyCreation({
supplierVendorCode: "SUPPLIER-002",
mediaFiles: ["/api/placeholder/400/403"],
dimensions: {
length: 35, // 35 см
width: 28, // 28 см
height: 6, // 6 см
weightBrutto: 0.4, // 400г
isValid: true
length: 35, // 35 см
width: 28, // 28 см
height: 6, // 6 см
weightBrutto: 0.4, // 400г
isValid: true,
},
sizes: [
{
@ -404,7 +415,10 @@ export function DirectSupplyCreation({
// Загружаем услуги и расходники при выборе фулфилмента
useEffect(() => {
if (selectedFulfillmentId) {
console.log('Загружаем услуги и расходники для фулфилмента:', selectedFulfillmentId);
console.log(
"Загружаем услуги и расходники для фулфилмента:",
selectedFulfillmentId
);
loadOrganizationServices(selectedFulfillmentId);
loadOrganizationSupplies(selectedFulfillmentId);
}
@ -439,7 +453,12 @@ export function DirectSupplyCreation({
const consumablesCost = getConsumablesCost();
onConsumablesCostChange(consumablesCost);
}
}, [selectedConsumables, selectedFulfillmentId, supplyItems.length, onConsumablesCostChange]);
}, [
selectedConsumables,
selectedFulfillmentId,
supplyItems.length,
onConsumablesCostChange,
]);
const loadCards = async () => {
setLoading(true);
@ -462,20 +481,34 @@ export function DirectSupplyCreation({
if (apiToken) {
console.log("Загружаем карточки из WB API...");
const cards = await WildberriesService.getAllCards(apiToken, 500);
// Логируем информацию о размерах товаров
cards.forEach(card => {
cards.forEach((card) => {
if (card.dimensions) {
const volume = (card.dimensions.length / 100) * (card.dimensions.width / 100) * (card.dimensions.height / 100);
console.log(`WB API: Карточка ${card.nmID} - размеры: ${card.dimensions.length}x${card.dimensions.width}x${card.dimensions.height} см, объем: ${volume.toFixed(6)} м³`);
const volume =
(card.dimensions.length / 100) *
(card.dimensions.width / 100) *
(card.dimensions.height / 100);
console.log(
`WB API: Карточка ${card.nmID} - размеры: ${
card.dimensions.length
}x${card.dimensions.width}x${
card.dimensions.height
} см, объем: ${volume.toFixed(6)} м³`
);
} else {
console.log(`WB API: Карточка ${card.nmID} - размеры отсутствуют`);
console.log(
`WB API: Карточка ${card.nmID} - размеры отсутствуют`
);
}
});
setWbCards(cards);
console.log("Загружено карточек из WB API:", cards.length);
console.log("Карточки с размерами:", cards.filter(card => card.dimensions).length);
console.log(
"Карточки с размерами:",
cards.filter((card) => card.dimensions).length
);
return;
}
}
@ -522,20 +555,34 @@ export function DirectSupplyCreation({
searchTerm,
100
);
// Логируем информацию о размерах найденных товаров
cards.forEach(card => {
cards.forEach((card) => {
if (card.dimensions) {
const volume = (card.dimensions.length / 100) * (card.dimensions.width / 100) * (card.dimensions.height / 100);
console.log(`WB API: Найденная карточка ${card.nmID} - размеры: ${card.dimensions.length}x${card.dimensions.width}x${card.dimensions.height} см, объем: ${volume.toFixed(6)} м³`);
const volume =
(card.dimensions.length / 100) *
(card.dimensions.width / 100) *
(card.dimensions.height / 100);
console.log(
`WB API: Найденная карточка ${card.nmID} - размеры: ${
card.dimensions.length
}x${card.dimensions.width}x${
card.dimensions.height
} см, объем: ${volume.toFixed(6)} м³`
);
} else {
console.log(`WB API: Найденная карточка ${card.nmID} - размеры отсутствуют`);
console.log(
`WB API: Найденная карточка ${card.nmID} - размеры отсутствуют`
);
}
});
setWbCards(cards);
console.log("Найдено карточек в WB API:", cards.length);
console.log("Найденные карточки с размерами:", cards.filter(card => card.dimensions).length);
console.log(
"Найденные карточки с размерами:",
cards.filter((card) => card.dimensions).length
);
return;
}
}
@ -650,12 +697,17 @@ export function DirectSupplyCreation({
const newItems = prev.map((item) => {
if (item.card.nmID === nmID) {
const updatedItem = { ...item, [field]: value };
// Пересчитываем totalPrice в зависимости от типа цены
if (field === "quantity" || field === "pricePerUnit" || field === "priceType") {
if (
field === "quantity" ||
field === "pricePerUnit" ||
field === "priceType"
) {
if (updatedItem.priceType === "perUnit") {
// Цена за штуку - умножаем на количество
updatedItem.totalPrice = updatedItem.quantity * updatedItem.pricePerUnit;
updatedItem.totalPrice =
updatedItem.quantity * updatedItem.pricePerUnit;
} else {
// Цена за общее количество - pricePerUnit становится общей ценой
updatedItem.totalPrice = updatedItem.pricePerUnit;
@ -669,13 +721,16 @@ export function DirectSupplyCreation({
// Если изменился поставщик, уведомляем родительский компонент асинхронно
if (field === "supplierId" && onSuppliersChange) {
// Создаем список поставщиков с информацией о выборе
const suppliersInfo = suppliers.map(supplier => ({
const suppliersInfo = suppliers.map((supplier) => ({
...supplier,
selected: newItems.some(item => item.supplierId === supplier.id)
selected: newItems.some((item) => item.supplierId === supplier.id),
}));
console.log("Обновление поставщиков из updateSupplyItem:", suppliersInfo);
console.log(
"Обновление поставщиков из updateSupplyItem:",
suppliersInfo
);
// Вызываем асинхронно чтобы не обновлять состояние во время рендера
setTimeout(() => {
onSuppliersChange(suppliersInfo);
@ -708,16 +763,22 @@ export function DirectSupplyCreation({
}
break;
}
setSupplierErrors(prev => ({...prev, [field]: error}));
setSupplierErrors((prev) => ({ ...prev, [field]: error }));
return error === "";
};
const validateAllSupplierFields = () => {
const nameValid = validateSupplierField("name", newSupplier.name);
const contactNameValid = validateSupplierField("contactName", newSupplier.contactName);
const contactNameValid = validateSupplierField(
"contactName",
newSupplier.contactName
);
const phoneValid = validateSupplierField("phone", newSupplier.phone);
const telegramValid = validateSupplierField("telegram", newSupplier.telegram);
const telegramValid = validateSupplierField(
"telegram",
newSupplier.telegram
);
return nameValid && contactNameValid && phoneValid && telegramValid;
};
@ -760,17 +821,24 @@ export function DirectSupplyCreation({
// Функция для расчета объема одного товара в м³
const calculateItemVolume = (card: WildberriesCard): number => {
if (!card.dimensions) return 0;
const { length, width, height } = card.dimensions;
// Проверяем что все размеры указаны и больше 0
if (!length || !width || !height || length <= 0 || width <= 0 || height <= 0) {
if (
!length ||
!width ||
!height ||
length <= 0 ||
width <= 0 ||
height <= 0
) {
return 0;
}
// Переводим из сантиметров в метры и рассчитываем объем
const volumeInM3 = (length / 100) * (width / 100) * (height / 100);
return volumeInM3;
};
@ -778,7 +846,7 @@ export function DirectSupplyCreation({
const getTotalVolume = () => {
return supplyItems.reduce((totalVolume, item) => {
const itemVolume = calculateItemVolume(item.card);
return totalVolume + (itemVolume * item.quantity);
return totalVolume + itemVolume * item.quantity;
}, 0);
};
@ -883,19 +951,29 @@ export function DirectSupplyCreation({
// Загрузка поставщиков из правильного источника
React.useEffect(() => {
if (suppliersData?.supplySuppliers) {
console.log("Загружаем поставщиков из БД:", suppliersData.supplySuppliers);
console.log(
"Загружаем поставщиков из БД:",
suppliersData.supplySuppliers
);
setSuppliers(suppliersData.supplySuppliers);
// Проверяем есть ли уже выбранные поставщики и уведомляем родителя
if (onSuppliersChange && supplyItems.length > 0) {
const suppliersInfo = suppliersData.supplySuppliers.map((supplier: { id: string; selected?: boolean }) => ({
...supplier,
selected: supplyItems.some(item => item.supplierId === supplier.id)
}));
const suppliersInfo = suppliersData.supplySuppliers.map(
(supplier: { id: string; selected?: boolean }) => ({
...supplier,
selected: supplyItems.some(
(item) => item.supplierId === supplier.id
),
})
);
if (suppliersInfo.some((s: { selected?: boolean }) => s.selected)) {
console.log("Найдены выбранные поставщики при загрузке:", suppliersInfo);
console.log(
"Найдены выбранные поставщики при загрузке:",
suppliersInfo
);
// Вызываем асинхронно чтобы не обновлять состояние во время рендера
setTimeout(() => {
onSuppliersChange(suppliersInfo);
@ -929,8 +1007,6 @@ export function DirectSupplyCreation({
<>
<style>{lineClampStyles}</style>
<div className="flex flex-col h-full space-y-2 w-full min-h-0">
{/* Элегантный блок поиска и товаров */}
<div className="relative">
{/* Главная карточка с градиентом */}
@ -1228,9 +1304,17 @@ export function DirectSupplyCreation({
<div className="text-white/60 text-[10px] flex space-x-2">
<span>WB: {item.card.nmID}</span>
{calculateItemVolume(item.card) > 0 ? (
<span className="text-blue-400">| {(calculateItemVolume(item.card) * item.quantity).toFixed(4)} м³</span>
<span className="text-blue-400">
|{" "}
{(
calculateItemVolume(item.card) * item.quantity
).toFixed(4)}{" "}
м³
</span>
) : (
<span className="text-orange-400">| размеры не указаны</span>
<span className="text-orange-400">
| размеры не указаны
</span>
)}
</div>
</div>
@ -1294,56 +1378,78 @@ export function DirectSupplyCreation({
{/* Создаем массив валидных параметров */}
{(() => {
const params = [];
// Бренд
if (item.card.brand && item.card.brand.trim() && item.card.brand !== '0') {
if (
item.card.brand &&
item.card.brand.trim() &&
item.card.brand !== "0"
) {
params.push({
value: item.card.brand,
color: 'bg-blue-500/80',
key: 'brand'
color: "bg-blue-500/80",
key: "brand",
});
}
// Категория (объект)
if (item.card.object && item.card.object.trim() && item.card.object !== '0') {
if (
item.card.object &&
item.card.object.trim() &&
item.card.object !== "0"
) {
params.push({
value: item.card.object,
color: 'bg-green-500/80',
key: 'object'
color: "bg-green-500/80",
key: "object",
});
}
// Страна (только если не пустая и не 0)
if (item.card.countryProduction && item.card.countryProduction.trim() && item.card.countryProduction !== '0') {
if (
item.card.countryProduction &&
item.card.countryProduction.trim() &&
item.card.countryProduction !== "0"
) {
params.push({
value: item.card.countryProduction,
color: 'bg-purple-500/80',
key: 'country'
color: "bg-purple-500/80",
key: "country",
});
}
// Цена WB
if (item.card.sizes?.[0]?.price && item.card.sizes[0].price > 0) {
if (
item.card.sizes?.[0]?.price &&
item.card.sizes[0].price > 0
) {
params.push({
value: formatCurrency(item.card.sizes[0].price),
color: 'bg-yellow-500/80',
key: 'price'
color: "bg-yellow-500/80",
key: "price",
});
}
// Внутренний артикул
if (item.card.vendorCode && item.card.vendorCode.trim() && item.card.vendorCode !== '0') {
if (
item.card.vendorCode &&
item.card.vendorCode.trim() &&
item.card.vendorCode !== "0"
) {
params.push({
value: item.card.vendorCode,
color: 'bg-gray-500/80',
key: 'vendor'
color: "bg-gray-500/80",
key: "vendor",
});
}
// НАМЕРЕННО НЕ ВКЛЮЧАЕМ techSize и wbSize так как они равны '0'
return params.map(param => (
<span key={param.key} className={`${param.color} text-white text-[9px] px-2 py-1 rounded font-medium`}>
return params.map((param) => (
<span
key={param.key}
className={`${param.color} text-white text-[9px] px-2 py-1 rounded font-medium`}
>
{param.value}
</span>
));
@ -1377,7 +1483,11 @@ export function DirectSupplyCreation({
<div className="flex mb-1">
<button
onClick={() =>
updateSupplyItem(item.card.nmID, "priceType", "perUnit")
updateSupplyItem(
item.card.nmID,
"priceType",
"perUnit"
)
}
className={`text-[9px] px-1 py-0.5 rounded-l ${
item.priceType === "perUnit"
@ -1389,7 +1499,11 @@ export function DirectSupplyCreation({
</button>
<button
onClick={() =>
updateSupplyItem(item.card.nmID, "priceType", "total")
updateSupplyItem(
item.card.nmID,
"priceType",
"total"
)
}
className={`text-[9px] px-1 py-0.5 rounded-r ${
item.priceType === "total"
@ -1400,7 +1514,7 @@ export function DirectSupplyCreation({
За все
</button>
</div>
<Input
type="number"
value={item.pricePerUnit || ""}
@ -1415,7 +1529,8 @@ export function DirectSupplyCreation({
placeholder="₽"
/>
<div className="text-white/80 text-xs font-medium text-center mt-1">
Итого: {formatCurrency(item.totalPrice).replace(" ₽", "₽")}
Итого:{" "}
{formatCurrency(item.totalPrice).replace(" ₽", "₽")}
</div>
</div>
@ -1423,11 +1538,15 @@ export function DirectSupplyCreation({
<div className="bg-white/10 rounded-lg p-2 flex flex-col justify-center h-20">
<div className="space-y-1 max-h-16 overflow-y-auto">
{/* DEBUG */}
{console.log('DEBUG SERVICES:', {
{console.log("DEBUG SERVICES:", {
selectedFulfillmentId,
hasServices: !!organizationServices[selectedFulfillmentId],
servicesCount: organizationServices[selectedFulfillmentId]?.length || 0,
allOrganizationServices: Object.keys(organizationServices)
hasServices:
!!organizationServices[selectedFulfillmentId],
servicesCount:
organizationServices[selectedFulfillmentId]
?.length || 0,
allOrganizationServices:
Object.keys(organizationServices),
})}
{selectedFulfillmentId &&
organizationServices[selectedFulfillmentId] ? (
@ -1463,13 +1582,17 @@ export function DirectSupplyCreation({
</span>
</div>
<span className="text-green-400 text-[10px] font-medium">
{service.price ? `${service.price}` : 'Бесплатно'}
{service.price
? `${service.price}`
: "Бесплатно"}
</span>
</label>
))
) : (
<span className="text-white/60 text-xs text-center">
{selectedFulfillmentId ? 'Нет услуг' : 'Выберите фулфилмент'}
{selectedFulfillmentId
? "Нет услуг"
: "Выберите фулфилмент"}
</span>
)}
</div>
@ -1481,7 +1604,11 @@ export function DirectSupplyCreation({
<Select
value={item.supplierId}
onValueChange={(value) =>
updateSupplyItem(item.card.nmID, "supplierId", value)
updateSupplyItem(
item.card.nmID,
"supplierId",
value
)
}
>
<SelectTrigger className="bg-white/20 border-white/20 text-white h-6 text-xs">
@ -1497,13 +1624,20 @@ export function DirectSupplyCreation({
</Select>
{/* Компактная информация о выбранном поставщике */}
{item.supplierId && suppliers.find((s) => s.id === item.supplierId) ? (
{item.supplierId &&
suppliers.find((s) => s.id === item.supplierId) ? (
<div className="text-center">
<div className="text-white/80 text-[10px] font-medium truncate">
{suppliers.find((s) => s.id === item.supplierId)?.contactName}
{
suppliers.find((s) => s.id === item.supplierId)
?.contactName
}
</div>
<div className="text-white/60 text-[9px] truncate">
{suppliers.find((s) => s.id === item.supplierId)?.phone}
{
suppliers.find((s) => s.id === item.supplierId)
?.phone
}
</div>
</div>
) : (
@ -1524,11 +1658,15 @@ export function DirectSupplyCreation({
<div className="bg-white/10 rounded-lg p-2 flex flex-col justify-center h-20">
<div className="space-y-1 max-h-16 overflow-y-auto">
{/* DEBUG для расходников */}
{console.log('DEBUG CONSUMABLES:', {
{console.log("DEBUG CONSUMABLES:", {
selectedFulfillmentId,
hasConsumables: !!organizationSupplies[selectedFulfillmentId],
consumablesCount: organizationSupplies[selectedFulfillmentId]?.length || 0,
allOrganizationSupplies: Object.keys(organizationSupplies)
hasConsumables:
!!organizationSupplies[selectedFulfillmentId],
consumablesCount:
organizationSupplies[selectedFulfillmentId]
?.length || 0,
allOrganizationSupplies:
Object.keys(organizationSupplies),
})}
{selectedFulfillmentId &&
organizationSupplies[selectedFulfillmentId] ? (
@ -1564,13 +1702,17 @@ export function DirectSupplyCreation({
</span>
</div>
<span className="text-orange-400 text-[10px] font-medium">
{supply.price ? `${supply.price}` : 'Бесплатно'}
{supply.price
? `${supply.price}`
: "Бесплатно"}
</span>
</label>
))
) : (
<span className="text-white/60 text-xs text-center">
{selectedFulfillmentId ? 'Нет расходников' : 'Выберите фулфилмент'}
{selectedFulfillmentId
? "Нет расходников"
: "Выберите фулфилмент"}
</span>
)}
</div>
@ -1581,7 +1723,7 @@ export function DirectSupplyCreation({
<div className="space-y-2">
<label className="flex items-center space-x-2 cursor-pointer">
<input type="checkbox" className="w-3 h-3" />
<span className="text-white text-xs">Упаковка</span>
<span className="text-white text-xs">Расходники</span>
</label>
<label className="flex items-center space-x-2 cursor-pointer">
<input type="checkbox" className="w-3 h-3" />
@ -1626,12 +1768,16 @@ export function DirectSupplyCreation({
validateSupplierField("name", value);
}}
className={`bg-white/10 border-white/20 text-white h-8 text-xs ${
supplierErrors.name ? 'border-red-400 focus:border-red-400' : ''
supplierErrors.name
? "border-red-400 focus:border-red-400"
: ""
}`}
placeholder="Название"
/>
{supplierErrors.name && (
<p className="text-red-400 text-xs mt-1">{supplierErrors.name}</p>
<p className="text-red-400 text-xs mt-1">
{supplierErrors.name}
</p>
)}
</div>
<div>
@ -1647,12 +1793,16 @@ export function DirectSupplyCreation({
validateSupplierField("contactName", value);
}}
className={`bg-white/10 border-white/20 text-white h-8 text-xs ${
supplierErrors.contactName ? 'border-red-400 focus:border-red-400' : ''
supplierErrors.contactName
? "border-red-400 focus:border-red-400"
: ""
}`}
placeholder="Имя"
/>
{supplierErrors.contactName && (
<p className="text-red-400 text-xs mt-1">{supplierErrors.contactName}</p>
<p className="text-red-400 text-xs mt-1">
{supplierErrors.contactName}
</p>
)}
</div>
</div>
@ -1670,12 +1820,16 @@ export function DirectSupplyCreation({
validateSupplierField("phone", value);
}}
className={`bg-white/10 border-white/20 text-white h-8 text-xs ${
supplierErrors.phone ? 'border-red-400 focus:border-red-400' : ''
supplierErrors.phone
? "border-red-400 focus:border-red-400"
: ""
}`}
placeholder="+7 (999) 123-45-67"
/>
{supplierErrors.phone && (
<p className="text-red-400 text-xs mt-1">{supplierErrors.phone}</p>
<p className="text-red-400 text-xs mt-1">
{supplierErrors.phone}
</p>
)}
</div>
<div>
@ -1744,12 +1898,16 @@ export function DirectSupplyCreation({
validateSupplierField("telegram", value);
}}
className={`bg-white/10 border-white/20 text-white h-8 text-xs ${
supplierErrors.telegram ? 'border-red-400 focus:border-red-400' : ''
supplierErrors.telegram
? "border-red-400 focus:border-red-400"
: ""
}`}
placeholder="@username"
/>
{supplierErrors.telegram && (
<p className="text-red-400 text-xs mt-1">{supplierErrors.telegram}</p>
<p className="text-red-400 text-xs mt-1">
{supplierErrors.telegram}
</p>
)}
</div>
@ -1763,7 +1921,15 @@ export function DirectSupplyCreation({
</Button>
<Button
onClick={handleCreateSupplier}
disabled={!newSupplier.name || !newSupplier.contactName || !newSupplier.phone || Object.values(supplierErrors).some(error => error !== "") || creatingSupplier}
disabled={
!newSupplier.name ||
!newSupplier.contactName ||
!newSupplier.phone ||
Object.values(supplierErrors).some(
(error) => error !== ""
) ||
creatingSupplier
}
className="flex-1 bg-gradient-to-r from-purple-500 to-pink-500 hover:from-purple-600 hover:to-pink-600 disabled:opacity-50 disabled:cursor-not-allowed h-8 text-xs"
>
{creatingSupplier ? (
@ -1772,7 +1938,7 @@ export function DirectSupplyCreation({
<span>Добавление...</span>
</div>
) : (
'Добавить'
"Добавить"
)}
</Button>
</div>