453 lines
19 KiB
TypeScript
453 lines
19 KiB
TypeScript
"use client";
|
||
|
||
import React, { useState } from "react";
|
||
import { Sidebar } from "@/components/dashboard/sidebar";
|
||
import { useSidebar } from "@/hooks/useSidebar";
|
||
import { useRouter } from "next/navigation";
|
||
import { DirectSupplyCreation } from "./direct-supply-creation";
|
||
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 {
|
||
Select,
|
||
SelectContent,
|
||
SelectItem,
|
||
SelectTrigger,
|
||
SelectValue,
|
||
} from "@/components/ui/select";
|
||
import { useQuery } from "@apollo/client";
|
||
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 [canCreateSupply, setCanCreateSupply] = useState(false);
|
||
const [isCreatingSupply, setIsCreatingSupply] = useState(false);
|
||
|
||
// Состояния для полей формы
|
||
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 formatCurrency = (amount: number) => {
|
||
return new Intl.NumberFormat("ru-RU", {
|
||
style: "currency",
|
||
currency: "RUB",
|
||
minimumFractionDigits: 0,
|
||
}).format(amount);
|
||
};
|
||
|
||
// Функция для обновления цены товаров из поставки
|
||
const handleItemsUpdate = (totalItemsPrice: number) => {
|
||
setGoodsPrice(totalItemsPrice);
|
||
};
|
||
|
||
// Функция для обновления статуса наличия товаров
|
||
const handleItemsCountChange = (hasItems: boolean) => {
|
||
setHasItemsInSupply(hasItems);
|
||
};
|
||
|
||
// Функция для обновления объема товаров из поставки
|
||
const handleVolumeUpdate = (totalVolume: number) => {
|
||
setGoodsVolume(totalVolume);
|
||
// После обновления объема пересчитываем логистику (если есть поставщик)
|
||
// calculateLogisticsPrice будет вызван из handleSuppliersUpdate
|
||
};
|
||
|
||
// Функция для обновления информации о поставщиках (для расчета логистики)
|
||
const handleSuppliersUpdate = (suppliersData: unknown[]) => {
|
||
// Находим рынок из выбранного поставщика
|
||
const selectedSupplier = suppliersData.find(
|
||
(supplier: unknown) => (supplier as { selected?: boolean }).selected
|
||
);
|
||
const supplierMarket = (selectedSupplier as { market?: string })?.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: {
|
||
fromLocation: string;
|
||
toLocation: string;
|
||
pricePerCubicMeter: number;
|
||
}) =>
|
||
route.fromLocation
|
||
.toLowerCase()
|
||
.includes(supplierMarket.toLowerCase()) ||
|
||
supplierMarket
|
||
.toLowerCase()
|
||
.includes(route.fromLocation.toLowerCase())
|
||
);
|
||
|
||
if (!logisticsRoute) {
|
||
console.log(`Логистика для рынка "${supplierMarket}" не найдена`);
|
||
setLogisticsPrice(0);
|
||
return;
|
||
}
|
||
|
||
// Выбираем цену в зависимости от объема
|
||
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 getTotalSum = () => {
|
||
return (
|
||
goodsPrice +
|
||
selectedServicesCost +
|
||
selectedConsumablesCost +
|
||
logisticsPrice
|
||
);
|
||
};
|
||
|
||
const handleSupplyComplete = () => {
|
||
router.push("/supplies");
|
||
};
|
||
|
||
const handleCreateSupplyClick = () => {
|
||
setIsCreatingSupply(true);
|
||
};
|
||
|
||
const handleCanCreateSupplyChange = (canCreate: boolean) => {
|
||
setCanCreateSupply(canCreate);
|
||
};
|
||
|
||
// Пересчитываем логистику при изменении фулфилмента (если есть поставщик)
|
||
React.useEffect(() => {
|
||
// Логистика пересчитается автоматически через handleSuppliersUpdate
|
||
// когда будет выбран поставщик с рынком
|
||
}, [selectedFulfillment, goodsVolume]);
|
||
|
||
const handleSupplyCompleted = () => {
|
||
setIsCreatingSupply(false);
|
||
handleSupplyComplete();
|
||
};
|
||
|
||
// Главная страница с табами в новом стиле интерфейса
|
||
return (
|
||
<div className="h-screen flex overflow-hidden">
|
||
<Sidebar />
|
||
<main
|
||
className={`flex-1 ${getSidebarMargin()} overflow-auto transition-all duration-300`}
|
||
>
|
||
<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>
|
||
|
||
{/* Основной контент - карточки 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>
|
||
|
||
{/* Правая колонка - Форма поставки */}
|
||
<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>
|
||
|
||
{/* Первая строка */}
|
||
<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>
|
||
|
||
{/* 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>
|
||
);
|
||
}
|