Добавлен новый модель SupplySupplier в схему Prisma и реализована логика для работы с поставщиками в компонентах создания поставок. Обновлены компоненты CreateSupplyPage и DirectSupplyCreation для интеграции новых функций, включая обработку поставщиков и расчет логистики. Оптимизирован интерфейс с использованием новых компонентов и улучшена логика отображения данных.
This commit is contained in:
@ -5,61 +5,62 @@ import { Sidebar } from "@/components/dashboard/sidebar";
|
||||
import { useSidebar } from "@/hooks/useSidebar";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { DirectSupplyCreation } from "./direct-supply-creation";
|
||||
import { WholesalerProductsPage } from "./wholesaler-products-page";
|
||||
import { TabsHeader } from "./tabs-header";
|
||||
import { WholesalerGrid } from "./wholesaler-grid";
|
||||
import { CartSummary } from "./cart-summary";
|
||||
import { FloatingCart } from "./floating-cart";
|
||||
import { Card } from "@/components/ui/card";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import {
|
||||
WholesalerForCreation,
|
||||
WholesalerProduct,
|
||||
SelectedProduct,
|
||||
CounterpartyWholesaler,
|
||||
} from "./types";
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/components/ui/select";
|
||||
import { useQuery } from "@apollo/client";
|
||||
import { GET_MY_COUNTERPARTIES, GET_ALL_PRODUCTS } from "@/graphql/queries";
|
||||
import { apolloClient } from "@/lib/apollo-client";
|
||||
import { GET_MY_COUNTERPARTIES, GET_ORGANIZATION_LOGISTICS } from "@/graphql/queries";
|
||||
import {
|
||||
ArrowLeft,
|
||||
Package,
|
||||
CalendarIcon,
|
||||
Building,
|
||||
} from "lucide-react";
|
||||
|
||||
// Компонент создания поставки товаров с новым интерфейсом
|
||||
|
||||
interface Organization {
|
||||
id: string;
|
||||
name?: string;
|
||||
fullName?: string;
|
||||
type: string;
|
||||
}
|
||||
|
||||
export function CreateSupplyPage() {
|
||||
const router = useRouter();
|
||||
const { getSidebarMargin } = useSidebar();
|
||||
const [activeTab, setActiveTab] = useState<"cards" | "wholesaler">("cards");
|
||||
const [selectedWholesaler, setSelectedWholesaler] =
|
||||
useState<WholesalerForCreation | null>(null);
|
||||
const [selectedProducts, setSelectedProducts] = useState<SelectedProduct[]>(
|
||||
[]
|
||||
);
|
||||
const [showSummary, setShowSummary] = useState(false);
|
||||
const [searchQuery, setSearchQuery] = useState("");
|
||||
const [canCreateSupply, setCanCreateSupply] = useState(false);
|
||||
const [isCreatingSupply, setIsCreatingSupply] = useState(false);
|
||||
|
||||
// Загружаем контрагентов-оптовиков
|
||||
const { data: counterpartiesData, loading: counterpartiesLoading } = useQuery(
|
||||
GET_MY_COUNTERPARTIES
|
||||
// Состояния для полей формы
|
||||
const [deliveryDate, setDeliveryDate] = useState<string>("");
|
||||
const [selectedFulfillment, setSelectedFulfillment] = useState<string>("");
|
||||
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 [selectedServicesCost, setSelectedServicesCost] = useState<number>(0);
|
||||
const [selectedConsumablesCost, setSelectedConsumablesCost] = useState<number>(0);
|
||||
const [hasItemsInSupply, setHasItemsInSupply] = useState<boolean>(false);
|
||||
|
||||
// Загружаем контрагентов-фулфилментов
|
||||
const { data: counterpartiesData } = useQuery(GET_MY_COUNTERPARTIES);
|
||||
|
||||
// Фильтруем только фулфилмент организации
|
||||
const fulfillmentOrgs = (counterpartiesData?.myCounterparties || []).filter(
|
||||
(org: Organization) => org.type === "FULFILLMENT"
|
||||
);
|
||||
|
||||
// Загружаем товары для выбранного оптовика
|
||||
const { data: productsData, loading: productsLoading } = useQuery(
|
||||
GET_ALL_PRODUCTS,
|
||||
{
|
||||
skip: !selectedWholesaler,
|
||||
variables: { search: null, category: null },
|
||||
}
|
||||
);
|
||||
|
||||
// Фильтруем только оптовиков
|
||||
const wholesalers: CounterpartyWholesaler[] = (
|
||||
counterpartiesData?.myCounterparties || []
|
||||
).filter((org: { type: string }) => org.type === "WHOLESALE");
|
||||
|
||||
// Фильтруем товары по выбранному оптовику
|
||||
const wholesalerProducts: WholesalerProduct[] = selectedWholesaler
|
||||
? (productsData?.allProducts || []).filter(
|
||||
(product: { organization: { id: string } }) =>
|
||||
product.organization.id === selectedWholesaler.id
|
||||
)
|
||||
: [];
|
||||
|
||||
const formatCurrency = (amount: number) => {
|
||||
return new Intl.NumberFormat("ru-RU", {
|
||||
style: "currency",
|
||||
@ -68,96 +69,87 @@ export function CreateSupplyPage() {
|
||||
}).format(amount);
|
||||
};
|
||||
|
||||
const updateProductQuantity = (productId: string, quantity: number) => {
|
||||
const product = wholesalerProducts.find((p) => p.id === productId);
|
||||
if (!product || !selectedWholesaler) return;
|
||||
// Функция для обновления цены товаров из поставки
|
||||
const handleItemsUpdate = (totalItemsPrice: number) => {
|
||||
setGoodsPrice(totalItemsPrice);
|
||||
};
|
||||
|
||||
setSelectedProducts((prev) => {
|
||||
const existing = prev.find(
|
||||
(p) => p.id === productId && p.wholesalerId === selectedWholesaler.id
|
||||
// Функция для обновления статуса наличия товаров
|
||||
const handleItemsCountChange = (hasItems: boolean) => {
|
||||
setHasItemsInSupply(hasItems);
|
||||
};
|
||||
|
||||
// Функция для обновления объема товаров из поставки
|
||||
const handleVolumeUpdate = (totalVolume: number) => {
|
||||
setGoodsVolume(totalVolume);
|
||||
// После обновления объема пересчитываем логистику (если есть поставщик)
|
||||
// calculateLogisticsPrice будет вызван из handleSuppliersUpdate
|
||||
};
|
||||
|
||||
// Функция для обновления информации о поставщиках (для расчета логистики)
|
||||
const handleSuppliersUpdate = (suppliersData: any[]) => {
|
||||
// Находим рынок из выбранного поставщика
|
||||
const selectedSupplier = suppliersData.find(supplier => supplier.selected);
|
||||
const supplierMarket = selectedSupplier?.market;
|
||||
|
||||
console.log("Обновление поставщиков:", { selectedSupplier, supplierMarket, volume: goodsVolume });
|
||||
|
||||
// Пересчитываем логистику с учетом рынка поставщика
|
||||
calculateLogisticsPrice(goodsVolume, supplierMarket);
|
||||
};
|
||||
|
||||
// Функция для расчета логистики по рынку поставщика и объему
|
||||
const calculateLogisticsPrice = async (volume: number, supplierMarket?: string) => {
|
||||
// Логистика рассчитывается ТОЛЬКО если есть:
|
||||
// 1. Выбранный фулфилмент
|
||||
// 2. Объем товаров > 0
|
||||
// 3. Рынок поставщика (откуда везти)
|
||||
if (!selectedFulfillment || !volume || volume <= 0 || !supplierMarket) {
|
||||
setLogisticsPrice(0);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
console.log(`Расчет логистики: ${supplierMarket} → ${selectedFulfillment}, объем: ${volume.toFixed(4)} м³`);
|
||||
|
||||
// Получаем логистику выбранного фулфилмента из БД
|
||||
const { data: logisticsData } = await apolloClient.query({
|
||||
query: GET_ORGANIZATION_LOGISTICS,
|
||||
variables: { organizationId: selectedFulfillment },
|
||||
fetchPolicy: 'network-only'
|
||||
});
|
||||
|
||||
const logistics = logisticsData?.organizationLogistics || [];
|
||||
console.log(`Логистика фулфилмента ${selectedFulfillment}:`, logistics);
|
||||
|
||||
// Ищем логистику для данного рынка
|
||||
const logisticsRoute = logistics.find((route: any) =>
|
||||
route.fromLocation.toLowerCase().includes(supplierMarket.toLowerCase()) ||
|
||||
supplierMarket.toLowerCase().includes(route.fromLocation.toLowerCase())
|
||||
);
|
||||
|
||||
if (quantity === 0) {
|
||||
return prev.filter(
|
||||
(p) =>
|
||||
!(p.id === productId && p.wholesalerId === selectedWholesaler.id)
|
||||
);
|
||||
if (!logisticsRoute) {
|
||||
console.log(`Логистика для рынка "${supplierMarket}" не найдена`);
|
||||
setLogisticsPrice(0);
|
||||
return;
|
||||
}
|
||||
|
||||
if (existing) {
|
||||
return prev.map((p) =>
|
||||
p.id === productId && p.wholesalerId === selectedWholesaler.id
|
||||
? { ...p, selectedQuantity: quantity }
|
||||
: p
|
||||
);
|
||||
} else {
|
||||
return [
|
||||
...prev,
|
||||
{
|
||||
...product,
|
||||
selectedQuantity: quantity,
|
||||
wholesalerId: selectedWholesaler.id,
|
||||
wholesalerName: selectedWholesaler.name,
|
||||
},
|
||||
];
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const getTotalAmount = () => {
|
||||
return selectedProducts.reduce((sum, product) => {
|
||||
const discountedPrice = product.discount
|
||||
? product.price * (1 - product.discount / 100)
|
||||
: product.price;
|
||||
return sum + discountedPrice * product.selectedQuantity;
|
||||
}, 0);
|
||||
};
|
||||
|
||||
const getTotalItems = () => {
|
||||
return selectedProducts.reduce(
|
||||
(sum, product) => sum + product.selectedQuantity,
|
||||
0
|
||||
);
|
||||
};
|
||||
|
||||
const handleCreateSupply = () => {
|
||||
if (activeTab === "cards") {
|
||||
console.log("Создание поставки с карточками Wildberries");
|
||||
} else {
|
||||
console.log("Создание поставки с товарами:", selectedProducts);
|
||||
}
|
||||
router.push("/supplies");
|
||||
};
|
||||
|
||||
const handleGoBack = () => {
|
||||
if (selectedWholesaler) {
|
||||
setSelectedWholesaler(null);
|
||||
setShowSummary(false);
|
||||
} else {
|
||||
router.push("/supplies");
|
||||
// Выбираем цену в зависимости от объема
|
||||
const pricePerM3 = volume <= 1 ? logisticsRoute.priceUnder1m3 : logisticsRoute.priceOver1m3;
|
||||
const calculatedPrice = volume * pricePerM3;
|
||||
|
||||
console.log(`Найдена логистика: ${logisticsRoute.fromLocation} → ${logisticsRoute.toLocation}`);
|
||||
console.log(`Цена: ${pricePerM3}₽/м³ (${volume <= 1 ? 'до 1м³' : 'больше 1м³'}) × ${volume.toFixed(4)}м³ = ${calculatedPrice.toFixed(2)}₽`);
|
||||
|
||||
setLogisticsPrice(calculatedPrice);
|
||||
} catch (error) {
|
||||
console.error("Error calculating logistics price:", error);
|
||||
setLogisticsPrice(0);
|
||||
}
|
||||
};
|
||||
|
||||
const handleRemoveProduct = (productId: string, wholesalerId: string) => {
|
||||
setSelectedProducts((prev) =>
|
||||
prev.filter(
|
||||
(p) => !(p.id === productId && p.wholesalerId === wholesalerId)
|
||||
)
|
||||
);
|
||||
};
|
||||
|
||||
const handleCartQuantityChange = (
|
||||
productId: string,
|
||||
wholesalerId: string,
|
||||
quantity: number
|
||||
) => {
|
||||
setSelectedProducts((prev) =>
|
||||
prev.map((p) =>
|
||||
p.id === productId && p.wholesalerId === wholesalerId
|
||||
? { ...p, selectedQuantity: quantity }
|
||||
: p
|
||||
)
|
||||
);
|
||||
const getTotalSum = () => {
|
||||
return goodsPrice + selectedServicesCost + selectedConsumablesCost + logisticsPrice;
|
||||
};
|
||||
|
||||
const handleSupplyComplete = () => {
|
||||
@ -172,100 +164,231 @@ export function CreateSupplyPage() {
|
||||
setCanCreateSupply(canCreate);
|
||||
};
|
||||
|
||||
// Пересчитываем логистику при изменении фулфилмента (если есть поставщик)
|
||||
React.useEffect(() => {
|
||||
// Логистика пересчитается автоматически через handleSuppliersUpdate
|
||||
// когда будет выбран поставщик с рынком
|
||||
}, [selectedFulfillment, goodsVolume]);
|
||||
|
||||
const handleSupplyCompleted = () => {
|
||||
setIsCreatingSupply(false);
|
||||
handleSupplyComplete();
|
||||
};
|
||||
|
||||
// Рендер страницы товаров оптовика
|
||||
if (selectedWholesaler && activeTab === "wholesaler") {
|
||||
return (
|
||||
<WholesalerProductsPage
|
||||
selectedWholesaler={selectedWholesaler}
|
||||
products={wholesalerProducts}
|
||||
selectedProducts={selectedProducts}
|
||||
onQuantityChange={updateProductQuantity}
|
||||
onBack={handleGoBack}
|
||||
onCreateSupply={handleCreateSupply}
|
||||
formatCurrency={formatCurrency}
|
||||
showSummary={showSummary}
|
||||
setShowSummary={setShowSummary}
|
||||
loading={productsLoading}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
// Главная страница с табами
|
||||
// Главная страница с табами в новом стиле интерфейса
|
||||
return (
|
||||
<div className="h-screen flex overflow-hidden">
|
||||
<Sidebar />
|
||||
<main
|
||||
className={`flex-1 ${getSidebarMargin()} overflow-hidden transition-all duration-300`}
|
||||
style={{ padding: '1rem' }}
|
||||
className={`flex-1 ${getSidebarMargin()} overflow-auto transition-all duration-300`}
|
||||
>
|
||||
<div className="flex flex-col" style={{ height: 'calc(100vh - 2rem)' }}>
|
||||
<TabsHeader
|
||||
activeTab={activeTab}
|
||||
onTabChange={setActiveTab}
|
||||
onBack={() => router.push("/supplies")}
|
||||
cartInfo={
|
||||
activeTab === "wholesaler" && selectedProducts.length > 0
|
||||
? {
|
||||
itemCount: selectedProducts.length,
|
||||
totalAmount: getTotalAmount(),
|
||||
formatCurrency,
|
||||
}
|
||||
: undefined
|
||||
}
|
||||
onCartClick={() => setShowSummary(true)}
|
||||
onCreateSupply={handleCreateSupplyClick}
|
||||
canCreateSupply={canCreateSupply}
|
||||
isCreatingSupply={isCreatingSupply}
|
||||
/>
|
||||
<div className="min-h-full w-full flex flex-col px-3 py-2">
|
||||
{/* Заголовок */}
|
||||
<div className="flex items-center justify-between mb-3 flex-shrink-0">
|
||||
<div>
|
||||
<h1 className="text-xl font-bold text-white mb-1">
|
||||
Создание поставки товаров
|
||||
</h1>
|
||||
<p className="text-white/60 text-sm">
|
||||
Выберите карточки товаров Wildberries для создания поставки
|
||||
</p>
|
||||
</div>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => router.push("/supplies")}
|
||||
className="text-white/60 hover:text-white hover:bg-white/10 text-sm"
|
||||
>
|
||||
<ArrowLeft className="h-4 w-4 mr-1" />
|
||||
Назад
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* Контент карточек - новый компонент прямого создания поставки */}
|
||||
{activeTab === "cards" && (
|
||||
<div className="flex-1 flex flex-col overflow-hidden min-h-0">
|
||||
{/* Основной контент - карточки Wildberries */}
|
||||
<div className="flex-1 flex gap-3 min-h-0">
|
||||
{/* Левая колонка - карточки товаров */}
|
||||
<div className="flex-1 min-h-0">
|
||||
<DirectSupplyCreation
|
||||
onComplete={handleSupplyCompleted}
|
||||
onCreateSupply={handleCreateSupplyClick}
|
||||
canCreateSupply={canCreateSupply}
|
||||
isCreatingSupply={isCreatingSupply}
|
||||
onCanCreateSupplyChange={handleCanCreateSupplyChange}
|
||||
selectedFulfillmentId={selectedFulfillment}
|
||||
onServicesCostChange={setSelectedServicesCost}
|
||||
onItemsPriceChange={handleItemsUpdate}
|
||||
onItemsCountChange={handleItemsCountChange}
|
||||
onConsumablesCostChange={setSelectedConsumablesCost}
|
||||
onVolumeChange={handleVolumeUpdate}
|
||||
onSuppliersChange={handleSuppliersUpdate}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Контент оптовиков */}
|
||||
{activeTab === "wholesaler" && (
|
||||
<div>
|
||||
<CartSummary
|
||||
selectedProducts={selectedProducts}
|
||||
onQuantityChange={handleCartQuantityChange}
|
||||
onRemoveProduct={handleRemoveProduct}
|
||||
onCreateSupply={handleCreateSupply}
|
||||
onToggleVisibility={() => setShowSummary(false)}
|
||||
formatCurrency={formatCurrency}
|
||||
visible={showSummary && selectedProducts.length > 0}
|
||||
/>
|
||||
{/* Правая колонка - Форма поставки */}
|
||||
<div className="w-80 flex-shrink-0">
|
||||
<Card className="bg-white/10 backdrop-blur-xl border-white/20 p-4 sticky top-0">
|
||||
<h3 className="text-white font-semibold mb-4 flex items-center text-sm">
|
||||
<Package className="h-4 w-4 mr-2" />
|
||||
Параметры поставки
|
||||
</h3>
|
||||
|
||||
<WholesalerGrid
|
||||
wholesalers={wholesalers}
|
||||
onWholesalerSelect={setSelectedWholesaler}
|
||||
searchQuery={searchQuery}
|
||||
onSearchChange={setSearchQuery}
|
||||
loading={counterpartiesLoading}
|
||||
/>
|
||||
{/* Первая строка */}
|
||||
<div className="space-y-3 mb-4">
|
||||
{/* 1. Модуль выбора даты */}
|
||||
<div>
|
||||
<Label className="text-white/80 text-xs mb-1 block flex items-center gap-1">
|
||||
<CalendarIcon className="h-3 w-3" />
|
||||
Дата
|
||||
</Label>
|
||||
<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-3 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>
|
||||
|
||||
<FloatingCart
|
||||
itemCount={selectedProducts.length}
|
||||
totalAmount={getTotalAmount()}
|
||||
formatCurrency={formatCurrency}
|
||||
onClick={() => setShowSummary(true)}
|
||||
visible={selectedProducts.length > 0 && !showSummary}
|
||||
/>
|
||||
{/* 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
|
||||
value={selectedFulfillment}
|
||||
onValueChange={(value) => {
|
||||
console.log('Выбран фулфилмент:', value);
|
||||
setSelectedFulfillment(value);
|
||||
}}
|
||||
>
|
||||
<SelectTrigger className="w-full h-8 py-0 px-3 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>
|
||||
<div className="h-8 bg-white/10 border border-white/20 rounded-lg flex items-center px-3">
|
||||
<span className="text-white/80 text-xs">
|
||||
{goodsVolume > 0 ? `${goodsVolume.toFixed(2)} м³` : 'Рассчитывается автоматически'}
|
||||
</span>
|
||||
</div>
|
||||
</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="space-y-3 mb-4">
|
||||
{/* 5. Цена товаров (автоматически) */}
|
||||
<div>
|
||||
<Label className="text-white/80 text-xs mb-1 block">
|
||||
Цена товаров
|
||||
</Label>
|
||||
<div className="h-8 bg-white/10 border border-white/20 rounded-lg flex items-center px-3">
|
||||
<span className="text-white/80 text-xs font-medium">
|
||||
{goodsPrice > 0 ? formatCurrency(goodsPrice) : 'Рассчитывается автоматически'}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 6. Цена услуг фулфилмента (автоматически) */}
|
||||
<div>
|
||||
<Label className="text-white/80 text-xs mb-1 block">
|
||||
Цена услуг фулфилмента
|
||||
</Label>
|
||||
<div className="h-8 bg-green-500/20 border border-green-400/30 rounded-lg flex items-center px-3">
|
||||
<span className="text-green-400 text-xs font-medium">
|
||||
{selectedServicesCost > 0 ? formatCurrency(selectedServicesCost) : 'Выберите услуги'}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 7. Цена расходников фулфилмента (автоматически) */}
|
||||
<div>
|
||||
<Label className="text-white/80 text-xs mb-1 block">
|
||||
Цена расходников фулфилмента
|
||||
</Label>
|
||||
<div className="h-8 bg-orange-500/20 border border-orange-400/30 rounded-lg flex items-center px-3">
|
||||
<span className="text-orange-400 text-xs font-medium">
|
||||
{selectedConsumablesCost > 0 ? formatCurrency(selectedConsumablesCost) : 'Выберите расходники'}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 8. Цена логистики (автоматически) */}
|
||||
<div>
|
||||
<Label className="text-white/80 text-xs mb-1 block">
|
||||
Логистика до фулфилмента
|
||||
</Label>
|
||||
<div className="h-8 bg-blue-500/20 border border-blue-400/30 rounded-lg flex items-center px-3">
|
||||
<span className="text-blue-400 text-xs font-medium">
|
||||
{logisticsPrice > 0 ? formatCurrency(logisticsPrice) : 'Выберите поставщика'}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
</div>
|
||||
|
||||
{/* 9. Итоговая сумма */}
|
||||
<div className="border-t border-white/20 pt-4 mb-4">
|
||||
<Label className="text-white/80 text-xs mb-2 block">
|
||||
Итого
|
||||
</Label>
|
||||
<div className="h-10 bg-gradient-to-r from-green-500/20 to-blue-500/20 rounded-lg flex items-center justify-center border border-green-400/30">
|
||||
<span className="text-white font-bold text-lg">
|
||||
{formatCurrency(getTotalSum())}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 10. Кнопка создания поставки */}
|
||||
<Button
|
||||
onClick={handleCreateSupplyClick}
|
||||
disabled={!canCreateSupply || isCreatingSupply || !deliveryDate || !selectedFulfillment || !hasItemsInSupply}
|
||||
className={`w-full h-12 text-sm font-medium transition-all duration-200 ${
|
||||
canCreateSupply && deliveryDate && selectedFulfillment && hasItemsInSupply && !isCreatingSupply
|
||||
? 'bg-gradient-to-r from-purple-500 to-blue-500 hover:from-purple-600 hover:to-blue-600 text-white'
|
||||
: 'bg-gray-500/20 text-gray-400 cursor-not-allowed'
|
||||
}`}
|
||||
>
|
||||
{isCreatingSupply ? (
|
||||
<div className="flex items-center space-x-2">
|
||||
<div className="animate-spin rounded-full h-4 w-4 border border-white/30 border-t-white"></div>
|
||||
<span>Создание...</span>
|
||||
</div>
|
||||
) : (
|
||||
'Создать поставку'
|
||||
)}
|
||||
</Button>
|
||||
</Card>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
|
@ -6,6 +6,8 @@ import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { PhoneInput } from "@/components/ui/phone-input";
|
||||
import { formatPhoneInput, isValidPhone, formatNameInput } from "@/lib/input-masks";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
@ -47,8 +49,9 @@ import {
|
||||
GET_MY_COUNTERPARTIES,
|
||||
GET_COUNTERPARTY_SERVICES,
|
||||
GET_COUNTERPARTY_SUPPLIES,
|
||||
GET_SUPPLY_SUPPLIERS,
|
||||
} from "@/graphql/queries";
|
||||
import { CREATE_WILDBERRIES_SUPPLY } from "@/graphql/mutations";
|
||||
import { CREATE_WILDBERRIES_SUPPLY, CREATE_SUPPLY_SUPPLIER } from "@/graphql/mutations";
|
||||
import { toast } from "sonner";
|
||||
import { format } from "date-fns";
|
||||
import { ru } from "date-fns/locale";
|
||||
@ -70,6 +73,7 @@ interface SupplyItem {
|
||||
pricePerUnit: number;
|
||||
totalPrice: number;
|
||||
supplierId: string;
|
||||
priceType: "perUnit" | "total"; // за штуку или за общее количество
|
||||
}
|
||||
|
||||
interface Organization {
|
||||
@ -103,6 +107,13 @@ interface DirectSupplyCreationProps {
|
||||
canCreateSupply: boolean;
|
||||
isCreatingSupply: boolean;
|
||||
onCanCreateSupplyChange?: (canCreate: boolean) => void;
|
||||
selectedFulfillmentId?: string;
|
||||
onServicesCostChange?: (cost: number) => void;
|
||||
onItemsPriceChange?: (totalPrice: number) => void;
|
||||
onItemsCountChange?: (hasItems: boolean) => void;
|
||||
onConsumablesCostChange?: (cost: number) => void;
|
||||
onVolumeChange?: (totalVolume: number) => void;
|
||||
onSuppliersChange?: (suppliers: any[]) => void;
|
||||
}
|
||||
|
||||
export function DirectSupplyCreation({
|
||||
@ -111,6 +122,13 @@ export function DirectSupplyCreation({
|
||||
canCreateSupply,
|
||||
isCreatingSupply,
|
||||
onCanCreateSupplyChange,
|
||||
selectedFulfillmentId,
|
||||
onServicesCostChange,
|
||||
onItemsPriceChange,
|
||||
onItemsCountChange,
|
||||
onConsumablesCostChange,
|
||||
onVolumeChange,
|
||||
onSuppliersChange,
|
||||
}: DirectSupplyCreationProps) {
|
||||
const { user } = useAuth();
|
||||
|
||||
@ -152,6 +170,12 @@ export function DirectSupplyCreation({
|
||||
place: "",
|
||||
telegram: "",
|
||||
});
|
||||
const [supplierErrors, setSupplierErrors] = useState({
|
||||
name: "",
|
||||
contactName: "",
|
||||
phone: "",
|
||||
telegram: "",
|
||||
});
|
||||
|
||||
// Данные для фулфилмента
|
||||
const [organizationServices, setOrganizationServices] = useState<{
|
||||
@ -163,8 +187,9 @@ export function DirectSupplyCreation({
|
||||
|
||||
// Загружаем контрагентов-фулфилментов
|
||||
const { data: counterpartiesData } = useQuery(GET_MY_COUNTERPARTIES);
|
||||
const { data: suppliersData, refetch: refetchSuppliers } = useQuery(GET_SUPPLY_SUPPLIERS);
|
||||
|
||||
// Мутация для создания поставки
|
||||
// Мутации
|
||||
const [createSupply, { loading: creatingSupply }] = useMutation(
|
||||
CREATE_WILDBERRIES_SUPPLY,
|
||||
{
|
||||
@ -182,6 +207,44 @@ export function DirectSupplyCreation({
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
const [createSupplierMutation, { loading: creatingSupplier }] = useMutation(
|
||||
CREATE_SUPPLY_SUPPLIER,
|
||||
{
|
||||
onCompleted: (data) => {
|
||||
if (data.createSupplySupplier.success) {
|
||||
toast.success("Поставщик добавлен успешно!");
|
||||
|
||||
// Обновляем список поставщиков из БД
|
||||
refetchSuppliers();
|
||||
|
||||
// Очищаем форму
|
||||
setNewSupplier({
|
||||
name: "",
|
||||
contactName: "",
|
||||
phone: "",
|
||||
market: "",
|
||||
address: "",
|
||||
place: "",
|
||||
telegram: "",
|
||||
});
|
||||
setSupplierErrors({
|
||||
name: "",
|
||||
contactName: "",
|
||||
phone: "",
|
||||
telegram: "",
|
||||
});
|
||||
setShowSupplierModal(false);
|
||||
} else {
|
||||
toast.error(data.createSupplySupplier.message || "Ошибка при добавлении поставщика");
|
||||
}
|
||||
},
|
||||
onError: (error) => {
|
||||
toast.error("Ошибка при создании поставщика");
|
||||
console.error("Error creating supplier:", error);
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
// Моковые данные товаров для демонстрации
|
||||
const getMockCards = (): WildberriesCard[] => [
|
||||
@ -196,6 +259,13 @@ export function DirectSupplyCreation({
|
||||
countryProduction: "Россия",
|
||||
supplierVendorCode: "SUPPLIER-001",
|
||||
mediaFiles: ["/api/placeholder/400/400"],
|
||||
dimensions: {
|
||||
length: 30, // 30 см
|
||||
width: 25, // 25 см
|
||||
height: 5, // 5 см
|
||||
weightBrutto: 0.3, // 300г
|
||||
isValid: true
|
||||
},
|
||||
sizes: [
|
||||
{
|
||||
chrtID: 123456,
|
||||
@ -218,6 +288,13 @@ export function DirectSupplyCreation({
|
||||
countryProduction: "Россия",
|
||||
supplierVendorCode: "SUPPLIER-002",
|
||||
mediaFiles: ["/api/placeholder/400/403"],
|
||||
dimensions: {
|
||||
length: 35, // 35 см
|
||||
width: 28, // 28 см
|
||||
height: 6, // 6 см
|
||||
weightBrutto: 0.4, // 400г
|
||||
isValid: true
|
||||
},
|
||||
sizes: [
|
||||
{
|
||||
chrtID: 987654,
|
||||
@ -324,6 +401,46 @@ export function DirectSupplyCreation({
|
||||
loadCards();
|
||||
}, [user]);
|
||||
|
||||
// Загружаем услуги и расходники при выборе фулфилмента
|
||||
useEffect(() => {
|
||||
if (selectedFulfillmentId) {
|
||||
console.log('Загружаем услуги и расходники для фулфилмента:', selectedFulfillmentId);
|
||||
loadOrganizationServices(selectedFulfillmentId);
|
||||
loadOrganizationSupplies(selectedFulfillmentId);
|
||||
}
|
||||
}, [selectedFulfillmentId]);
|
||||
|
||||
// Уведомляем об изменении стоимости услуг
|
||||
useEffect(() => {
|
||||
if (onServicesCostChange) {
|
||||
const servicesCost = getServicesCost();
|
||||
onServicesCostChange(servicesCost);
|
||||
}
|
||||
}, [selectedServices, selectedFulfillmentId, onServicesCostChange]);
|
||||
|
||||
// Уведомляем об изменении общей стоимости товаров
|
||||
useEffect(() => {
|
||||
if (onItemsPriceChange) {
|
||||
const totalItemsPrice = getTotalItemsCost();
|
||||
onItemsPriceChange(totalItemsPrice);
|
||||
}
|
||||
}, [supplyItems, onItemsPriceChange]);
|
||||
|
||||
// Уведомляем об изменении количества товаров
|
||||
useEffect(() => {
|
||||
if (onItemsCountChange) {
|
||||
onItemsCountChange(supplyItems.length > 0);
|
||||
}
|
||||
}, [supplyItems.length, onItemsCountChange]);
|
||||
|
||||
// Уведомляем об изменении стоимости расходников
|
||||
useEffect(() => {
|
||||
if (onConsumablesCostChange) {
|
||||
const consumablesCost = getConsumablesCost();
|
||||
onConsumablesCostChange(consumablesCost);
|
||||
}
|
||||
}, [selectedConsumables, selectedFulfillmentId, supplyItems.length, onConsumablesCostChange]);
|
||||
|
||||
const loadCards = async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
@ -344,9 +461,21 @@ export function DirectSupplyCreation({
|
||||
|
||||
if (apiToken) {
|
||||
console.log("Загружаем карточки из WB API...");
|
||||
const cards = await WildberriesService.getAllCards(apiToken, 20);
|
||||
const cards = await WildberriesService.getAllCards(apiToken, 500);
|
||||
|
||||
// Логируем информацию о размерах товаров
|
||||
cards.forEach(card => {
|
||||
if (card.dimensions) {
|
||||
const volume = (card.dimensions.length / 100) * (card.dimensions.width / 100) * (card.dimensions.height / 100);
|
||||
console.log(`WB API: Карточка ${card.nmID} - размеры: ${card.dimensions.length}x${card.dimensions.width}x${card.dimensions.height} см, объем: ${volume.toFixed(6)} м³`);
|
||||
} else {
|
||||
console.log(`WB API: Карточка ${card.nmID} - размеры отсутствуют`);
|
||||
}
|
||||
});
|
||||
|
||||
setWbCards(cards);
|
||||
console.log("Загружено карточек из WB API:", cards.length);
|
||||
console.log("Карточки с размерами:", cards.filter(card => card.dimensions).length);
|
||||
return;
|
||||
}
|
||||
}
|
||||
@ -391,10 +520,22 @@ export function DirectSupplyCreation({
|
||||
const cards = await WildberriesService.searchCards(
|
||||
apiToken,
|
||||
searchTerm,
|
||||
20
|
||||
100
|
||||
);
|
||||
|
||||
// Логируем информацию о размерах найденных товаров
|
||||
cards.forEach(card => {
|
||||
if (card.dimensions) {
|
||||
const volume = (card.dimensions.length / 100) * (card.dimensions.width / 100) * (card.dimensions.height / 100);
|
||||
console.log(`WB API: Найденная карточка ${card.nmID} - размеры: ${card.dimensions.length}x${card.dimensions.width}x${card.dimensions.height} см, объем: ${volume.toFixed(6)} м³`);
|
||||
} else {
|
||||
console.log(`WB API: Найденная карточка ${card.nmID} - размеры отсутствуют`);
|
||||
}
|
||||
});
|
||||
|
||||
setWbCards(cards);
|
||||
console.log("Найдено карточек в WB API:", cards.length);
|
||||
console.log("Найденные карточки с размерами:", cards.filter(card => card.dimensions).length);
|
||||
return;
|
||||
}
|
||||
}
|
||||
@ -485,10 +626,11 @@ export function DirectSupplyCreation({
|
||||
|
||||
const newItem: SupplyItem = {
|
||||
card,
|
||||
quantity: 1200,
|
||||
quantity: 0,
|
||||
pricePerUnit: 0,
|
||||
totalPrice: 0,
|
||||
supplierId: "",
|
||||
priceType: "perUnit",
|
||||
};
|
||||
|
||||
setSupplyItems((prev) => [...prev, newItem]);
|
||||
@ -504,45 +646,105 @@ export function DirectSupplyCreation({
|
||||
field: keyof SupplyItem,
|
||||
value: string | number
|
||||
) => {
|
||||
setSupplyItems((prev) =>
|
||||
prev.map((item) => {
|
||||
setSupplyItems((prev) => {
|
||||
const newItems = prev.map((item) => {
|
||||
if (item.card.nmID === nmID) {
|
||||
const updatedItem = { ...item, [field]: value };
|
||||
if (field === "quantity" || field === "pricePerUnit") {
|
||||
updatedItem.totalPrice =
|
||||
updatedItem.quantity * updatedItem.pricePerUnit;
|
||||
|
||||
// Пересчитываем totalPrice в зависимости от типа цены
|
||||
if (field === "quantity" || field === "pricePerUnit" || field === "priceType") {
|
||||
if (updatedItem.priceType === "perUnit") {
|
||||
// Цена за штуку - умножаем на количество
|
||||
updatedItem.totalPrice = updatedItem.quantity * updatedItem.pricePerUnit;
|
||||
} else {
|
||||
// Цена за общее количество - pricePerUnit становится общей ценой
|
||||
updatedItem.totalPrice = updatedItem.pricePerUnit;
|
||||
}
|
||||
}
|
||||
return updatedItem;
|
||||
}
|
||||
return item;
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
// Если изменился поставщик, уведомляем родительский компонент асинхронно
|
||||
if (field === "supplierId" && onSuppliersChange) {
|
||||
// Создаем список поставщиков с информацией о выборе
|
||||
const suppliersInfo = suppliers.map(supplier => ({
|
||||
...supplier,
|
||||
selected: newItems.some(item => item.supplierId === supplier.id)
|
||||
}));
|
||||
|
||||
console.log("Обновление поставщиков из updateSupplyItem:", suppliersInfo);
|
||||
|
||||
// Вызываем асинхронно чтобы не обновлять состояние во время рендера
|
||||
setTimeout(() => {
|
||||
onSuppliersChange(suppliersInfo);
|
||||
}, 0);
|
||||
}
|
||||
|
||||
return newItems;
|
||||
});
|
||||
};
|
||||
|
||||
// Валидация полей поставщика
|
||||
const validateSupplierField = (field: string, value: string) => {
|
||||
let error = "";
|
||||
switch (field) {
|
||||
case "name":
|
||||
if (!value.trim()) error = "Название обязательно";
|
||||
else if (value.length < 2) error = "Минимум 2 символа";
|
||||
break;
|
||||
case "contactName":
|
||||
if (!value.trim()) error = "Имя обязательно";
|
||||
else if (value.length < 2) error = "Минимум 2 символа";
|
||||
break;
|
||||
case "phone":
|
||||
if (!value.trim()) error = "Телефон обязателен";
|
||||
else if (!isValidPhone(value)) error = "Неверный формат телефона";
|
||||
break;
|
||||
case "telegram":
|
||||
if (value && !value.match(/^@[a-zA-Z0-9_]{5,32}$/)) {
|
||||
error = "Формат: @username (5-32 символа)";
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
setSupplierErrors(prev => ({...prev, [field]: error}));
|
||||
return error === "";
|
||||
};
|
||||
|
||||
const validateAllSupplierFields = () => {
|
||||
const nameValid = validateSupplierField("name", newSupplier.name);
|
||||
const contactNameValid = validateSupplierField("contactName", newSupplier.contactName);
|
||||
const phoneValid = validateSupplierField("phone", newSupplier.phone);
|
||||
const telegramValid = validateSupplierField("telegram", newSupplier.telegram);
|
||||
return nameValid && contactNameValid && phoneValid && telegramValid;
|
||||
};
|
||||
|
||||
// Работа с поставщиками
|
||||
const handleCreateSupplier = () => {
|
||||
if (!newSupplier.name || !newSupplier.contactName || !newSupplier.phone) {
|
||||
toast.error("Заполните обязательные поля");
|
||||
const handleCreateSupplier = async () => {
|
||||
if (!validateAllSupplierFields()) {
|
||||
toast.error("Исправьте ошибки в форме");
|
||||
return;
|
||||
}
|
||||
|
||||
const supplier: Supplier = {
|
||||
id: Date.now().toString(),
|
||||
...newSupplier,
|
||||
};
|
||||
|
||||
setSuppliers((prev) => [...prev, supplier]);
|
||||
setNewSupplier({
|
||||
name: "",
|
||||
contactName: "",
|
||||
phone: "",
|
||||
market: "",
|
||||
address: "",
|
||||
place: "",
|
||||
telegram: "",
|
||||
});
|
||||
setShowSupplierModal(false);
|
||||
toast.success("Поставщик создан");
|
||||
try {
|
||||
await createSupplierMutation({
|
||||
variables: {
|
||||
input: {
|
||||
name: newSupplier.name,
|
||||
contactName: newSupplier.contactName,
|
||||
phone: newSupplier.phone,
|
||||
market: newSupplier.market || null,
|
||||
address: newSupplier.address || null,
|
||||
place: newSupplier.place || null,
|
||||
telegram: newSupplier.telegram || null,
|
||||
},
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
// Ошибка обрабатывается в onError мутации
|
||||
}
|
||||
};
|
||||
|
||||
// Расчеты для нового блока
|
||||
@ -555,14 +757,39 @@ export function DirectSupplyCreation({
|
||||
return supplyItems.reduce((sum, item) => sum + item.quantity, 0);
|
||||
};
|
||||
|
||||
// Функция для расчета объема одного товара в м³
|
||||
const calculateItemVolume = (card: WildberriesCard): number => {
|
||||
if (!card.dimensions) return 0;
|
||||
|
||||
const { length, width, height } = card.dimensions;
|
||||
|
||||
// Проверяем что все размеры указаны и больше 0
|
||||
if (!length || !width || !height || length <= 0 || width <= 0 || height <= 0) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Переводим из сантиметров в метры и рассчитываем объем
|
||||
const volumeInM3 = (length / 100) * (width / 100) * (height / 100);
|
||||
|
||||
return volumeInM3;
|
||||
};
|
||||
|
||||
// Функция для расчета общего объема всех товаров в поставке
|
||||
const getTotalVolume = () => {
|
||||
return supplyItems.reduce((totalVolume, item) => {
|
||||
const itemVolume = calculateItemVolume(item.card);
|
||||
return totalVolume + (itemVolume * item.quantity);
|
||||
}, 0);
|
||||
};
|
||||
|
||||
const getTotalItemsCost = () => {
|
||||
return supplyItems.reduce((sum, item) => sum + item.totalPrice, 0);
|
||||
};
|
||||
|
||||
const getServicesCost = () => {
|
||||
if (!selectedFulfillmentOrg || selectedServices.length === 0) return 0;
|
||||
if (!selectedFulfillmentId || selectedServices.length === 0) return 0;
|
||||
|
||||
const services = organizationServices[selectedFulfillmentOrg] || [];
|
||||
const services = organizationServices[selectedFulfillmentId] || [];
|
||||
return (
|
||||
selectedServices.reduce((sum, serviceId) => {
|
||||
const service = services.find((s) => s.id === serviceId);
|
||||
@ -572,9 +799,9 @@ export function DirectSupplyCreation({
|
||||
};
|
||||
|
||||
const getConsumablesCost = () => {
|
||||
if (!selectedFulfillmentOrg || selectedConsumables.length === 0) return 0;
|
||||
if (!selectedFulfillmentId || selectedConsumables.length === 0) return 0;
|
||||
|
||||
const supplies = organizationSupplies[selectedFulfillmentOrg] || [];
|
||||
const supplies = organizationSupplies[selectedFulfillmentId] || [];
|
||||
return (
|
||||
selectedConsumables.reduce((sum, supplyId) => {
|
||||
const supply = supplies.find((s) => s.id === supplyId);
|
||||
@ -645,6 +872,39 @@ export function DirectSupplyCreation({
|
||||
}
|
||||
}, [isCreatingSupply]);
|
||||
|
||||
// Уведомление об изменении объема товаров
|
||||
React.useEffect(() => {
|
||||
const totalVolume = getTotalVolume();
|
||||
if (onVolumeChange) {
|
||||
onVolumeChange(totalVolume);
|
||||
}
|
||||
}, [supplyItems, onVolumeChange]);
|
||||
|
||||
// Загрузка поставщиков из правильного источника
|
||||
React.useEffect(() => {
|
||||
if (suppliersData?.supplySuppliers) {
|
||||
console.log("Загружаем поставщиков из БД:", suppliersData.supplySuppliers);
|
||||
setSuppliers(suppliersData.supplySuppliers);
|
||||
|
||||
// Проверяем есть ли уже выбранные поставщики и уведомляем родителя
|
||||
if (onSuppliersChange && supplyItems.length > 0) {
|
||||
const suppliersInfo = suppliersData.supplySuppliers.map((supplier: any) => ({
|
||||
...supplier,
|
||||
selected: supplyItems.some(item => item.supplierId === supplier.id)
|
||||
}));
|
||||
|
||||
if (suppliersInfo.some((s: any) => s.selected)) {
|
||||
console.log("Найдены выбранные поставщики при загрузке:", suppliersInfo);
|
||||
|
||||
// Вызываем асинхронно чтобы не обновлять состояние во время рендера
|
||||
setTimeout(() => {
|
||||
onSuppliersChange(suppliersInfo);
|
||||
}, 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
}, [suppliersData]);
|
||||
|
||||
// Обновление статуса возможности создания поставки
|
||||
React.useEffect(() => {
|
||||
const canCreate =
|
||||
@ -669,142 +929,7 @@ export function DirectSupplyCreation({
|
||||
<>
|
||||
<style>{lineClampStyles}</style>
|
||||
<div className="flex flex-col h-full space-y-2 w-full min-h-0">
|
||||
{/* НОВЫЙ БЛОК СОЗДАНИЯ ПОСТАВКИ */}
|
||||
<Card className="bg-white/10 backdrop-blur-xl border border-white/20 p-2">
|
||||
{/* Первая строка */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-4 gap-1.5 items-end mb-0.5">
|
||||
{/* 1. Модуль выбора даты */}
|
||||
<div>
|
||||
<Label className="text-white/80 text-xs mb-0.5 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-7 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>
|
||||
<Label className="text-white/80 text-xs mb-0.5 block flex items-center gap-1">
|
||||
<Building className="h-3 w-3" />
|
||||
Фулфилмент
|
||||
</Label>
|
||||
<Select
|
||||
value={selectedFulfillment}
|
||||
onValueChange={setSelectedFulfillment}
|
||||
>
|
||||
<SelectTrigger className="w-full h-7 py-0 px-2 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-0.5 block">
|
||||
Объём товаров
|
||||
</Label>
|
||||
<Input
|
||||
type="number"
|
||||
value={goodsVolume || ""}
|
||||
onChange={(e) =>
|
||||
setGoodsVolume(parseFloat(e.target.value) || 0)
|
||||
}
|
||||
placeholder="м³"
|
||||
className="h-7 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-0.5 block">
|
||||
Грузовые места
|
||||
</Label>
|
||||
<Input
|
||||
type="number"
|
||||
value={cargoPlaces || ""}
|
||||
onChange={(e) => setCargoPlaces(parseInt(e.target.value) || 0)}
|
||||
placeholder="шт"
|
||||
className="h-7 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-1.5 items-end">
|
||||
{/* 5. Цена товаров */}
|
||||
<div>
|
||||
<Label className="text-white/80 text-xs mb-0.5 block">
|
||||
Цена товаров
|
||||
</Label>
|
||||
<Input
|
||||
type="number"
|
||||
value={goodsPrice || ""}
|
||||
onChange={(e) => setGoodsPrice(parseFloat(e.target.value) || 0)}
|
||||
placeholder="₽"
|
||||
className="h-7 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-0.5 block">
|
||||
Цена услуг фулфилмент
|
||||
</Label>
|
||||
<Input
|
||||
type="number"
|
||||
value={fulfillmentServicesPrice || ""}
|
||||
onChange={(e) =>
|
||||
setFulfillmentServicesPrice(parseFloat(e.target.value) || 0)
|
||||
}
|
||||
placeholder="₽"
|
||||
className="h-7 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-0.5 block">
|
||||
Логистика до фулфилмента
|
||||
</Label>
|
||||
<Input
|
||||
type="number"
|
||||
value={logisticsPrice || ""}
|
||||
onChange={(e) =>
|
||||
setLogisticsPrice(parseFloat(e.target.value) || 0)
|
||||
}
|
||||
placeholder="₽"
|
||||
className="h-7 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-0.5 block">
|
||||
Итого
|
||||
</Label>
|
||||
<div className="h-7 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>
|
||||
|
||||
{/* Элегантный блок поиска и товаров */}
|
||||
<div className="relative">
|
||||
@ -917,17 +1042,9 @@ export function DirectSupplyCreation({
|
||||
<h4 className="text-white font-medium text-sm line-clamp-2 mb-1">
|
||||
{card.title}
|
||||
</h4>
|
||||
<p className="text-white/80 text-xs mb-1">
|
||||
Арт: {card.vendorCode}
|
||||
<p className="text-white/80 text-xs">
|
||||
WB: {card.nmID}
|
||||
</p>
|
||||
{card.sizes && card.sizes[0] && (
|
||||
<p className="text-purple-300 font-semibold text-sm">
|
||||
от{" "}
|
||||
{card.sizes[0].discountedPrice ||
|
||||
card.sizes[0].price}{" "}
|
||||
₽
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Индикаторы */}
|
||||
@ -1079,6 +1196,11 @@ export function DirectSupplyCreation({
|
||||
<span className="text-white font-medium text-sm">
|
||||
Товары в поставке
|
||||
</span>
|
||||
{supplyItems.length > 0 && (
|
||||
<span className="text-blue-400 text-xs font-medium bg-blue-500/20 px-2 py-1 rounded">
|
||||
∑ {getTotalVolume().toFixed(4)} м³
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{supplyItems.length === 0 ? (
|
||||
@ -1098,13 +1220,18 @@ export function DirectSupplyCreation({
|
||||
className="bg-white/5 border-white/10 p-1.5"
|
||||
>
|
||||
{/* Компактный заголовок товара */}
|
||||
<div className="flex items-center justify-between mb-1">
|
||||
<div className="flex items-center space-x-2 min-w-0">
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<div className="flex flex-col space-y-1 min-w-0 flex-1">
|
||||
<div className="text-white font-medium text-xs line-clamp-1 truncate">
|
||||
{item.card.title}
|
||||
</div>
|
||||
<div className="text-white/60 text-[10px] flex-shrink-0">
|
||||
Арт: {item.card.vendorCode}
|
||||
<div className="text-white/60 text-[10px] flex space-x-2">
|
||||
<span>WB: {item.card.nmID}</span>
|
||||
{calculateItemVolume(item.card) > 0 ? (
|
||||
<span className="text-blue-400">| {(calculateItemVolume(item.card) * item.quantity).toFixed(4)} м³</span>
|
||||
) : (
|
||||
<span className="text-orange-400">| размеры не указаны</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<Button
|
||||
@ -1162,19 +1289,65 @@ export function DirectSupplyCreation({
|
||||
</div>
|
||||
|
||||
{/* Блок 2: Параметры */}
|
||||
<div className="bg-white/10 rounded-lg p-2 flex flex-col justify-center h-20">
|
||||
<div className="space-y-1">
|
||||
<div className="text-white/70 text-xs text-center">
|
||||
{item.card.object}
|
||||
</div>
|
||||
<div className="text-white/70 text-xs text-center">
|
||||
{item.card.countryProduction}
|
||||
</div>
|
||||
{item.card.sizes && item.card.sizes[0] && (
|
||||
<div className="text-white/70 text-xs text-center">
|
||||
{item.card.sizes[0].techSize}
|
||||
</div>
|
||||
)}
|
||||
<div className="bg-white/10 rounded-lg p-3 flex flex-col justify-center h-20">
|
||||
<div className="flex flex-wrap gap-1 justify-center items-center">
|
||||
{/* Создаем массив валидных параметров */}
|
||||
{(() => {
|
||||
const params = [];
|
||||
|
||||
// Бренд
|
||||
if (item.card.brand && item.card.brand.trim() && item.card.brand !== '0') {
|
||||
params.push({
|
||||
value: item.card.brand,
|
||||
color: 'bg-blue-500/80',
|
||||
key: 'brand'
|
||||
});
|
||||
}
|
||||
|
||||
// Категория (объект)
|
||||
if (item.card.object && item.card.object.trim() && item.card.object !== '0') {
|
||||
params.push({
|
||||
value: item.card.object,
|
||||
color: 'bg-green-500/80',
|
||||
key: 'object'
|
||||
});
|
||||
}
|
||||
|
||||
// Страна (только если не пустая и не 0)
|
||||
if (item.card.countryProduction && item.card.countryProduction.trim() && item.card.countryProduction !== '0') {
|
||||
params.push({
|
||||
value: item.card.countryProduction,
|
||||
color: 'bg-purple-500/80',
|
||||
key: 'country'
|
||||
});
|
||||
}
|
||||
|
||||
// Цена WB
|
||||
if (item.card.sizes?.[0]?.price && item.card.sizes[0].price > 0) {
|
||||
params.push({
|
||||
value: formatCurrency(item.card.sizes[0].price),
|
||||
color: 'bg-yellow-500/80',
|
||||
key: 'price'
|
||||
});
|
||||
}
|
||||
|
||||
// Внутренний артикул
|
||||
if (item.card.vendorCode && item.card.vendorCode.trim() && item.card.vendorCode !== '0') {
|
||||
params.push({
|
||||
value: item.card.vendorCode,
|
||||
color: 'bg-gray-500/80',
|
||||
key: 'vendor'
|
||||
});
|
||||
}
|
||||
|
||||
// НАМЕРЕННО НЕ ВКЛЮЧАЕМ techSize и wbSize так как они равны '0'
|
||||
|
||||
return params.map(param => (
|
||||
<span key={param.key} className={`${param.color} text-white text-[9px] px-2 py-1 rounded font-medium`}>
|
||||
{param.value}
|
||||
</span>
|
||||
));
|
||||
})()}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -1200,9 +1373,34 @@ export function DirectSupplyCreation({
|
||||
|
||||
{/* Блок 4: Цена */}
|
||||
<div className="bg-white/10 rounded-lg p-2 flex flex-col justify-center h-20">
|
||||
<div className="text-white/60 text-xs mb-1 text-center">
|
||||
За единицу
|
||||
{/* Переключатель типа цены */}
|
||||
<div className="flex mb-1">
|
||||
<button
|
||||
onClick={() =>
|
||||
updateSupplyItem(item.card.nmID, "priceType", "perUnit")
|
||||
}
|
||||
className={`text-[9px] px-1 py-0.5 rounded-l ${
|
||||
item.priceType === "perUnit"
|
||||
? "bg-blue-500 text-white"
|
||||
: "bg-white/20 text-white/60"
|
||||
}`}
|
||||
>
|
||||
За шт
|
||||
</button>
|
||||
<button
|
||||
onClick={() =>
|
||||
updateSupplyItem(item.card.nmID, "priceType", "total")
|
||||
}
|
||||
className={`text-[9px] px-1 py-0.5 rounded-r ${
|
||||
item.priceType === "total"
|
||||
? "bg-blue-500 text-white"
|
||||
: "bg-white/20 text-white/60"
|
||||
}`}
|
||||
>
|
||||
За все
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<Input
|
||||
type="number"
|
||||
value={item.pricePerUnit || ""}
|
||||
@ -1217,144 +1415,162 @@ export function DirectSupplyCreation({
|
||||
placeholder="₽"
|
||||
/>
|
||||
<div className="text-white/80 text-xs font-medium text-center mt-1">
|
||||
{formatCurrency(item.totalPrice).replace(" ₽", "₽")}
|
||||
Итого: {formatCurrency(item.totalPrice).replace(" ₽", "₽")}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Блок 5: Услуги фулфилмента */}
|
||||
<div className="bg-white/10 rounded-lg p-2 flex flex-col justify-center h-20">
|
||||
<div className="space-y-2 max-h-16 overflow-y-auto">
|
||||
{selectedFulfillmentOrg &&
|
||||
organizationServices[selectedFulfillmentOrg] ? (
|
||||
organizationServices[selectedFulfillmentOrg]
|
||||
.slice(0, 4)
|
||||
<div className="space-y-1 max-h-16 overflow-y-auto">
|
||||
{/* DEBUG */}
|
||||
{console.log('DEBUG SERVICES:', {
|
||||
selectedFulfillmentId,
|
||||
hasServices: !!organizationServices[selectedFulfillmentId],
|
||||
servicesCount: organizationServices[selectedFulfillmentId]?.length || 0,
|
||||
allOrganizationServices: Object.keys(organizationServices)
|
||||
})}
|
||||
{selectedFulfillmentId &&
|
||||
organizationServices[selectedFulfillmentId] ? (
|
||||
organizationServices[selectedFulfillmentId]
|
||||
.slice(0, 3)
|
||||
.map((service) => (
|
||||
<label
|
||||
key={service.id}
|
||||
className="flex items-center space-x-2 cursor-pointer"
|
||||
className="flex items-center justify-between cursor-pointer text-xs"
|
||||
>
|
||||
<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.substring(0, 8)}...
|
||||
<div className="flex items-center space-x-2">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={selectedServices.includes(
|
||||
service.id
|
||||
)}
|
||||
onChange={(e) => {
|
||||
if (e.target.checked) {
|
||||
setSelectedServices((prev) => [
|
||||
...prev,
|
||||
service.id,
|
||||
]);
|
||||
} else {
|
||||
setSelectedServices((prev) =>
|
||||
prev.filter((id) => id !== service.id)
|
||||
);
|
||||
}
|
||||
}}
|
||||
className="w-3 h-3"
|
||||
/>
|
||||
<span className="text-white text-[10px]">
|
||||
{service.name.substring(0, 10)}
|
||||
</span>
|
||||
</div>
|
||||
<span className="text-green-400 text-[10px] font-medium">
|
||||
{service.price ? `${service.price}₽` : 'Бесплатно'}
|
||||
</span>
|
||||
</label>
|
||||
))
|
||||
) : (
|
||||
<span className="text-white/60 text-xs text-center">
|
||||
Выберите фулфилмент
|
||||
{selectedFulfillmentId ? 'Нет услуг' : 'Выберите фулфилмент'}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Блок 6: Поставщик */}
|
||||
<div className="bg-white/10 rounded-lg p-2 flex flex-col justify-center space-y-2 h-20">
|
||||
<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-xs">
|
||||
<SelectValue placeholder="Выбрать" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{suppliers.map((supplier) => (
|
||||
<SelectItem key={supplier.id} value={supplier.id}>
|
||||
{supplier.name}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<div className="bg-white/10 rounded-lg p-2 flex flex-col justify-center h-20">
|
||||
<div className="space-y-1">
|
||||
<Select
|
||||
value={item.supplierId}
|
||||
onValueChange={(value) =>
|
||||
updateSupplyItem(item.card.nmID, "supplierId", value)
|
||||
}
|
||||
>
|
||||
<SelectTrigger className="bg-white/20 border-white/20 text-white h-6 text-xs">
|
||||
<SelectValue placeholder="Выбрать" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{suppliers.map((supplier) => (
|
||||
<SelectItem key={supplier.id} value={supplier.id}>
|
||||
{supplier.name}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
|
||||
{/* Информация о выбранном поставщике */}
|
||||
{item.supplierId &&
|
||||
suppliers.find((s) => s.id === item.supplierId) && (
|
||||
<div className="text-xs text-white/60 space-y-1">
|
||||
<div className="truncate">
|
||||
{
|
||||
suppliers.find((s) => s.id === item.supplierId)
|
||||
?.contactName
|
||||
}
|
||||
{/* Компактная информация о выбранном поставщике */}
|
||||
{item.supplierId && suppliers.find((s) => s.id === item.supplierId) ? (
|
||||
<div className="text-center">
|
||||
<div className="text-white/80 text-[10px] font-medium truncate">
|
||||
{suppliers.find((s) => s.id === item.supplierId)?.contactName}
|
||||
</div>
|
||||
<div className="truncate">
|
||||
{
|
||||
suppliers.find((s) => s.id === item.supplierId)
|
||||
?.phone
|
||||
}
|
||||
<div className="text-white/60 text-[9px] truncate">
|
||||
{suppliers.find((s) => s.id === item.supplierId)?.phone}
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<Button
|
||||
onClick={() => setShowSupplierModal(true)}
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="bg-white/5 border-white/20 text-white hover:bg-white/10 h-5 px-2 text-[10px] w-full"
|
||||
>
|
||||
<Plus className="h-2 w-2 mr-1" />
|
||||
Добавить
|
||||
</Button>
|
||||
)}
|
||||
|
||||
{/* Кнопка добавления поставщика */}
|
||||
<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 w-full"
|
||||
>
|
||||
<Plus className="h-3 w-3 mr-1" />
|
||||
Добавить
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Блок 7: Расходники фф */}
|
||||
<div className="bg-white/10 rounded-lg p-2 flex flex-col justify-center h-20">
|
||||
<div className="space-y-2 max-h-16 overflow-y-auto">
|
||||
{selectedFulfillmentOrg &&
|
||||
organizationSupplies[selectedFulfillmentOrg] ? (
|
||||
organizationSupplies[selectedFulfillmentOrg]
|
||||
.slice(0, 4)
|
||||
<div className="space-y-1 max-h-16 overflow-y-auto">
|
||||
{/* DEBUG для расходников */}
|
||||
{console.log('DEBUG CONSUMABLES:', {
|
||||
selectedFulfillmentId,
|
||||
hasConsumables: !!organizationSupplies[selectedFulfillmentId],
|
||||
consumablesCount: organizationSupplies[selectedFulfillmentId]?.length || 0,
|
||||
allOrganizationSupplies: Object.keys(organizationSupplies)
|
||||
})}
|
||||
{selectedFulfillmentId &&
|
||||
organizationSupplies[selectedFulfillmentId] ? (
|
||||
organizationSupplies[selectedFulfillmentId]
|
||||
.slice(0, 3)
|
||||
.map((supply) => (
|
||||
<label
|
||||
key={supply.id}
|
||||
className="flex items-center space-x-2 cursor-pointer"
|
||||
className="flex items-center justify-between cursor-pointer text-xs"
|
||||
>
|
||||
<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.substring(0, 6)}...
|
||||
<div className="flex items-center space-x-2">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={selectedConsumables.includes(
|
||||
supply.id
|
||||
)}
|
||||
onChange={(e) => {
|
||||
if (e.target.checked) {
|
||||
setSelectedConsumables((prev) => [
|
||||
...prev,
|
||||
supply.id,
|
||||
]);
|
||||
} else {
|
||||
setSelectedConsumables((prev) =>
|
||||
prev.filter((id) => id !== supply.id)
|
||||
);
|
||||
}
|
||||
}}
|
||||
className="w-3 h-3"
|
||||
/>
|
||||
<span className="text-white text-[10px]">
|
||||
{supply.name.substring(0, 10)}
|
||||
</span>
|
||||
</div>
|
||||
<span className="text-orange-400 text-[10px] font-medium">
|
||||
{supply.price ? `${supply.price}₽` : 'Бесплатно'}
|
||||
</span>
|
||||
</label>
|
||||
))
|
||||
) : (
|
||||
<span className="text-white/60 text-xs text-center">
|
||||
Выберите фулфилмент
|
||||
{selectedFulfillmentId ? 'Нет расходников' : 'Выберите фулфилмент'}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
@ -1389,8 +1605,11 @@ export function DirectSupplyCreation({
|
||||
<DialogContent className="glass-card border-white/10 max-w-md">
|
||||
<DialogHeader>
|
||||
<DialogTitle className="text-white">
|
||||
Создать поставщика
|
||||
Добавить поставщика
|
||||
</DialogTitle>
|
||||
<p className="text-white/60 text-xs">
|
||||
Контактная информация поставщика для этой поставки
|
||||
</p>
|
||||
</DialogHeader>
|
||||
<div className="space-y-3">
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
@ -1398,46 +1617,66 @@ export function DirectSupplyCreation({
|
||||
<Label className="text-white/60 text-xs">Название *</Label>
|
||||
<Input
|
||||
value={newSupplier.name}
|
||||
onChange={(e) =>
|
||||
onChange={(e) => {
|
||||
const value = formatNameInput(e.target.value);
|
||||
setNewSupplier((prev) => ({
|
||||
...prev,
|
||||
name: e.target.value,
|
||||
}))
|
||||
}
|
||||
className="bg-white/10 border-white/20 text-white h-8 text-xs"
|
||||
name: value,
|
||||
}));
|
||||
validateSupplierField("name", value);
|
||||
}}
|
||||
className={`bg-white/10 border-white/20 text-white h-8 text-xs ${
|
||||
supplierErrors.name ? 'border-red-400 focus:border-red-400' : ''
|
||||
}`}
|
||||
placeholder="Название"
|
||||
/>
|
||||
{supplierErrors.name && (
|
||||
<p className="text-red-400 text-xs mt-1">{supplierErrors.name}</p>
|
||||
)}
|
||||
</div>
|
||||
<div>
|
||||
<Label className="text-white/60 text-xs">Имя *</Label>
|
||||
<Input
|
||||
value={newSupplier.contactName}
|
||||
onChange={(e) =>
|
||||
onChange={(e) => {
|
||||
const value = formatNameInput(e.target.value);
|
||||
setNewSupplier((prev) => ({
|
||||
...prev,
|
||||
contactName: e.target.value,
|
||||
}))
|
||||
}
|
||||
className="bg-white/10 border-white/20 text-white h-8 text-xs"
|
||||
contactName: value,
|
||||
}));
|
||||
validateSupplierField("contactName", value);
|
||||
}}
|
||||
className={`bg-white/10 border-white/20 text-white h-8 text-xs ${
|
||||
supplierErrors.contactName ? 'border-red-400 focus:border-red-400' : ''
|
||||
}`}
|
||||
placeholder="Имя"
|
||||
/>
|
||||
{supplierErrors.contactName && (
|
||||
<p className="text-red-400 text-xs mt-1">{supplierErrors.contactName}</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
<div>
|
||||
<Label className="text-white/60 text-xs">Телефон *</Label>
|
||||
<Input
|
||||
<PhoneInput
|
||||
value={newSupplier.phone}
|
||||
onChange={(e) =>
|
||||
onChange={(value) => {
|
||||
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"
|
||||
phone: value,
|
||||
}));
|
||||
validateSupplierField("phone", value);
|
||||
}}
|
||||
className={`bg-white/10 border-white/20 text-white h-8 text-xs ${
|
||||
supplierErrors.phone ? 'border-red-400 focus:border-red-400' : ''
|
||||
}`}
|
||||
placeholder="+7 (999) 123-45-67"
|
||||
/>
|
||||
{supplierErrors.phone && (
|
||||
<p className="text-red-400 text-xs mt-1">{supplierErrors.phone}</p>
|
||||
)}
|
||||
</div>
|
||||
<div>
|
||||
<Label className="text-white/60 text-xs">Рынок</Label>
|
||||
@ -1496,15 +1735,22 @@ export function DirectSupplyCreation({
|
||||
<Label className="text-white/60 text-xs">Телеграм</Label>
|
||||
<Input
|
||||
value={newSupplier.telegram}
|
||||
onChange={(e) =>
|
||||
onChange={(e) => {
|
||||
const value = e.target.value;
|
||||
setNewSupplier((prev) => ({
|
||||
...prev,
|
||||
telegram: e.target.value,
|
||||
}))
|
||||
}
|
||||
className="bg-white/10 border-white/20 text-white h-8 text-xs"
|
||||
telegram: value,
|
||||
}));
|
||||
validateSupplierField("telegram", value);
|
||||
}}
|
||||
className={`bg-white/10 border-white/20 text-white h-8 text-xs ${
|
||||
supplierErrors.telegram ? 'border-red-400 focus:border-red-400' : ''
|
||||
}`}
|
||||
placeholder="@username"
|
||||
/>
|
||||
{supplierErrors.telegram && (
|
||||
<p className="text-red-400 text-xs mt-1">{supplierErrors.telegram}</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="flex space-x-2">
|
||||
@ -1517,9 +1763,17 @@ export function DirectSupplyCreation({
|
||||
</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"
|
||||
disabled={!newSupplier.name || !newSupplier.contactName || !newSupplier.phone || Object.values(supplierErrors).some(error => error !== "") || creatingSupplier}
|
||||
className="flex-1 bg-gradient-to-r from-purple-500 to-pink-500 hover:from-purple-600 hover:to-pink-600 disabled:opacity-50 disabled:cursor-not-allowed h-8 text-xs"
|
||||
>
|
||||
Создать
|
||||
{creatingSupplier ? (
|
||||
<div className="flex items-center space-x-2">
|
||||
<div className="animate-spin rounded-full h-3 w-3 border border-white/30 border-t-white"></div>
|
||||
<span>Добавление...</span>
|
||||
</div>
|
||||
) : (
|
||||
'Добавить'
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
Reference in New Issue
Block a user