Compare commits

...

2 Commits

2 changed files with 1263 additions and 690 deletions

View File

@ -172,8 +172,8 @@ export function CreateSupplyPage() {
return (
<div className="h-screen flex overflow-hidden">
<Sidebar />
<main className={`flex-1 ${getSidebarMargin()} px-4 py-3 overflow-hidden transition-all duration-300`}>
<div className="p-4">
<main className={`flex-1 ${getSidebarMargin()} px-4 py-3 overflow-y-auto transition-all duration-300`}>
<div className="p-4 min-h-full">
<TabsHeader
activeTab={activeTab}
onTabChange={setActiveTab}

View File

@ -1,351 +1,486 @@
"use client"
"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 { 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,
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 {
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
} 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 } from '@/graphql/queries'
import { CREATE_WILDBERRIES_SUPPLY } from '@/graphql/mutations'
import { toast } from 'sonner'
import { format } from 'date-fns'
import { ru } from 'date-fns/locale'
import { WildberriesCard } from '@/types/supplies'
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,
} from "@/graphql/queries";
import { CREATE_WILDBERRIES_SUPPLY } 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
card: WildberriesCard;
quantity: number;
pricePerUnit: number;
totalPrice: number;
supplierId: string;
}
interface Organization {
id: string
name?: string
fullName?: string
type: string
id: string;
name?: string;
fullName?: string;
type: string;
}
interface FulfillmentService {
id: string
name: string
description?: string
price: number
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
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
onComplete: () => void;
onCreateSupply: () => void;
canCreateSupply: boolean;
isCreatingSupply: boolean;
onCanCreateSupplyChange?: (canCreate: boolean) => void;
}
export function DirectSupplyCreation({ onComplete, onCreateSupply, canCreateSupply, isCreatingSupply, onCanCreateSupplyChange }: DirectSupplyCreationProps) {
const { user } = useAuth()
// Состояние для товаров
const [searchTerm, setSearchTerm] = useState('')
const [loading, setLoading] = useState(false)
const [wbCards, setWbCards] = useState<WildberriesCard[]>([])
const [supplyItems, setSupplyItems] = useState<SupplyItem[]>([])
// Общие настройки
const [deliveryDate, setDeliveryDate] = useState<Date | undefined>(undefined)
const [selectedFulfillmentOrg, setSelectedFulfillmentOrg] = useState<string>('')
const [selectedServices, setSelectedServices] = useState<string[]>([])
const [selectedConsumables, setSelectedConsumables] = useState<string[]>([])
export function DirectSupplyCreation({
onComplete,
onCreateSupply,
canCreateSupply,
isCreatingSupply,
onCanCreateSupplyChange,
}: 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 [suppliers, setSuppliers] = useState<Supplier[]>([]);
const [showSupplierModal, setShowSupplierModal] = useState(false);
const [newSupplier, setNewSupplier] = useState({
name: '',
contactName: '',
phone: '',
market: '',
address: '',
place: '',
telegram: ''
})
name: "",
contactName: "",
phone: "",
market: "",
address: "",
place: "",
telegram: "",
});
// Данные для фулфилмента
const [organizationServices, setOrganizationServices] = useState<{[orgId: string]: FulfillmentService[]}>({})
const [organizationSupplies, setOrganizationSupplies] = useState<{[orgId: string]: FulfillmentService[]}>({})
const [organizationServices, setOrganizationServices] = useState<{
[orgId: string]: FulfillmentService[];
}>({});
const [organizationSupplies, setOrganizationSupplies] = useState<{
[orgId: string]: FulfillmentService[];
}>({});
// Загружаем контрагентов-фулфилментов
const { data: counterpartiesData } = useQuery(GET_MY_COUNTERPARTIES)
const { data: counterpartiesData } = useQuery(GET_MY_COUNTERPARTIES);
// Мутация для создания поставки
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 [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 getMockCards = (): WildberriesCard[] => [
{
nmID: 123456789,
vendorCode: 'SKU001',
title: 'Платье летнее розовое',
description: 'Легкое летнее платье из натурального хлопка',
brand: 'Fashion',
object: 'Платья',
parent: 'Одежда',
countryProduction: 'Россия',
supplierVendorCode: 'SUPPLIER-001',
mediaFiles: ['/api/placeholder/400/400'],
sizes: [{ chrtID: 123456, techSize: 'M', wbSize: 'M Розовый', price: 2500, discountedPrice: 2000, quantity: 50 }]
vendorCode: "SKU001",
title: "Платье летнее розовое",
description: "Легкое летнее платье из натурального хлопка",
brand: "Fashion",
object: "Платья",
parent: "Одежда",
countryProduction: "Россия",
supplierVendorCode: "SUPPLIER-001",
mediaFiles: ["/api/placeholder/400/400"],
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'],
sizes: [{ chrtID: 987654, techSize: 'M', wbSize: 'M Черный', price: 3500, discountedPrice: 3000, quantity: 30 }]
vendorCode: "SKU002",
title: "Платье черное вечернее",
description: "Элегантное вечернее платье для особых случаев",
brand: "Fashion",
object: "Платья",
parent: "Одежда",
countryProduction: "Россия",
supplierVendorCode: "SUPPLIER-002",
mediaFiles: ["/api/placeholder/400/403"],
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 }]
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 }]
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 }]
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 }]
}
]
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])
loadCards();
}, [user]);
const loadCards = async () => {
setLoading(true)
setLoading(true);
try {
const wbApiKey = user?.organization?.apiKeys?.find(key => key.marketplace === 'WILDBERRIES')
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
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, 20)
setWbCards(cards)
console.log('Загружено карточек из WB API:', cards.length)
return
console.log("Загружаем карточки из WB API...");
const cards = await WildberriesService.getAllCards(apiToken, 20);
setWbCards(cards);
console.log("Загружено карточек из WB API:", cards.length);
return;
}
}
// Если API ключ не настроен, показываем моковые данные
console.log('API ключ WB не настроен, показываем моковые данные')
setWbCards(getMockCards())
console.log("API ключ WB не настроен, показываем моковые данные");
setWbCards(getMockCards());
} catch (error) {
console.error('Ошибка загрузки карточек WB:', error)
console.error("Ошибка загрузки карточек WB:", error);
// При ошибке API показываем моковые данные
setWbCards(getMockCards())
setWbCards(getMockCards());
} finally {
setLoading(false)
setLoading(false);
}
}
};
const searchCards = async () => {
if (!searchTerm.trim()) {
loadCards()
return
loadCards();
return;
}
setLoading(true)
setLoading(true);
try {
const wbApiKey = user?.organization?.apiKeys?.find(key => key.marketplace === 'WILDBERRIES')
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
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, 20)
setWbCards(cards)
console.log('Найдено карточек в WB API:', cards.length)
return
console.log("Поиск в WB API:", searchTerm);
const cards = await WildberriesService.searchCards(
apiToken,
searchTerm,
20
);
setWbCards(cards);
console.log("Найдено карточек в WB API:", cards.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)
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)
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)
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)
setLoading(false);
}
}
};
// Функции для работы с услугами и расходниками
const loadOrganizationServices = async (organizationId: string) => {
if (organizationServices[organizationId]) return
if (organizationServices[organizationId]) return;
try {
const response = await apolloClient.query({
query: GET_COUNTERPARTY_SERVICES,
variables: { organizationId }
})
variables: { organizationId },
});
if (response.data?.counterpartyServices) {
setOrganizationServices(prev => ({
setOrganizationServices((prev) => ({
...prev,
[organizationId]: response.data.counterpartyServices
}))
[organizationId]: response.data.counterpartyServices,
}));
}
} catch (error) {
console.error('Ошибка загрузки услуг организации:', error)
console.error("Ошибка загрузки услуг организации:", error);
}
}
};
const loadOrganizationSupplies = async (organizationId: string) => {
if (organizationSupplies[organizationId]) return
if (organizationSupplies[organizationId]) return;
try {
const response = await apolloClient.query({
query: GET_COUNTERPARTY_SUPPLIES,
variables: { organizationId }
})
variables: { organizationId },
});
if (response.data?.counterpartySupplies) {
setOrganizationSupplies(prev => ({
setOrganizationSupplies((prev) => ({
...prev,
[organizationId]: response.data.counterpartySupplies
}))
[organizationId]: response.data.counterpartySupplies,
}));
}
} catch (error) {
console.error('Ошибка загрузки расходников организации:', error)
console.error("Ошибка загрузки расходников организации:", error);
}
}
};
// Работа с товарами поставки
const addToSupply = (card: WildberriesCard) => {
const existingItem = supplyItems.find(item => item.card.nmID === card.nmID)
const existingItem = supplyItems.find(
(item) => item.card.nmID === card.nmID
);
if (existingItem) {
toast.info('Товар уже добавлен в поставку')
return
toast.info("Товар уже добавлен в поставку");
return;
}
const newItem: SupplyItem = {
@ -353,114 +488,132 @@ export function DirectSupplyCreation({ onComplete, onCreateSupply, canCreateSupp
quantity: 1200,
pricePerUnit: 0,
totalPrice: 0,
supplierId: ''
}
supplierId: "",
};
setSupplyItems(prev => [...prev, newItem])
toast.success('Товар добавлен в поставку')
}
setSupplyItems((prev) => [...prev, newItem]);
toast.success("Товар добавлен в поставку");
};
const removeFromSupply = (nmID: number) => {
setSupplyItems(prev => prev.filter(item => item.card.nmID !== nmID))
}
setSupplyItems((prev) => prev.filter((item) => item.card.nmID !== nmID));
};
const updateSupplyItem = (nmID: number, field: keyof SupplyItem, value: string | number) => {
setSupplyItems(prev => prev.map(item => {
if (item.card.nmID === nmID) {
const updatedItem = { ...item, [field]: value }
if (field === 'quantity' || field === 'pricePerUnit') {
updatedItem.totalPrice = updatedItem.quantity * updatedItem.pricePerUnit
const updateSupplyItem = (
nmID: number,
field: keyof SupplyItem,
value: string | number
) => {
setSupplyItems((prev) =>
prev.map((item) => {
if (item.card.nmID === nmID) {
const updatedItem = { ...item, [field]: value };
if (field === "quantity" || field === "pricePerUnit") {
updatedItem.totalPrice =
updatedItem.quantity * updatedItem.pricePerUnit;
}
return updatedItem;
}
return updatedItem
}
return item
}))
}
return item;
})
);
};
// Работа с поставщиками
const handleCreateSupplier = () => {
if (!newSupplier.name || !newSupplier.contactName || !newSupplier.phone) {
toast.error('Заполните обязательные поля')
return
toast.error("Заполните обязательные поля");
return;
}
const supplier: Supplier = {
id: Date.now().toString(),
...newSupplier
}
...newSupplier,
};
setSuppliers(prev => [...prev, supplier])
setSuppliers((prev) => [...prev, supplier]);
setNewSupplier({
name: '',
contactName: '',
phone: '',
market: '',
address: '',
place: '',
telegram: ''
})
setShowSupplierModal(false)
toast.success('Поставщик создан')
}
name: "",
contactName: "",
phone: "",
market: "",
address: "",
place: "",
telegram: "",
});
setShowSupplierModal(false);
toast.success("Поставщик создан");
};
// Расчеты
// Расчеты для нового блока
const getTotalSum = () => {
return goodsPrice + fulfillmentServicesPrice + logisticsPrice;
};
// Оригинальные расчеты
const getTotalQuantity = () => {
return supplyItems.reduce((sum, item) => sum + item.quantity, 0)
}
return supplyItems.reduce((sum, item) => sum + item.quantity, 0);
};
const getTotalItemsCost = () => {
return supplyItems.reduce((sum, item) => sum + item.totalPrice, 0)
}
return supplyItems.reduce((sum, item) => sum + item.totalPrice, 0);
};
const getServicesCost = () => {
if (!selectedFulfillmentOrg || selectedServices.length === 0) return 0
const services = organizationServices[selectedFulfillmentOrg] || []
return selectedServices.reduce((sum, serviceId) => {
const service = services.find(s => s.id === serviceId)
return sum + (service ? service.price : 0)
}, 0) * getTotalQuantity()
}
if (!selectedFulfillmentOrg || selectedServices.length === 0) return 0;
const services = organizationServices[selectedFulfillmentOrg] || [];
return (
selectedServices.reduce((sum, serviceId) => {
const service = services.find((s) => s.id === serviceId);
return sum + (service ? service.price : 0);
}, 0) * getTotalQuantity()
);
};
const getConsumablesCost = () => {
if (!selectedFulfillmentOrg || selectedConsumables.length === 0) return 0
const supplies = organizationSupplies[selectedFulfillmentOrg] || []
return selectedConsumables.reduce((sum, supplyId) => {
const supply = supplies.find(s => s.id === supplyId)
return sum + (supply ? supply.price : 0)
}, 0) * getTotalQuantity()
}
if (!selectedFulfillmentOrg || selectedConsumables.length === 0) return 0;
const supplies = organizationSupplies[selectedFulfillmentOrg] || [];
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)
}
return new Intl.NumberFormat("ru-RU", {
style: "currency",
currency: "RUB",
minimumFractionDigits: 0,
}).format(amount);
};
// Создание поставки
const handleCreateSupplyInternal = async () => {
if (supplyItems.length === 0) {
toast.error('Добавьте товары в поставку')
return
toast.error("Добавьте товары в поставку");
return;
}
if (!deliveryDate) {
toast.error('Выберите дату поставки')
return
if (!deliveryDateOriginal) {
toast.error("Выберите дату поставки");
return;
}
if (supplyItems.some(item => item.quantity <= 0 || item.pricePerUnit <= 0)) {
toast.error('Укажите количество и цену для всех товаров')
return
if (
supplyItems.some((item) => item.quantity <= 0 || item.pricePerUnit <= 0)
) {
toast.error("Укажите количество и цену для всех товаров");
return;
}
try {
const supplyInput = {
deliveryDate: deliveryDate.toISOString().split('T')[0],
cards: supplyItems.map(item => ({
deliveryDate: deliveryDateOriginal.toISOString().split("T")[0],
cards: supplyItems.map((item) => ({
nmId: item.card.nmID.toString(),
vendorCode: item.card.vendorCode,
title: item.card.title,
@ -471,426 +624,846 @@ export function DirectSupplyCreation({ onComplete, onCreateSupply, canCreateSupp
selectedFulfillmentServices: selectedServices,
selectedConsumableOrg: selectedFulfillmentOrg,
selectedConsumableServices: selectedConsumables,
deliveryDate: deliveryDate.toISOString().split('T')[0],
mediaFiles: item.card.mediaFiles
}))
}
deliveryDate: deliveryDateOriginal.toISOString().split("T")[0],
mediaFiles: item.card.mediaFiles,
})),
};
await createSupply({ variables: { input: supplyInput } })
toast.success('Поставка успешно создана!')
onComplete()
await createSupply({ variables: { input: supplyInput } });
toast.success("Поставка успешно создана!");
onComplete();
} catch (error) {
console.error('Error creating supply:', error)
toast.error('Ошибка при создании поставки')
console.error("Error creating supply:", error);
toast.error("Ошибка при создании поставки");
}
}
};
// Обработка внешнего вызова создания поставки
React.useEffect(() => {
if (isCreatingSupply) {
handleCreateSupplyInternal()
handleCreateSupplyInternal();
}
}, [isCreatingSupply])
}, [isCreatingSupply]);
// Обновление статуса возможности создания поставки
React.useEffect(() => {
const canCreate = supplyItems.length > 0 &&
deliveryDate !== null &&
supplyItems.every(item => item.quantity > 0 && item.pricePerUnit > 0)
if (onCanCreateSupplyChange) {
onCanCreateSupplyChange(canCreate)
}
}, [supplyItems, deliveryDate, onCanCreateSupplyChange])
const canCreate =
supplyItems.length > 0 &&
deliveryDateOriginal !== null &&
supplyItems.every((item) => item.quantity > 0 && item.pricePerUnit > 0);
const fulfillmentOrgs = (counterpartiesData?.myCounterparties || []).filter((org: Organization) => org.type === 'FULFILLMENT')
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: 'ТЯК Москва' }
]
{ value: "sadovod", label: "Садовод" },
{ value: "tyak-moscow", label: "ТЯК Москва" },
];
return (
<div className="space-y-3">
{/* Основные настройки */}
<Card className="bg-gradient-to-r from-purple-500 to-pink-500 p-3">
<div className="grid grid-cols-2 md:grid-cols-6 gap-3 text-xs">
{/* Дата поставки */}
<div>
<Popover>
<PopoverTrigger asChild>
<button className="w-full bg-white/20 rounded px-2 py-1 text-white hover:bg-white/30 transition-colors text-xs">
<CalendarIcon className="h-3 w-3 inline mr-1" />
{deliveryDate ? format(deliveryDate, "dd.MM") : "Дата"}
</button>
</PopoverTrigger>
<PopoverContent className="w-auto p-0">
<DatePicker
selected={deliveryDate}
onChange={(date: Date | null) => setDeliveryDate(date || undefined)}
minDate={new Date()}
inline
locale="ru"
/>
</PopoverContent>
</Popover>
</div>
{/* Фулфилмент */}
<div className="md:col-span-2">
<Select
value={selectedFulfillmentOrg}
onValueChange={(value) => {
setSelectedFulfillmentOrg(value)
setSelectedServices([])
setSelectedConsumables([])
if (value) {
loadOrganizationServices(value)
loadOrganizationSupplies(value)
}
}}
>
<SelectTrigger className="bg-white/20 border-0 text-white h-7 text-xs">
<SelectValue placeholder="Фулфилмент" />
</SelectTrigger>
<SelectContent>
{fulfillmentOrgs.map((org: Organization) => (
<SelectItem key={org.id} value={org.id}>
{org.name || org.fullName}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
{/* Показатели */}
<div className="text-center">
<div className="text-white/80">Товаров</div>
<div className="font-bold text-white">{getTotalQuantity()}</div>
</div>
<div className="text-center">
<div className="text-white/80">Стоимость</div>
<div className="font-bold text-white">{formatCurrency(getTotalItemsCost()).replace(' ₽', '₽')}</div>
</div>
<div className="text-center">
<div className="text-white/80">Услуги ФФ</div>
<div className="font-bold text-white">{formatCurrency(getServicesCost() + getConsumablesCost()).replace(' ₽', '₽')}</div>
</div>
</div>
</Card>
{/* Поиск и карточки */}
<Card className="bg-white/10 backdrop-blur border-white/20 p-2">
<div className="flex items-center space-x-2 mb-2">
<Input
placeholder="Поиск товаров..."
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
className="bg-white/5 border-white/20 text-white placeholder-white/50 h-7 text-xs flex-1"
onKeyPress={(e) => e.key === 'Enter' && searchCards()}
/>
<Button onClick={searchCards} disabled={loading} variant="secondary" size="sm" className="h-7 px-2 text-xs">
<Search className="h-3 w-3" />
</Button>
</div>
<div className="flex space-x-2 overflow-x-auto pb-1">
{loading ? (
[...Array(6)].map((_, i) => (
<div key={i} className="flex-shrink-0 w-16 h-20 bg-white/5 rounded animate-pulse"></div>
))
) : (
wbCards.map((card) => {
const isInSupply = supplyItems.some(item => item.card.nmID === card.nmID)
return (
<div
key={card.nmID}
className={`flex-shrink-0 w-16 cursor-pointer transition-all hover:scale-105 relative ${isInSupply ? 'ring-1 ring-purple-400' : ''}`}
onClick={() => addToSupply(card)}
>
<img
src={WildberriesService.getCardImage(card, 'c246x328') || '/api/placeholder/64/80'}
alt={card.title}
className="w-16 h-20 rounded object-cover"
/>
{isInSupply && (
<div className="absolute -top-1 -right-1 bg-purple-500 text-white rounded-full w-3 h-3 flex items-center justify-center text-[8px]">
</div>
)}
</div>
)
})
)}
</div>
</Card>
{/* Услуги и расходники в одной строке */}
{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">
<>
<style>{lineClampStyles}</style>
<div className="space-y-3 w-full">
{/* НОВЫЙ БЛОК СОЗДАНИЯ ПОСТАВКИ */}
<Card className="bg-white/10 backdrop-blur-xl border border-white/20 p-3">
{/* Первая строка */}
<div className="grid grid-cols-1 md:grid-cols-4 gap-3 items-end mb-2">
{/* 1. Модуль выбора даты */}
<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>
)}
<Label className="text-white/80 text-xs mb-1 block flex items-center gap-1">
<CalendarIcon className="h-3 w-3" />
Дата
</Label>
<div className="relative">
<input
type="date"
value={deliveryDate}
onChange={(e) => setDeliveryDate(e.target.value)}
className="w-full h-8 rounded-lg border-0 bg-white/20 backdrop-blur px-2 py-1 text-white placeholder:text-white/50 focus:bg-white/30 focus:outline-none focus:ring-1 focus:ring-white/20 text-xs font-medium"
min={new Date().toISOString().split("T")[0]}
/>
</div>
</div>
{/* 2. Модуль выбора фулфилмента */}
<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>
)}
<Label className="text-white/80 text-xs mb-1 block flex items-center gap-1">
<Building className="h-3 w-3" />
Фулфилмент
</Label>
<Select
value={selectedFulfillment}
onValueChange={setSelectedFulfillment}
>
<SelectTrigger className="h-8 bg-white/20 border-0 text-white focus:bg-white/30 focus:ring-1 focus:ring-white/20 text-xs">
<SelectValue placeholder="ФУЛФИЛМЕНТ ИВАНОВО" />
</SelectTrigger>
<SelectContent>
{fulfillmentOrgs.map((org: Organization) => (
<SelectItem key={org.id} value={org.id}>
{org.name || org.fullName}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
{/* 3. Объём товаров */}
<div>
<Label className="text-white/80 text-xs mb-1 block">
Объём товаров
</Label>
<Input
type="number"
value={goodsVolume || ""}
onChange={(e) =>
setGoodsVolume(parseFloat(e.target.value) || 0)
}
placeholder="м³"
className="h-8 bg-white/20 border-0 text-white placeholder:text-white/50 focus:bg-white/30 focus:ring-1 focus:ring-white/20 text-xs"
/>
</div>
{/* 4. Грузовые места */}
<div>
<Label className="text-white/80 text-xs mb-1 block">
Грузовые места
</Label>
<Input
type="number"
value={cargoPlaces || ""}
onChange={(e) => setCargoPlaces(parseInt(e.target.value) || 0)}
placeholder="шт"
className="h-8 bg-white/20 border-0 text-white placeholder:text-white/50 focus:bg-white/30 focus:ring-1 focus:ring-white/20 text-xs"
/>
</div>
</div>
{/* Вторая строка */}
<div className="grid grid-cols-1 md:grid-cols-4 gap-3 items-end">
{/* 5. Цена товаров */}
<div>
<Label className="text-white/80 text-xs mb-1 block">
Цена товаров
</Label>
<Input
type="number"
value={goodsPrice || ""}
onChange={(e) => setGoodsPrice(parseFloat(e.target.value) || 0)}
placeholder="₽"
className="h-8 bg-white/20 border-0 text-white placeholder:text-white/50 focus:bg-white/30 focus:ring-1 focus:ring-white/20 text-xs"
/>
</div>
{/* 6. Цена услуг фулфилмента */}
<div>
<Label className="text-white/80 text-xs mb-1 block">
Цена услуг фулфилмент
</Label>
<Input
type="number"
value={fulfillmentServicesPrice || ""}
onChange={(e) =>
setFulfillmentServicesPrice(parseFloat(e.target.value) || 0)
}
placeholder="₽"
className="h-8 bg-white/20 border-0 text-white placeholder:text-white/50 focus:bg-white/30 focus:ring-1 focus:ring-white/20 text-xs"
/>
</div>
{/* 7. Цена логистики */}
<div>
<Label className="text-white/80 text-xs mb-1 block">
Логистика до фулфилмента
</Label>
<Input
type="number"
value={logisticsPrice || ""}
onChange={(e) =>
setLogisticsPrice(parseFloat(e.target.value) || 0)
}
placeholder="₽"
className="h-8 bg-white/20 border-0 text-white placeholder:text-white/50 focus:bg-white/30 focus:ring-1 focus:ring-white/20 text-xs"
/>
</div>
{/* 8. Итоговая сумма */}
<div>
<Label className="text-white/80 text-xs mb-1 block">Итого</Label>
<div className="h-8 bg-white/10 rounded-lg flex items-center justify-center">
<span className="text-white font-bold text-sm">
{formatCurrency(getTotalSum()).replace(" ₽", " ₽")}
</span>
</div>
</div>
</div>
</Card>
)}
{/* Компактная таблица товаров */}
<Card className="bg-white/10 backdrop-blur border-white/20 p-2">
<div className="flex items-center justify-between mb-2">
<span className="text-white font-medium text-sm">Товары в поставке</span>
<Button
onClick={() => setShowSupplierModal(true)}
variant="outline"
size="sm"
className="bg-white/5 border-white/20 text-white hover:bg-white/10 h-6 px-2 text-xs"
>
<Plus className="h-3 w-3 mr-1" />
Поставщик
</Button>
</div>
{supplyItems.length === 0 ? (
<div className="text-center py-4">
<Package className="h-6 w-6 text-white/20 mx-auto mb-1" />
<p className="text-white/60 text-xs">Добавьте товары из карточек выше</p>
{/* Блок поиска товаров - оптимизированное расположение */}
<Card className="bg-white/10 backdrop-blur-xl border border-white/20 p-3">
<div className="mb-1">
<Label className="text-white/80 text-xs mb-2 block flex items-center gap-1">
<Search className="h-3 w-3" />
Поиск товаров Wildberries
</Label>
<div className="flex items-center space-x-2">
<Input
placeholder="Введите название товара, артикул или бренд..."
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
className="bg-white/20 border-0 text-white placeholder-white/60 h-8 text-xs flex-1 focus:bg-white/30 focus:ring-1 focus:ring-white/20"
onKeyPress={(e) => e.key === "Enter" && searchCards()}
/>
<Button
onClick={searchCards}
disabled={loading}
className="h-8 px-4 bg-white/20 hover:bg-white/30 border-0 text-white text-xs font-medium backdrop-blur"
>
{loading ? (
<div className="animate-spin rounded-full h-3 w-3 border-b-2 border-white"></div>
) : (
<>
<Search className="h-3 w-3 mr-1" />
Поиск
</>
)}
</Button>
</div>
</div>
) : (
{/* Карточки товаров - увеличенный размер и единообразность */}
<div className="space-y-1">
{supplyItems.map((item) => (
<div key={item.card.nmID} className="grid grid-cols-12 gap-2 items-center bg-white/5 rounded p-2 text-xs">
{/* Товар */}
<div className="col-span-4 flex items-center space-x-2">
<img
src={WildberriesService.getCardImage(item.card, 'c246x328') || '/api/placeholder/24/24'}
alt={item.card.title}
className="w-6 h-6 rounded object-cover"
/>
<div className="min-w-0">
<div className="text-white font-medium truncate text-xs">{item.card.title}</div>
<div className="text-white/60 text-[10px]">Арт: {item.card.vendorCode}</div>
<div className="flex items-center justify-between">
<span className="text-white/80 text-xs font-medium">
Найдено товаров: {wbCards.length}
</span>
{supplyItems.length > 0 && (
<Badge
variant="secondary"
className="bg-purple-500/20 text-purple-200 text-xs"
>
В поставке: {supplyItems.length}
</Badge>
)}
</div>
<div className="grid grid-cols-3 sm:grid-cols-4 md:grid-cols-6 lg:grid-cols-8 xl:grid-cols-10 gap-2">
{loading
? [...Array(12)].map((_, i) => (
<div
key={i}
className="aspect-[3/4] bg-white/5 rounded-lg animate-pulse"
></div>
))
: wbCards.map((card) => {
const isInSupply = supplyItems.some(
(item) => item.card.nmID === card.nmID
);
return (
<div
key={card.nmID}
className={`group relative cursor-pointer transition-all duration-200 hover:scale-105 hover:z-10 ${
isInSupply
? "ring-2 ring-purple-400 ring-offset-1 ring-offset-transparent"
: ""
}`}
onClick={() => addToSupply(card)}
>
<div className="aspect-[3/4] bg-white/10 rounded-lg overflow-hidden backdrop-blur-sm border border-white/20 hover:border-white/40 transition-all">
<img
src={
WildberriesService.getCardImage(
card,
"c516x688"
) || "/api/placeholder/120/160"
}
alt={card.title}
className="w-full h-full object-cover transition-transform group-hover:scale-110"
loading="lazy"
/>
{/* Оверлей с информацией */}
<div className="absolute inset-0 bg-gradient-to-t from-black/80 via-transparent to-transparent opacity-0 group-hover:opacity-100 transition-opacity">
<div className="absolute bottom-2 left-2 right-2">
<div className="text-white text-xs font-medium line-clamp-2 mb-1">
{card.title}
</div>
<div className="text-white/70 text-[10px]">
Арт: {card.vendorCode}
</div>
{card.sizes && card.sizes[0] && (
<div className="text-purple-300 text-[10px] font-medium">
от{" "}
{card.sizes[0].discountedPrice ||
card.sizes[0].price}{" "}
</div>
)}
</div>
</div>
{/* Индикатор добавления в поставку */}
{isInSupply && (
<div className="absolute top-2 right-2 bg-purple-500 text-white rounded-full w-6 h-6 flex items-center justify-center text-xs font-bold shadow-lg">
</div>
)}
{/* Индикатор при наведении */}
<div className="absolute top-2 left-2 bg-white/20 backdrop-blur text-white rounded-full w-6 h-6 flex items-center justify-center opacity-0 group-hover:opacity-100 transition-opacity">
<Plus className="h-3 w-3" />
</div>
</div>
</div>
);
})}
</div>
{!loading && wbCards.length === 0 && (
<div className="text-center py-4">
<Package className="h-8 w-8 text-white/20 mx-auto mb-2" />
<p className="text-white/60 text-xs">
{searchTerm
? "Товары не найдены"
: "Введите запрос для поиска товаров"}
</p>
{searchTerm && (
<p className="text-white/40 text-[10px] mt-1">
Попробуйте изменить условия поиска
</p>
)}
</div>
)}
</div>
</Card>
{/* Услуги и расходники в одной строке */}
{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-3">
<div className="flex items-center justify-between mb-3">
<span className="text-white font-medium text-sm">
Товары в поставке
</span>
<Button
onClick={() => setShowSupplierModal(true)}
variant="outline"
size="sm"
className="bg-white/5 border-white/20 text-white hover:bg-white/10 h-7 px-3 text-xs"
>
<Plus className="h-3 w-3 mr-1" />
Поставщик
</Button>
</div>
{supplyItems.length === 0 ? (
<div className="text-center py-6">
<Package className="h-8 w-8 text-white/20 mx-auto mb-2" />
<p className="text-white/60 text-xs">
Добавьте товары из карточек выше
</p>
</div>
) : (
<div className="space-y-4">
{supplyItems.map((item) => (
<Card
key={item.card.nmID}
className="bg-white/5 border-white/10 p-3"
>
{/* Заголовок товара с кнопкой удаления */}
<div className="flex items-center justify-between mb-3">
<div className="flex items-center space-x-2">
<div className="text-white font-medium text-sm line-clamp-1">
{item.card.title}
</div>
<div className="text-white/60 text-xs">
Арт: {item.card.vendorCode}
</div>
</div>
<Button
onClick={() => removeFromSupply(item.card.nmID)}
size="sm"
variant="ghost"
className="h-6 w-6 p-0 text-white/60 hover:text-red-400"
>
<X className="h-4 w-4" />
</Button>
</div>
</div>
{/* Количество */}
<div className="col-span-2">
<Input
type="number"
value={item.quantity}
onChange={(e) => updateSupplyItem(item.card.nmID, 'quantity', parseInt(e.target.value) || 0)}
className="bg-purple-500 border-0 text-white text-center h-6 text-xs font-bold"
min="1"
/>
</div>
{/* Цена */}
<div className="col-span-2">
<Input
type="number"
value={item.pricePerUnit || ''}
onChange={(e) => updateSupplyItem(item.card.nmID, 'pricePerUnit', parseFloat(e.target.value) || 0)}
className="bg-white/10 border-white/20 text-white text-center h-6 text-xs"
placeholder="Цена"
/>
</div>
{/* Поставщик */}
<div className="col-span-3">
<Select
value={item.supplierId}
onValueChange={(value) => updateSupplyItem(item.card.nmID, 'supplierId', value)}
{/* Названия блоков */}
<div
className="grid grid-cols-8 gap-2"
style={{ marginBottom: "4px" }}
>
<SelectTrigger className="bg-white/5 border-white/20 text-white h-6 text-xs">
<SelectValue placeholder="Поставщик" />
<div className="text-white/80 text-xs font-medium text-center">
Товар
</div>
<div className="text-white/80 text-xs font-medium text-center">
Параметры
</div>
<div className="text-white/80 text-xs font-medium text-center">
Заказать
</div>
<div className="text-white/80 text-xs font-medium text-center">
Цена
</div>
<div className="text-white/80 text-xs font-medium text-center">
Услуги фулфилмента
</div>
<div className="text-white/80 text-xs font-medium text-center">
Поставщик
</div>
<div className="text-white/80 text-xs font-medium text-center">
Расходники фулфилмента
</div>
<div className="text-white/80 text-xs font-medium text-center">
Расходники селлера
</div>
</div>
{/* Оптимизированная сетка для 13" - все блоки в одну строку */}
<div className="grid grid-cols-8 gap-2">
{/* Блок 1: Картинка товара */}
<div className="bg-white/10 rounded-lg overflow-hidden relative">
<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-2 flex flex-col justify-center">
<div className="space-y-1">
<div className="text-white/70 text-[9px] text-center">
{item.card.object}
</div>
<div className="text-white/70 text-[9px] text-center">
{item.card.countryProduction}
</div>
{item.card.sizes && item.card.sizes[0] && (
<div className="text-white/70 text-[9px] text-center">
{item.card.sizes[0].techSize}
</div>
)}
</div>
</div>
{/* Блок 3: Заказать */}
<div className="bg-white/10 rounded-lg p-2 flex flex-col justify-center">
<div className="text-white/60 text-[10px] mb-1 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-7 text-xs font-bold"
min="1"
/>
</div>
{/* Блок 4: Цена */}
<div className="bg-white/10 rounded-lg p-2 flex flex-col justify-center">
<div className="text-white/60 text-[10px] mb-1 text-center">
За единицу
</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-[9px] 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">
<div className="space-y-1 max-h-16 overflow-y-auto">
{selectedFulfillmentOrg &&
organizationServices[selectedFulfillmentOrg] ? (
organizationServices[selectedFulfillmentOrg]
.slice(0, 2)
.map((service) => (
<label
key={service.id}
className="flex items-center space-x-1 cursor-pointer"
>
<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-2 h-2"
/>
<span className="text-white text-[9px]">
{service.name.substring(0, 8)}...
</span>
</label>
))
) : (
<span className="text-white/60 text-[9px] text-center">
Выберите фулфилмент
</span>
)}
</div>
</div>
{/* Блок 6: Поставщик */}
<div className="bg-white/10 rounded-lg p-2 flex flex-col justify-center">
<Select
value={item.supplierId}
onValueChange={(value) =>
updateSupplyItem(item.card.nmID, "supplierId", value)
}
>
<SelectTrigger className="bg-white/20 border-white/20 text-white h-7 text-[10px]">
<SelectValue placeholder="Выбрать" />
</SelectTrigger>
<SelectContent>
{suppliers.map((supplier) => (
<SelectItem key={supplier.id} value={supplier.id}>
{supplier.name}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
{/* Блок 7: Расходники фулфилмента */}
<div className="bg-white/10 rounded-lg p-2 flex flex-col justify-center">
<div className="space-y-1 max-h-16 overflow-y-auto">
{selectedFulfillmentOrg &&
organizationSupplies[selectedFulfillmentOrg] ? (
organizationSupplies[selectedFulfillmentOrg]
.slice(0, 2)
.map((supply) => (
<label
key={supply.id}
className="flex items-center space-x-1 cursor-pointer"
>
<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-2 h-2"
/>
<span className="text-white text-[9px]">
{supply.name.substring(0, 6)}...
</span>
</label>
))
) : (
<span className="text-white/60 text-[9px] text-center">
Выберите фулфилмент
</span>
)}
</div>
</div>
{/* Блок 8: Расходники селлера */}
<div className="bg-white/10 rounded-lg p-2 flex flex-col justify-center">
<div className="space-y-1">
<label className="flex items-center space-x-1 cursor-pointer">
<input type="checkbox" className="w-2 h-2" />
<span className="text-white text-[9px]">
Упаковка
</span>
</label>
<label className="flex items-center space-x-1 cursor-pointer">
<input type="checkbox" className="w-2 h-2" />
<span className="text-white text-[9px]">
Этикетки
</span>
</label>
<label className="flex items-center space-x-1 cursor-pointer">
<input type="checkbox" className="w-2 h-2" />
<span className="text-white text-[9px]">Пакеты</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>
</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) =>
setNewSupplier((prev) => ({
...prev,
name: 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.contactName}
onChange={(e) =>
setNewSupplier((prev) => ({
...prev,
contactName: e.target.value,
}))
}
className="bg-white/10 border-white/20 text-white h-8 text-xs"
placeholder="Имя"
/>
</div>
</div>
<div className="grid grid-cols-2 gap-3">
<div>
<Label className="text-white/60 text-xs">Телефон *</Label>
<Input
value={newSupplier.phone}
onChange={(e) =>
setNewSupplier((prev) => ({
...prev,
phone: e.target.value,
}))
}
className="bg-white/10 border-white/20 text-white h-8 text-xs"
placeholder="+7 999 123-45-67"
/>
</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>
{suppliers.map((supplier) => (
<SelectItem key={supplier.id} value={supplier.id}>
{supplier.name}
{markets.map((market) => (
<SelectItem key={market.value} value={market.value}>
{market.label}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
</div>
{/* Сумма и удаление */}
<div className="col-span-1 flex items-center justify-between">
<span className="text-white font-bold text-xs">
{formatCurrency(item.totalPrice).replace(' ₽', '₽')}
</span>
<Button
onClick={() => removeFromSupply(item.card.nmID)}
size="sm"
variant="ghost"
className="h-4 w-4 p-0 text-white/60 hover:text-red-400"
>
<X className="h-3 w-3" />
</Button>
<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>
)}
</Card>
{/* Модальное окно создания поставщика */}
<Dialog open={showSupplierModal} onOpenChange={setShowSupplierModal}>
<DialogContent className="glass-card border-white/10 max-w-md">
<DialogHeader>
<DialogTitle className="text-white">Создать поставщика</DialogTitle>
</DialogHeader>
<div className="space-y-3">
<div className="grid grid-cols-2 gap-3">
<div>
<Label className="text-white/60 text-xs">Название *</Label>
<Label className="text-white/60 text-xs">Телеграм</Label>
<Input
value={newSupplier.name}
onChange={(e) => setNewSupplier(prev => ({ ...prev, name: e.target.value }))}
value={newSupplier.telegram}
onChange={(e) =>
setNewSupplier((prev) => ({
...prev,
telegram: e.target.value,
}))
}
className="bg-white/10 border-white/20 text-white h-8 text-xs"
placeholder="Название"
placeholder="@username"
/>
</div>
<div>
<Label className="text-white/60 text-xs">Имя *</Label>
<Input
value={newSupplier.contactName}
onChange={(e) => setNewSupplier(prev => ({ ...prev, contactName: e.target.value }))}
className="bg-white/10 border-white/20 text-white h-8 text-xs"
placeholder="Имя"
/>
</div>
</div>
<div className="grid grid-cols-2 gap-3">
<div>
<Label className="text-white/60 text-xs">Телефон *</Label>
<Input
value={newSupplier.phone}
onChange={(e) => setNewSupplier(prev => ({ ...prev, phone: e.target.value }))}
className="bg-white/10 border-white/20 text-white h-8 text-xs"
placeholder="+7 999 123-45-67"
/>
</div>
<div>
<Label className="text-white/60 text-xs">Рынок</Label>
<Select
value={newSupplier.market}
onValueChange={(value) => setNewSupplier(prev => ({ ...prev, market: value }))}
<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"
>
<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>
Отмена
</Button>
<Button
onClick={handleCreateSupplier}
className="flex-1 bg-gradient-to-r from-purple-500 to-pink-500 hover:from-purple-600 hover:to-pink-600 h-8 text-xs"
>
Создать
</Button>
</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) => setNewSupplier(prev => ({ ...prev, telegram: e.target.value }))}
className="bg-white/10 border-white/20 text-white h-8 text-xs"
placeholder="@username"
/>
</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}
className="flex-1 bg-gradient-to-r from-purple-500 to-pink-500 hover:from-purple-600 hover:to-pink-600 h-8 text-xs"
>
Создать
</Button>
</div>
</div>
</DialogContent>
</Dialog>
</div>
)
}
</DialogContent>
</Dialog>
</div>
</>
);
}