diff --git a/prisma/schema.prisma b/prisma/schema.prisma index e2b912e..6cbcbba 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -433,6 +433,9 @@ enum SupplyOrderStatus { PENDING CONFIRMED IN_TRANSIT + SUPPLIER_APPROVED + LOGISTICS_CONFIRMED + SHIPPED DELIVERED CANCELLED } diff --git a/src/app/logistics-orders/page.tsx b/src/app/logistics-orders/page.tsx new file mode 100644 index 0000000..ccbaed9 --- /dev/null +++ b/src/app/logistics-orders/page.tsx @@ -0,0 +1,10 @@ +import { AuthGuard } from "@/components/auth-guard"; +import { LogisticsOrdersDashboard } from "@/components/logistics-orders/logistics-orders-dashboard"; + +export default function LogisticsOrdersPage() { + return ( + + + + ); +} \ No newline at end of file diff --git a/src/app/supplier-orders/page.tsx b/src/app/supplier-orders/page.tsx new file mode 100644 index 0000000..8a17e3d --- /dev/null +++ b/src/app/supplier-orders/page.tsx @@ -0,0 +1,10 @@ +import { AuthGuard } from "@/components/auth-guard"; +import { SupplierOrdersDashboard } from "@/components/supplier-orders/supplier-orders-dashboard"; + +export default function SupplierOrdersPage() { + return ( + + + + ); +} \ No newline at end of file diff --git a/src/components/dashboard/sidebar.tsx b/src/components/dashboard/sidebar.tsx index b0bc31d..42afa4e 100644 --- a/src/components/dashboard/sidebar.tsx +++ b/src/components/dashboard/sidebar.tsx @@ -150,7 +150,7 @@ export function Sidebar() { router.push("/supplies"); break; case "LOGIST": - router.push("/logistics"); + router.push("/logistics-orders"); break; default: router.push("/supplies"); 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 1fdf653..1d8af74 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 @@ -14,7 +14,7 @@ import { GET_MY_SUPPLIES, GET_WAREHOUSE_PRODUCTS, } from "@/graphql/queries"; -import { UPDATE_SUPPLY_ORDER_STATUS } from "@/graphql/mutations"; +import { FULFILLMENT_RECEIVE_ORDER } from "@/graphql/mutations"; import { useAuth } from "@/hooks/useAuth"; import { toast } from "sonner"; import { @@ -96,25 +96,38 @@ const formatDate = (dateString: string) => { const getStatusBadge = (status: string) => { const statusConfig = { PENDING: { - label: "Ожидает", + label: "Ожидает одобрения поставщика", color: "bg-yellow-500/20 text-yellow-300 border-yellow-500/30", }, - CONFIRMED: { - label: "Подтверждён", + SUPPLIER_APPROVED: { + label: "Ожидает подтверждения логистики", color: "bg-blue-500/20 text-blue-300 border-blue-500/30", }, - IN_TRANSIT: { + LOGISTICS_CONFIRMED: { + label: "Ожидает отправки поставщиком", + color: "bg-cyan-500/20 text-cyan-300 border-cyan-500/30", + }, + SHIPPED: { label: "В пути", color: "bg-orange-500/20 text-orange-300 border-orange-500/30", }, DELIVERED: { - label: "Доставлен", + label: "Доставлено", color: "bg-green-500/20 text-green-300 border-green-500/30", }, CANCELLED: { - label: "Отменён", + label: "Отменено", color: "bg-red-500/20 text-red-300 border-red-500/30", }, + // Устаревшие статусы для обратной совместимости + CONFIRMED: { + label: "Подтверждён (устаревший)", + color: "bg-blue-500/20 text-blue-300 border-blue-500/30", + }, + IN_TRANSIT: { + label: "В пути (устаревший)", + color: "bg-orange-500/20 text-orange-300 border-orange-500/30", + }, }; const config = @@ -128,16 +141,24 @@ export function FulfillmentDetailedSuppliesTab() { const { user } = useAuth(); const [expandedOrders, setExpandedOrders] = useState>(new Set()); - // Мутация для обновления статуса заказа - const [updateSupplyOrderStatus] = useMutation(UPDATE_SUPPLY_ORDER_STATUS, { + // Убираем устаревшую мутацию updateSupplyOrderStatus + + const [fulfillmentReceiveOrder] = useMutation(FULFILLMENT_RECEIVE_ORDER, { refetchQueries: [ - { query: GET_SUPPLY_ORDERS }, // Обновляем заказы поставок - { query: GET_MY_SUPPLIES }, // Обновляем склад фулфилмента (расходники фулфилмента) - { query: GET_WAREHOUSE_PRODUCTS }, // Обновляем товары склада + { query: GET_SUPPLY_ORDERS }, + { query: GET_MY_SUPPLIES }, + { query: GET_WAREHOUSE_PRODUCTS }, ], + onCompleted: (data) => { + if (data.fulfillmentReceiveOrder.success) { + toast.success(data.fulfillmentReceiveOrder.message); + } else { + toast.error(data.fulfillmentReceiveOrder.message); + } + }, onError: (error) => { - console.error("Error updating supply order status:", error); - toast.error("Ошибка при обновлении статуса заказа"); + console.error("Error receiving supply order:", error); + toast.error("Ошибка при приеме заказа поставки"); }, }); @@ -177,30 +198,25 @@ export function FulfillmentDetailedSuppliesTab() { setExpandedOrders(newExpanded); }; - // Функция для обновления статуса заказа - const handleStatusUpdate = async (orderId: string, newStatus: string) => { + // Убираем устаревшую функцию handleStatusUpdate + + // Проверяем, можно ли принять заказ (для фулфилмента) + const canReceiveOrder = (status: string) => { + return status === "SHIPPED"; + }; + + // Функция для приема заказа фулфилментом + const handleReceiveOrder = async (orderId: string) => { try { - await updateSupplyOrderStatus({ - variables: { - id: orderId, - status: newStatus, - }, + await fulfillmentReceiveOrder({ + variables: { id: orderId }, }); - toast.success("Статус заказа обновлен"); } catch (error) { - console.error("Error updating status:", error); + console.error("Error receiving order:", error); } }; - // Проверяем, можно ли отметить как доставленный - const canMarkAsDelivered = (status: string) => { - return status === "IN_TRANSIT"; - }; - - // Проверяем, можно ли отметить как в пути - const canMarkAsInTransit = (status: string) => { - return status === "CONFIRMED"; - }; + // Убираем устаревшие функции проверки статусов if (loading) { return ( @@ -406,34 +422,24 @@ export function FulfillmentDetailedSuppliesTab() {
{getStatusBadge(order.status)} - {/* Кнопка "В пути" для подтвержденных заказов */} - {canMarkAsInTransit(order.status) && ( - - )} + {/* Убираем устаревшую кнопку "В пути" */} - {/* Кнопка "Получено" для заказов в пути */} - {canMarkAsDelivered(order.status) && ( + {/* Кнопка "Принять" для заказов в статусе SHIPPED */} + {canReceiveOrder(order.status) && ( )} + + {/* Убираем устаревшую кнопку "Получено" */}
diff --git a/src/components/logistics-orders/logistics-orders-dashboard.tsx b/src/components/logistics-orders/logistics-orders-dashboard.tsx new file mode 100644 index 0000000..feab6cd --- /dev/null +++ b/src/components/logistics-orders/logistics-orders-dashboard.tsx @@ -0,0 +1,592 @@ +"use client"; + +import { useState } from "react"; +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 { Separator } from "@/components/ui/separator"; +import { Avatar, AvatarImage, AvatarFallback } from "@/components/ui/avatar"; +import { Sidebar } from "@/components/dashboard/sidebar"; +import { useSidebar } from "@/hooks/useSidebar"; +import { useAuth } from "@/hooks/useAuth"; +import { GET_SUPPLY_ORDERS } from "@/graphql/queries"; +import { + LOGISTICS_CONFIRM_ORDER, + LOGISTICS_REJECT_ORDER +} from "@/graphql/mutations"; +import { toast } from "sonner"; +import { + Calendar, + Package, + Truck, + User, + CheckCircle, + Clock, + XCircle, + MapPin, + Phone, + Mail, + Building, + Hash, + AlertTriangle, +} from "lucide-react"; + +interface SupplyOrder { + id: string; + organizationId: string; + partnerId: string; + deliveryDate: string; + status: "PENDING" | "SUPPLIER_APPROVED" | "LOGISTICS_CONFIRMED" | "SHIPPED" | "DELIVERED" | "CANCELLED"; + totalAmount: number; + totalItems: number; + createdAt: string; + organization: { + id: string; + name?: string; + fullName?: string; + type: string; + }; + partner: { + id: string; + name?: string; + fullName?: string; + type: string; + }; + logisticsPartner?: { + id: string; + name?: string; + fullName?: string; + type: string; + }; + items: Array<{ + id: string; + quantity: number; + price: number; + totalPrice: number; + product: { + id: string; + name: string; + article: string; + description?: string; + category?: { + id: string; + name: string; + }; + }; + }>; +} + +export function LogisticsOrdersDashboard() { + const { getSidebarMargin } = useSidebar(); + const { user } = useAuth(); + const [expandedOrders, setExpandedOrders] = useState>(new Set()); + const [rejectReason, setRejectReason] = useState(""); + const [showRejectModal, setShowRejectModal] = useState(null); + + // Загружаем заказы поставок + const { data, loading, error, refetch } = useQuery(GET_SUPPLY_ORDERS, { + fetchPolicy: "cache-and-network", + }); + + console.log(`DEBUG ЛОГИСТИКА: loading=${loading}, error=${error?.message}, totalOrders=${data?.supplyOrders?.length || 0}`); + + // Мутации для действий логистики + const [logisticsConfirmOrder] = useMutation(LOGISTICS_CONFIRM_ORDER, { + refetchQueries: [{ query: GET_SUPPLY_ORDERS }], + onCompleted: (data) => { + if (data.logisticsConfirmOrder.success) { + toast.success(data.logisticsConfirmOrder.message); + } else { + toast.error(data.logisticsConfirmOrder.message); + } + }, + onError: (error) => { + console.error("Error confirming order:", error); + toast.error("Ошибка при подтверждении заказа"); + }, + }); + + const [logisticsRejectOrder] = useMutation(LOGISTICS_REJECT_ORDER, { + refetchQueries: [{ query: GET_SUPPLY_ORDERS }], + onCompleted: (data) => { + if (data.logisticsRejectOrder.success) { + toast.success(data.logisticsRejectOrder.message); + } else { + toast.error(data.logisticsRejectOrder.message); + } + setShowRejectModal(null); + setRejectReason(""); + }, + onError: (error) => { + console.error("Error rejecting order:", error); + toast.error("Ошибка при отклонении заказа"); + }, + }); + + const toggleOrderExpansion = (orderId: string) => { + const newExpanded = new Set(expandedOrders); + if (newExpanded.has(orderId)) { + newExpanded.delete(orderId); + } else { + newExpanded.add(orderId); + } + setExpandedOrders(newExpanded); + }; + + // Фильтруем заказы где текущая организация является логистическим партнером + const logisticsOrders: SupplyOrder[] = (data?.supplyOrders || []).filter( + (order: SupplyOrder) => { + const isLogisticsPartner = order.logisticsPartner?.id === user?.organization?.id; + console.log(`DEBUG ЛОГИСТИКА: Заказ ${order.id.slice(-8)} - статус: ${order.status}, logisticsPartnerId: ${order.logisticsPartner?.id}, currentOrgId: ${user?.organization?.id}, isLogisticsPartner: ${isLogisticsPartner}`); + return isLogisticsPartner; + } + ); + + const getStatusBadge = (status: SupplyOrder["status"]) => { + const statusMap = { + PENDING: { + label: "Ожидает поставщика", + color: "bg-gray-500/20 text-gray-300 border-gray-500/30", + icon: Clock, + }, + SUPPLIER_APPROVED: { + label: "Требует подтверждения", + color: "bg-yellow-500/20 text-yellow-300 border-yellow-500/30", + icon: AlertTriangle, + }, + LOGISTICS_CONFIRMED: { + label: "Подтверждено", + color: "bg-blue-500/20 text-blue-300 border-blue-500/30", + icon: CheckCircle, + }, + SHIPPED: { + label: "В пути", + color: "bg-orange-500/20 text-orange-300 border-orange-500/30", + icon: Truck, + }, + DELIVERED: { + label: "Доставлено", + color: "bg-green-500/20 text-green-300 border-green-500/30", + icon: Package, + }, + CANCELLED: { + label: "Отменено", + color: "bg-red-500/20 text-red-300 border-red-500/30", + icon: XCircle, + }, + // Устаревшие статусы для обратной совместимости + CONFIRMED: { + label: "Подтверждён (устаревший)", + color: "bg-blue-500/20 text-blue-300 border-blue-500/30", + icon: CheckCircle, + }, + IN_TRANSIT: { + label: "В пути (устаревший)", + color: "bg-orange-500/20 text-orange-300 border-orange-500/30", + icon: Truck, + }, + }; + + const config = statusMap[status as keyof typeof statusMap]; + if (!config) { + console.warn(`Unknown status: ${status}`); + // Fallback для неизвестных статусов + return ( + + + {status} + + ); + } + + const { label, color, icon: Icon } = config; + return ( + + + {label} + + ); + }; + + const handleConfirmOrder = async (orderId: string) => { + await logisticsConfirmOrder({ variables: { id: orderId } }); + }; + + const handleRejectOrder = async (orderId: string) => { + await logisticsRejectOrder({ + variables: { id: orderId, reason: rejectReason || undefined }, + }); + }; + + const formatDate = (dateString: string) => { + return new Date(dateString).toLocaleDateString("ru-RU", { + day: "2-digit", + month: "2-digit", + year: "numeric", + }); + }; + + const formatCurrency = (amount: number) => { + return new Intl.NumberFormat("ru-RU", { + style: "currency", + currency: "RUB", + }).format(amount); + }; + + const getInitials = (name: string): string => { + return name + .split(" ") + .map((word) => word.charAt(0)) + .join("") + .toUpperCase() + .slice(0, 2); + }; + + if (loading) { + return ( +
+ +
+
+
Загрузка заказов...
+
+
+
+ ); + } + + if (error) { + return ( +
+ +
+
+
Ошибка загрузки заказов: {error.message}
+
+
+
+ ); + } + + return ( +
+ +
+
+ {/* Заголовок */} +
+
+

+ Логистические заказы +

+

+ Управление заказами поставок и логистическими операциями +

+
+
+ + {/* Статистика */} +
+ +
+
+ +
+
+

Требуют подтверждения

+

+ {logisticsOrders.filter(order => order.status === "SUPPLIER_APPROVED").length} +

+
+
+
+ + +
+
+ +
+
+

Подтверждено

+

+ {logisticsOrders.filter(order => order.status === "LOGISTICS_CONFIRMED").length} +

+
+
+
+ + +
+
+ +
+
+

В пути

+

+ {logisticsOrders.filter(order => order.status === "SHIPPED").length} +

+
+
+
+ + +
+
+ +
+
+

Доставлено

+

+ {logisticsOrders.filter(order => order.status === "DELIVERED").length} +

+
+
+
+
+ + {/* Список заказов */} +
+ {logisticsOrders.length === 0 ? ( + +
+ +

+ Нет логистических заказов +

+

+ Заказы поставок, требующие логистического сопровождения, будут отображаться здесь +

+
+
+ ) : ( + logisticsOrders.map((order) => ( + toggleOrderExpansion(order.id)} + > + {/* Основная информация о заказе */} +
+
+ {/* Левая часть */} +
+ {/* Номер заказа */} +
+ + + {order.id.slice(-8)} + +
+ + {/* Маршрут */} +
+
+ + + {getInitials(order.partner.name || order.partner.fullName || "П")} + + + + + + {getInitials(order.organization.name || order.organization.fullName || "ФФ")} + + +
+
+

+ {order.partner.name || order.partner.fullName} → {order.organization.name || order.organization.fullName} +

+

+ Поставщик → Фулфилмент +

+
+
+ + {/* Краткая информация */} +
+
+ + + {formatDate(order.deliveryDate)} + +
+
+ + + {order.totalItems} шт. + +
+
+
+ + {/* Правая часть - статус и действия */} +
+ {getStatusBadge(order.status)} + + {/* Кнопки действий для логистики */} + {order.status === "SUPPLIER_APPROVED" && ( +
+ + +
+ )} +
+
+ + {/* Развернутые детали */} + {expandedOrders.has(order.id) && ( + <> + + + {/* Сумма заказа */} +
+
+ Общая сумма: + + {formatCurrency(order.totalAmount)} + +
+
+ + {/* Информация о маршруте */} +
+
+

+ + Поставщик +

+
+

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

+
+
+
+

+ + Получатель +

+
+

+ {order.organization.name || order.organization.fullName} +

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

+ + Товары к доставке ({order.items.length}) +

+
+ {order.items.map((item) => ( +
+
+
+ {item.product.name} +
+

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

+ {item.product.category && ( + + {item.product.category.name} + + )} +
+
+

+ {item.quantity} шт. +

+

+ {formatCurrency(item.price)} +

+

+ {formatCurrency(item.totalPrice)} +

+
+
+ ))} +
+
+ + )} +
+
+ )) + )} +
+
+ + {/* Модальное окно для отклонения заказа */} + {showRejectModal && ( +
+ +

+ Отклонить логистический заказ +

+

+ Укажите причину отклонения заказа (необязательно): +

+