1786 lines
73 KiB
TypeScript
1786 lines
73 KiB
TypeScript
"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>
|
||
</>
|
||
);
|
||
}
|