Compare commits

..

2 Commits

2 changed files with 1263 additions and 690 deletions

View File

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

View File

@ -1,16 +1,31 @@
"use client" "use client";
import React, { useState, useEffect } from 'react' import React, { useState, useEffect } from "react";
import { Card } from '@/components/ui/card' import { Card } from "@/components/ui/card";
import { Button } from '@/components/ui/button' import { Button } from "@/components/ui/button";
import { Input } from '@/components/ui/input' import { Input } from "@/components/ui/input";
import { Badge } from '@/components/ui/badge' import { Badge } from "@/components/ui/badge";
import { Label } from '@/components/ui/label' import { Label } from "@/components/ui/label";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select' import {
import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@/components/ui/dialog' Select,
import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover' SelectContent,
import DatePicker from "react-datepicker" SelectItem,
import "react-datepicker/dist/react-datepicker.css" 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 { import {
Search, Search,
Plus, Plus,
@ -20,332 +35,452 @@ import {
X, X,
User, User,
Phone, Phone,
MapPin MapPin,
} from 'lucide-react' Building,
import { WildberriesService } from '@/services/wildberries-service' Truck,
import { useAuth } from '@/hooks/useAuth' } from "lucide-react";
import { useQuery, useMutation } from '@apollo/client' import { WildberriesService } from "@/services/wildberries-service";
import { apolloClient } from '@/lib/apollo-client' import { useAuth } from "@/hooks/useAuth";
import { GET_MY_COUNTERPARTIES, GET_COUNTERPARTY_SERVICES, GET_COUNTERPARTY_SUPPLIES } from '@/graphql/queries' import { useQuery, useMutation } from "@apollo/client";
import { CREATE_WILDBERRIES_SUPPLY } from '@/graphql/mutations' import { apolloClient } from "@/lib/apollo-client";
import { toast } from 'sonner' import {
import { format } from 'date-fns' GET_MY_COUNTERPARTIES,
import { ru } from 'date-fns/locale' GET_COUNTERPARTY_SERVICES,
import { WildberriesCard } from '@/types/supplies' 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 { interface SupplyItem {
card: WildberriesCard card: WildberriesCard;
quantity: number quantity: number;
pricePerUnit: number pricePerUnit: number;
totalPrice: number totalPrice: number;
supplierId: string supplierId: string;
} }
interface Organization { interface Organization {
id: string id: string;
name?: string name?: string;
fullName?: string fullName?: string;
type: string type: string;
} }
interface FulfillmentService { interface FulfillmentService {
id: string id: string;
name: string name: string;
description?: string description?: string;
price: number price: number;
} }
interface Supplier { interface Supplier {
id: string id: string;
name: string name: string;
contactName: string contactName: string;
phone: string phone: string;
market: string market: string;
address: string address: string;
place: string place: string;
telegram: string telegram: string;
} }
interface DirectSupplyCreationProps { interface DirectSupplyCreationProps {
onComplete: () => void onComplete: () => void;
onCreateSupply: () => void onCreateSupply: () => void;
canCreateSupply: boolean canCreateSupply: boolean;
isCreatingSupply: boolean isCreatingSupply: boolean;
onCanCreateSupplyChange?: (canCreate: boolean) => void onCanCreateSupplyChange?: (canCreate: boolean) => void;
} }
export function DirectSupplyCreation({ onComplete, onCreateSupply, canCreateSupply, isCreatingSupply, onCanCreateSupplyChange }: DirectSupplyCreationProps) { export function DirectSupplyCreation({
const { user } = useAuth() onComplete,
onCreateSupply,
canCreateSupply,
isCreatingSupply,
onCanCreateSupplyChange,
}: DirectSupplyCreationProps) {
const { user } = useAuth();
// Состояние для товаров // Новые состояния для блока создания поставки
const [searchTerm, setSearchTerm] = useState('') const [deliveryDate, setDeliveryDate] = useState<string>("");
const [loading, setLoading] = useState(false) const [selectedFulfillment, setSelectedFulfillment] = useState<string>("");
const [wbCards, setWbCards] = useState<WildberriesCard[]>([]) const [goodsQuantity, setGoodsQuantity] = useState<number>(1200);
const [supplyItems, setSupplyItems] = useState<SupplyItem[]>([]) 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 [deliveryDate, setDeliveryDate] = useState<Date | undefined>(undefined) const [searchTerm, setSearchTerm] = useState("");
const [selectedFulfillmentOrg, setSelectedFulfillmentOrg] = useState<string>('') const [loading, setLoading] = useState(false);
const [selectedServices, setSelectedServices] = useState<string[]>([]) const [wbCards, setWbCards] = useState<WildberriesCard[]>([]);
const [selectedConsumables, setSelectedConsumables] = useState<string[]>([]) 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 [suppliers, setSuppliers] = useState<Supplier[]>([]);
const [showSupplierModal, setShowSupplierModal] = useState(false) const [showSupplierModal, setShowSupplierModal] = useState(false);
const [newSupplier, setNewSupplier] = useState({ const [newSupplier, setNewSupplier] = useState({
name: '', name: "",
contactName: '', contactName: "",
phone: '', phone: "",
market: '', market: "",
address: '', address: "",
place: '', place: "",
telegram: '' telegram: "",
}) });
// Данные для фулфилмента // Данные для фулфилмента
const [organizationServices, setOrganizationServices] = useState<{[orgId: string]: FulfillmentService[]}>({}) const [organizationServices, setOrganizationServices] = useState<{
const [organizationSupplies, setOrganizationSupplies] = useState<{[orgId: string]: FulfillmentService[]}>({}) [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, { const [createSupply, { loading: creatingSupply }] = useMutation(
CREATE_WILDBERRIES_SUPPLY,
{
onCompleted: (data) => { onCompleted: (data) => {
if (data.createWildberriesSupply.success) { if (data.createWildberriesSupply.success) {
toast.success(data.createWildberriesSupply.message) toast.success(data.createWildberriesSupply.message);
onComplete() onComplete();
} else { } else {
toast.error(data.createWildberriesSupply.message) toast.error(data.createWildberriesSupply.message);
} }
}, },
onError: (error) => { onError: (error) => {
toast.error('Ошибка при создании поставки') toast.error("Ошибка при создании поставки");
console.error('Error creating supply:', error) console.error("Error creating supply:", error);
},
} }
}) );
// Моковые данные товаров для демонстрации // Моковые данные товаров для демонстрации
const getMockCards = (): WildberriesCard[] => [ const getMockCards = (): WildberriesCard[] => [
{ {
nmID: 123456789, nmID: 123456789,
vendorCode: 'SKU001', vendorCode: "SKU001",
title: 'Платье летнее розовое', title: "Платье летнее розовое",
description: 'Легкое летнее платье из натурального хлопка', description: "Легкое летнее платье из натурального хлопка",
brand: 'Fashion', brand: "Fashion",
object: 'Платья', object: "Платья",
parent: 'Одежда', parent: "Одежда",
countryProduction: 'Россия', countryProduction: "Россия",
supplierVendorCode: 'SUPPLIER-001', supplierVendorCode: "SUPPLIER-001",
mediaFiles: ['/api/placeholder/400/400'], mediaFiles: ["/api/placeholder/400/400"],
sizes: [{ chrtID: 123456, techSize: 'M', wbSize: 'M Розовый', price: 2500, discountedPrice: 2000, quantity: 50 }] sizes: [
{
chrtID: 123456,
techSize: "M",
wbSize: "M Розовый",
price: 2500,
discountedPrice: 2000,
quantity: 50,
},
],
}, },
{ {
nmID: 987654321, nmID: 987654321,
vendorCode: 'SKU002', vendorCode: "SKU002",
title: 'Платье черное вечернее', title: "Платье черное вечернее",
description: 'Элегантное вечернее платье для особых случаев', description: "Элегантное вечернее платье для особых случаев",
brand: 'Fashion', brand: "Fashion",
object: 'Платья', object: "Платья",
parent: 'Одежда', parent: "Одежда",
countryProduction: 'Россия', countryProduction: "Россия",
supplierVendorCode: 'SUPPLIER-002', supplierVendorCode: "SUPPLIER-002",
mediaFiles: ['/api/placeholder/400/403'], mediaFiles: ["/api/placeholder/400/403"],
sizes: [{ chrtID: 987654, techSize: 'M', wbSize: 'M Черный', price: 3500, discountedPrice: 3000, quantity: 30 }] sizes: [
{
chrtID: 987654,
techSize: "M",
wbSize: "M Черный",
price: 3500,
discountedPrice: 3000,
quantity: 30,
},
],
}, },
{ {
nmID: 555666777, nmID: 555666777,
vendorCode: 'SKU003', vendorCode: "SKU003",
title: 'Блузка белая офисная', title: "Блузка белая офисная",
description: 'Классическая белая блузка для офиса', description: "Классическая белая блузка для офиса",
brand: 'Office', brand: "Office",
object: 'Блузки', object: "Блузки",
parent: 'Одежда', parent: "Одежда",
countryProduction: 'Турция', countryProduction: "Турция",
supplierVendorCode: 'SUPPLIER-003', supplierVendorCode: "SUPPLIER-003",
mediaFiles: ['/api/placeholder/400/405'], mediaFiles: ["/api/placeholder/400/405"],
sizes: [{ chrtID: 555666, techSize: 'L', wbSize: 'L Белый', price: 1800, discountedPrice: 1500, quantity: 40 }] sizes: [
{
chrtID: 555666,
techSize: "L",
wbSize: "L Белый",
price: 1800,
discountedPrice: 1500,
quantity: 40,
},
],
}, },
{ {
nmID: 444333222, nmID: 444333222,
vendorCode: 'SKU004', vendorCode: "SKU004",
title: 'Джинсы женские синие', title: "Джинсы женские синие",
description: 'Классические женские джинсы прямого кроя', description: "Классические женские джинсы прямого кроя",
brand: 'Denim', brand: "Denim",
object: 'Джинсы', object: "Джинсы",
parent: 'Одежда', parent: "Одежда",
countryProduction: 'Бангладеш', countryProduction: "Бангладеш",
supplierVendorCode: 'SUPPLIER-004', supplierVendorCode: "SUPPLIER-004",
mediaFiles: ['/api/placeholder/400/408'], mediaFiles: ["/api/placeholder/400/408"],
sizes: [{ chrtID: 444333, techSize: '30', wbSize: '30 Синий', price: 2800, discountedPrice: 2300, quantity: 25 }] sizes: [
{
chrtID: 444333,
techSize: "30",
wbSize: "30 Синий",
price: 2800,
discountedPrice: 2300,
quantity: 25,
},
],
}, },
{ {
nmID: 111222333, nmID: 111222333,
vendorCode: 'SKU005', vendorCode: "SKU005",
title: 'Кроссовки женские белые', title: "Кроссовки женские белые",
description: 'Удобные женские кроссовки для повседневной носки', description: "Удобные женские кроссовки для повседневной носки",
brand: 'Sport', brand: "Sport",
object: 'Кроссовки', object: "Кроссовки",
parent: 'Обувь', parent: "Обувь",
countryProduction: 'Вьетнам', countryProduction: "Вьетнам",
supplierVendorCode: 'SUPPLIER-005', supplierVendorCode: "SUPPLIER-005",
mediaFiles: ['/api/placeholder/400/410'], mediaFiles: ["/api/placeholder/400/410"],
sizes: [{ chrtID: 111222, techSize: '37', wbSize: '37 Белый', price: 3200, discountedPrice: 2800, quantity: 35 }] sizes: [
{
chrtID: 111222,
techSize: "37",
wbSize: "37 Белый",
price: 3200,
discountedPrice: 2800,
quantity: 35,
},
],
}, },
{ {
nmID: 777888999, nmID: 777888999,
vendorCode: 'SKU006', vendorCode: "SKU006",
title: 'Сумка женская черная', title: "Сумка женская черная",
description: 'Стильная женская сумка из экокожи', description: "Стильная женская сумка из экокожи",
brand: 'Accessories', brand: "Accessories",
object: 'Сумки', object: "Сумки",
parent: 'Аксессуары', parent: "Аксессуары",
countryProduction: 'Китай', countryProduction: "Китай",
supplierVendorCode: 'SUPPLIER-006', supplierVendorCode: "SUPPLIER-006",
mediaFiles: ['/api/placeholder/400/411'], mediaFiles: ["/api/placeholder/400/411"],
sizes: [{ chrtID: 777888, techSize: 'Универсальный', wbSize: 'Черный', price: 1500, discountedPrice: 1200, quantity: 60 }] sizes: [
} {
] chrtID: 777888,
techSize: "Универсальный",
wbSize: "Черный",
price: 1500,
discountedPrice: 1200,
quantity: 60,
},
],
},
];
// Загружаем товары при инициализации // Загружаем товары при инициализации
useEffect(() => { useEffect(() => {
loadCards() loadCards();
}, [user]) }, [user]);
const loadCards = async () => { const loadCards = async () => {
setLoading(true) setLoading(true);
try { try {
const wbApiKey = user?.organization?.apiKeys?.find(key => key.marketplace === 'WILDBERRIES') const wbApiKey = user?.organization?.apiKeys?.find(
(key) => key.marketplace === "WILDBERRIES"
);
if (wbApiKey?.isActive) { if (wbApiKey?.isActive) {
const validationData = wbApiKey.validationData as Record<string, string> const validationData = wbApiKey.validationData as Record<
const apiToken = validationData?.token || string,
string
>;
const apiToken =
validationData?.token ||
validationData?.apiKey || validationData?.apiKey ||
validationData?.key || validationData?.key ||
(wbApiKey as { apiKey?: string }).apiKey (wbApiKey as { apiKey?: string }).apiKey;
if (apiToken) { if (apiToken) {
console.log('Загружаем карточки из WB API...') console.log("Загружаем карточки из WB API...");
const cards = await WildberriesService.getAllCards(apiToken, 20) const cards = await WildberriesService.getAllCards(apiToken, 20);
setWbCards(cards) setWbCards(cards);
console.log('Загружено карточек из WB API:', cards.length) console.log("Загружено карточек из WB API:", cards.length);
return return;
} }
} }
// Если API ключ не настроен, показываем моковые данные // Если API ключ не настроен, показываем моковые данные
console.log('API ключ WB не настроен, показываем моковые данные') console.log("API ключ WB не настроен, показываем моковые данные");
setWbCards(getMockCards()) setWbCards(getMockCards());
} catch (error) { } catch (error) {
console.error('Ошибка загрузки карточек WB:', error) console.error("Ошибка загрузки карточек WB:", error);
// При ошибке API показываем моковые данные // При ошибке API показываем моковые данные
setWbCards(getMockCards()) setWbCards(getMockCards());
} finally { } finally {
setLoading(false) setLoading(false);
}
} }
};
const searchCards = async () => { const searchCards = async () => {
if (!searchTerm.trim()) { if (!searchTerm.trim()) {
loadCards() loadCards();
return return;
} }
setLoading(true) setLoading(true);
try { try {
const wbApiKey = user?.organization?.apiKeys?.find(key => key.marketplace === 'WILDBERRIES') const wbApiKey = user?.organization?.apiKeys?.find(
(key) => key.marketplace === "WILDBERRIES"
);
if (wbApiKey?.isActive) { if (wbApiKey?.isActive) {
const validationData = wbApiKey.validationData as Record<string, string> const validationData = wbApiKey.validationData as Record<
const apiToken = validationData?.token || string,
string
>;
const apiToken =
validationData?.token ||
validationData?.apiKey || validationData?.apiKey ||
validationData?.key || validationData?.key ||
(wbApiKey as { apiKey?: string }).apiKey (wbApiKey as { apiKey?: string }).apiKey;
if (apiToken) { if (apiToken) {
console.log('Поиск в WB API:', searchTerm) console.log("Поиск в WB API:", searchTerm);
const cards = await WildberriesService.searchCards(apiToken, searchTerm, 20) const cards = await WildberriesService.searchCards(
setWbCards(cards) apiToken,
console.log('Найдено карточек в WB API:', cards.length) searchTerm,
return 20
);
setWbCards(cards);
console.log("Найдено карточек в WB API:", cards.length);
return;
} }
} }
// Если API ключ не настроен, ищем в моковых данных // Если API ключ не настроен, ищем в моковых данных
console.log('API ключ WB не настроен, поиск в моковых данных:', searchTerm) console.log(
const mockCards = getMockCards() "API ключ WB не настроен, поиск в моковых данных:",
const filteredCards = mockCards.filter(card => searchTerm
);
const mockCards = getMockCards();
const filteredCards = mockCards.filter(
(card) =>
card.title.toLowerCase().includes(searchTerm.toLowerCase()) || card.title.toLowerCase().includes(searchTerm.toLowerCase()) ||
card.brand.toLowerCase().includes(searchTerm.toLowerCase()) || card.brand.toLowerCase().includes(searchTerm.toLowerCase()) ||
card.nmID.toString().includes(searchTerm.toLowerCase()) || card.nmID.toString().includes(searchTerm.toLowerCase()) ||
card.object?.toLowerCase().includes(searchTerm.toLowerCase()) card.object?.toLowerCase().includes(searchTerm.toLowerCase())
) );
setWbCards(filteredCards) setWbCards(filteredCards);
console.log('Найдено моковых товаров:', filteredCards.length) console.log("Найдено моковых товаров:", filteredCards.length);
} catch (error) { } catch (error) {
console.error('Ошибка поиска карточек WB:', error) console.error("Ошибка поиска карточек WB:", error);
// При ошибке ищем в моковых данных // При ошибке ищем в моковых данных
const mockCards = getMockCards() const mockCards = getMockCards();
const filteredCards = mockCards.filter(card => const filteredCards = mockCards.filter(
(card) =>
card.title.toLowerCase().includes(searchTerm.toLowerCase()) || card.title.toLowerCase().includes(searchTerm.toLowerCase()) ||
card.brand.toLowerCase().includes(searchTerm.toLowerCase()) || card.brand.toLowerCase().includes(searchTerm.toLowerCase()) ||
card.nmID.toString().includes(searchTerm.toLowerCase()) || card.nmID.toString().includes(searchTerm.toLowerCase()) ||
card.object?.toLowerCase().includes(searchTerm.toLowerCase()) card.object?.toLowerCase().includes(searchTerm.toLowerCase())
) );
setWbCards(filteredCards) setWbCards(filteredCards);
console.log('Найдено моковых товаров (fallback):', filteredCards.length) console.log("Найдено моковых товаров (fallback):", filteredCards.length);
} finally { } finally {
setLoading(false) setLoading(false);
}
} }
};
// Функции для работы с услугами и расходниками // Функции для работы с услугами и расходниками
const loadOrganizationServices = async (organizationId: string) => { const loadOrganizationServices = async (organizationId: string) => {
if (organizationServices[organizationId]) return if (organizationServices[organizationId]) return;
try { try {
const response = await apolloClient.query({ const response = await apolloClient.query({
query: GET_COUNTERPARTY_SERVICES, query: GET_COUNTERPARTY_SERVICES,
variables: { organizationId } variables: { organizationId },
}) });
if (response.data?.counterpartyServices) { if (response.data?.counterpartyServices) {
setOrganizationServices(prev => ({ setOrganizationServices((prev) => ({
...prev, ...prev,
[organizationId]: response.data.counterpartyServices [organizationId]: response.data.counterpartyServices,
})) }));
} }
} catch (error) { } catch (error) {
console.error('Ошибка загрузки услуг организации:', error) console.error("Ошибка загрузки услуг организации:", error);
}
} }
};
const loadOrganizationSupplies = async (organizationId: string) => { const loadOrganizationSupplies = async (organizationId: string) => {
if (organizationSupplies[organizationId]) return if (organizationSupplies[organizationId]) return;
try { try {
const response = await apolloClient.query({ const response = await apolloClient.query({
query: GET_COUNTERPARTY_SUPPLIES, query: GET_COUNTERPARTY_SUPPLIES,
variables: { organizationId } variables: { organizationId },
}) });
if (response.data?.counterpartySupplies) { if (response.data?.counterpartySupplies) {
setOrganizationSupplies(prev => ({ setOrganizationSupplies((prev) => ({
...prev, ...prev,
[organizationId]: response.data.counterpartySupplies [organizationId]: response.data.counterpartySupplies,
})) }));
} }
} catch (error) { } catch (error) {
console.error('Ошибка загрузки расходников организации:', error) console.error("Ошибка загрузки расходников организации:", error);
}
} }
};
// Работа с товарами поставки // Работа с товарами поставки
const addToSupply = (card: WildberriesCard) => { 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) { if (existingItem) {
toast.info('Товар уже добавлен в поставку') toast.info("Товар уже добавлен в поставку");
return return;
} }
const newItem: SupplyItem = { const newItem: SupplyItem = {
@ -353,114 +488,132 @@ export function DirectSupplyCreation({ onComplete, onCreateSupply, canCreateSupp
quantity: 1200, quantity: 1200,
pricePerUnit: 0, pricePerUnit: 0,
totalPrice: 0, totalPrice: 0,
supplierId: '' supplierId: "",
} };
setSupplyItems(prev => [...prev, newItem]) setSupplyItems((prev) => [...prev, newItem]);
toast.success('Товар добавлен в поставку') toast.success("Товар добавлен в поставку");
} };
const removeFromSupply = (nmID: number) => { 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) => { const updateSupplyItem = (
setSupplyItems(prev => prev.map(item => { nmID: number,
field: keyof SupplyItem,
value: string | number
) => {
setSupplyItems((prev) =>
prev.map((item) => {
if (item.card.nmID === nmID) { if (item.card.nmID === nmID) {
const updatedItem = { ...item, [field]: value } const updatedItem = { ...item, [field]: value };
if (field === 'quantity' || field === 'pricePerUnit') { if (field === "quantity" || field === "pricePerUnit") {
updatedItem.totalPrice = updatedItem.quantity * updatedItem.pricePerUnit updatedItem.totalPrice =
updatedItem.quantity * updatedItem.pricePerUnit;
} }
return updatedItem return updatedItem;
}
return item
}))
} }
return item;
})
);
};
// Работа с поставщиками // Работа с поставщиками
const handleCreateSupplier = () => { const handleCreateSupplier = () => {
if (!newSupplier.name || !newSupplier.contactName || !newSupplier.phone) { if (!newSupplier.name || !newSupplier.contactName || !newSupplier.phone) {
toast.error('Заполните обязательные поля') toast.error("Заполните обязательные поля");
return return;
} }
const supplier: Supplier = { const supplier: Supplier = {
id: Date.now().toString(), id: Date.now().toString(),
...newSupplier ...newSupplier,
} };
setSuppliers(prev => [...prev, supplier]) setSuppliers((prev) => [...prev, supplier]);
setNewSupplier({ setNewSupplier({
name: '', name: "",
contactName: '', contactName: "",
phone: '', phone: "",
market: '', market: "",
address: '', address: "",
place: '', place: "",
telegram: '' telegram: "",
}) });
setShowSupplierModal(false) setShowSupplierModal(false);
toast.success('Поставщик создан') toast.success("Поставщик создан");
} };
// Расчеты // Расчеты для нового блока
const getTotalSum = () => {
return goodsPrice + fulfillmentServicesPrice + logisticsPrice;
};
// Оригинальные расчеты
const getTotalQuantity = () => { const getTotalQuantity = () => {
return supplyItems.reduce((sum, item) => sum + item.quantity, 0) return supplyItems.reduce((sum, item) => sum + item.quantity, 0);
} };
const getTotalItemsCost = () => { const getTotalItemsCost = () => {
return supplyItems.reduce((sum, item) => sum + item.totalPrice, 0) return supplyItems.reduce((sum, item) => sum + item.totalPrice, 0);
} };
const getServicesCost = () => { const getServicesCost = () => {
if (!selectedFulfillmentOrg || selectedServices.length === 0) return 0 if (!selectedFulfillmentOrg || selectedServices.length === 0) return 0;
const services = organizationServices[selectedFulfillmentOrg] || [] const services = organizationServices[selectedFulfillmentOrg] || [];
return selectedServices.reduce((sum, serviceId) => { return (
const service = services.find(s => s.id === serviceId) selectedServices.reduce((sum, serviceId) => {
return sum + (service ? service.price : 0) const service = services.find((s) => s.id === serviceId);
return sum + (service ? service.price : 0);
}, 0) * getTotalQuantity() }, 0) * getTotalQuantity()
} );
};
const getConsumablesCost = () => { const getConsumablesCost = () => {
if (!selectedFulfillmentOrg || selectedConsumables.length === 0) return 0 if (!selectedFulfillmentOrg || selectedConsumables.length === 0) return 0;
const supplies = organizationSupplies[selectedFulfillmentOrg] || [] const supplies = organizationSupplies[selectedFulfillmentOrg] || [];
return selectedConsumables.reduce((sum, supplyId) => { return (
const supply = supplies.find(s => s.id === supplyId) selectedConsumables.reduce((sum, supplyId) => {
return sum + (supply ? supply.price : 0) const supply = supplies.find((s) => s.id === supplyId);
return sum + (supply ? supply.price : 0);
}, 0) * getTotalQuantity() }, 0) * getTotalQuantity()
} );
};
const formatCurrency = (amount: number) => { const formatCurrency = (amount: number) => {
return new Intl.NumberFormat('ru-RU', { return new Intl.NumberFormat("ru-RU", {
style: 'currency', style: "currency",
currency: 'RUB', currency: "RUB",
minimumFractionDigits: 0 minimumFractionDigits: 0,
}).format(amount) }).format(amount);
} };
// Создание поставки // Создание поставки
const handleCreateSupplyInternal = async () => { const handleCreateSupplyInternal = async () => {
if (supplyItems.length === 0) { if (supplyItems.length === 0) {
toast.error('Добавьте товары в поставку') toast.error("Добавьте товары в поставку");
return return;
} }
if (!deliveryDate) { if (!deliveryDateOriginal) {
toast.error('Выберите дату поставки') toast.error("Выберите дату поставки");
return return;
} }
if (supplyItems.some(item => item.quantity <= 0 || item.pricePerUnit <= 0)) { if (
toast.error('Укажите количество и цену для всех товаров') supplyItems.some((item) => item.quantity <= 0 || item.pricePerUnit <= 0)
return ) {
toast.error("Укажите количество и цену для всех товаров");
return;
} }
try { try {
const supplyInput = { const supplyInput = {
deliveryDate: deliveryDate.toISOString().split('T')[0], deliveryDate: deliveryDateOriginal.toISOString().split("T")[0],
cards: supplyItems.map(item => ({ cards: supplyItems.map((item) => ({
nmId: item.card.nmID.toString(), nmId: item.card.nmID.toString(),
vendorCode: item.card.vendorCode, vendorCode: item.card.vendorCode,
title: item.card.title, title: item.card.title,
@ -471,87 +624,84 @@ export function DirectSupplyCreation({ onComplete, onCreateSupply, canCreateSupp
selectedFulfillmentServices: selectedServices, selectedFulfillmentServices: selectedServices,
selectedConsumableOrg: selectedFulfillmentOrg, selectedConsumableOrg: selectedFulfillmentOrg,
selectedConsumableServices: selectedConsumables, selectedConsumableServices: selectedConsumables,
deliveryDate: deliveryDate.toISOString().split('T')[0], deliveryDate: deliveryDateOriginal.toISOString().split("T")[0],
mediaFiles: item.card.mediaFiles mediaFiles: item.card.mediaFiles,
})) })),
} };
await createSupply({ variables: { input: supplyInput } }) await createSupply({ variables: { input: supplyInput } });
toast.success('Поставка успешно создана!') toast.success("Поставка успешно создана!");
onComplete() onComplete();
} catch (error) { } catch (error) {
console.error('Error creating supply:', error) console.error("Error creating supply:", error);
toast.error('Ошибка при создании поставки') toast.error("Ошибка при создании поставки");
}
} }
};
// Обработка внешнего вызова создания поставки // Обработка внешнего вызова создания поставки
React.useEffect(() => { React.useEffect(() => {
if (isCreatingSupply) { if (isCreatingSupply) {
handleCreateSupplyInternal() handleCreateSupplyInternal();
} }
}, [isCreatingSupply]) }, [isCreatingSupply]);
// Обновление статуса возможности создания поставки // Обновление статуса возможности создания поставки
React.useEffect(() => { React.useEffect(() => {
const canCreate = supplyItems.length > 0 && const canCreate =
deliveryDate !== null && supplyItems.length > 0 &&
supplyItems.every(item => item.quantity > 0 && item.pricePerUnit > 0) deliveryDateOriginal !== null &&
supplyItems.every((item) => item.quantity > 0 && item.pricePerUnit > 0);
if (onCanCreateSupplyChange) { if (onCanCreateSupplyChange) {
onCanCreateSupplyChange(canCreate) onCanCreateSupplyChange(canCreate);
} }
}, [supplyItems, deliveryDate, onCanCreateSupplyChange]) }, [supplyItems, deliveryDateOriginal, onCanCreateSupplyChange]);
const fulfillmentOrgs = (counterpartiesData?.myCounterparties || []).filter((org: Organization) => org.type === 'FULFILLMENT') const fulfillmentOrgs = (counterpartiesData?.myCounterparties || []).filter(
(org: Organization) => org.type === "FULFILLMENT"
);
const markets = [ const markets = [
{ value: 'sadovod', label: 'Садовод' }, { value: "sadovod", label: "Садовод" },
{ value: 'tyak-moscow', label: 'ТЯК Москва' } { value: "tyak-moscow", label: "ТЯК Москва" },
] ];
return ( return (
<div className="space-y-3"> <>
{/* Основные настройки */} <style>{lineClampStyles}</style>
<Card className="bg-gradient-to-r from-purple-500 to-pink-500 p-3"> <div className="space-y-3 w-full">
{/* НОВЫЙ БЛОК СОЗДАНИЯ ПОСТАВКИ */}
<div className="grid grid-cols-2 md:grid-cols-6 gap-3 text-xs"> <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>
<Popover> <Label className="text-white/80 text-xs mb-1 block flex items-center gap-1">
<PopoverTrigger asChild> <CalendarIcon className="h-3 w-3" />
<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" /> </Label>
{deliveryDate ? format(deliveryDate, "dd.MM") : "Дата"} <div className="relative">
</button> <input
</PopoverTrigger> type="date"
<PopoverContent className="w-auto p-0"> value={deliveryDate}
<DatePicker onChange={(e) => setDeliveryDate(e.target.value)}
selected={deliveryDate} 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"
onChange={(date: Date | null) => setDeliveryDate(date || undefined)} min={new Date().toISOString().split("T")[0]}
minDate={new Date()}
inline
locale="ru"
/> />
</PopoverContent> </div>
</Popover>
</div> </div>
{/* Фулфилмент */} {/* 2. Модуль выбора фулфилмента */}
<div className="md:col-span-2"> <div>
<Label className="text-white/80 text-xs mb-1 block flex items-center gap-1">
<Building className="h-3 w-3" />
Фулфилмент
</Label>
<Select <Select
value={selectedFulfillmentOrg} value={selectedFulfillment}
onValueChange={(value) => { onValueChange={setSelectedFulfillment}
setSelectedFulfillmentOrg(value)
setSelectedServices([])
setSelectedConsumables([])
if (value) {
loadOrganizationServices(value)
loadOrganizationSupplies(value)
}
}}
> >
<SelectTrigger className="bg-white/20 border-0 text-white h-7 text-xs"> <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=улфилмент" /> <SelectValue placeholder=УЛФИЛМЕНТ ИВАНОВО" />
</SelectTrigger> </SelectTrigger>
<SelectContent> <SelectContent>
{fulfillmentOrgs.map((org: Organization) => ( {fulfillmentOrgs.map((org: Organization) => (
@ -563,64 +713,231 @@ export function DirectSupplyCreation({ onComplete, onCreateSupply, canCreateSupp
</Select> </Select>
</div> </div>
{/* Показатели */} {/* 3. Объём товаров */}
<div className="text-center"> <div>
<div className="text-white/80">Товаров</div> <Label className="text-white/80 text-xs mb-1 block">
<div className="font-bold text-white">{getTotalQuantity()}</div> Объём товаров
</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> </div>
<div className="text-center">
<div className="text-white/80">Стоимость</div> {/* 4. Грузовые места */}
<div className="font-bold text-white">{formatCurrency(getTotalItemsCost()).replace(' ₽', '₽')}</div> <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 className="text-center">
<div className="text-white/80">Услуги ФФ</div>
<div className="font-bold text-white">{formatCurrency(getServicesCost() + getConsumablesCost()).replace(' ₽', '₽')}</div>
</div> </div>
</div> </div>
</Card> </Card>
{/* Поиск и карточки */} {/* Блок поиска товаров - оптимизированное расположение */}
<Card className="bg-white/10 backdrop-blur border-white/20 p-2"> <Card className="bg-white/10 backdrop-blur-xl border border-white/20 p-3">
<div className="flex items-center space-x-2 mb-2"> <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 <Input
placeholder="Поиск товаров..." placeholder="Введите название товара, артикул или бренд..."
value={searchTerm} value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)} onChange={(e) => setSearchTerm(e.target.value)}
className="bg-white/5 border-white/20 text-white placeholder-white/50 h-7 text-xs flex-1" 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()} onKeyPress={(e) => e.key === "Enter" && searchCards()}
/> />
<Button onClick={searchCards} disabled={loading} variant="secondary" size="sm" className="h-7 px-2 text-xs"> <Button
<Search className="h-3 w-3" /> 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> </Button>
</div> </div>
</div>
<div className="flex space-x-2 overflow-x-auto pb-1"> {/* Карточки товаров - увеличенный размер и единообразность */}
{loading ? ( <div className="space-y-1">
[...Array(6)].map((_, i) => ( <div className="flex items-center justify-between">
<div key={i} className="flex-shrink-0 w-16 h-20 bg-white/5 rounded animate-pulse"></div> <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) => {
wbCards.map((card) => { const isInSupply = supplyItems.some(
const isInSupply = supplyItems.some(item => item.card.nmID === card.nmID) (item) => item.card.nmID === card.nmID
);
return ( return (
<div <div
key={card.nmID} key={card.nmID}
className={`flex-shrink-0 w-16 cursor-pointer transition-all hover:scale-105 relative ${isInSupply ? 'ring-1 ring-purple-400' : ''}`} 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)} 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 <img
src={WildberriesService.getCardImage(card, 'c246x328') || '/api/placeholder/64/80'} src={
WildberriesService.getCardImage(
card,
"c516x688"
) || "/api/placeholder/120/160"
}
alt={card.title} alt={card.title}
className="w-16 h-20 rounded object-cover" className="w-full h-full object-cover transition-transform group-hover:scale-110"
loading="lazy"
/> />
{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 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> </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> </div>
</Card> </Card>
@ -633,23 +950,35 @@ export function DirectSupplyCreation({ onComplete, onCreateSupply, canCreateSupp
<div className="text-white/80 mb-1">Услуги фулфилмента:</div> <div className="text-white/80 mb-1">Услуги фулфилмента:</div>
<div className="flex flex-wrap gap-1"> <div className="flex flex-wrap gap-1">
{organizationServices[selectedFulfillmentOrg] ? ( {organizationServices[selectedFulfillmentOrg] ? (
organizationServices[selectedFulfillmentOrg].map((service) => ( organizationServices[selectedFulfillmentOrg].map(
<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"> (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 <input
type="checkbox" type="checkbox"
checked={selectedServices.includes(service.id)} checked={selectedServices.includes(service.id)}
onChange={(e) => { onChange={(e) => {
if (e.target.checked) { if (e.target.checked) {
setSelectedServices(prev => [...prev, service.id]) setSelectedServices((prev) => [
...prev,
service.id,
]);
} else { } else {
setSelectedServices(prev => prev.filter(id => id !== service.id)) setSelectedServices((prev) =>
prev.filter((id) => id !== service.id)
);
} }
}} }}
className="w-3 h-3" className="w-3 h-3"
/> />
<span className="text-white text-xs">{service.name} ({service.price})</span> <span className="text-white text-xs">
{service.name} ({service.price})
</span>
</label> </label>
)) )
)
) : ( ) : (
<span className="text-white/60">Загрузка...</span> <span className="text-white/60">Загрузка...</span>
)} )}
@ -660,23 +989,35 @@ export function DirectSupplyCreation({ onComplete, onCreateSupply, canCreateSupp
<div className="text-white/80 mb-1">Расходные материалы:</div> <div className="text-white/80 mb-1">Расходные материалы:</div>
<div className="flex flex-wrap gap-1"> <div className="flex flex-wrap gap-1">
{organizationSupplies[selectedFulfillmentOrg] ? ( {organizationSupplies[selectedFulfillmentOrg] ? (
organizationSupplies[selectedFulfillmentOrg].map((supply) => ( organizationSupplies[selectedFulfillmentOrg].map(
<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"> (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 <input
type="checkbox" type="checkbox"
checked={selectedConsumables.includes(supply.id)} checked={selectedConsumables.includes(supply.id)}
onChange={(e) => { onChange={(e) => {
if (e.target.checked) { if (e.target.checked) {
setSelectedConsumables(prev => [...prev, supply.id]) setSelectedConsumables((prev) => [
...prev,
supply.id,
]);
} else { } else {
setSelectedConsumables(prev => prev.filter(id => id !== supply.id)) setSelectedConsumables((prev) =>
prev.filter((id) => id !== supply.id)
);
} }
}} }}
className="w-3 h-3" className="w-3 h-3"
/> />
<span className="text-white text-xs">{supply.name} ({supply.price})</span> <span className="text-white text-xs">
{supply.name} ({supply.price})
</span>
</label> </label>
)) )
)
) : ( ) : (
<span className="text-white/60">Загрузка...</span> <span className="text-white/60">Загрузка...</span>
)} )}
@ -686,15 +1027,17 @@ export function DirectSupplyCreation({ onComplete, onCreateSupply, canCreateSupp
</Card> </Card>
)} )}
{/* Компактная таблица товаров */} {/* Модуль товаров в поставке - новый дизайн */}
<Card className="bg-white/10 backdrop-blur border-white/20 p-2"> <Card className="bg-white/10 backdrop-blur border-white/20 p-3">
<div className="flex items-center justify-between mb-2"> <div className="flex items-center justify-between mb-3">
<span className="text-white font-medium text-sm">Товары в поставке</span> <span className="text-white font-medium text-sm">
Товары в поставке
</span>
<Button <Button
onClick={() => setShowSupplierModal(true)} onClick={() => setShowSupplierModal(true)}
variant="outline" variant="outline"
size="sm" size="sm"
className="bg-white/5 border-white/20 text-white hover:bg-white/10 h-6 px-2 text-xs" 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" /> <Plus className="h-3 w-3 mr-1" />
Поставщик Поставщик
@ -702,57 +1045,200 @@ export function DirectSupplyCreation({ onComplete, onCreateSupply, canCreateSupp
</div> </div>
{supplyItems.length === 0 ? ( {supplyItems.length === 0 ? (
<div className="text-center py-4"> <div className="text-center py-6">
<Package className="h-6 w-6 text-white/20 mx-auto mb-1" /> <Package className="h-8 w-8 text-white/20 mx-auto mb-2" />
<p className="text-white/60 text-xs">Добавьте товары из карточек выше</p> <p className="text-white/60 text-xs">
Добавьте товары из карточек выше
</p>
</div> </div>
) : ( ) : (
<div className="space-y-1"> <div className="space-y-4">
{supplyItems.map((item) => ( {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"> <Card
{/* Товар */} key={item.card.nmID}
<div className="col-span-4 flex items-center space-x-2"> className="bg-white/5 border-white/10 p-3"
<img >
src={WildberriesService.getCardImage(item.card, 'c246x328') || '/api/placeholder/24/24'} {/* Заголовок товара с кнопкой удаления */}
alt={item.card.title} <div className="flex items-center justify-between mb-3">
className="w-6 h-6 rounded object-cover" <div className="flex items-center space-x-2">
/> <div className="text-white font-medium text-sm line-clamp-1">
<div className="min-w-0"> {item.card.title}
<div className="text-white font-medium truncate text-xs">{item.card.title}</div> </div>
<div className="text-white/60 text-[10px]">Арт: {item.card.vendorCode}</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
className="grid grid-cols-8 gap-2"
style={{ marginBottom: "4px" }}
>
<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>
</div> </div>
{/* Количество */} {/* Оптимизированная сетка для 13" - все блоки в одну строку */}
<div className="col-span-2"> <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 <Input
type="number" type="number"
value={item.quantity} value={item.quantity}
onChange={(e) => updateSupplyItem(item.card.nmID, 'quantity', parseInt(e.target.value) || 0)} onChange={(e) =>
className="bg-purple-500 border-0 text-white text-center h-6 text-xs font-bold" 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" min="1"
/> />
</div> </div>
{/* Цена */} {/* Блок 4: Цена */}
<div className="col-span-2"> <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 <Input
type="number" type="number"
value={item.pricePerUnit || ''} value={item.pricePerUnit || ""}
onChange={(e) => updateSupplyItem(item.card.nmID, 'pricePerUnit', parseFloat(e.target.value) || 0)} onChange={(e) =>
className="bg-white/10 border-white/20 text-white text-center h-6 text-xs" updateSupplyItem(
placeholder="Цена" 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> </div>
{/* Поставщик */} {/* Блок 5: Услуги фулфилмента */}
<div className="col-span-3"> <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 <Select
value={item.supplierId} value={item.supplierId}
onValueChange={(value) => updateSupplyItem(item.card.nmID, 'supplierId', value)} onValueChange={(value) =>
updateSupplyItem(item.card.nmID, "supplierId", value)
}
> >
<SelectTrigger className="bg-white/5 border-white/20 text-white h-6 text-xs"> <SelectTrigger className="bg-white/20 border-white/20 text-white h-7 text-[10px]">
<SelectValue placeholder="Поставщик" /> <SelectValue placeholder="Выбрать" />
</SelectTrigger> </SelectTrigger>
<SelectContent> <SelectContent>
{suppliers.map((supplier) => ( {suppliers.map((supplier) => (
@ -764,21 +1250,73 @@ export function DirectSupplyCreation({ onComplete, onCreateSupply, canCreateSupp
</Select> </Select>
</div> </div>
{/* Сумма и удаление */} {/* Блок 7: Расходники фулфилмента */}
<div className="col-span-1 flex items-center justify-between"> <div className="bg-white/10 rounded-lg p-2 flex flex-col justify-center">
<span className="text-white font-bold text-xs"> <div className="space-y-1 max-h-16 overflow-y-auto">
{formatCurrency(item.totalPrice).replace(' ₽', '₽')} {selectedFulfillmentOrg &&
</span> organizationSupplies[selectedFulfillmentOrg] ? (
<Button organizationSupplies[selectedFulfillmentOrg]
onClick={() => removeFromSupply(item.card.nmID)} .slice(0, 2)
size="sm" .map((supply) => (
variant="ghost" <label
className="h-4 w-4 p-0 text-white/60 hover:text-red-400" key={supply.id}
className="flex items-center space-x-1 cursor-pointer"
> >
<X className="h-3 w-3" /> <input
</Button> 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>
</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> </div>
)} )}
@ -788,7 +1326,9 @@ export function DirectSupplyCreation({ onComplete, onCreateSupply, canCreateSupp
<Dialog open={showSupplierModal} onOpenChange={setShowSupplierModal}> <Dialog open={showSupplierModal} onOpenChange={setShowSupplierModal}>
<DialogContent className="glass-card border-white/10 max-w-md"> <DialogContent className="glass-card border-white/10 max-w-md">
<DialogHeader> <DialogHeader>
<DialogTitle className="text-white">Создать поставщика</DialogTitle> <DialogTitle className="text-white">
Создать поставщика
</DialogTitle>
</DialogHeader> </DialogHeader>
<div className="space-y-3"> <div className="space-y-3">
<div className="grid grid-cols-2 gap-3"> <div className="grid grid-cols-2 gap-3">
@ -796,7 +1336,12 @@ export function DirectSupplyCreation({ onComplete, onCreateSupply, canCreateSupp
<Label className="text-white/60 text-xs">Название *</Label> <Label className="text-white/60 text-xs">Название *</Label>
<Input <Input
value={newSupplier.name} value={newSupplier.name}
onChange={(e) => setNewSupplier(prev => ({ ...prev, name: e.target.value }))} onChange={(e) =>
setNewSupplier((prev) => ({
...prev,
name: e.target.value,
}))
}
className="bg-white/10 border-white/20 text-white h-8 text-xs" className="bg-white/10 border-white/20 text-white h-8 text-xs"
placeholder="Название" placeholder="Название"
/> />
@ -805,7 +1350,12 @@ export function DirectSupplyCreation({ onComplete, onCreateSupply, canCreateSupp
<Label className="text-white/60 text-xs">Имя *</Label> <Label className="text-white/60 text-xs">Имя *</Label>
<Input <Input
value={newSupplier.contactName} value={newSupplier.contactName}
onChange={(e) => setNewSupplier(prev => ({ ...prev, contactName: e.target.value }))} onChange={(e) =>
setNewSupplier((prev) => ({
...prev,
contactName: e.target.value,
}))
}
className="bg-white/10 border-white/20 text-white h-8 text-xs" className="bg-white/10 border-white/20 text-white h-8 text-xs"
placeholder="Имя" placeholder="Имя"
/> />
@ -817,7 +1367,12 @@ export function DirectSupplyCreation({ onComplete, onCreateSupply, canCreateSupp
<Label className="text-white/60 text-xs">Телефон *</Label> <Label className="text-white/60 text-xs">Телефон *</Label>
<Input <Input
value={newSupplier.phone} value={newSupplier.phone}
onChange={(e) => setNewSupplier(prev => ({ ...prev, phone: e.target.value }))} onChange={(e) =>
setNewSupplier((prev) => ({
...prev,
phone: e.target.value,
}))
}
className="bg-white/10 border-white/20 text-white h-8 text-xs" className="bg-white/10 border-white/20 text-white h-8 text-xs"
placeholder="+7 999 123-45-67" placeholder="+7 999 123-45-67"
/> />
@ -826,7 +1381,9 @@ export function DirectSupplyCreation({ onComplete, onCreateSupply, canCreateSupp
<Label className="text-white/60 text-xs">Рынок</Label> <Label className="text-white/60 text-xs">Рынок</Label>
<Select <Select
value={newSupplier.market} value={newSupplier.market}
onValueChange={(value) => setNewSupplier(prev => ({ ...prev, market: value }))} onValueChange={(value) =>
setNewSupplier((prev) => ({ ...prev, market: value }))
}
> >
<SelectTrigger className="bg-white/10 border-white/20 text-white h-8 text-xs"> <SelectTrigger className="bg-white/10 border-white/20 text-white h-8 text-xs">
<SelectValue placeholder="Рынок" /> <SelectValue placeholder="Рынок" />
@ -847,7 +1404,12 @@ export function DirectSupplyCreation({ onComplete, onCreateSupply, canCreateSupp
<Label className="text-white/60 text-xs">Адрес</Label> <Label className="text-white/60 text-xs">Адрес</Label>
<Input <Input
value={newSupplier.address} value={newSupplier.address}
onChange={(e) => setNewSupplier(prev => ({ ...prev, address: e.target.value }))} onChange={(e) =>
setNewSupplier((prev) => ({
...prev,
address: e.target.value,
}))
}
className="bg-white/10 border-white/20 text-white h-8 text-xs" className="bg-white/10 border-white/20 text-white h-8 text-xs"
placeholder="Адрес" placeholder="Адрес"
/> />
@ -856,7 +1418,12 @@ export function DirectSupplyCreation({ onComplete, onCreateSupply, canCreateSupp
<Label className="text-white/60 text-xs">Место</Label> <Label className="text-white/60 text-xs">Место</Label>
<Input <Input
value={newSupplier.place} value={newSupplier.place}
onChange={(e) => setNewSupplier(prev => ({ ...prev, place: e.target.value }))} onChange={(e) =>
setNewSupplier((prev) => ({
...prev,
place: e.target.value,
}))
}
className="bg-white/10 border-white/20 text-white h-8 text-xs" className="bg-white/10 border-white/20 text-white h-8 text-xs"
placeholder="Павильон/место" placeholder="Павильон/место"
/> />
@ -867,7 +1434,12 @@ export function DirectSupplyCreation({ onComplete, onCreateSupply, canCreateSupp
<Label className="text-white/60 text-xs">Телеграм</Label> <Label className="text-white/60 text-xs">Телеграм</Label>
<Input <Input
value={newSupplier.telegram} value={newSupplier.telegram}
onChange={(e) => setNewSupplier(prev => ({ ...prev, telegram: e.target.value }))} onChange={(e) =>
setNewSupplier((prev) => ({
...prev,
telegram: e.target.value,
}))
}
className="bg-white/10 border-white/20 text-white h-8 text-xs" className="bg-white/10 border-white/20 text-white h-8 text-xs"
placeholder="@username" placeholder="@username"
/> />
@ -892,5 +1464,6 @@ export function DirectSupplyCreation({ onComplete, onCreateSupply, canCreateSupp
</DialogContent> </DialogContent>
</Dialog> </Dialog>
</div> </div>
) </>
);
} }