diff --git a/src/app/fulfillment-supplies/create-consumables/page.tsx b/src/app/fulfillment-supplies/create-consumables/page.tsx new file mode 100644 index 0000000..7edd57a --- /dev/null +++ b/src/app/fulfillment-supplies/create-consumables/page.tsx @@ -0,0 +1,10 @@ +import { AuthGuard } from "@/components/auth-guard"; +import { CreateFulfillmentConsumablesSupplyPage } from "@/components/fulfillment-supplies/create-fulfillment-consumables-supply-page"; + +export default function CreateFulfillmentConsumablesSupplyPageRoute() { + return ( + + + + ); +} diff --git a/src/components/fulfillment-supplies/create-fulfillment-consumables-supply-page.tsx b/src/components/fulfillment-supplies/create-fulfillment-consumables-supply-page.tsx new file mode 100644 index 0000000..8559f65 --- /dev/null +++ b/src/components/fulfillment-supplies/create-fulfillment-consumables-supply-page.tsx @@ -0,0 +1,769 @@ +"use client"; + +import React, { useState } from "react"; +import { useRouter } from "next/navigation"; +import { useQuery, useMutation } from "@apollo/client"; +import { Sidebar } from "@/components/dashboard/sidebar"; +import { useSidebar } from "@/hooks/useSidebar"; +import { Card } from "@/components/ui/card"; +import { Button } from "@/components/ui/button"; +import { Badge } from "@/components/ui/badge"; +import { Input } from "@/components/ui/input"; +import { + ArrowLeft, + Building2, + MapPin, + Phone, + Mail, + Star, + Search, + Package, + Plus, + Minus, + ShoppingCart, + Wrench, + Box, +} from "lucide-react"; +import { + GET_MY_COUNTERPARTIES, + GET_ALL_PRODUCTS, + GET_SUPPLY_ORDERS, +} from "@/graphql/queries"; +import { CREATE_SUPPLY_ORDER } from "@/graphql/mutations"; +import { OrganizationAvatar } from "@/components/market/organization-avatar"; +import { toast } from "sonner"; +import Image from "next/image"; + +interface FulfillmentConsumableSupplier { + id: string; + inn: string; + name?: string; + fullName?: string; + type: "FULFILLMENT" | "SELLER" | "LOGIST" | "WHOLESALE"; + address?: string; + phones?: Array<{ value: string }>; + emails?: Array<{ value: string }>; + users?: Array<{ id: string; avatar?: string; managerName?: string }>; + createdAt: string; +} + +interface FulfillmentConsumableProduct { + id: string; + name: string; + description?: string; + price: number; + category?: { name: string }; + images: string[]; + mainImage?: string; + organization: { + id: string; + name: string; + }; + stock?: number; + unit?: string; +} + +interface SelectedFulfillmentConsumable { + id: string; + name: string; + price: number; + selectedQuantity: number; + unit?: string; + category?: string; + supplierId: string; + supplierName: string; +} + +export function CreateFulfillmentConsumablesSupplyPage() { + const router = useRouter(); + const { getSidebarMargin } = useSidebar(); + const [selectedSupplier, setSelectedSupplier] = + useState(null); + const [selectedConsumables, setSelectedConsumables] = useState< + SelectedFulfillmentConsumable[] + >([]); + const [searchQuery, setSearchQuery] = useState(""); + const [productSearchQuery, setProductSearchQuery] = useState(""); + const [deliveryDate, setDeliveryDate] = useState(""); + const [isCreatingSupply, setIsCreatingSupply] = useState(false); + + // Загружаем контрагентов-поставщиков расходников + const { data: counterpartiesData, loading: counterpartiesLoading } = useQuery( + GET_MY_COUNTERPARTIES + ); + + // Загружаем товары для выбранного поставщика + const { data: productsData, loading: productsLoading } = useQuery( + GET_ALL_PRODUCTS, + { + skip: !selectedSupplier, + variables: { search: productSearchQuery || null, category: null }, + } + ); + + // Мутация для создания заказа поставки расходников + const [createSupplyOrder] = useMutation(CREATE_SUPPLY_ORDER); + + // Фильтруем только поставщиков расходников (оптовиков) + const consumableSuppliers = ( + counterpartiesData?.myCounterparties || [] + ).filter((org: FulfillmentConsumableSupplier) => org.type === "WHOLESALE"); + + // Фильтруем поставщиков по поисковому запросу + const filteredSuppliers = consumableSuppliers.filter( + (supplier: FulfillmentConsumableSupplier) => + supplier.name?.toLowerCase().includes(searchQuery.toLowerCase()) || + supplier.fullName?.toLowerCase().includes(searchQuery.toLowerCase()) || + supplier.inn?.toLowerCase().includes(searchQuery.toLowerCase()) + ); + + // Фильтруем товары по выбранному поставщику + const supplierProducts = selectedSupplier + ? (productsData?.allProducts || []).filter( + (product: FulfillmentConsumableProduct) => + product.organization.id === selectedSupplier.id + ) + : []; + + const formatCurrency = (amount: number) => { + return new Intl.NumberFormat("ru-RU", { + style: "currency", + currency: "RUB", + minimumFractionDigits: 0, + }).format(amount); + }; + + const renderStars = (rating: number = 4.5) => { + return Array.from({ length: 5 }, (_, i) => ( + + )); + }; + + const updateConsumableQuantity = (productId: string, quantity: number) => { + const product = supplierProducts.find( + (p: FulfillmentConsumableProduct) => p.id === productId + ); + if (!product || !selectedSupplier) return; + + setSelectedConsumables((prev) => { + const existing = prev.find((p) => p.id === productId); + + if (quantity === 0) { + // Удаляем расходник если количество 0 + return prev.filter((p) => p.id !== productId); + } + + if (existing) { + // Обновляем количество существующего расходника + return prev.map((p) => + p.id === productId ? { ...p, selectedQuantity: quantity } : p + ); + } else { + // Добавляем новый расходник + return [ + ...prev, + { + id: product.id, + name: product.name, + price: product.price, + selectedQuantity: quantity, + unit: product.unit || "шт", + category: product.category?.name || "Расходники", + supplierId: selectedSupplier.id, + supplierName: + selectedSupplier.name || selectedSupplier.fullName || "Поставщик", + }, + ]; + } + }); + }; + + const getSelectedQuantity = (productId: string): number => { + const selected = selectedConsumables.find((p) => p.id === productId); + return selected ? selected.selectedQuantity : 0; + }; + + const getTotalAmount = () => { + return selectedConsumables.reduce( + (sum, consumable) => sum + consumable.price * consumable.selectedQuantity, + 0 + ); + }; + + const getTotalItems = () => { + return selectedConsumables.reduce( + (sum, consumable) => sum + consumable.selectedQuantity, + 0 + ); + }; + + const handleCreateSupply = async () => { + if ( + !selectedSupplier || + selectedConsumables.length === 0 || + !deliveryDate + ) { + toast.error("Заполните все обязательные поля"); + return; + } + + setIsCreatingSupply(true); + + try { + const result = await createSupplyOrder({ + variables: { + input: { + partnerId: selectedSupplier.id, + deliveryDate: deliveryDate, + // Для фулфилмента не требуется выбор фулфилмент-центра, поставка идет на свой склад + items: selectedConsumables.map((consumable) => ({ + productId: consumable.id, + quantity: consumable.selectedQuantity, + })), + }, + }, + refetchQueries: [{ query: GET_SUPPLY_ORDERS }], + }); + + if (result.data?.createSupplyOrder?.success) { + toast.success("Заказ поставки расходников фулфилмента создан успешно!"); + // Очищаем форму + setSelectedSupplier(null); + setSelectedConsumables([]); + setDeliveryDate(""); + setProductSearchQuery(""); + setSearchQuery(""); + + // Перенаправляем на страницу поставок фулфилмента + router.push("/fulfillment-supplies"); + } else { + toast.error( + result.data?.createSupplyOrder?.message || + "Ошибка при создании заказа поставки" + ); + } + } catch (error) { + console.error("Error creating fulfillment consumables supply:", error); + toast.error("Ошибка при создании поставки расходников фулфилмента"); + } finally { + setIsCreatingSupply(false); + } + }; + + return ( +
+ +
+
+ {/* Заголовок */} +
+
+

+ Создание поставки расходников фулфилмента +

+

+ Выберите поставщика и добавьте расходники в заказ для вашего + фулфилмент-центра +

+
+ +
+ + {/* Основной контент с двумя блоками */} +
+ {/* Левая колонка - Поставщики и Расходники */} +
+ {/* Блок "Поставщики" */} + +
+
+

+ + Поставщики расходников +

+
+ + setSearchQuery(e.target.value)} + className="bg-white/20 backdrop-blur border-white/30 text-white placeholder-white/50 pl-10 h-8 text-sm rounded-full shadow-inner focus:ring-2 focus:ring-purple-400/50 focus:border-purple-400/50 transition-all duration-300" + /> +
+ {selectedSupplier && ( + + )} +
+
+ +
+ {counterpartiesLoading ? ( +
+
+

+ Загружаем поставщиков... +

+
+ ) : filteredSuppliers.length === 0 ? ( +
+
+ +
+

+ {searchQuery + ? "Поставщики не найдены" + : "Добавьте поставщиков"} +

+
+ ) : ( +
+ {filteredSuppliers + .slice(0, 7) + .map( + (supplier: FulfillmentConsumableSupplier, index) => ( + setSelectedSupplier(supplier)} + > +
+
+ ({ + id: user.id, + avatar: user.avatar, + }) + ), + }} + size="sm" + /> + {selectedSupplier?.id === supplier.id && ( +
+ + ✓ + +
+ )} +
+
+

+ {( + supplier.name || + supplier.fullName || + "Поставщик" + ).slice(0, 10)} +

+
+ + ★ + + + 4.5 + +
+
+
+
+
+
+ + {/* Hover эффект */} +
+
+ ) + )} + {filteredSuppliers.length > 7 && ( +
+
+ +{filteredSuppliers.length - 7} +
+
ещё
+
+ )} +
+ )} +
+
+ + {/* Блок "Расходники" */} + +
+
+

+ + Расходники для фулфилмента + {selectedSupplier && ( + + - {selectedSupplier.name || selectedSupplier.fullName} + + )} +

+
+ {selectedSupplier && ( +
+ + setProductSearchQuery(e.target.value)} + className="bg-white/10 border-white/20 text-white placeholder-white/40 pl-7 h-8 text-sm" + /> +
+ )} +
+ +
+ {!selectedSupplier ? ( +
+ +

+ Выберите поставщика для просмотра расходников +

+
+ ) : productsLoading ? ( +
+
+

Загрузка...

+
+ ) : supplierProducts.length === 0 ? ( +
+ +

+ Нет доступных расходников +

+
+ ) : ( +
+ {supplierProducts.map( + (product: FulfillmentConsumableProduct, index) => { + const selectedQuantity = getSelectedQuantity( + product.id + ); + return ( + 0 + ? "ring-2 ring-green-400/50 bg-gradient-to-br from-green-500/20 via-green-400/10 to-green-500/20" + : "hover:from-white/20 hover:via-white/10 hover:to-white/20 hover:border-white/40" + }`} + style={{ + animationDelay: `${index * 50}ms`, + minHeight: "200px", + width: "100%", + }} + > +
+ {/* Изображение товара */} +
+ {product.images && + product.images.length > 0 && + product.images[0] ? ( + {product.name} + ) : product.mainImage ? ( + {product.name} + ) : ( +
+ +
+ )} + {selectedQuantity > 0 && ( +
+ + {selectedQuantity > 999 + ? "999+" + : selectedQuantity} + +
+ )} +
+ + {/* Информация о товаре */} +
+

+ {product.name} +

+ {product.category && ( + + {product.category.name.slice(0, 10)} + + )} +
+ + {formatCurrency(product.price)} + + {product.stock && ( + + {product.stock} + + )} +
+
+ + {/* Управление количеством */} +
+
+ + { + let inputValue = e.target.value; + + // Удаляем все нецифровые символы + inputValue = inputValue.replace( + /[^0-9]/g, + "" + ); + + // Удаляем ведущие нули + inputValue = inputValue.replace( + /^0+/, + "" + ); + + // Если строка пустая после удаления нулей, устанавливаем 0 + const numericValue = + inputValue === "" + ? 0 + : parseInt(inputValue); + + // Ограничиваем значение максимумом 99999 + const clampedValue = Math.min( + numericValue, + 99999 + ); + + updateConsumableQuantity( + product.id, + clampedValue + ); + }} + onBlur={(e) => { + // При потере фокуса, если поле пустое, устанавливаем 0 + if (e.target.value === "") { + updateConsumableQuantity( + product.id, + 0 + ); + } + }} + className="w-16 h-7 text-center text-sm bg-white/10 border-white/20 text-white rounded px-1 focus:ring-2 focus:ring-purple-400/50 focus:border-purple-400/50" + placeholder="0" + /> + +
+ + {selectedQuantity > 0 && ( +
+ + {formatCurrency( + product.price * selectedQuantity + )} + +
+ )} +
+
+ + {/* Hover эффект */} +
+
+ ); + } + )} +
+ )} +
+
+
+ + {/* Правая колонка - Корзина */} +
+ +

+ + Корзина ({getTotalItems()} шт) +

+ + {selectedConsumables.length === 0 ? ( +
+
+ +
+

+ Корзина пуста +

+

+ Добавьте расходники для создания поставки +

+
+ ) : ( +
+ {selectedConsumables.map((consumable) => ( +
+
+

+ {consumable.name} +

+

+ {formatCurrency(consumable.price)} ×{" "} + {consumable.selectedQuantity} +

+
+
+ + {formatCurrency( + consumable.price * consumable.selectedQuantity + )} + + +
+
+ ))} +
+ )} + +
+
+ + setDeliveryDate(e.target.value)} + className="bg-white/10 border-white/20 text-white h-8 text-sm" + min={new Date().toISOString().split("T")[0]} + required + /> +
+
+ + Итого: + + + {formatCurrency(getTotalAmount())} + +
+ +
+
+
+
+
+
+
+ ); +} diff --git a/src/components/fulfillment-supplies/fulfillment-supplies/fulfillment-detailed-supplies-tab.tsx b/src/components/fulfillment-supplies/fulfillment-supplies/fulfillment-detailed-supplies-tab.tsx index abfb20d..e3d43a1 100644 --- a/src/components/fulfillment-supplies/fulfillment-supplies/fulfillment-detailed-supplies-tab.tsx +++ b/src/components/fulfillment-supplies/fulfillment-supplies/fulfillment-detailed-supplies-tab.tsx @@ -6,6 +6,7 @@ import { Button } from "@/components/ui/button"; import { Badge } from "@/components/ui/badge"; import { StatsCard } from "../../supplies/ui/stats-card"; import { StatsGrid } from "../../supplies/ui/stats-grid"; +import { useRouter } from "next/navigation"; import { Calendar, MapPin, @@ -17,6 +18,7 @@ import { Box, Package2, Tags, + Plus, } from "lucide-react"; // Типы данных для расходников ФФ детально @@ -213,6 +215,7 @@ const mockFulfillmentConsumablesDetailed: FulfillmentConsumableSupply[] = [ ]; export function FulfillmentDetailedSuppliesTab() { + const router = useRouter(); const [expandedSupplies, setExpandedSupplies] = useState>( new Set() ); @@ -336,6 +339,25 @@ export function FulfillmentDetailedSuppliesTab() { return (
+ {/* Заголовок с кнопкой создания поставки */} +
+
+

+ Наши расходники +

+

+ Управление поставками расходников фулфилмента +

+
+ +
+ {/* Статистика расходников ФФ детально */} {/* Основная строка поставки расходников ФФ детально */} - toggleSupplyExpansion(supply.id)} > @@ -488,7 +510,7 @@ export function FulfillmentDetailedSuppliesTab() { const isRouteExpanded = expandedRoutes.has(route.id); return ( - toggleRouteExpansion(route.id)} > @@ -580,9 +602,11 @@ export function FulfillmentDetailedSuppliesTab() { expandedSuppliers.has(supplier.id); return ( - toggleSupplierExpansion(supplier.id)} + onClick={() => + toggleSupplierExpansion(supplier.id) + } >
@@ -662,9 +686,13 @@ export function FulfillmentDetailedSuppliesTab() { ); return ( - toggleConsumableExpansion(consumable.id)} + onClick={() => + toggleConsumableExpansion( + consumable.id + ) + } >