Files
sfera/src/components/supplies/direct-supply-creation.tsx

1786 lines
73 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 React, { useState, useEffect } from "react";
import { Card } from "@/components/ui/card";
import { Button } from "@/components/ui/button";
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 {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog";
import {
Popover,
PopoverContent,
PopoverTrigger,
} from "@/components/ui/popover";
import DatePicker from "react-datepicker";
import "react-datepicker/dist/react-datepicker.css";
import {
Search,
Plus,
Calendar as CalendarIcon,
Package,
Check,
X,
User,
Phone,
MapPin,
Building,
Truck,
} from "lucide-react";
import { WildberriesService } from "@/services/wildberries-service";
import { useAuth } from "@/hooks/useAuth";
import { useQuery, useMutation } from "@apollo/client";
import { apolloClient } from "@/lib/apollo-client";
import {
GET_MY_COUNTERPARTIES,
GET_COUNTERPARTY_SERVICES,
GET_COUNTERPARTY_SUPPLIES,
GET_SUPPLY_SUPPLIERS,
} from "@/graphql/queries";
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";
import { WildberriesCard } from "@/types/supplies";
// Добавляем CSS стили для line-clamp
const lineClampStyles = `
.line-clamp-2 {
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
}
`;
interface SupplyItem {
card: WildberriesCard;
quantity: number;
pricePerUnit: number;
totalPrice: number;
supplierId: string;
priceType: "perUnit" | "total"; // за штуку или за общее количество
}
interface Organization {
id: string;
name?: string;
fullName?: string;
type: string;
}
interface FulfillmentService {
id: string;
name: string;
description?: string;
price: number;
}
interface Supplier {
id: string;
name: string;
contactName: string;
phone: string;
market: string;
address: string;
place: string;
telegram: string;
}
interface DirectSupplyCreationProps {
onComplete: () => void;
onCreateSupply: () => void;
canCreateSupply: boolean;
isCreatingSupply: boolean;
onCanCreateSupplyChange?: (canCreate: boolean) => void;
selectedFulfillmentId?: string;
onServicesCostChange?: (cost: number) => void;
onItemsPriceChange?: (totalPrice: number) => void;
onItemsCountChange?: (hasItems: boolean) => void;
onConsumablesCostChange?: (cost: number) => void;
onVolumeChange?: (totalVolume: number) => void;
onSuppliersChange?: (suppliers: any[]) => void;
}
export function DirectSupplyCreation({
onComplete,
onCreateSupply,
canCreateSupply,
isCreatingSupply,
onCanCreateSupplyChange,
selectedFulfillmentId,
onServicesCostChange,
onItemsPriceChange,
onItemsCountChange,
onConsumablesCostChange,
onVolumeChange,
onSuppliersChange,
}: DirectSupplyCreationProps) {
const { user } = useAuth();
// Новые состояния для блока создания поставки
const [deliveryDate, setDeliveryDate] = useState<string>("");
const [selectedFulfillment, setSelectedFulfillment] = useState<string>("");
const [goodsQuantity, setGoodsQuantity] = useState<number>(1200);
const [goodsVolume, setGoodsVolume] = useState<number>(0);
const [cargoPlaces, setCargoPlaces] = useState<number>(0);
const [goodsPrice, setGoodsPrice] = useState<number>(0);
const [fulfillmentServicesPrice, setFulfillmentServicesPrice] =
useState<number>(0);
const [logisticsPrice, setLogisticsPrice] = useState<number>(0);
// Оригинальные состояния для товаров
const [searchTerm, setSearchTerm] = useState("");
const [loading, setLoading] = useState(false);
const [wbCards, setWbCards] = useState<WildberriesCard[]>([]);
const [supplyItems, setSupplyItems] = useState<SupplyItem[]>([]);
// Общие настройки (оригинальные)
const [deliveryDateOriginal, setDeliveryDateOriginal] = useState<
Date | undefined
>(undefined);
const [selectedFulfillmentOrg, setSelectedFulfillmentOrg] =
useState<string>("");
const [selectedServices, setSelectedServices] = useState<string[]>([]);
const [selectedConsumables, setSelectedConsumables] = useState<string[]>([]);
// Поставщики
const [suppliers, setSuppliers] = useState<Supplier[]>([]);
const [showSupplierModal, setShowSupplierModal] = useState(false);
const [newSupplier, setNewSupplier] = useState({
name: "",
contactName: "",
phone: "",
market: "",
address: "",
place: "",
telegram: "",
});
const [supplierErrors, setSupplierErrors] = useState({
name: "",
contactName: "",
phone: "",
telegram: "",
});
// Данные для фулфилмента
const [organizationServices, setOrganizationServices] = useState<{
[orgId: string]: FulfillmentService[];
}>({});
const [organizationSupplies, setOrganizationSupplies] = useState<{
[orgId: string]: FulfillmentService[];
}>({});
// Загружаем контрагентов-фулфилментов
const { data: counterpartiesData } = useQuery(GET_MY_COUNTERPARTIES);
const { data: suppliersData, refetch: refetchSuppliers } = useQuery(GET_SUPPLY_SUPPLIERS);
// Мутации
const [createSupply, { loading: creatingSupply }] = useMutation(
CREATE_WILDBERRIES_SUPPLY,
{
onCompleted: (data) => {
if (data.createWildberriesSupply.success) {
toast.success(data.createWildberriesSupply.message);
onComplete();
} else {
toast.error(data.createWildberriesSupply.message);
}
},
onError: (error) => {
toast.error("Ошибка при создании поставки");
console.error("Error creating supply:", error);
},
}
);
const [createSupplierMutation, { loading: creatingSupplier }] = useMutation(
CREATE_SUPPLY_SUPPLIER,
{
onCompleted: (data) => {
if (data.createSupplySupplier.success) {
toast.success("Поставщик добавлен успешно!");
// Обновляем список поставщиков из БД
refetchSuppliers();
// Очищаем форму
setNewSupplier({
name: "",
contactName: "",
phone: "",
market: "",
address: "",
place: "",
telegram: "",
});
setSupplierErrors({
name: "",
contactName: "",
phone: "",
telegram: "",
});
setShowSupplierModal(false);
} else {
toast.error(data.createSupplySupplier.message || "Ошибка при добавлении поставщика");
}
},
onError: (error) => {
toast.error("Ошибка при создании поставщика");
console.error("Error creating supplier:", error);
},
}
);
// Моковые данные товаров для демонстрации
const getMockCards = (): WildberriesCard[] => [
{
nmID: 123456789,
vendorCode: "SKU001",
title: "Платье летнее розовое",
description: "Легкое летнее платье из натурального хлопка",
brand: "Fashion",
object: "Платья",
parent: "Одежда",
countryProduction: "Россия",
supplierVendorCode: "SUPPLIER-001",
mediaFiles: ["/api/placeholder/400/400"],
dimensions: {
length: 30, // 30 см
width: 25, // 25 см
height: 5, // 5 см
weightBrutto: 0.3, // 300г
isValid: true
},
sizes: [
{
chrtID: 123456,
techSize: "M",
wbSize: "M Розовый",
price: 2500,
discountedPrice: 2000,
quantity: 50,
},
],
},
{
nmID: 987654321,
vendorCode: "SKU002",
title: "Платье черное вечернее",
description: "Элегантное вечернее платье для особых случаев",
brand: "Fashion",
object: "Платья",
parent: "Одежда",
countryProduction: "Россия",
supplierVendorCode: "SUPPLIER-002",
mediaFiles: ["/api/placeholder/400/403"],
dimensions: {
length: 35, // 35 см
width: 28, // 28 см
height: 6, // 6 см
weightBrutto: 0.4, // 400г
isValid: true
},
sizes: [
{
chrtID: 987654,
techSize: "M",
wbSize: "M Черный",
price: 3500,
discountedPrice: 3000,
quantity: 30,
},
],
},
{
nmID: 555666777,
vendorCode: "SKU003",
title: "Блузка белая офисная",
description: "Классическая белая блузка для офиса",
brand: "Office",
object: "Блузки",
parent: "Одежда",
countryProduction: "Турция",
supplierVendorCode: "SUPPLIER-003",
mediaFiles: ["/api/placeholder/400/405"],
sizes: [
{
chrtID: 555666,
techSize: "L",
wbSize: "L Белый",
price: 1800,
discountedPrice: 1500,
quantity: 40,
},
],
},
{
nmID: 444333222,
vendorCode: "SKU004",
title: "Джинсы женские синие",
description: "Классические женские джинсы прямого кроя",
brand: "Denim",
object: "Джинсы",
parent: "Одежда",
countryProduction: "Бангладеш",
supplierVendorCode: "SUPPLIER-004",
mediaFiles: ["/api/placeholder/400/408"],
sizes: [
{
chrtID: 444333,
techSize: "30",
wbSize: "30 Синий",
price: 2800,
discountedPrice: 2300,
quantity: 25,
},
],
},
{
nmID: 111222333,
vendorCode: "SKU005",
title: "Кроссовки женские белые",
description: "Удобные женские кроссовки для повседневной носки",
brand: "Sport",
object: "Кроссовки",
parent: "Обувь",
countryProduction: "Вьетнам",
supplierVendorCode: "SUPPLIER-005",
mediaFiles: ["/api/placeholder/400/410"],
sizes: [
{
chrtID: 111222,
techSize: "37",
wbSize: "37 Белый",
price: 3200,
discountedPrice: 2800,
quantity: 35,
},
],
},
{
nmID: 777888999,
vendorCode: "SKU006",
title: "Сумка женская черная",
description: "Стильная женская сумка из экокожи",
brand: "Accessories",
object: "Сумки",
parent: "Аксессуары",
countryProduction: "Китай",
supplierVendorCode: "SUPPLIER-006",
mediaFiles: ["/api/placeholder/400/411"],
sizes: [
{
chrtID: 777888,
techSize: "Универсальный",
wbSize: "Черный",
price: 1500,
discountedPrice: 1200,
quantity: 60,
},
],
},
];
// Загружаем товары при инициализации
useEffect(() => {
loadCards();
}, [user]);
// Загружаем услуги и расходники при выборе фулфилмента
useEffect(() => {
if (selectedFulfillmentId) {
console.log('Загружаем услуги и расходники для фулфилмента:', selectedFulfillmentId);
loadOrganizationServices(selectedFulfillmentId);
loadOrganizationSupplies(selectedFulfillmentId);
}
}, [selectedFulfillmentId]);
// Уведомляем об изменении стоимости услуг
useEffect(() => {
if (onServicesCostChange) {
const servicesCost = getServicesCost();
onServicesCostChange(servicesCost);
}
}, [selectedServices, selectedFulfillmentId, onServicesCostChange]);
// Уведомляем об изменении общей стоимости товаров
useEffect(() => {
if (onItemsPriceChange) {
const totalItemsPrice = getTotalItemsCost();
onItemsPriceChange(totalItemsPrice);
}
}, [supplyItems, onItemsPriceChange]);
// Уведомляем об изменении количества товаров
useEffect(() => {
if (onItemsCountChange) {
onItemsCountChange(supplyItems.length > 0);
}
}, [supplyItems.length, onItemsCountChange]);
// Уведомляем об изменении стоимости расходников
useEffect(() => {
if (onConsumablesCostChange) {
const consumablesCost = getConsumablesCost();
onConsumablesCostChange(consumablesCost);
}
}, [selectedConsumables, selectedFulfillmentId, supplyItems.length, onConsumablesCostChange]);
const loadCards = async () => {
setLoading(true);
try {
const wbApiKey = user?.organization?.apiKeys?.find(
(key) => key.marketplace === "WILDBERRIES"
);
if (wbApiKey?.isActive) {
const validationData = wbApiKey.validationData as Record<
string,
string
>;
const apiToken =
validationData?.token ||
validationData?.apiKey ||
validationData?.key ||
(wbApiKey as { apiKey?: string }).apiKey;
if (apiToken) {
console.log("Загружаем карточки из WB API...");
const cards = await WildberriesService.getAllCards(apiToken, 500);
// Логируем информацию о размерах товаров
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)} м³`);
} else {
console.log(`WB API: Карточка ${card.nmID} - размеры отсутствуют`);
}
});
setWbCards(cards);
console.log("Загружено карточек из WB API:", cards.length);
console.log("Карточки с размерами:", cards.filter(card => card.dimensions).length);
return;
}
}
// Если API ключ не настроен, показываем моковые данные
console.log("API ключ WB не настроен, показываем моковые данные");
setWbCards(getMockCards());
} catch (error) {
console.error("Ошибка загрузки карточек WB:", error);
// При ошибке API показываем моковые данные
setWbCards(getMockCards());
} finally {
setLoading(false);
}
};
const searchCards = async () => {
if (!searchTerm.trim()) {
loadCards();
return;
}
setLoading(true);
try {
const wbApiKey = user?.organization?.apiKeys?.find(
(key) => key.marketplace === "WILDBERRIES"
);
if (wbApiKey?.isActive) {
const validationData = wbApiKey.validationData as Record<
string,
string
>;
const apiToken =
validationData?.token ||
validationData?.apiKey ||
validationData?.key ||
(wbApiKey as { apiKey?: string }).apiKey;
if (apiToken) {
console.log("Поиск в WB API:", searchTerm);
const cards = await WildberriesService.searchCards(
apiToken,
searchTerm,
100
);
// Логируем информацию о размерах найденных товаров
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)} м³`);
} else {
console.log(`WB API: Найденная карточка ${card.nmID} - размеры отсутствуют`);
}
});
setWbCards(cards);
console.log("Найдено карточек в WB API:", cards.length);
console.log("Найденные карточки с размерами:", cards.filter(card => card.dimensions).length);
return;
}
}
// Если API ключ не настроен, ищем в моковых данных
console.log(
"API ключ WB не настроен, поиск в моковых данных:",
searchTerm
);
const mockCards = getMockCards();
const filteredCards = mockCards.filter(
(card) =>
card.title.toLowerCase().includes(searchTerm.toLowerCase()) ||
card.brand.toLowerCase().includes(searchTerm.toLowerCase()) ||
card.nmID.toString().includes(searchTerm.toLowerCase()) ||
card.object?.toLowerCase().includes(searchTerm.toLowerCase())
);
setWbCards(filteredCards);
console.log("Найдено моковых товаров:", filteredCards.length);
} catch (error) {
console.error("Ошибка поиска карточек WB:", error);
// При ошибке ищем в моковых данных
const mockCards = getMockCards();
const filteredCards = mockCards.filter(
(card) =>
card.title.toLowerCase().includes(searchTerm.toLowerCase()) ||
card.brand.toLowerCase().includes(searchTerm.toLowerCase()) ||
card.nmID.toString().includes(searchTerm.toLowerCase()) ||
card.object?.toLowerCase().includes(searchTerm.toLowerCase())
);
setWbCards(filteredCards);
console.log("Найдено моковых товаров (fallback):", filteredCards.length);
} finally {
setLoading(false);
}
};
// Функции для работы с услугами и расходниками
const loadOrganizationServices = async (organizationId: string) => {
if (organizationServices[organizationId]) return;
try {
const response = await apolloClient.query({
query: GET_COUNTERPARTY_SERVICES,
variables: { organizationId },
});
if (response.data?.counterpartyServices) {
setOrganizationServices((prev) => ({
...prev,
[organizationId]: response.data.counterpartyServices,
}));
}
} catch (error) {
console.error("Ошибка загрузки услуг организации:", error);
}
};
const loadOrganizationSupplies = async (organizationId: string) => {
if (organizationSupplies[organizationId]) return;
try {
const response = await apolloClient.query({
query: GET_COUNTERPARTY_SUPPLIES,
variables: { organizationId },
});
if (response.data?.counterpartySupplies) {
setOrganizationSupplies((prev) => ({
...prev,
[organizationId]: response.data.counterpartySupplies,
}));
}
} catch (error) {
console.error("Ошибка загрузки расходников организации:", error);
}
};
// Работа с товарами поставки
const addToSupply = (card: WildberriesCard) => {
const existingItem = supplyItems.find(
(item) => item.card.nmID === card.nmID
);
if (existingItem) {
toast.info("Товар уже добавлен в поставку");
return;
}
const newItem: SupplyItem = {
card,
quantity: 0,
pricePerUnit: 0,
totalPrice: 0,
supplierId: "",
priceType: "perUnit",
};
setSupplyItems((prev) => [...prev, newItem]);
toast.success("Товар добавлен в поставку");
};
const removeFromSupply = (nmID: number) => {
setSupplyItems((prev) => prev.filter((item) => item.card.nmID !== nmID));
};
const updateSupplyItem = (
nmID: number,
field: keyof SupplyItem,
value: string | number
) => {
setSupplyItems((prev) => {
const newItems = prev.map((item) => {
if (item.card.nmID === nmID) {
const updatedItem = { ...item, [field]: value };
// Пересчитываем totalPrice в зависимости от типа цены
if (field === "quantity" || field === "pricePerUnit" || field === "priceType") {
if (updatedItem.priceType === "perUnit") {
// Цена за штуку - умножаем на количество
updatedItem.totalPrice = updatedItem.quantity * updatedItem.pricePerUnit;
} else {
// Цена за общее количество - pricePerUnit становится общей ценой
updatedItem.totalPrice = updatedItem.pricePerUnit;
}
}
return updatedItem;
}
return item;
});
// Если изменился поставщик, уведомляем родительский компонент асинхронно
if (field === "supplierId" && onSuppliersChange) {
// Создаем список поставщиков с информацией о выборе
const suppliersInfo = suppliers.map(supplier => ({
...supplier,
selected: newItems.some(item => item.supplierId === supplier.id)
}));
console.log("Обновление поставщиков из updateSupplyItem:", suppliersInfo);
// Вызываем асинхронно чтобы не обновлять состояние во время рендера
setTimeout(() => {
onSuppliersChange(suppliersInfo);
}, 0);
}
return newItems;
});
};
// Валидация полей поставщика
const validateSupplierField = (field: string, value: string) => {
let error = "";
switch (field) {
case "name":
if (!value.trim()) error = "Название обязательно";
else if (value.length < 2) error = "Минимум 2 символа";
break;
case "contactName":
if (!value.trim()) error = "Имя обязательно";
else if (value.length < 2) error = "Минимум 2 символа";
break;
case "phone":
if (!value.trim()) error = "Телефон обязателен";
else if (!isValidPhone(value)) error = "Неверный формат телефона";
break;
case "telegram":
if (value && !value.match(/^@[a-zA-Z0-9_]{5,32}$/)) {
error = "Формат: @username (5-32 символа)";
}
break;
}
setSupplierErrors(prev => ({...prev, [field]: error}));
return error === "";
};
const validateAllSupplierFields = () => {
const nameValid = validateSupplierField("name", newSupplier.name);
const contactNameValid = validateSupplierField("contactName", newSupplier.contactName);
const phoneValid = validateSupplierField("phone", newSupplier.phone);
const telegramValid = validateSupplierField("telegram", newSupplier.telegram);
return nameValid && contactNameValid && phoneValid && telegramValid;
};
// Работа с поставщиками
const handleCreateSupplier = async () => {
if (!validateAllSupplierFields()) {
toast.error("Исправьте ошибки в форме");
return;
}
try {
await createSupplierMutation({
variables: {
input: {
name: newSupplier.name,
contactName: newSupplier.contactName,
phone: newSupplier.phone,
market: newSupplier.market || null,
address: newSupplier.address || null,
place: newSupplier.place || null,
telegram: newSupplier.telegram || null,
},
},
});
} catch (error) {
// Ошибка обрабатывается в onError мутации
}
};
// Расчеты для нового блока
const getTotalSum = () => {
return goodsPrice + fulfillmentServicesPrice + logisticsPrice;
};
// Оригинальные расчеты
const getTotalQuantity = () => {
return supplyItems.reduce((sum, item) => sum + item.quantity, 0);
};
// Функция для расчета объема одного товара в м³
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) {
return 0;
}
// Переводим из сантиметров в метры и рассчитываем объем
const volumeInM3 = (length / 100) * (width / 100) * (height / 100);
return volumeInM3;
};
// Функция для расчета общего объема всех товаров в поставке
const getTotalVolume = () => {
return supplyItems.reduce((totalVolume, item) => {
const itemVolume = calculateItemVolume(item.card);
return totalVolume + (itemVolume * item.quantity);
}, 0);
};
const getTotalItemsCost = () => {
return supplyItems.reduce((sum, item) => sum + item.totalPrice, 0);
};
const getServicesCost = () => {
if (!selectedFulfillmentId || selectedServices.length === 0) return 0;
const services = organizationServices[selectedFulfillmentId] || [];
return (
selectedServices.reduce((sum, serviceId) => {
const service = services.find((s) => s.id === serviceId);
return sum + (service ? service.price : 0);
}, 0) * getTotalQuantity()
);
};
const getConsumablesCost = () => {
if (!selectedFulfillmentId || selectedConsumables.length === 0) return 0;
const supplies = organizationSupplies[selectedFulfillmentId] || [];
return (
selectedConsumables.reduce((sum, supplyId) => {
const supply = supplies.find((s) => s.id === supplyId);
return sum + (supply ? supply.price : 0);
}, 0) * getTotalQuantity()
);
};
const formatCurrency = (amount: number) => {
return new Intl.NumberFormat("ru-RU", {
style: "currency",
currency: "RUB",
minimumFractionDigits: 0,
}).format(amount);
};
// Создание поставки
const handleCreateSupplyInternal = async () => {
if (supplyItems.length === 0) {
toast.error("Добавьте товары в поставку");
return;
}
if (!deliveryDateOriginal) {
toast.error("Выберите дату поставки");
return;
}
if (
supplyItems.some((item) => item.quantity <= 0 || item.pricePerUnit <= 0)
) {
toast.error("Укажите количество и цену для всех товаров");
return;
}
try {
const supplyInput = {
deliveryDate: deliveryDateOriginal.toISOString().split("T")[0],
cards: supplyItems.map((item) => ({
nmId: item.card.nmID.toString(),
vendorCode: item.card.vendorCode,
title: item.card.title,
brand: item.card.brand,
selectedQuantity: item.quantity,
customPrice: item.totalPrice,
selectedFulfillmentOrg: selectedFulfillmentOrg,
selectedFulfillmentServices: selectedServices,
selectedConsumableOrg: selectedFulfillmentOrg,
selectedConsumableServices: selectedConsumables,
deliveryDate: deliveryDateOriginal.toISOString().split("T")[0],
mediaFiles: item.card.mediaFiles,
})),
};
await createSupply({ variables: { input: supplyInput } });
toast.success("Поставка успешно создана!");
onComplete();
} catch (error) {
console.error("Error creating supply:", error);
toast.error("Ошибка при создании поставки");
}
};
// Обработка внешнего вызова создания поставки
React.useEffect(() => {
if (isCreatingSupply) {
handleCreateSupplyInternal();
}
}, [isCreatingSupply]);
// Уведомление об изменении объема товаров
React.useEffect(() => {
const totalVolume = getTotalVolume();
if (onVolumeChange) {
onVolumeChange(totalVolume);
}
}, [supplyItems, onVolumeChange]);
// Загрузка поставщиков из правильного источника
React.useEffect(() => {
if (suppliersData?.supplySuppliers) {
console.log("Загружаем поставщиков из БД:", suppliersData.supplySuppliers);
setSuppliers(suppliersData.supplySuppliers);
// Проверяем есть ли уже выбранные поставщики и уведомляем родителя
if (onSuppliersChange && supplyItems.length > 0) {
const suppliersInfo = suppliersData.supplySuppliers.map((supplier: any) => ({
...supplier,
selected: supplyItems.some(item => item.supplierId === supplier.id)
}));
if (suppliersInfo.some((s: any) => s.selected)) {
console.log("Найдены выбранные поставщики при загрузке:", suppliersInfo);
// Вызываем асинхронно чтобы не обновлять состояние во время рендера
setTimeout(() => {
onSuppliersChange(suppliersInfo);
}, 0);
}
}
}
}, [suppliersData]);
// Обновление статуса возможности создания поставки
React.useEffect(() => {
const canCreate =
supplyItems.length > 0 &&
deliveryDateOriginal !== null &&
supplyItems.every((item) => item.quantity > 0 && item.pricePerUnit > 0);
if (onCanCreateSupplyChange) {
onCanCreateSupplyChange(canCreate);
}
}, [supplyItems, deliveryDateOriginal, onCanCreateSupplyChange]);
const fulfillmentOrgs = (counterpartiesData?.myCounterparties || []).filter(
(org: Organization) => org.type === "FULFILLMENT"
);
const markets = [
{ value: "sadovod", label: "Садовод" },
{ value: "tyak-moscow", label: "ТЯК Москва" },
];
return (
<>
<style>{lineClampStyles}</style>
<div className="flex flex-col h-full space-y-2 w-full min-h-0">
{/* Элегантный блок поиска и товаров */}
<div className="relative">
{/* Главная карточка с градиентом */}
<div className="bg-gradient-to-br from-white/15 via-white/10 to-white/5 backdrop-blur-xl border border-white/20 rounded-2xl p-4 shadow-2xl">
{/* Компактный заголовок с поиском */}
<div className="flex items-center justify-between mb-2">
<div className="flex items-center space-x-3">
<div className="w-8 h-8 bg-gradient-to-r from-purple-500 to-blue-500 rounded-lg flex items-center justify-center shadow-lg">
<Search className="h-4 w-4 text-white" />
</div>
<div>
<h3 className="text-white font-semibold text-base">
Каталог товаров
</h3>
<p className="text-white/60 text-xs">
Найдено: {wbCards.length}
</p>
</div>
</div>
{/* Поиск в заголовке */}
<div className="flex items-center space-x-3 flex-1 max-w-md ml-4">
<div className="relative flex-1">
<Input
placeholder="Поиск товаров..."
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
className="pl-3 pr-16 py-2 bg-white/10 border border-white/20 rounded-lg text-white placeholder-white/50 focus:bg-white/15 focus:border-white/40 text-sm h-8"
onKeyPress={(e) => e.key === "Enter" && searchCards()}
/>
<Button
onClick={searchCards}
disabled={loading}
className="absolute right-1 top-1 h-6 px-2 bg-gradient-to-r from-purple-500 to-blue-500 hover:from-purple-600 hover:to-blue-600 text-white border-0 rounded text-xs"
>
{loading ? (
<div className="animate-spin rounded-full h-3 w-3 border border-white/30 border-t-white"></div>
) : (
"Найти"
)}
</Button>
</div>
</div>
{/* Статистика в поставке */}
{supplyItems.length > 0 && (
<div className="bg-gradient-to-r from-purple-500/20 to-blue-500/20 backdrop-blur border border-purple-400/30 rounded-lg px-3 py-1 ml-3">
<div className="flex items-center space-x-2">
<div className="w-1.5 h-1.5 bg-purple-400 rounded-full animate-pulse"></div>
<span className="text-purple-200 font-medium text-xs">
В поставке: {supplyItems.length}
</span>
</div>
</div>
)}
</div>
{/* Сетка товаров */}
<div className="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-6 xl:grid-cols-8 gap-3">
{loading ? (
// Красивые skeleton-карточки
[...Array(16)].map((_, i) => (
<div key={i} className="group">
<div className="aspect-[3/4] bg-gradient-to-br from-white/10 to-white/5 rounded-xl animate-pulse">
<div className="w-full h-full bg-white/5 rounded-xl"></div>
</div>
<div className="mt-1 px-1">
<div className="h-3 bg-white/10 rounded animate-pulse"></div>
</div>
</div>
))
) : wbCards.length > 0 ? (
// Красивые карточки товаров
wbCards.map((card) => {
const isInSupply = supplyItems.some(
(item) => item.card.nmID === card.nmID
);
return (
<div
key={card.nmID}
className={`group cursor-pointer transition-all duration-300 hover:scale-105 ${
isInSupply ? "scale-105" : ""
}`}
onClick={() => addToSupply(card)}
>
{/* Карточка товара */}
<div
className={`relative aspect-[3/4] rounded-xl overflow-hidden shadow-lg transition-all duration-300 ${
isInSupply
? "ring-2 ring-purple-400 shadow-purple-400/25 bg-gradient-to-br from-purple-500/20 to-blue-500/20"
: "bg-white/10 hover:bg-white/15 hover:shadow-xl"
}`}
>
<img
src={
WildberriesService.getCardImage(card, "c516x688") ||
"/api/placeholder/200/267"
}
alt={card.title}
className="w-full h-full object-cover transition-transform duration-500 group-hover:scale-110"
loading="lazy"
/>
{/* Градиентный оверлей */}
<div className="absolute inset-0 bg-gradient-to-t from-black/60 via-transparent to-transparent opacity-0 group-hover:opacity-100 transition-opacity duration-300" />
{/* Информация при наведении */}
<div className="absolute bottom-0 left-0 right-0 p-3 transform translate-y-full group-hover:translate-y-0 transition-transform duration-300">
<h4 className="text-white font-medium text-sm line-clamp-2 mb-1">
{card.title}
</h4>
<p className="text-white/80 text-xs">
WB: {card.nmID}
</p>
</div>
{/* Индикаторы */}
{isInSupply ? (
<div className="absolute top-3 right-3 w-8 h-8 bg-gradient-to-r from-purple-500 to-blue-500 rounded-full flex items-center justify-center shadow-lg">
<svg
className="w-4 h-4 text-white"
fill="currentColor"
viewBox="0 0 20 20"
>
<path
fillRule="evenodd"
d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z"
clipRule="evenodd"
/>
</svg>
</div>
) : (
<div className="absolute top-3 right-3 w-8 h-8 bg-white/20 backdrop-blur rounded-full flex items-center justify-center opacity-0 group-hover:opacity-100 transition-opacity duration-300">
<Plus className="w-4 h-4 text-white" />
</div>
)}
{/* Эффект при клике */}
<div className="absolute inset-0 bg-white/20 opacity-0 group-active:opacity-100 transition-opacity duration-150" />
</div>
{/* Название под карточкой */}
<div className="mt-1 px-1">
<h4 className="text-white/90 font-medium text-xs line-clamp-2 leading-tight">
{card.title}
</h4>
</div>
</div>
);
})
) : (
// Пустое состояние
<div className="col-span-full flex flex-col items-center justify-center py-8">
<div className="w-16 h-16 bg-gradient-to-r from-purple-500/20 to-blue-500/20 rounded-2xl flex items-center justify-center mb-3">
<Package className="w-8 h-8 text-white/40" />
</div>
<h3 className="text-white/80 font-medium text-base mb-1">
{searchTerm ? "Товары не найдены" : "Начните поиск товаров"}
</h3>
<p className="text-white/50 text-sm text-center max-w-md">
{searchTerm
? "Попробуйте изменить поисковый запрос"
: "Введите название товара в поле поиска"}
</p>
</div>
)}
</div>
</div>
{/* Декоративные элементы */}
<div className="absolute -top-1 -left-1 w-4 h-4 bg-gradient-to-r from-purple-500 to-blue-500 rounded-full opacity-60 animate-pulse" />
<div className="absolute -bottom-1 -right-1 w-3 h-3 bg-gradient-to-r from-blue-500 to-purple-500 rounded-full opacity-40 animate-pulse delay-700" />
</div>
{/* Услуги и расходники в одной строке */}
{selectedFulfillmentOrg && (
<Card className="bg-white/10 backdrop-blur border-white/20 p-2">
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 text-xs">
<div>
<div className="text-white/80 mb-1">Услуги фулфилмента:</div>
<div className="flex flex-wrap gap-1">
{organizationServices[selectedFulfillmentOrg] ? (
organizationServices[selectedFulfillmentOrg].map(
(service) => (
<label
key={service.id}
className="flex items-center space-x-1 cursor-pointer bg-white/5 rounded px-2 py-1 hover:bg-white/10"
>
<input
type="checkbox"
checked={selectedServices.includes(service.id)}
onChange={(e) => {
if (e.target.checked) {
setSelectedServices((prev) => [
...prev,
service.id,
]);
} else {
setSelectedServices((prev) =>
prev.filter((id) => id !== service.id)
);
}
}}
className="w-3 h-3"
/>
<span className="text-white text-xs">
{service.name} ({service.price})
</span>
</label>
)
)
) : (
<span className="text-white/60">Загрузка...</span>
)}
</div>
</div>
<div>
<div className="text-white/80 mb-1">Расходные материалы:</div>
<div className="flex flex-wrap gap-1">
{organizationSupplies[selectedFulfillmentOrg] ? (
organizationSupplies[selectedFulfillmentOrg].map(
(supply) => (
<label
key={supply.id}
className="flex items-center space-x-1 cursor-pointer bg-white/5 rounded px-2 py-1 hover:bg-white/10"
>
<input
type="checkbox"
checked={selectedConsumables.includes(supply.id)}
onChange={(e) => {
if (e.target.checked) {
setSelectedConsumables((prev) => [
...prev,
supply.id,
]);
} else {
setSelectedConsumables((prev) =>
prev.filter((id) => id !== supply.id)
);
}
}}
className="w-3 h-3"
/>
<span className="text-white text-xs">
{supply.name} ({supply.price})
</span>
</label>
)
)
) : (
<span className="text-white/60">Загрузка...</span>
)}
</div>
</div>
</div>
</Card>
)}
{/* Модуль товаров в поставке - растягивается до низа */}
<Card className="bg-white/10 backdrop-blur border-white/20 p-2 flex-1 flex flex-col min-h-0">
<div className="flex items-center justify-between mb-2 flex-shrink-0">
<span className="text-white font-medium text-sm">
Товары в поставке
</span>
{supplyItems.length > 0 && (
<span className="text-blue-400 text-xs font-medium bg-blue-500/20 px-2 py-1 rounded">
{getTotalVolume().toFixed(4)} м³
</span>
)}
</div>
{supplyItems.length === 0 ? (
<div className="flex-1 flex items-center justify-center">
<div className="text-center">
<Package className="h-8 w-8 text-white/20 mx-auto mb-2" />
<p className="text-white/60 text-xs">
Добавьте товары из карточек выше
</p>
</div>
</div>
) : (
<div className="flex-1 overflow-y-auto space-y-1">
{supplyItems.map((item) => (
<Card
key={item.card.nmID}
className="bg-white/5 border-white/10 p-1.5"
>
{/* Компактный заголовок товара */}
<div className="flex items-center justify-between mb-2">
<div className="flex flex-col space-y-1 min-w-0 flex-1">
<div className="text-white font-medium text-xs line-clamp-1 truncate">
{item.card.title}
</div>
<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-orange-400">| размеры не указаны</span>
)}
</div>
</div>
<Button
onClick={() => removeFromSupply(item.card.nmID)}
size="sm"
variant="ghost"
className="h-5 w-5 p-0 text-white/60 hover:text-red-400 flex-shrink-0"
>
<X className="h-3 w-3" />
</Button>
</div>
{/* Компактные названия блоков */}
<div className="grid grid-cols-8 gap-1 mb-1">
<div className="text-white/80 text-[9px] font-medium text-center">
Товар
</div>
<div className="text-white/80 text-[9px] font-medium text-center">
Параметры
</div>
<div className="text-white/80 text-[9px] font-medium text-center">
Заказать
</div>
<div className="text-white/80 text-[9px] font-medium text-center">
Цена
</div>
<div className="text-white/80 text-[9px] font-medium text-center">
Услуги фф
</div>
<div className="text-white/80 text-[9px] font-medium text-center">
Поставщик
</div>
<div className="text-white/80 text-[9px] font-medium text-center">
Расходники фф
</div>
<div className="text-white/80 text-[9px] font-medium text-center">
Расходники
</div>
</div>
{/* Компактная сетка блоков */}
<div className="grid grid-cols-8 gap-1">
{/* Блок 1: Картинка товара */}
<div className="bg-white/10 rounded-lg overflow-hidden relative h-20">
<img
src={
WildberriesService.getCardImage(
item.card,
"c246x328"
) || "/api/placeholder/60/60"
}
alt={item.card.title}
className="w-full h-full object-cover"
/>
</div>
{/* Блок 2: Параметры */}
<div className="bg-white/10 rounded-lg p-3 flex flex-col justify-center h-20">
<div className="flex flex-wrap gap-1 justify-center items-center">
{/* Создаем массив валидных параметров */}
{(() => {
const params = [];
// Бренд
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'
});
}
// Категория (объект)
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'
});
}
// Страна (только если не пустая и не 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'
});
}
// Цена WB
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'
});
}
// Внутренний артикул
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'
});
}
// НАМЕРЕННО НЕ ВКЛЮЧАЕМ 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`}>
{param.value}
</span>
));
})()}
</div>
</div>
{/* Блок 3: Заказать */}
<div className="bg-white/10 rounded-lg p-2 flex flex-col justify-center h-20">
<div className="text-white/60 text-xs mb-2 text-center">
Количество
</div>
<Input
type="number"
value={item.quantity}
onChange={(e) =>
updateSupplyItem(
item.card.nmID,
"quantity",
parseInt(e.target.value) || 0
)
}
className="bg-purple-500/20 border-purple-400/30 text-white text-center h-8 text-sm font-bold"
min="1"
/>
</div>
{/* Блок 4: Цена */}
<div className="bg-white/10 rounded-lg p-2 flex flex-col justify-center h-20">
{/* Переключатель типа цены */}
<div className="flex mb-1">
<button
onClick={() =>
updateSupplyItem(item.card.nmID, "priceType", "perUnit")
}
className={`text-[9px] px-1 py-0.5 rounded-l ${
item.priceType === "perUnit"
? "bg-blue-500 text-white"
: "bg-white/20 text-white/60"
}`}
>
За шт
</button>
<button
onClick={() =>
updateSupplyItem(item.card.nmID, "priceType", "total")
}
className={`text-[9px] px-1 py-0.5 rounded-r ${
item.priceType === "total"
? "bg-blue-500 text-white"
: "bg-white/20 text-white/60"
}`}
>
За все
</button>
</div>
<Input
type="number"
value={item.pricePerUnit || ""}
onChange={(e) =>
updateSupplyItem(
item.card.nmID,
"pricePerUnit",
parseFloat(e.target.value) || 0
)
}
className="bg-white/20 border-white/20 text-white text-center h-7 text-xs"
placeholder="₽"
/>
<div className="text-white/80 text-xs font-medium text-center mt-1">
Итого: {formatCurrency(item.totalPrice).replace(" ₽", "₽")}
</div>
</div>
{/* Блок 5: Услуги фулфилмента */}
<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:', {
selectedFulfillmentId,
hasServices: !!organizationServices[selectedFulfillmentId],
servicesCount: organizationServices[selectedFulfillmentId]?.length || 0,
allOrganizationServices: Object.keys(organizationServices)
})}
{selectedFulfillmentId &&
organizationServices[selectedFulfillmentId] ? (
organizationServices[selectedFulfillmentId]
.slice(0, 3)
.map((service) => (
<label
key={service.id}
className="flex items-center justify-between cursor-pointer text-xs"
>
<div className="flex items-center space-x-2">
<input
type="checkbox"
checked={selectedServices.includes(
service.id
)}
onChange={(e) => {
if (e.target.checked) {
setSelectedServices((prev) => [
...prev,
service.id,
]);
} else {
setSelectedServices((prev) =>
prev.filter((id) => id !== service.id)
);
}
}}
className="w-3 h-3"
/>
<span className="text-white text-[10px]">
{service.name.substring(0, 10)}
</span>
</div>
<span className="text-green-400 text-[10px] font-medium">
{service.price ? `${service.price}` : 'Бесплатно'}
</span>
</label>
))
) : (
<span className="text-white/60 text-xs text-center">
{selectedFulfillmentId ? 'Нет услуг' : 'Выберите фулфилмент'}
</span>
)}
</div>
</div>
{/* Блок 6: Поставщик */}
<div className="bg-white/10 rounded-lg p-2 flex flex-col justify-center h-20">
<div className="space-y-1">
<Select
value={item.supplierId}
onValueChange={(value) =>
updateSupplyItem(item.card.nmID, "supplierId", value)
}
>
<SelectTrigger className="bg-white/20 border-white/20 text-white h-6 text-xs">
<SelectValue placeholder="Выбрать" />
</SelectTrigger>
<SelectContent>
{suppliers.map((supplier) => (
<SelectItem key={supplier.id} value={supplier.id}>
{supplier.name}
</SelectItem>
))}
</SelectContent>
</Select>
{/* Компактная информация о выбранном поставщике */}
{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}
</div>
<div className="text-white/60 text-[9px] truncate">
{suppliers.find((s) => s.id === item.supplierId)?.phone}
</div>
</div>
) : (
<Button
onClick={() => setShowSupplierModal(true)}
variant="outline"
size="sm"
className="bg-white/5 border-white/20 text-white hover:bg-white/10 h-5 px-2 text-[10px] w-full"
>
<Plus className="h-2 w-2 mr-1" />
Добавить
</Button>
)}
</div>
</div>
{/* Блок 7: Расходники фф */}
<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:', {
selectedFulfillmentId,
hasConsumables: !!organizationSupplies[selectedFulfillmentId],
consumablesCount: organizationSupplies[selectedFulfillmentId]?.length || 0,
allOrganizationSupplies: Object.keys(organizationSupplies)
})}
{selectedFulfillmentId &&
organizationSupplies[selectedFulfillmentId] ? (
organizationSupplies[selectedFulfillmentId]
.slice(0, 3)
.map((supply) => (
<label
key={supply.id}
className="flex items-center justify-between cursor-pointer text-xs"
>
<div className="flex items-center space-x-2">
<input
type="checkbox"
checked={selectedConsumables.includes(
supply.id
)}
onChange={(e) => {
if (e.target.checked) {
setSelectedConsumables((prev) => [
...prev,
supply.id,
]);
} else {
setSelectedConsumables((prev) =>
prev.filter((id) => id !== supply.id)
);
}
}}
className="w-3 h-3"
/>
<span className="text-white text-[10px]">
{supply.name.substring(0, 10)}
</span>
</div>
<span className="text-orange-400 text-[10px] font-medium">
{supply.price ? `${supply.price}` : 'Бесплатно'}
</span>
</label>
))
) : (
<span className="text-white/60 text-xs text-center">
{selectedFulfillmentId ? 'Нет расходников' : 'Выберите фулфилмент'}
</span>
)}
</div>
</div>
{/* Блок 8: Расходники селлера */}
<div className="bg-white/10 rounded-lg p-2 flex flex-col justify-center h-20">
<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>
</label>
<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>
</label>
<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>
</label>
</div>
</div>
</div>
</Card>
))}
</div>
)}
</Card>
{/* Модальное окно создания поставщика */}
<Dialog open={showSupplierModal} onOpenChange={setShowSupplierModal}>
<DialogContent className="glass-card border-white/10 max-w-md">
<DialogHeader>
<DialogTitle className="text-white">
Добавить поставщика
</DialogTitle>
<p className="text-white/60 text-xs">
Контактная информация поставщика для этой поставки
</p>
</DialogHeader>
<div className="space-y-3">
<div className="grid grid-cols-2 gap-3">
<div>
<Label className="text-white/60 text-xs">Название *</Label>
<Input
value={newSupplier.name}
onChange={(e) => {
const value = formatNameInput(e.target.value);
setNewSupplier((prev) => ({
...prev,
name: value,
}));
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' : ''
}`}
placeholder="Название"
/>
{supplierErrors.name && (
<p className="text-red-400 text-xs mt-1">{supplierErrors.name}</p>
)}
</div>
<div>
<Label className="text-white/60 text-xs">Имя *</Label>
<Input
value={newSupplier.contactName}
onChange={(e) => {
const value = formatNameInput(e.target.value);
setNewSupplier((prev) => ({
...prev,
contactName: value,
}));
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' : ''
}`}
placeholder="Имя"
/>
{supplierErrors.contactName && (
<p className="text-red-400 text-xs mt-1">{supplierErrors.contactName}</p>
)}
</div>
</div>
<div className="grid grid-cols-2 gap-3">
<div>
<Label className="text-white/60 text-xs">Телефон *</Label>
<PhoneInput
value={newSupplier.phone}
onChange={(value) => {
setNewSupplier((prev) => ({
...prev,
phone: value,
}));
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' : ''
}`}
placeholder="+7 (999) 123-45-67"
/>
{supplierErrors.phone && (
<p className="text-red-400 text-xs mt-1">{supplierErrors.phone}</p>
)}
</div>
<div>
<Label className="text-white/60 text-xs">Рынок</Label>
<Select
value={newSupplier.market}
onValueChange={(value) =>
setNewSupplier((prev) => ({ ...prev, market: value }))
}
>
<SelectTrigger className="bg-white/10 border-white/20 text-white h-8 text-xs">
<SelectValue placeholder="Рынок" />
</SelectTrigger>
<SelectContent>
{markets.map((market) => (
<SelectItem key={market.value} value={market.value}>
{market.label}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
</div>
<div className="grid grid-cols-2 gap-3">
<div>
<Label className="text-white/60 text-xs">Адрес</Label>
<Input
value={newSupplier.address}
onChange={(e) =>
setNewSupplier((prev) => ({
...prev,
address: e.target.value,
}))
}
className="bg-white/10 border-white/20 text-white h-8 text-xs"
placeholder="Адрес"
/>
</div>
<div>
<Label className="text-white/60 text-xs">Место</Label>
<Input
value={newSupplier.place}
onChange={(e) =>
setNewSupplier((prev) => ({
...prev,
place: e.target.value,
}))
}
className="bg-white/10 border-white/20 text-white h-8 text-xs"
placeholder="Павильон/место"
/>
</div>
</div>
<div>
<Label className="text-white/60 text-xs">Телеграм</Label>
<Input
value={newSupplier.telegram}
onChange={(e) => {
const value = e.target.value;
setNewSupplier((prev) => ({
...prev,
telegram: value,
}));
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' : ''
}`}
placeholder="@username"
/>
{supplierErrors.telegram && (
<p className="text-red-400 text-xs mt-1">{supplierErrors.telegram}</p>
)}
</div>
<div className="flex space-x-2">
<Button
onClick={() => setShowSupplierModal(false)}
variant="outline"
className="flex-1 bg-white/5 border-white/20 text-white hover:bg-white/10 h-8 text-xs"
>
Отмена
</Button>
<Button
onClick={handleCreateSupplier}
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 ? (
<div className="flex items-center space-x-2">
<div className="animate-spin rounded-full h-3 w-3 border border-white/30 border-t-white"></div>
<span>Добавление...</span>
</div>
) : (
'Добавить'
)}
</Button>
</div>
</div>
</DialogContent>
</Dialog>
</div>
</>
);
}