diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 4cae4fa..c13eb6b 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -97,6 +97,8 @@ model Organization { supplies Supply[] users User[] logistics Logistics[] + supplyOrders SupplyOrder[] + partnerSupplyOrders SupplyOrder[] @relation("SupplyOrderPartner") @@map("organizations") } @@ -229,6 +231,7 @@ model Product { organizationId String cartItems CartItem[] favorites Favorites[] + supplyOrderItems SupplyOrderItem[] category Category? @relation(fields: [categoryId], references: [id]) organization Organization @relation(fields: [organizationId], references: [id], onDelete: Cascade) @@ -363,6 +366,14 @@ enum ScheduleStatus { ABSENT } +enum SupplyOrderStatus { + PENDING + CONFIRMED + IN_TRANSIT + DELIVERED + CANCELLED +} + model Logistics { id String @id @default(cuid()) fromLocation String @@ -377,3 +388,36 @@ model Logistics { @@map("logistics") } + +model SupplyOrder { + id String @id @default(cuid()) + partnerId String + deliveryDate DateTime + status SupplyOrderStatus @default(PENDING) + totalAmount Decimal @db.Decimal(12, 2) + totalItems Int + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + organizationId String + items SupplyOrderItem[] + organization Organization @relation(fields: [organizationId], references: [id], onDelete: Cascade) + partner Organization @relation("SupplyOrderPartner", fields: [partnerId], references: [id]) + + @@map("supply_orders") +} + +model SupplyOrderItem { + id String @id @default(cuid()) + supplyOrderId String + productId String + quantity Int + price Decimal @db.Decimal(12, 2) + totalPrice Decimal @db.Decimal(12, 2) + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + supplyOrder SupplyOrder @relation(fields: [supplyOrderId], references: [id], onDelete: Cascade) + product Product @relation(fields: [productId], references: [id]) + + @@unique([supplyOrderId, productId]) + @@map("supply_order_items") +} diff --git a/src/app/fulfillment-supplies/materials/order/page.tsx b/src/app/fulfillment-supplies/materials/order/page.tsx new file mode 100644 index 0000000..23041ee --- /dev/null +++ b/src/app/fulfillment-supplies/materials/order/page.tsx @@ -0,0 +1,10 @@ +import { AuthGuard } from "@/components/auth-guard"; +import { MaterialsOrderForm } from "@/components/fulfillment-supplies/materials-supplies/materials-order-form"; + +export default function MaterialsOrderPage() { + return ( + + + + ); +} diff --git a/src/components/fulfillment-supplies/materials-supplies/materials-order-form.tsx b/src/components/fulfillment-supplies/materials-supplies/materials-order-form.tsx new file mode 100644 index 0000000..d63343a --- /dev/null +++ b/src/components/fulfillment-supplies/materials-supplies/materials-order-form.tsx @@ -0,0 +1,610 @@ +"use client"; + +import React, { useState } from "react"; +import { useRouter } from "next/navigation"; +import { useQuery, useMutation } from "@apollo/client"; +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 { Sidebar } from "@/components/dashboard/sidebar"; +import { useSidebar } from "@/hooks/useSidebar"; +import { + ArrowLeft, + Building2, + MapPin, + Phone, + Mail, + Star, + Search, + Calendar, + Package, + Plus, + Minus, + ShoppingCart, +} from "lucide-react"; +import { GET_MY_COUNTERPARTIES, GET_ALL_PRODUCTS } 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 Partner { + 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 Product { + id: string; + name: string; + article: string; + description?: string; + price: number; + quantity: number; + category?: { id: string; name: string }; + brand?: string; + color?: string; + size?: string; + weight?: number; + dimensions?: string; + material?: string; + images: string[]; + mainImage?: string; + isActive: boolean; + organization: { + id: string; + inn: string; + name?: string; + fullName?: string; + }; +} + +interface SelectedProduct extends Product { + selectedQuantity: number; +} + +export function MaterialsOrderForm() { + const router = useRouter(); + const { getSidebarMargin } = useSidebar(); + const [selectedPartner, setSelectedPartner] = useState(null); + const [selectedProducts, setSelectedProducts] = useState( + [] + ); + const [searchQuery, setSearchQuery] = useState(""); + const [deliveryDate, setDeliveryDate] = useState(""); + + // Загружаем контрагентов-оптовиков + const { data: counterpartiesData, loading: counterpartiesLoading } = useQuery( + GET_MY_COUNTERPARTIES + ); + + // Загружаем товары для выбранного партнера + const { data: productsData, loading: productsLoading } = useQuery( + GET_ALL_PRODUCTS, + { + skip: !selectedPartner, + variables: { search: null, category: null }, + } + ); + + // Мутация для создания заказа поставки + const [createSupplyOrder, { loading: isCreatingOrder }] = useMutation(CREATE_SUPPLY_ORDER); + + // Фильтруем только оптовиков из партнеров + const wholesalePartners = (counterpartiesData?.myCounterparties || []).filter( + (org: Partner) => org.type === "WHOLESALE" + ); + + // Фильтруем партнеров по поисковому запросу + const filteredPartners = wholesalePartners.filter( + (partner: Partner) => + partner.name?.toLowerCase().includes(searchQuery.toLowerCase()) || + partner.fullName?.toLowerCase().includes(searchQuery.toLowerCase()) || + partner.inn?.toLowerCase().includes(searchQuery.toLowerCase()) + ); + + // Фильтруем товары по выбранному партнеру + const partnerProducts = selectedPartner + ? (productsData?.allProducts || []).filter( + (product: Product) => product.organization.id === selectedPartner.id + ) + : []; + + const formatCurrency = (amount: number) => { + return new Intl.NumberFormat("ru-RU", { + style: "currency", + currency: "RUB", + minimumFractionDigits: 0, + }).format(amount); + }; + + const updateProductQuantity = (productId: string, quantity: number) => { + const product = partnerProducts.find((p: Product) => p.id === productId); + if (!product) return; + + setSelectedProducts((prev) => { + const existing = prev.find((p) => p.id === productId); + + if (quantity === 0) { + return prev.filter((p) => p.id !== productId); + } + + if (existing) { + return prev.map((p) => + p.id === productId ? { ...p, selectedQuantity: quantity } : p + ); + } else { + return [...prev, { ...product, selectedQuantity: quantity }]; + } + }); + }; + + const getSelectedQuantity = (productId: string): number => { + const selected = selectedProducts.find((p) => p.id === productId); + return selected ? selected.selectedQuantity : 0; + }; + + const getTotalAmount = () => { + return selectedProducts.reduce( + (sum, product) => sum + product.price * product.selectedQuantity, + 0 + ); + }; + + const getTotalItems = () => { + return selectedProducts.reduce( + (sum, product) => sum + product.selectedQuantity, + 0 + ); + }; + + const handleCreateOrder = async () => { + if (!selectedPartner || selectedProducts.length === 0 || !deliveryDate) { + toast.error("Заполните все обязательные поля"); + return; + } + + try { + const result = await createSupplyOrder({ + variables: { + input: { + partnerId: selectedPartner.id, + deliveryDate: deliveryDate, + items: selectedProducts.map(product => ({ + productId: product.id, + quantity: product.selectedQuantity + })) + } + } + }); + + if (result.data?.createSupplyOrder?.success) { + toast.success("Заказ поставки создан успешно!"); + router.push("/fulfillment-supplies"); + } else { + toast.error(result.data?.createSupplyOrder?.message || "Ошибка при создании заказа"); + } + } catch (error) { + console.error("Error creating supply order:", error); + toast.error("Ошибка при создании заказа поставки"); + } + }; + + const renderStars = (rating: number = 4.5) => { + return Array.from({ length: 5 }, (_, i) => ( + + )); + }; + + // Если выбран партнер и есть товары, показываем товары + if (selectedPartner && partnerProducts.length > 0) { + return ( +
+ +
+
+ {/* Заголовок */} +
+
+ +
+

+ Товары партнера +

+

+ {selectedPartner.name || selectedPartner.fullName} +

+
+
+ +
+ +
+ {/* Список товаров */} +
+ +
+

+ Доступные товары +

+
+
+ {partnerProducts.map((product: Product) => { + const selectedQuantity = getSelectedQuantity( + product.id + ); + + return ( + +
+ {/* Изображение товара */} + {product.mainImage && ( +
+ {product.name} +
+ )} + + {/* Информация о товаре */} +
+

+ {product.name} +

+

+ Артикул: {product.article} +

+ {product.description && ( +

+ {product.description} +

+ )} +
+ + {/* Цена и наличие */} +
+
+
+ {formatCurrency(product.price)} +
+
+ В наличии: {product.quantity} +
+
+
+ + {/* Выбор количества */} +
+ + { + const value = Math.max( + 0, + Math.min( + product.quantity, + parseInt(e.target.value) || 0 + ) + ); + updateProductQuantity(product.id, value); + }} + className="h-8 w-16 text-center bg-white/10 border-white/20 text-white" + min={0} + max={product.quantity} + /> + +
+
+
+ ); + })} +
+
+
+
+
+ + {/* Сводка заказа */} +
+ +
+

+ Сводка заказа +

+ + {/* Дата поставки */} +
+ + setDeliveryDate(e.target.value)} + className="bg-white/10 border-white/20 text-white" + required + /> +
+ + {/* Выбранные товары */} +
+ {selectedProducts.length === 0 ? ( +
+ +

+ Товары не выбраны +

+
+ ) : ( +
+ {selectedProducts.map((product) => ( + +
+
+ {product.name} +
+
+ + {product.selectedQuantity} шт ×{" "} + {formatCurrency(product.price)} + + + {formatCurrency( + product.price * product.selectedQuantity + )} + +
+
+
+ ))} +
+ )} +
+ + {/* Итого */} +
+
+ Товаров: + {getTotalItems()} шт +
+
+ Итого: + {formatCurrency(getTotalAmount())} +
+
+ + {/* Кнопка создания заказа */} + +
+
+
+
+
+
+
+ ); + } + + // Основная форма выбора партнера + return ( +
+ +
+
+ {/* Заголовок */} +
+
+ +
+

+ Заказ расходников +

+

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

+
+
+
+ + {/* Поиск */} +
+
+ + setSearchQuery(e.target.value)} + className="pl-10 bg-white/10 border-white/20 text-white placeholder:text-white/40" + /> +
+
+ + {/* Список партнеров */} + +
+ {counterpartiesLoading ? ( +
+
Загрузка партнеров...
+
+ ) : filteredPartners.length === 0 ? ( +
+
+ +

+ {wholesalePartners.length === 0 + ? "У вас пока нет партнеров-оптовиков" + : "Партнеры не найдены"} +

+

+ Добавьте партнеров в разделе "Партнеры" +

+
+
+ ) : ( +
+
+ {filteredPartners.map((partner: Partner) => ( + setSelectedPartner(partner)} + > +
+ {/* Заголовок карточки */} +
+ +
+

+ {partner.name || partner.fullName} +

+
+ {renderStars()} + + 4.5 + +
+
+
+ + {/* Информация */} +
+ {partner.address && ( +
+ + + {partner.address} + +
+ )} + + {partner.phones && partner.phones.length > 0 && ( +
+ + + {partner.phones[0].value} + +
+ )} + + {partner.emails && partner.emails.length > 0 && ( +
+ + + {partner.emails[0].value} + +
+ )} +
+ + {/* ИНН */} +
+

+ ИНН: {partner.inn} +

+
+
+
+ ))} +
+
+ )} +
+
+
+
+
+ ); +} diff --git a/src/components/fulfillment-supplies/materials-supplies/materials-supplies-tab.tsx b/src/components/fulfillment-supplies/materials-supplies/materials-supplies-tab.tsx index 4e256f7..75c2f84 100644 --- a/src/components/fulfillment-supplies/materials-supplies/materials-supplies-tab.tsx +++ b/src/components/fulfillment-supplies/materials-supplies/materials-supplies-tab.tsx @@ -175,6 +175,7 @@ export function MaterialsSuppliesTab() {