diff --git a/src/app/fulfillment-warehouse/supplies/page.tsx b/src/app/fulfillment-warehouse/supplies/page.tsx new file mode 100644 index 0000000..3578468 --- /dev/null +++ b/src/app/fulfillment-warehouse/supplies/page.tsx @@ -0,0 +1,5 @@ +import { FulfillmentSuppliesPage } from "@/components/fulfillment-warehouse/fulfillment-supplies-page"; + +export default function FulfillmentWarehouseSuppliesPage() { + 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 index 6ef26cf..72b9036 100644 --- a/src/components/fulfillment-supplies/create-fulfillment-consumables-supply-page.tsx +++ b/src/components/fulfillment-supplies/create-fulfillment-consumables-supply-page.tsx @@ -28,6 +28,7 @@ import { GET_MY_COUNTERPARTIES, GET_ALL_PRODUCTS, GET_SUPPLY_ORDERS, + GET_MY_SUPPLIES, } from "@/graphql/queries"; import { CREATE_SUPPLY_ORDER } from "@/graphql/mutations"; import { OrganizationAvatar } from "@/components/market/organization-avatar"; @@ -232,7 +233,10 @@ export function CreateFulfillmentConsumablesSupplyPage() { })), }, }, - refetchQueries: [{ query: GET_SUPPLY_ORDERS }], + refetchQueries: [ + { query: GET_SUPPLY_ORDERS }, // Обновляем заказы поставок + { query: GET_MY_SUPPLIES }, // Обновляем расходники фулфилмента + ], }); if (result.data?.createSupplyOrder?.success) { diff --git a/src/components/fulfillment-supplies/fulfillment-supplies/fulfillment-consumables-orders-tab.tsx b/src/components/fulfillment-supplies/fulfillment-supplies/fulfillment-consumables-orders-tab.tsx index dd7b65b..9d16e06 100644 --- a/src/components/fulfillment-supplies/fulfillment-supplies/fulfillment-consumables-orders-tab.tsx +++ b/src/components/fulfillment-supplies/fulfillment-supplies/fulfillment-consumables-orders-tab.tsx @@ -36,57 +36,6 @@ import { AlertTriangle, } from "lucide-react"; -// Компонент уведомлений о непринятых поставках -function PendingSuppliesAlert() { - const { data: pendingData } = useQuery(GET_PENDING_SUPPLIES_COUNT, { - pollInterval: 30000, // Обновляем каждые 30 секунд - fetchPolicy: "cache-first", - errorPolicy: "ignore", - }); - - const pendingCount = pendingData?.pendingSuppliesCount?.total || 0; - const supplyOrdersCount = - pendingData?.pendingSuppliesCount?.supplyOrders || 0; - const incomingRequestsCount = - pendingData?.pendingSuppliesCount?.incomingRequests || 0; - - if (pendingCount === 0) return null; - - return ( - -
-
- -
-
-

- - Требует вашего внимания -

-
- {supplyOrdersCount > 0 && ( -

- • {supplyOrdersCount} поставок требуют вашего действия - (подтверждение/получение) -

- )} - {incomingRequestsCount > 0 && ( -

- • {incomingRequestsCount} заявок на партнерство ожидают ответа -

- )} -
-
-
-
- {pendingCount > 99 ? "99+" : pendingCount} -
-
-
-
- ); -} - interface SupplyOrder { id: string; partnerId: string; @@ -147,7 +96,7 @@ export function FulfillmentConsumablesOrdersTab() { }, refetchQueries: [ { query: GET_SUPPLY_ORDERS }, // Обновляем заказы поставок - { query: GET_MY_SUPPLIES }, // Обновляем склад фулфилмента + { query: GET_MY_SUPPLIES }, // Обновляем склад фулфилмента (расходники фф) { query: GET_WAREHOUSE_PRODUCTS }, // Обновляем товары склада ], onError: (error) => { @@ -288,9 +237,6 @@ export function FulfillmentConsumablesOrdersTab() { return (
- {/* Уведомления о непринятых поставках */} - - {/* Компактная статистика */}
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 7218992..731de1c 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 @@ -11,6 +11,8 @@ import { useQuery, useMutation } from "@apollo/client"; import { GET_SUPPLY_ORDERS, GET_PENDING_SUPPLIES_COUNT, + GET_MY_SUPPLIES, + GET_WAREHOUSE_PRODUCTS, } from "@/graphql/queries"; import { UPDATE_SUPPLY_ORDER_STATUS } from "@/graphql/mutations"; import { useAuth } from "@/hooks/useAuth"; @@ -31,56 +33,7 @@ import { CheckCircle, } from "lucide-react"; -// Компонент уведомлений о непринятых поставках -function PendingSuppliesAlert() { - const { data: pendingData } = useQuery(GET_PENDING_SUPPLIES_COUNT, { - pollInterval: 30000, // Обновляем каждые 30 секунд - fetchPolicy: "cache-first", - errorPolicy: "ignore", - }); - const pendingCount = pendingData?.pendingSuppliesCount?.total || 0; - const supplyOrdersCount = - pendingData?.pendingSuppliesCount?.supplyOrders || 0; - const incomingRequestsCount = - pendingData?.pendingSuppliesCount?.incomingRequests || 0; - - if (pendingCount === 0) return null; - - return ( - -
-
- -
-
-

- - Требует вашего внимания -

-
- {supplyOrdersCount > 0 && ( -

- • {supplyOrdersCount} поставок требуют вашего действия - (подтверждение/получение) -

- )} - {incomingRequestsCount > 0 && ( -

- • {incomingRequestsCount} заявок на партнерство ожидают ответа -

- )} -
-
-
-
- {pendingCount > 99 ? "99+" : pendingCount} -
-
-
-
- ); -} // Интерфейс для заказа interface SupplyOrder { @@ -92,6 +45,7 @@ interface SupplyOrder { totalAmount: number; status: string; fulfillmentCenterId: string; + number?: number; // Порядковый номер organization: { id: string; name?: string; @@ -170,7 +124,11 @@ export function FulfillmentDetailedSuppliesTab() { // Мутация для обновления статуса заказа const [updateSupplyOrderStatus] = useMutation(UPDATE_SUPPLY_ORDER_STATUS, { - refetchQueries: [{ query: GET_SUPPLY_ORDERS }], + refetchQueries: [ + { query: GET_SUPPLY_ORDERS }, // Обновляем заказы поставок + { query: GET_MY_SUPPLIES }, // Обновляем склад фулфилмента (расходники фф) + { query: GET_WAREHOUSE_PRODUCTS }, // Обновляем товары склада + ], onError: (error) => { console.error("Error updating supply order status:", error); toast.error("Ошибка при обновлении статуса заказа"); @@ -197,7 +155,11 @@ export function FulfillmentDetailedSuppliesTab() { } ); - // Убираем разделение на createdByUs и createdForUs, так как здесь только наши поставки + // Генерируем порядковые номера для заказов (сверху вниз от большего к меньшему) + const ordersWithNumbers = ourSupplyOrders.map((order, index) => ({ + ...order, + number: ourSupplyOrders.length - index, // Обратный порядок для новых заказов сверху + })); const toggleOrderExpansion = (orderId: string) => { const newExpanded = new Set(expandedOrders); @@ -261,9 +223,6 @@ export function FulfillmentDetailedSuppliesTab() { return (
- {/* Уведомления о непринятых поставках */} - - {/* Заголовок с кнопкой создания поставки */}
@@ -380,7 +339,7 @@ export function FulfillmentDetailedSuppliesTab() { - {ourSupplyOrders.map((order: SupplyOrder) => { + {ordersWithNumbers.map((order: SupplyOrder) => { const isOrderExpanded = expandedOrders.has(order.id); return ( @@ -393,7 +352,7 @@ export function FulfillmentDetailedSuppliesTab() {
- #{order.id.slice(-8)} + {order.number}
diff --git a/src/components/fulfillment-supplies/fulfillment-supplies/fulfillment-goods-tab.tsx b/src/components/fulfillment-supplies/fulfillment-supplies/fulfillment-goods-tab.tsx index d295c8d..f4f1a39 100644 --- a/src/components/fulfillment-supplies/fulfillment-supplies/fulfillment-goods-tab.tsx +++ b/src/components/fulfillment-supplies/fulfillment-supplies/fulfillment-goods-tab.tsx @@ -40,56 +40,7 @@ import { } from "lucide-react"; import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; -// Компонент уведомлений о непринятых поставках -function PendingSuppliesAlert() { - const { data: pendingData } = useQuery(GET_PENDING_SUPPLIES_COUNT, { - pollInterval: 30000, // Обновляем каждые 30 секунд - fetchPolicy: "cache-first", - errorPolicy: "ignore", - }); - const pendingCount = pendingData?.pendingSuppliesCount?.total || 0; - const supplyOrdersCount = - pendingData?.pendingSuppliesCount?.supplyOrders || 0; - const incomingRequestsCount = - pendingData?.pendingSuppliesCount?.incomingRequests || 0; - - if (pendingCount === 0) return null; - - return ( - -
-
- -
-
-

- - Требует вашего внимания -

-
- {supplyOrdersCount > 0 && ( -

- • {supplyOrdersCount} поставок требуют вашего действия - (подтверждение/получение) -

- )} - {incomingRequestsCount > 0 && ( -

- • {incomingRequestsCount} заявок на партнерство ожидают ответа -

- )} -
-
-
-
- {pendingCount > 99 ? "99+" : pendingCount} -
-
-
-
- ); -} // Интерфейсы для данных interface Employee { @@ -712,9 +663,6 @@ export function FulfillmentGoodsTab() { return (
- {/* Уведомления о непринятых поставках */} - - ({ + items: selectedProducts.map((product) => ({ productId: product.id, - quantity: product.selectedQuantity - })) - } - } + quantity: product.selectedQuantity, + })), + }, + }, + refetchQueries: [ + { query: GET_SUPPLY_ORDERS }, // Обновляем заказы поставок + { query: GET_MY_SUPPLIES }, // Обновляем расходники фулфилмента + ], }); if (result.data?.createSupplyOrder?.success) { toast.success("Заказ поставки создан успешно!"); router.push("/fulfillment-supplies"); } else { - toast.error(result.data?.createSupplyOrder?.message || "Ошибка при создании заказа"); + toast.error( + result.data?.createSupplyOrder?.message || + "Ошибка при создании заказа" + ); } } catch (error) { console.error("Error creating supply order:", error); @@ -447,14 +460,20 @@ export function MaterialsOrderForm() {
- {/* Кнопка создания заказа */} -
diff --git a/src/components/fulfillment-warehouse/delivery-details.tsx b/src/components/fulfillment-warehouse/delivery-details.tsx new file mode 100644 index 0000000..f36048d --- /dev/null +++ b/src/components/fulfillment-warehouse/delivery-details.tsx @@ -0,0 +1,297 @@ +"use client"; + +import React, { useMemo } from "react"; +import { Card } from "@/components/ui/card"; +import { Badge } from "@/components/ui/badge"; +import { + Truck, + Package, + Calendar, + DollarSign, + TrendingUp, + CheckCircle, + AlertTriangle, + Clock, +} from "lucide-react"; +import { DeliveryDetailsProps } from "./types"; + +const DELIVERY_STATUS_CONFIG = { + delivered: { + label: "Доставлено", + color: "bg-green-500/20 text-green-300", + icon: CheckCircle, + }, + "in-transit": { + label: "В пути", + color: "bg-blue-500/20 text-blue-300", + icon: Truck, + }, + pending: { + label: "Ожидание", + color: "bg-yellow-500/20 text-yellow-300", + icon: Clock, + }, + delayed: { + label: "Задержка", + color: "bg-red-500/20 text-red-300", + icon: AlertTriangle, + }, +} as const; + +export function DeliveryDetails({ + supply, + deliveries, + viewMode, + getStatusConfig, +}: DeliveryDetailsProps) { + const formatCurrency = (amount: number) => { + return new Intl.NumberFormat("ru-RU", { + style: "currency", + currency: "RUB", + minimumFractionDigits: 0, + }).format(amount); + }; + + const formatNumber = (num: number) => { + return new Intl.NumberFormat("ru-RU").format(num); + }; + + const getDeliveryStatusConfig = (status: string) => { + return ( + DELIVERY_STATUS_CONFIG[status as keyof typeof DELIVERY_STATUS_CONFIG] || + DELIVERY_STATUS_CONFIG.pending + ); + }; + + const totalStats = useMemo(() => { + const totalQuantity = deliveries.reduce((sum, d) => sum + d.quantity, 0); + const totalStock = deliveries.reduce((sum, d) => sum + d.currentStock, 0); + const totalCost = deliveries.reduce( + (sum, d) => sum + d.price * d.currentStock, + 0 + ); + const avgPrice = + deliveries.length > 0 + ? deliveries.reduce((sum, d) => sum + d.price, 0) / deliveries.length + : 0; + + return { totalQuantity, totalStock, totalCost, avgPrice }; + }, [deliveries]); + + if (viewMode === "grid") { + return ( +
+
+ + История поставок ({deliveries.length}) +
+ + {/* Общая статистика */} + +

+ + Общая статистика +

+
+
+

Общий заказ

+

+ {formatNumber(totalStats.totalQuantity)} {supply.unit} +

+
+
+

Общий остаток

+

+ {formatNumber(totalStats.totalStock)} {supply.unit} +

+
+
+

Общая стоимость

+

+ {formatCurrency(totalStats.totalCost)} +

+
+
+

Средняя цена

+

+ {formatCurrency(totalStats.avgPrice)} +

+
+
+
+ + {/* Список поставок */} +
+ {deliveries.map((delivery, index) => { + const deliveryStatusConfig = getDeliveryStatusConfig( + delivery.status + ); + const DeliveryStatusIcon = deliveryStatusConfig.icon; + + return ( + +
+
+ + + {deliveryStatusConfig.label} + + + {new Date(delivery.createdAt).toLocaleDateString( + "ru-RU", + { + year: "numeric", + month: "short", + day: "numeric", + hour: "2-digit", + minute: "2-digit", + } + )} + +
+
+ +
+
+

Остаток

+

+ {formatNumber(delivery.currentStock)} {delivery.unit} +

+
+
+

Заказано

+

+ {formatNumber(delivery.quantity)} {delivery.unit} +

+
+
+

Цена

+

+ {formatCurrency(delivery.price)} +

+
+
+

Стоимость

+

+ {formatCurrency(delivery.price * delivery.currentStock)} +

+
+
+ + {delivery.description !== supply.description && ( +
+

+ Описание: {delivery.description} +

+
+ )} +
+ ); + })} +
+
+ ); + } + + // List view - компактное отображение + return ( +
+
+ + История поставок +
+ +
+ {deliveries.map((delivery, index) => { + const deliveryStatusConfig = getDeliveryStatusConfig(delivery.status); + const DeliveryStatusIcon = deliveryStatusConfig.icon; + + return ( + +
+
+

Дата

+

+ {new Date(delivery.createdAt).toLocaleDateString("ru-RU", { + month: "short", + day: "numeric", + hour: "2-digit", + minute: "2-digit", + })} +

+
+ +
+

Заказано

+

+ {formatNumber(delivery.quantity)} {delivery.unit} +

+
+ +
+

Поставлено

+

+ {formatNumber(delivery.quantity)} {delivery.unit} +

+
+ +
+

Отправлено

+

+ {formatNumber(delivery.shippedQuantity || 0)}{" "} + {delivery.unit} +

+
+ +
+

Остаток

+

+ {formatNumber( + delivery.quantity - (delivery.shippedQuantity || 0) + )}{" "} + {delivery.unit} +

+
+ +
+

Цена

+

+ {formatCurrency(delivery.price)} +

+
+ +
+

Стоимость

+

+ {formatCurrency(delivery.price * delivery.quantity)} +

+
+ +
+ + + {deliveryStatusConfig.label} + +
+
+ + {delivery.description !== supply.description && ( +
+

+ Описание: {delivery.description} +

+
+ )} +
+ ); + })} +
+
+ ); +} diff --git a/src/components/fulfillment-warehouse/fulfillment-supplies-page.tsx b/src/components/fulfillment-warehouse/fulfillment-supplies-page.tsx new file mode 100644 index 0000000..f3b2e3d --- /dev/null +++ b/src/components/fulfillment-warehouse/fulfillment-supplies-page.tsx @@ -0,0 +1,412 @@ +"use client"; + +import React, { useState, useMemo, useCallback } from "react"; +import { Sidebar } from "@/components/dashboard/sidebar"; +import { useSidebar } from "@/hooks/useSidebar"; +import { useQuery } from "@apollo/client"; +import { GET_MY_SUPPLIES } from "@/graphql/queries"; +import { + Package, + Wrench, + AlertTriangle, + CheckCircle, + Clock, +} from "lucide-react"; +import { toast } from "sonner"; + +// Новые компоненты +import { SuppliesHeader } from "./supplies-header"; +import { SuppliesStats } from "./supplies-stats"; +import { SuppliesGrid } from "./supplies-grid"; +import { SuppliesList } from "./supplies-list"; + +// Типы +import { + Supply, + FilterState, + SortState, + ViewMode, + GroupBy, + StatusConfig, +} from "./types"; + +// Статусы расходников с цветами +const STATUS_CONFIG = { + available: { + label: "Доступен", + color: "bg-green-500/20 text-green-300", + icon: CheckCircle, + }, + "low-stock": { + label: "Мало на складе", + color: "bg-yellow-500/20 text-yellow-300", + icon: AlertTriangle, + }, + "out-of-stock": { + label: "Нет в наличии", + color: "bg-red-500/20 text-red-300", + icon: AlertTriangle, + }, + "in-transit": { + label: "В пути", + color: "bg-blue-500/20 text-blue-300", + icon: Clock, + }, + reserved: { + label: "Зарезервирован", + color: "bg-purple-500/20 text-purple-300", + icon: Package, + }, +} as const; + +export function FulfillmentSuppliesPage() { + const { getSidebarMargin } = useSidebar(); + + // Состояния + const [viewMode, setViewMode] = useState("grid"); + const [filters, setFilters] = useState({ + search: "", + category: "", + status: "", + supplier: "", + lowStock: false, + }); + const [sort, setSort] = useState({ + field: "name", + direction: "asc", + }); + const [showFilters, setShowFilters] = useState(false); + const [groupBy, setGroupBy] = useState("none"); + const [expandedSupplies, setExpandedSupplies] = useState>( + new Set() + ); + + // Загрузка данных + const { + data: suppliesData, + loading, + error, + refetch, + } = useQuery(GET_MY_SUPPLIES, { + fetchPolicy: "cache-and-network", + onError: (error) => { + toast.error("Ошибка загрузки расходников: " + error.message); + }, + }); + + const supplies: Supply[] = suppliesData?.mySupplies || []; + + // Логирование для отладки + console.log("🔥🔥🔥 FULFILLMENT SUPPLIES PAGE DATA 🔥🔥🔥", { + suppliesCount: supplies.length, + supplies: supplies.map((s) => ({ + id: s.id, + name: s.name, + status: s.status, + currentStock: s.currentStock, + quantity: s.quantity, + })), + }); + + // Функции + const getStatusConfig = useCallback((status: string): StatusConfig => { + return ( + STATUS_CONFIG[status as keyof typeof STATUS_CONFIG] || + STATUS_CONFIG.available + ); + }, []); + + const getSupplyDeliveries = useCallback( + (supply: Supply): Supply[] => { + return supplies.filter( + (s) => s.name === supply.name && s.category === supply.category + ); + }, + [supplies] + ); + + // Объединение одинаковых расходников + const consolidatedSupplies = useMemo(() => { + const grouped = supplies.reduce((acc, supply) => { + const key = `${supply.name}-${supply.category}`; + if (!acc[key]) { + acc[key] = { + ...supply, + currentStock: 0, + quantity: 0, // Общее количество поставленного (= заказанному) + price: 0, + totalCost: 0, // Общая стоимость + shippedQuantity: 0, // Общее отправленное количество + }; + } + + // Суммируем поставленное количество (заказано = поставлено) + acc[key].quantity += supply.quantity; + + // Суммируем отправленное количество + acc[key].shippedQuantity += supply.shippedQuantity || 0; + + // Остаток = Поставлено - Отправлено + // Если ничего не отправлено, то остаток = поставлено + acc[key].currentStock = acc[key].quantity - acc[key].shippedQuantity; + + // Рассчитываем общую стоимость (количество × цена) + acc[key].totalCost += supply.quantity * supply.price; + + // Средневзвешенная цена за единицу + if (acc[key].quantity > 0) { + acc[key].price = acc[key].totalCost / acc[key].quantity; + } + + return acc; + }, {} as Record); + + return Object.values(grouped); + }, [supplies]); + + // Фильтрация и сортировка + const filteredAndSortedSupplies = useMemo(() => { + let filtered = consolidatedSupplies.filter((supply) => { + const matchesSearch = + supply.name.toLowerCase().includes(filters.search.toLowerCase()) || + supply.description.toLowerCase().includes(filters.search.toLowerCase()); + const matchesCategory = + !filters.category || supply.category === filters.category; + const matchesStatus = !filters.status || supply.status === filters.status; + const matchesSupplier = + !filters.supplier || + supply.supplier.toLowerCase().includes(filters.supplier.toLowerCase()); + const matchesLowStock = + !filters.lowStock || + (supply.currentStock <= supply.minStock && supply.currentStock > 0); + + return ( + matchesSearch && + matchesCategory && + matchesStatus && + matchesSupplier && + matchesLowStock + ); + }); + + // Сортировка + filtered.sort((a, b) => { + let aValue: any = a[sort.field]; + let bValue: any = b[sort.field]; + + if (typeof aValue === "string") { + aValue = aValue.toLowerCase(); + bValue = bValue.toLowerCase(); + } + + if (sort.direction === "asc") { + return aValue > bValue ? 1 : -1; + } else { + return aValue < bValue ? 1 : -1; + } + }); + + return filtered; + }, [consolidatedSupplies, filters, sort]); + + // Группировка + const groupedSupplies = useMemo(() => { + if (groupBy === "none") + return { "Все расходники": filteredAndSortedSupplies }; + + return filteredAndSortedSupplies.reduce((acc, supply) => { + const key = supply[groupBy] || "Без категории"; + if (!acc[key]) acc[key] = []; + acc[key].push(supply); + return acc; + }, {} as Record); + }, [filteredAndSortedSupplies, groupBy]); + + // Обработчики + const handleSort = useCallback((field: SortState["field"]) => { + setSort((prev) => ({ + field, + direction: + prev.field === field && prev.direction === "asc" ? "desc" : "asc", + })); + }, []); + + const toggleSupplyExpansion = useCallback((supplyId: string) => { + setExpandedSupplies((prev) => { + const newSet = new Set(prev); + if (newSet.has(supplyId)) { + newSet.delete(supplyId); + } else { + newSet.add(supplyId); + } + return newSet; + }); + }, []); + + const handleExport = useCallback(() => { + const csvData = filteredAndSortedSupplies.map((supply) => ({ + Название: supply.name, + Описание: supply.description, + Категория: supply.category, + Статус: getStatusConfig(supply.status).label, + "Текущий остаток": supply.currentStock, + "Минимальный остаток": supply.minStock, + Единица: supply.unit, + Цена: supply.price, + Поставщик: supply.supplier, + "Дата создания": new Date(supply.createdAt).toLocaleDateString("ru-RU"), + })); + + const csv = [ + Object.keys(csvData[0]).join(","), + ...csvData.map((row) => Object.values(row).join(",")), + ].join("\n"); + + const blob = new Blob([csv], { type: "text/csv;charset=utf-8;" }); + const link = document.createElement("a"); + link.href = URL.createObjectURL(blob); + link.download = `расходники_фф_${ + new Date().toISOString().split("T")[0] + }.csv`; + link.click(); + + toast.success("Данные экспортированы в CSV"); + }, [filteredAndSortedSupplies, getStatusConfig]); + + const handleRefresh = useCallback(() => { + refetch(); + toast.success("Данные обновлены"); + }, [refetch]); + + if (loading) { + return ( +
+ +
+
+
Загрузка...
+
+
+
+ ); + } + + if (error) { + return ( +
+ +
+
+
Ошибка загрузки: {error.message}
+
+
+
+ ); + } + + return ( +
+ +
+
+ {/* Заголовок и фильтры */} + setShowFilters(!showFilters)} + onExport={handleExport} + onRefresh={handleRefresh} + /> + + {/* Статистика */} + + + {/* Основной контент */} +
+ {groupBy === "none" ? ( + // Без группировки + <> + {viewMode === "grid" && ( + + )} + + {viewMode === "list" && ( + + )} + + {viewMode === "analytics" && ( +
+ Аналитический режим будет добавлен позже +
+ )} + + ) : ( + // С группировкой +
+ {Object.entries(groupedSupplies).map( + ([groupName, groupSupplies]) => ( +
+

+ {groupName} + + ({groupSupplies.length}) + +

+ + {viewMode === "grid" && ( + + )} + + {viewMode === "list" && ( + + )} +
+ ) + )} +
+ )} +
+
+
+
+ ); +} diff --git a/src/components/fulfillment-warehouse/fulfillment-supplies-page.tsx.backup b/src/components/fulfillment-warehouse/fulfillment-supplies-page.tsx.backup new file mode 100644 index 0000000..eb00a7a --- /dev/null +++ b/src/components/fulfillment-warehouse/fulfillment-supplies-page.tsx.backup @@ -0,0 +1,1840 @@ +"use client"; + +import React, { useState, useMemo, useCallback } from "react"; +import { useRouter } from "next/navigation"; +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 { useQuery } from "@apollo/client"; +import { GET_MY_SUPPLIES } from "@/graphql/queries"; +import { + ArrowLeft, + Search, + Filter, + SortAsc, + SortDesc, + Package, + Wrench, + AlertTriangle, + CheckCircle, + Clock, + TrendingUp, + TrendingDown, + BarChart3, + Grid3X3, + List, + Download, + Eye, + Calendar, + MapPin, + User, + DollarSign, + Hash, + Activity, + Layers, + PieChart, + FileSpreadsheet, + Zap, + Target, + Sparkles, + Truck, + ChevronRight, + ChevronDown, +} from "lucide-react"; +import { toast } from "sonner"; + +// Типы данных +interface Supply { + id: string; + name: string; + description: string; + price: number; + quantity: number; + unit: string; + category: string; + status: string; + date: string; + supplier: string; + minStock: number; + currentStock: number; + imageUrl?: string; + createdAt: string; + updatedAt: string; +} + +interface FilterState { + search: string; + category: string; + status: string; + supplier: string; + lowStock: boolean; +} + +interface SortState { + field: keyof Supply; + direction: "asc" | "desc"; +} + +// Статусы расходников с цветами +const STATUS_CONFIG = { + available: { + label: "Доступен", + color: "bg-green-500/20 text-green-300", + icon: CheckCircle, + }, + "low-stock": { + label: "Мало на складе", + color: "bg-yellow-500/20 text-yellow-300", + icon: AlertTriangle, + }, + "out-of-stock": { + label: "Нет в наличии", + color: "bg-red-500/20 text-red-300", + icon: AlertTriangle, + }, + "in-transit": { + label: "В пути", + color: "bg-blue-500/20 text-blue-300", + icon: Clock, + }, + reserved: { + label: "Зарезервирован", + color: "bg-purple-500/20 text-purple-300", + icon: Package, + }, +} as const; + +export function FulfillmentSuppliesPage() { + const router = useRouter(); + const { getSidebarMargin } = useSidebar(); + + // Состояния + const [viewMode, setViewMode] = useState<"grid" | "list" | "analytics">( + "grid" + ); + const [filters, setFilters] = useState({ + search: "", + category: "", + status: "", + supplier: "", + lowStock: false, + }); + const [sort, setSort] = useState({ + field: "name", + direction: "asc", + }); + const [showFilters, setShowFilters] = useState(false); + const [groupBy, setGroupBy] = useState< + "none" | "category" | "status" | "supplier" + >("none"); + const [expandedSupplies, setExpandedSupplies] = useState>( + new Set() + ); + + // Загрузка данных + const { + data: suppliesData, + loading, + error, + refetch, + } = useQuery(GET_MY_SUPPLIES, { + fetchPolicy: "cache-and-network", + onError: (error) => { + toast.error("Ошибка загрузки расходников: " + error.message); + }, + }); + + const supplies: Supply[] = suppliesData?.mySupplies || []; + + // Объединение идентичных расходников + const consolidatedSupplies = useMemo(() => { + const suppliesMap = new Map(); + + supplies.forEach((supply) => { + const key = `${supply.name}-${supply.category}-${supply.supplier}`; + + if (suppliesMap.has(key)) { + const existing = suppliesMap.get(key)!; + // Суммируем количества + existing.currentStock += supply.currentStock; + existing.quantity += supply.quantity; + // Берем максимальный минимальный остаток + existing.minStock = Math.max(existing.minStock, supply.minStock); + // Обновляем статус на основе суммарного остатка + if (existing.currentStock === 0) { + existing.status = "out-of-stock"; + } else if (existing.currentStock <= existing.minStock) { + existing.status = "low-stock"; + } else { + existing.status = "available"; + } + // Обновляем дату на более позднюю + if (new Date(supply.updatedAt) > new Date(existing.updatedAt)) { + existing.updatedAt = supply.updatedAt; + } + } else { + // Создаем копию с правильным статусом + const consolidatedSupply = { ...supply }; + if (consolidatedSupply.currentStock === 0) { + consolidatedSupply.status = "out-of-stock"; + } else if ( + consolidatedSupply.currentStock <= consolidatedSupply.minStock + ) { + consolidatedSupply.status = "low-stock"; + } else { + consolidatedSupply.status = "available"; + } + suppliesMap.set(key, consolidatedSupply); + } + }); + + return Array.from(suppliesMap.values()); + }, [supplies]); + + // Статистика на основе объединенных данных + const stats = useMemo(() => { + const total = consolidatedSupplies.length; + const available = consolidatedSupplies.filter( + (s) => s.status === "available" + ).length; + const lowStock = consolidatedSupplies.filter( + (s) => s.currentStock <= s.minStock && s.currentStock > 0 + ).length; + const outOfStock = consolidatedSupplies.filter( + (s) => s.currentStock === 0 + ).length; + const inTransit = consolidatedSupplies.filter( + (s) => s.status === "in-transit" + ).length; + const totalValue = consolidatedSupplies.reduce( + (sum, s) => sum + s.price * s.currentStock, + 0 + ); + const categories = new Set(consolidatedSupplies.map((s) => s.category)) + .size; + const suppliers = new Set(consolidatedSupplies.map((s) => s.supplier)).size; + + return { + total, + available, + lowStock, + outOfStock, + inTransit, + totalValue, + categories, + suppliers, + }; + }, [consolidatedSupplies]); + + // Фильтрация и сортировка объединенных данных + const filteredAndSortedSupplies = useMemo(() => { + let filtered = consolidatedSupplies.filter((supply) => { + const matchesSearch = + supply.name.toLowerCase().includes(filters.search.toLowerCase()) || + supply.description + .toLowerCase() + .includes(filters.search.toLowerCase()) || + supply.supplier.toLowerCase().includes(filters.search.toLowerCase()); + + const matchesCategory = + !filters.category || supply.category === filters.category; + const matchesStatus = !filters.status || supply.status === filters.status; + const matchesSupplier = + !filters.supplier || supply.supplier === filters.supplier; + const matchesLowStock = + !filters.lowStock || supply.currentStock <= supply.minStock; + + return ( + matchesSearch && + matchesCategory && + matchesStatus && + matchesSupplier && + matchesLowStock + ); + }); + + // Сортировка + filtered.sort((a, b) => { + const aValue = a[sort.field]; + const bValue = b[sort.field]; + + if (typeof aValue === "string" && typeof bValue === "string") { + return sort.direction === "asc" + ? aValue.localeCompare(bValue) + : bValue.localeCompare(aValue); + } + + if (typeof aValue === "number" && typeof bValue === "number") { + return sort.direction === "asc" ? aValue - bValue : bValue - aValue; + } + + return 0; + }); + + return filtered; + }, [consolidatedSupplies, filters, sort]); + + // Уникальные значения для фильтров на основе объединенных данных + const uniqueCategories = useMemo( + () => [...new Set(consolidatedSupplies.map((s) => s.category))].sort(), + [consolidatedSupplies] + ); + const uniqueStatuses = useMemo( + () => [...new Set(consolidatedSupplies.map((s) => s.status))].sort(), + [consolidatedSupplies] + ); + const uniqueSuppliers = useMemo( + () => [...new Set(consolidatedSupplies.map((s) => s.supplier))].sort(), + [consolidatedSupplies] + ); + + // Обработчики + const handleSort = useCallback((field: keyof Supply) => { + setSort((prev) => ({ + field, + direction: + prev.field === field && prev.direction === "asc" ? "desc" : "asc", + })); + }, []); + + const handleFilterChange = useCallback( + (key: keyof FilterState, value: string | boolean) => { + setFilters((prev) => ({ ...prev, [key]: value })); + }, + [] + ); + + const clearFilters = useCallback(() => { + setFilters({ + search: "", + category: "", + status: "", + supplier: "", + lowStock: false, + }); + }, []); + + // Получение всех поставок для конкретного расходника + const getSupplyDeliveries = useCallback( + (supply: Supply) => { + const key = `${supply.name}-${supply.category}-${supply.supplier}`; + return supplies.filter( + (s) => `${s.name}-${s.category}-${s.supplier}` === key + ); + }, + [supplies] + ); + + // Обработчик разворачивания/сворачивания расходника + const toggleSupplyExpansion = useCallback((supplyId: string) => { + setExpandedSupplies((prev) => { + const newSet = new Set(prev); + if (newSet.has(supplyId)) { + newSet.delete(supplyId); + } else { + newSet.add(supplyId); + } + return newSet; + }); + }, []); + + const formatCurrency = (value: number) => + new Intl.NumberFormat("ru-RU", { + style: "currency", + currency: "RUB", + }).format(value); + + const formatNumber = (value: number) => + new Intl.NumberFormat("ru-RU").format(value); + + const getStatusConfig = (status: string) => + STATUS_CONFIG[status as keyof typeof STATUS_CONFIG] || + STATUS_CONFIG.available; + + // Группировка данных + const groupedSupplies = useMemo(() => { + if (groupBy === "none") + return { "Все расходники": filteredAndSortedSupplies }; + + const groups: Record = {}; + + filteredAndSortedSupplies.forEach((supply) => { + const key = supply[groupBy] || "Не указано"; + if (!groups[key]) groups[key] = []; + groups[key].push(supply); + }); + + return groups; + }, [filteredAndSortedSupplies, groupBy]); + + // Экспорт данных + const exportData = useCallback( + (format: "csv" | "json") => { + const data = filteredAndSortedSupplies.map((supply) => ({ + Название: supply.name, + Описание: supply.description, + Категория: supply.category, + Статус: getStatusConfig(supply.status).label, + "Остаток (шт)": supply.currentStock, + "Мин. остаток (шт)": supply.minStock, + "Цена (руб)": supply.price, + Поставщик: supply.supplier, + "Дата создания": new Date(supply.createdAt).toLocaleDateString("ru-RU"), + })); + + if (format === "csv") { + const csv = [ + Object.keys(data[0]).join(","), + ...data.map((row) => Object.values(row).join(",")), + ].join("\n"); + + const blob = new Blob([csv], { type: "text/csv;charset=utf-8;" }); + const link = document.createElement("a"); + link.href = URL.createObjectURL(blob); + link.download = `расходники_${ + new Date().toISOString().split("T")[0] + }.csv`; + link.click(); + } else { + const json = JSON.stringify(data, null, 2); + const blob = new Blob([json], { type: "application/json" }); + const link = document.createElement("a"); + link.href = URL.createObjectURL(blob); + link.download = `расходники_${ + new Date().toISOString().split("T")[0] + }.json`; + link.click(); + } + + toast.success(`Данные экспортированы в формате ${format.toUpperCase()}`); + }, + [filteredAndSortedSupplies, getStatusConfig] + ); + + // Аналитические данные для графиков на основе объединенных данных + const analyticsData = useMemo(() => { + const categoryStats = uniqueCategories.map((category) => { + const categorySupplies = consolidatedSupplies.filter( + (s) => s.category === category + ); + return { + name: category, + count: categorySupplies.length, + value: categorySupplies.reduce( + (sum, s) => sum + s.price * s.currentStock, + 0 + ), + stock: categorySupplies.reduce((sum, s) => sum + s.currentStock, 0), + }; + }); + + const statusStats = uniqueStatuses.map((status) => { + const statusSupplies = consolidatedSupplies.filter( + (s) => s.status === status + ); + return { + name: getStatusConfig(status).label, + count: statusSupplies.length, + value: statusSupplies.reduce( + (sum, s) => sum + s.price * s.currentStock, + 0 + ), + }; + }); + + const supplierStats = uniqueSuppliers.map((supplier) => { + const supplierSupplies = consolidatedSupplies.filter( + (s) => s.supplier === supplier + ); + return { + name: supplier, + count: supplierSupplies.length, + value: supplierSupplies.reduce( + (sum, s) => sum + s.price * s.currentStock, + 0 + ), + }; + }); + + return { categoryStats, statusStats, supplierStats }; + }, [ + consolidatedSupplies, + uniqueCategories, + uniqueStatuses, + uniqueSuppliers, + getStatusConfig, + ]); + + if (error) { + return ( +
+ +
+ +

+ Ошибка загрузки +

+

{error.message}

+ +
+
+
+ ); + } + + return ( +
+ + +
+
+ {/* Хлебные крошки и заголовок */} +
+
+ +
+

+
+ +
+ Расходники фулфилмента +

+

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

+
+
+ +
+ {/* Переключатель режимов просмотра */} +
+ + + +
+ + {/* Группировка */} + + + {/* Экспорт */} +
+ +
+
+ + +
+
+
+
+
+ + {/* Статистические карточки */} +
+ +
+
+ +
+
+

Всего

+

{stats.total}

+
+
+
+ + +
+
+ +
+
+

Доступно

+

+ {stats.available} +

+
+
+
+ + +
+
+ +
+
+

Мало

+

+ {stats.lowStock} +

+
+
+
+ + +
+
+ +
+
+

Нет в наличии

+

+ {stats.outOfStock} +

+
+
+
+ + +
+
+ +
+
+

В пути

+

+ {stats.inTransit} +

+
+
+
+ + +
+
+ +
+
+

Стоимость

+

+ {formatCurrency(stats.totalValue)} +

+
+
+
+ + +
+
+ +
+
+

Категории

+

+ {stats.categories} +

+
+
+
+ + +
+
+ +
+
+

Поставщики

+

+ {stats.suppliers} +

+
+
+
+
+ + {/* Панель фильтров и поиска */} + +
+ {/* Основная строка поиска и кнопок */} +
+
+ + + handleFilterChange("search", e.target.value) + } + className="pl-10 bg-white/5 border-white/10 text-white placeholder:text-white/40" + /> +
+ + + + +
+ + {/* Расширенные фильтры */} + {showFilters && ( +
+
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ +
+
+ )} +
+
+ + {/* Заголовки сортировки для списочного вида */} + {viewMode === "list" && ( + +
+ + + + + + + + Действия +
+
+ )} + + {/* Список расходников */} + {loading ? ( +
+ {Array.from({ length: 8 }).map((_, i) => ( + +
+
+
+
+
+
+
+ ))} +
+ ) : filteredAndSortedSupplies.length === 0 ? ( + + +

+ Расходники не найдены +

+

+ Попробуйте изменить параметры поиска или фильтрации +

+
+ ) : viewMode === "analytics" ? ( + // Аналитический режим с графиками +
+ {/* Графики аналитики */} +
+ {/* Распределение по категориям */} + +
+
+ +
+

+ По категориям +

+
+
+ {analyticsData.categoryStats.map((item, index) => { + const colors = [ + "bg-blue-500", + "bg-green-500", + "bg-yellow-500", + "bg-purple-500", + "bg-pink-500", + ]; + const color = colors[index % colors.length]; + const percentage = + stats.total > 0 ? (item.count / stats.total) * 100 : 0; + + return ( +
+
+ {item.name} + + {item.count} + +
+
+
+
+
+ {percentage.toFixed(1)}% + {formatCurrency(item.value)} +
+
+ ); + })} +
+
+ + {/* Распределение по статусам */} + +
+
+ +
+

+ По статусам +

+
+
+ {analyticsData.statusStats.map((item, index) => { + const colors = [ + "bg-green-500", + "bg-yellow-500", + "bg-red-500", + "bg-blue-500", + "bg-purple-500", + ]; + const color = colors[index % colors.length]; + const percentage = + stats.total > 0 ? (item.count / stats.total) * 100 : 0; + + return ( +
+
+ {item.name} + + {item.count} + +
+
+
+
+
+ {percentage.toFixed(1)}% + {formatCurrency(item.value)} +
+
+ ); + })} +
+
+ + {/* ТОП поставщики */} + +
+
+ +
+

+ ТОП поставщики +

+
+
+ {analyticsData.supplierStats + .sort((a, b) => b.value - a.value) + .slice(0, 5) + .map((item, index) => { + const colors = [ + "bg-gold-500", + "bg-silver-500", + "bg-bronze-500", + "bg-blue-500", + "bg-green-500", + ]; + const color = colors[index] || "bg-gray-500"; + const maxValue = Math.max( + ...analyticsData.supplierStats.map((s) => s.value) + ); + const percentage = + maxValue > 0 ? (item.value / maxValue) * 100 : 0; + + return ( +
+
+
+ + #{index + 1} + + + {item.name} + +
+ + {item.count} + +
+
+
+
+
+ {formatCurrency(item.value)} +
+
+ ); + })} +
+
+
+ + {/* Дополнительные метрики */} +
+ +
+
+ +
+
+

Средняя цена

+

+ {consolidatedSupplies.length > 0 + ? formatCurrency( + consolidatedSupplies.reduce( + (sum, s) => sum + s.price, + 0 + ) / consolidatedSupplies.length + ) + : "0 ₽"} +

+
+
+
+ + +
+
+ +
+
+

Средний остаток

+

+ {consolidatedSupplies.length > 0 + ? Math.round( + consolidatedSupplies.reduce( + (sum, s) => sum + s.currentStock, + 0 + ) / consolidatedSupplies.length + ) + : 0} +

+
+
+
+ + +
+
+ +
+
+

+ Критический остаток +

+

+ { + consolidatedSupplies.filter( + (s) => s.currentStock === 0 + ).length + } +

+
+
+
+ + +
+
+ +
+
+

Оборачиваемость

+

+ {((stats.available / stats.total) * 100).toFixed(1)}% +

+
+
+
+
+
+ ) : groupBy !== "none" ? ( + // Группированный вид +
+ {Object.entries(groupedSupplies).map( + ([groupName, groupSupplies]) => ( + +
+
+

+ + {groupName} + + {groupSupplies.length} + +

+
+ Общая стоимость:{" "} + {formatCurrency( + groupSupplies.reduce( + (sum, s) => sum + s.price * s.currentStock, + 0 + ) + )} +
+
+
+
+
+ {groupSupplies.map((supply) => { + const statusConfig = getStatusConfig(supply.status); + const StatusIcon = statusConfig.icon; + const isLowStock = + supply.currentStock <= supply.minStock && + supply.currentStock > 0; + const stockPercentage = + supply.minStock > 0 + ? (supply.currentStock / supply.minStock) * 100 + : 100; + + return ( +
+ toggleSupplyExpansion(supply.id)} + > +
+
+
+ {expandedSupplies.has(supply.id) ? ( + + ) : ( + + )} +
+
+

+ {supply.name} +

+

+ {supply.description} +

+
+
+
+ + {getSupplyDeliveries(supply).length} + + + + {statusConfig.label} + +
+
+ +
+
+ + Остаток: + + + {formatNumber(supply.currentStock)}{" "} + {supply.unit} + +
+
+
+
+
+ Цена: + + {formatCurrency(supply.price)} + +
+
+
+ + {/* Развернутые поставки для группированного режима */} + {expandedSupplies.has(supply.id) && ( +
+
+ + Поставки +
+ + {getSupplyDeliveries(supply).map((delivery, deliveryIndex) => { + const deliveryStatusConfig = getStatusConfig(delivery.status); + const DeliveryStatusIcon = deliveryStatusConfig.icon; + + return ( + +
+
+ + + {deliveryStatusConfig.label} + + + {new Date(delivery.createdAt).toLocaleDateString("ru-RU", { + month: "short", + day: "numeric" + })} + +
+
+ + {formatNumber(delivery.currentStock)} {delivery.unit} + + + {formatCurrency(delivery.price * delivery.currentStock)} + +
+
+
+ ); + })} +
+ )} +
+ ); + })} +
+
+
+ ) + )} +
+ ) : viewMode === "grid" ? ( +
const statusConfig = getStatusConfig(supply.status); + const StatusIcon = statusConfig.icon; + const isLowStock = + supply.currentStock <= supply.minStock && + supply.currentStock > 0; + const stockPercentage = + supply.minStock > 0 + ? (supply.currentStock / supply.minStock) * 100 + : 100; + + return ( +
+ {/* Основная карточка расходника */} + toggleSupplyExpansion(supply.id)} + > + {/* Заголовок карточки */} +
+
+
+ {expandedSupplies.has(supply.id) ? ( + + ) : ( + + )} +
+
+

+ {supply.name} +

+

+ {supply.description} +

+
+
+
+ + {getSupplyDeliveries(supply).length} поставок + + + + {statusConfig.label} + +
+
+ + {/* Статистика остатков */} +
+
+ Остаток: + + {formatNumber(supply.currentStock)} {supply.unit} + +
+ + {/* Прогресс-бар остатков */} +
+
+
+ +
+ Мин. остаток: + + {formatNumber(supply.minStock)} {supply.unit} + +
+
+ + {/* Дополнительная информация */} +
+
+ Категория: + + {supply.category} + +
+
+ Цена: + + {formatCurrency(supply.price)} + +
+
+ Поставщик: + + {supply.supplier} + +
+
+ + {/* Действия */} +
+ + +
+
+ + {/* Развернутые поставки */} + {expandedSupplies.has(supply.id) && ( +
+
+ + История поставок +
+ + {getSupplyDeliveries(supply).map( + (delivery, deliveryIndex) => { + const deliveryStatusConfig = getStatusConfig( + delivery.status + ); + const DeliveryStatusIcon = + deliveryStatusConfig.icon; + + return ( + +
+
+
+ + + {deliveryStatusConfig.label} + + + {new Date( + delivery.createdAt + ).toLocaleDateString("ru-RU", { + year: "numeric", + month: "short", + day: "numeric", + hour: "2-digit", + minute: "2-digit", + })} + +
+ +
+
+

Остаток

+

+ {formatNumber(delivery.currentStock)}{" "} + {delivery.unit} +

+
+
+

+ Заказано +

+

+ {formatNumber(delivery.quantity)}{" "} + {delivery.unit} +

+
+
+

Цена

+

+ {formatCurrency(delivery.price)} +

+
+
+

+ Стоимость +

+

+ {formatCurrency( + delivery.price * + delivery.currentStock + )} +

+
+
+ + {delivery.description && + delivery.description !== + supply.description && ( +
+

+ Описание +

+

+ {delivery.description} +

+
+ )} +
+
+
+ ); + } + )} + + {/* Итоговая статистика по поставкам */} + +
+
+

Всего поставок

+

+ {getSupplyDeliveries(supply).length} +

+
+
+

Общая стоимость

+

+ {formatCurrency( + getSupplyDeliveries(supply).reduce( + (sum, d) => sum + d.price * d.currentStock, + 0 + ) + )} +

+
+
+
+
+ )} +
+ ); + })} +
+ ) : ( + // Списочный вид +
+ {filteredAndSortedSupplies.map((supply) => { + const statusConfig = getStatusConfig(supply.status); + const StatusIcon = statusConfig.icon; + const isLowStock = + supply.currentStock <= supply.minStock && + supply.currentStock > 0; + + return ( +
+ toggleSupplyExpansion(supply.id)} + > +
+
+
+ {expandedSupplies.has(supply.id) ? ( + + ) : ( + + )} +
+
+

{supply.name}

+

+ {supply.description} +

+
+
+ +
+ + {supply.category} + +
+ +
+ + {getSupplyDeliveries(supply).length} + + + + {statusConfig.label} + +
+ +
+ {formatNumber(supply.currentStock)} {supply.unit} +
+ +
+ {formatNumber(supply.minStock)} {supply.unit} +
+ +
+ {formatCurrency(supply.price)} +
+ +
+ {supply.supplier} +
+ +
+ + +
+
+
+ + {/* Развернутые поставки для списочного режима */} + {expandedSupplies.has(supply.id) && ( +
+
+ + История поставок +
+ + {getSupplyDeliveries(supply).map((delivery, deliveryIndex) => { + const deliveryStatusConfig = getStatusConfig(delivery.status); + const DeliveryStatusIcon = deliveryStatusConfig.icon; + + return ( + +
+
+ + + {deliveryStatusConfig.label} + +
+ +
+

Дата

+

+ {new Date(delivery.createdAt).toLocaleDateString("ru-RU", { + month: "short", + day: "numeric", + hour: "2-digit", + minute: "2-digit" + })} +

+
+ +
+

Остаток

+

+ {formatNumber(delivery.currentStock)} {delivery.unit} +

+
+ +
+

Заказано

+

+ {formatNumber(delivery.quantity)} {delivery.unit} +

+
+ +
+

Цена

+

+ {formatCurrency(delivery.price)} +

+
+ +
+

Стоимость

+

+ {formatCurrency(delivery.price * delivery.currentStock)} +

+
+
+ + {delivery.description && delivery.description !== supply.description && ( +
+

Описание: {delivery.description}

+
+ )} +
+ ); + })} +
+ )} +
+ ); + })} +
+ )} + + {/* Пагинация (если нужна) */} + {filteredAndSortedSupplies.length > 0 && ( +
+

+ Показано {filteredAndSortedSupplies.length} из{" "} + {consolidatedSupplies.length} расходников (объединено из{" "} + {supplies.length} записей) +

+
+ )} +
+
+ + +
+ ); +} diff --git a/src/components/fulfillment-warehouse/fulfillment-warehouse-dashboard.tsx b/src/components/fulfillment-warehouse/fulfillment-warehouse-dashboard.tsx index 98d09be..5238e29 100644 --- a/src/components/fulfillment-warehouse/fulfillment-warehouse-dashboard.tsx +++ b/src/components/fulfillment-warehouse/fulfillment-warehouse-dashboard.tsx @@ -1,6 +1,7 @@ "use client"; import { useState, useMemo } from "react"; +import { useRouter } from "next/navigation"; import { Card } from "@/components/ui/card"; import { Button } from "@/components/ui/button"; import { Badge } from "@/components/ui/badge"; @@ -8,6 +9,7 @@ import { Input } from "@/components/ui/input"; 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 { useQuery } from "@apollo/client"; import { GET_MY_COUNTERPARTIES, @@ -158,7 +160,9 @@ interface SupplyOrder { * - Контрастный цвет текста для лучшей читаемости */ export function FulfillmentWarehouseDashboard() { + const router = useRouter(); const { getSidebarMargin } = useSidebar(); + const { user } = useAuth(); // Состояния для поиска и фильтрации const [searchTerm, setSearchTerm] = useState(""); @@ -387,6 +391,38 @@ export function FulfillmentWarehouseDashboard() { 0 ); + // Подсчитываем расходники ФФ (расходники, которые получил фулфилмент-центр) + const fulfillmentConsumablesOrders = supplyOrders.filter((order) => { + // Заказы где текущий фулфилмент-центр является получателем + const isRecipient = + order.fulfillmentCenter?.id === user?.organization?.id; + // НО создатель заказа НЕ мы (т.е. селлер создал заказ для нас) + const isCreatedByOther = + order.organization?.id !== user?.organization?.id; + // И статус DELIVERED (получено) + const isDelivered = order.status === "DELIVERED"; + + return isRecipient && isCreatedByOther && isDelivered; + }); + + // Подсчитываем общее количество расходников ФФ из доставленных заказов + const totalFulfillmentSupplies = fulfillmentConsumablesOrders.reduce( + (sum, order) => sum + (order.totalItems || 0), + 0 + ); + + // Подсчитываем изменения за сегодня (расходники ФФ, полученные сегодня) + const today = new Date(); + today.setHours(0, 0, 0, 0); + + const fulfillmentSuppliesReceivedToday = fulfillmentConsumablesOrders + .filter((order) => { + const orderDate = new Date(order.updatedAt || order.createdAt); + orderDate.setHours(0, 0, 0, 0); + return orderDate.getTime() === today.getTime(); + }) + .reduce((sum, order) => sum + (order.totalItems || 0), 0); + return { products: { current: 0, // Нет данных о готовых продуктах для продажи @@ -405,8 +441,8 @@ export function FulfillmentWarehouseDashboard() { change: 0, // Нет реальных данных об изменениях возвратов }, fulfillmentSupplies: { - current: 0, // Нет реальных данных о расходниках ФФ - change: 0, // Нет реальных данных об изменениях расходников ФФ + current: totalFulfillmentSupplies, // Реальное количество расходников ФФ + change: fulfillmentSuppliesReceivedToday, // Расходники ФФ, полученные сегодня }, sellerSupplies: { current: totalSellerSupplies, // Реальное количество расходников селлера из базы @@ -422,6 +458,7 @@ export function FulfillmentWarehouseDashboard() { suppliesUsedToday, productsReceivedToday, productsUsedToday, + user?.organization?.id, ]); // Создаем структурированные данные склада на основе уникальных товаров @@ -710,7 +747,9 @@ export function FulfillmentWarehouseDashboard() { ) || 1)) * (suppliesReceivedToday - suppliesUsedToday) ) - : Math.floor((suppliesReceivedToday - suppliesUsedToday) / totalVirtualPartners); + : Math.floor( + (suppliesReceivedToday - suppliesUsedToday) / totalVirtualPartners + ); return { id: `virtual-partner-${index + 1}`, @@ -958,18 +997,23 @@ export function FulfillmentWarehouseDashboard() { current, change, description, + onClick, }: { title: string; icon: React.ComponentType<{ className?: string }>; current: number; change: number; description: string; + onClick?: () => void; }) => { const percentChange = current > 0 ? (change / current) * 100 : 0; return (
@@ -1000,18 +1044,28 @@ export function FulfillmentWarehouseDashboard() {
{/* Изменения - всегда показываем */}
-
= 0 ? 'bg-green-500/20' : 'bg-red-500/20' - }`}> - = 0 ? 'text-green-400' : 'text-red-400' - }`}> - {change >= 0 ? '+' : ''}{change} +
= 0 ? "bg-green-500/20" : "bg-red-500/20" + }`} + > + = 0 ? "text-green-400" : "text-red-400" + }`} + > + {change >= 0 ? "+" : ""} + {change}
{description}
+ {onClick && ( +
+ +
+ )}
); }; @@ -1196,6 +1250,7 @@ export function FulfillmentWarehouseDashboard() { current={warehouseStats.fulfillmentSupplies.current} change={warehouseStats.fulfillmentSupplies.change} description="Расходники, этикетки" + onClick={() => router.push("/fulfillment-warehouse/supplies")} /> + {supplies.map((supply) => { + const statusConfig = getStatusConfig(supply.status); + const isExpanded = expandedSupplies.has(supply.id); + const deliveries = getSupplyDeliveries(supply); + + return ( +
+ {/* Основная карточка расходника */} + + + {/* Развернутые поставки */} + {isExpanded && ( + + )} +
+ ); + })} +
+ ); +} diff --git a/src/components/fulfillment-warehouse/supplies-header.tsx b/src/components/fulfillment-warehouse/supplies-header.tsx new file mode 100644 index 0000000..2490618 --- /dev/null +++ b/src/components/fulfillment-warehouse/supplies-header.tsx @@ -0,0 +1,263 @@ +"use client"; + +import React from "react"; +import { useRouter } from "next/navigation"; +import { Button } from "@/components/ui/button"; +import { Input } from "@/components/ui/input"; +import { Badge } from "@/components/ui/badge"; +import { + ArrowLeft, + Search, + Filter, + BarChart3, + Grid3X3, + List, + Download, + RotateCcw, + Layers, +} from "lucide-react"; +import { SuppliesHeaderProps } from "./types"; + +export function SuppliesHeader({ + viewMode, + onViewModeChange, + groupBy, + onGroupByChange, + filters, + onFiltersChange, + showFilters, + onToggleFilters, + onExport, + onRefresh, +}: SuppliesHeaderProps) { + const router = useRouter(); + + const handleFilterChange = (key: keyof typeof filters, value: any) => { + onFiltersChange({ ...filters, [key]: value }); + }; + + return ( +
+ {/* Заголовок страницы */} +
+
+ + +
+

+ Расходники фулфилмента +

+

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

+
+
+ +
+ {/* Экспорт данных */} + + + {/* Обновить */} + +
+
+ + {/* Панель управления */} +
+
+ {/* Поиск */} +
+ + handleFilterChange("search", e.target.value)} + className="pl-10 w-64 bg-white/5 border-white/20 text-white placeholder:text-white/40 focus:border-blue-400" + /> +
+ + {/* Фильтры */} + +
+ +
+ {/* Переключатель режимов просмотра */} +
+ + + +
+ + {/* Группировка */} + {viewMode !== "analytics" && ( +
+ + +
+ )} +
+
+ + {/* Развернутые фильтры */} + {showFilters && ( +
+
+
+ + +
+ +
+ + +
+ +
+ + handleFilterChange("supplier", e.target.value)} + className="bg-white/5 border-white/20 text-white placeholder:text-white/40 focus:border-blue-400" + /> +
+ +
+ + handleFilterChange("lowStock", e.target.checked) + } + className="rounded border-white/20 bg-white/5 text-blue-500 focus:ring-blue-400" + /> + +
+
+
+ )} +
+ ); +} diff --git a/src/components/fulfillment-warehouse/supplies-list.tsx b/src/components/fulfillment-warehouse/supplies-list.tsx new file mode 100644 index 0000000..10c060e --- /dev/null +++ b/src/components/fulfillment-warehouse/supplies-list.tsx @@ -0,0 +1,174 @@ +"use client"; + +import React from "react"; +import { Card } from "@/components/ui/card"; +import { Badge } from "@/components/ui/badge"; +import { SortAsc, SortDesc, User, Calendar } from "lucide-react"; +import { SuppliesListProps } from "./types"; +import { DeliveryDetails } from "./delivery-details"; + +export function SuppliesList({ + supplies, + expandedSupplies, + onToggleExpansion, + getSupplyDeliveries, + getStatusConfig, + sort, + onSort, +}: SuppliesListProps) { + const formatCurrency = (amount: number) => { + return new Intl.NumberFormat("ru-RU", { + style: "currency", + currency: "RUB", + minimumFractionDigits: 0, + }).format(amount); + }; + + const formatNumber = (num: number) => { + return new Intl.NumberFormat("ru-RU").format(num); + }; + + return ( +
+ {/* Заголовки столбцов */} + +
+ + + Поставлено + Отправлено + + + Поставки + Статус +
+
+ + {/* Список расходников */} + {supplies.map((supply) => { + const statusConfig = getStatusConfig(supply.status); + const StatusIcon = statusConfig.icon; + const isLowStock = + supply.currentStock <= supply.minStock && supply.currentStock > 0; + const isExpanded = expandedSupplies.has(supply.id); + const deliveries = getSupplyDeliveries(supply); + + return ( +
+ onToggleExpansion(supply.id)} + > +
+
+
+

{supply.name}

+

+ {supply.description} +

+
+
+ +
+ + {supply.category} + +
+ +
+ {formatNumber(supply.quantity)} {supply.unit} +
+ +
+ {formatNumber(supply.shippedQuantity || 0)} {supply.unit} +
+ +
+ {formatNumber(supply.currentStock)} {supply.unit} +
+ +
+ {supply.supplier} +
+ +
+ + {deliveries.length} поставок + +
+ +
+ + + {statusConfig.label} + +
+
+
+ + {/* Развернутые поставки для списочного режима */} + {isExpanded && ( + + )} +
+ ); + })} +
+ ); +} diff --git a/src/components/fulfillment-warehouse/supplies-stats.tsx b/src/components/fulfillment-warehouse/supplies-stats.tsx new file mode 100644 index 0000000..5ed8430 --- /dev/null +++ b/src/components/fulfillment-warehouse/supplies-stats.tsx @@ -0,0 +1,167 @@ +"use client"; + +import React, { useMemo } from "react"; +import { Card } from "@/components/ui/card"; +import { + Package, + AlertTriangle, + TrendingUp, + TrendingDown, + DollarSign, + Activity, +} from "lucide-react"; +import { SuppliesStatsProps } from "./types"; + +export function SuppliesStats({ supplies }: SuppliesStatsProps) { + const stats = useMemo(() => { + const total = supplies.length; + const available = supplies.filter((s) => s.status === "available").length; + const lowStock = supplies.filter((s) => s.status === "low-stock").length; + const outOfStock = supplies.filter( + (s) => s.status === "out-of-stock" + ).length; + const inTransit = supplies.filter((s) => s.status === "in-transit").length; + + const totalValue = supplies.reduce( + (sum, s) => sum + (s.totalCost || s.price * s.quantity), + 0 + ); + const totalStock = supplies.reduce((sum, s) => sum + s.currentStock, 0); + + const categories = [...new Set(supplies.map((s) => s.category))].length; + const suppliers = [...new Set(supplies.map((s) => s.supplier))].length; + + return { + total, + available, + lowStock, + outOfStock, + inTransit, + totalValue, + totalStock, + categories, + suppliers, + }; + }, [supplies]); + + const formatCurrency = (amount: number) => { + return new Intl.NumberFormat("ru-RU", { + style: "currency", + currency: "RUB", + minimumFractionDigits: 0, + }).format(amount); + }; + + const formatNumber = (num: number) => { + return new Intl.NumberFormat("ru-RU").format(num); + }; + + return ( +
+ {/* Общее количество */} + +
+
+

+ Всего позиций +

+

+ {formatNumber(stats.total)} +

+
+
+ +
+
+
+ + {/* Доступно */} + +
+
+

+ Доступно +

+

+ {formatNumber(stats.available)} +

+
+
+ +
+
+
+ + {/* Мало на складе */} + +
+
+

+ Мало на складе +

+

+ {formatNumber(stats.lowStock)} +

+
+
+ +
+
+
+ + {/* Нет в наличии */} + +
+
+

+ Нет в наличии +

+

+ {formatNumber(stats.outOfStock)} +

+
+
+ +
+
+
+ + {/* Общая стоимость */} + +
+
+

+ Общая стоимость +

+

+ {formatCurrency(stats.totalValue)} +

+
+
+ +
+
+
+ + {/* Активность */} + +
+
+

+ В пути +

+

+ {formatNumber(stats.inTransit)} +

+

+ {stats.categories} категорий +

+
+
+ +
+
+
+
+ ); +} diff --git a/src/components/fulfillment-warehouse/supply-card.tsx b/src/components/fulfillment-warehouse/supply-card.tsx new file mode 100644 index 0000000..56ee960 --- /dev/null +++ b/src/components/fulfillment-warehouse/supply-card.tsx @@ -0,0 +1,171 @@ +"use client"; + +import React from "react"; +import { Card } from "@/components/ui/card"; +import { Badge } from "@/components/ui/badge"; +import { Progress } from "@/components/ui/progress"; +import { + Package, + TrendingUp, + TrendingDown, + Calendar, + MapPin, + User, +} from "lucide-react"; +import { SupplyCardProps } from "./types"; + +export function SupplyCard({ + supply, + isExpanded, + onToggleExpansion, + statusConfig, + getSupplyDeliveries, +}: SupplyCardProps) { + const formatCurrency = (amount: number) => { + return new Intl.NumberFormat("ru-RU", { + style: "currency", + currency: "RUB", + minimumFractionDigits: 0, + }).format(amount); + }; + + const formatNumber = (num: number) => { + return new Intl.NumberFormat("ru-RU").format(num); + }; + + const StatusIcon = statusConfig.icon; + const isLowStock = + supply.currentStock <= supply.minStock && supply.currentStock > 0; + const stockPercentage = + supply.minStock > 0 ? (supply.currentStock / supply.minStock) * 100 : 100; + + return ( +
+ {/* Основная карточка расходника */} + onToggleExpansion(supply.id)} + > + {/* Заголовок карточки */} +
+
+
+

+ {supply.name} +

+
+

+ {supply.description} +

+
+
+ + + {statusConfig.label} + +
+
+ + {/* Основная информация */} +
+ {/* Остатки и прогресс-бар */} +
+
+ Остаток + + {formatNumber(supply.currentStock)} /{" "} + {formatNumber(supply.minStock)} {supply.unit} + +
+ 50 + ? "#10b981" + : stockPercentage > 20 + ? "#f59e0b" + : "#ef4444" + } 0%, ${ + stockPercentage > 50 + ? "#10b981" + : stockPercentage > 20 + ? "#f59e0b" + : "#ef4444" + } ${Math.min( + stockPercentage, + 100 + )}%, rgba(255,255,255,0.1) ${Math.min(stockPercentage, 100)}%)`, + }} + /> +
+ + {/* Метрики */} +
+
+
+ +
+
+

Цена

+

+ {formatCurrency(supply.price)} +

+
+
+ +
+
+ +
+
+

Стоимость

+

+ {formatCurrency( + supply.totalCost || supply.price * supply.quantity + )} +

+
+
+
+ + {/* Дополнительная информация */} +
+
+ + {supply.category} + + + {getSupplyDeliveries(supply).length} поставок + +
+
+ + {/* Поставщик и дата */} +
+
+ + + {supply.supplier} + +
+
+ + + {new Date(supply.createdAt).toLocaleDateString("ru-RU")} + +
+
+
+
+
+ ); +} diff --git a/src/components/fulfillment-warehouse/types.ts b/src/components/fulfillment-warehouse/types.ts new file mode 100644 index 0000000..84199f4 --- /dev/null +++ b/src/components/fulfillment-warehouse/types.ts @@ -0,0 +1,101 @@ +import { LucideIcon } from "lucide-react"; + +// Основные типы данных +export interface Supply { + id: string; + name: string; + description: string; + price: number; + quantity: number; + unit: string; + category: string; + status: string; + date: string; + supplier: string; + minStock: number; + currentStock: number; + imageUrl?: string; + createdAt: string; + updatedAt: string; + totalCost?: number; // Общая стоимость (количество × цена) + shippedQuantity?: number; // Отправленное количество +} + +export interface FilterState { + search: string; + category: string; + status: string; + supplier: string; + lowStock: boolean; +} + +export interface SortState { + field: "name" | "category" | "status" | "currentStock" | "price" | "supplier"; + direction: "asc" | "desc"; +} + +export interface StatusConfig { + label: string; + color: string; + icon: LucideIcon; +} + +export interface DeliveryStatusConfig { + label: string; + color: string; + icon: LucideIcon; +} + +export type ViewMode = "grid" | "list" | "analytics"; +export type GroupBy = "none" | "category" | "status" | "supplier"; + +// Пропсы для компонентов +export interface SupplyCardProps { + supply: Supply; + isExpanded: boolean; + onToggleExpansion: (id: string) => void; + statusConfig: StatusConfig; + getSupplyDeliveries: (supply: Supply) => Supply[]; +} + +export interface SuppliesGridProps { + supplies: Supply[]; + expandedSupplies: Set; + onToggleExpansion: (id: string) => void; + getSupplyDeliveries: (supply: Supply) => Supply[]; + getStatusConfig: (status: string) => StatusConfig; +} + +export interface SuppliesListProps { + supplies: Supply[]; + expandedSupplies: Set; + onToggleExpansion: (id: string) => void; + getSupplyDeliveries: (supply: Supply) => Supply[]; + getStatusConfig: (status: string) => StatusConfig; + sort: SortState; + onSort: (field: SortState["field"]) => void; +} + +export interface SuppliesHeaderProps { + viewMode: ViewMode; + onViewModeChange: (mode: ViewMode) => void; + groupBy: GroupBy; + onGroupByChange: (group: GroupBy) => void; + filters: FilterState; + onFiltersChange: (filters: FilterState) => void; + showFilters: boolean; + onToggleFilters: () => void; + onExport: () => void; + onRefresh: () => void; +} + +export interface SuppliesStatsProps { + supplies: Supply[]; +} + +export interface DeliveryDetailsProps { + supply: Supply; + deliveries: Supply[]; + viewMode: "grid" | "list"; + getStatusConfig: (status: string) => StatusConfig; +} diff --git a/src/components/supplies/create-consumables-supply-page.tsx b/src/components/supplies/create-consumables-supply-page.tsx index 57d7f1d..9e424cf 100644 --- a/src/components/supplies/create-consumables-supply-page.tsx +++ b/src/components/supplies/create-consumables-supply-page.tsx @@ -28,6 +28,7 @@ import { GET_MY_COUNTERPARTIES, GET_ALL_PRODUCTS, GET_SUPPLY_ORDERS, + GET_MY_SUPPLIES, } from "@/graphql/queries"; import { CREATE_SUPPLY_ORDER } from "@/graphql/mutations"; import { OrganizationAvatar } from "@/components/market/organization-avatar"; @@ -243,7 +244,10 @@ export function CreateConsumablesSupplyPage() { })), }, }, - refetchQueries: [{ query: GET_SUPPLY_ORDERS }], + refetchQueries: [ + { query: GET_SUPPLY_ORDERS }, // Обновляем заказы поставок + { query: GET_MY_SUPPLIES }, // Обновляем расходники фулфилмента + ], }); if (result.data?.createSupplyOrder?.success) { diff --git a/src/graphql/resolvers.ts b/src/graphql/resolvers.ts index 4f22ad8..295dcec 100644 --- a/src/graphql/resolvers.ts +++ b/src/graphql/resolvers.ts @@ -691,7 +691,7 @@ export const resolvers = { }); }, - // Мои расходники + // Мои расходники (объединенные данные из supply и supplyOrder) mySupplies: async (_: unknown, __: unknown, context: Context) => { if (!context.user) { throw new GraphQLError("Требуется авторизация", { @@ -708,11 +708,101 @@ export const resolvers = { throw new GraphQLError("У пользователя нет организации"); } - return await prisma.supply.findMany({ + // Получаем расходники из таблицы supply (уже доставленные) + const existingSupplies = await prisma.supply.findMany({ where: { organizationId: currentUser.organization.id }, include: { organization: true }, orderBy: { createdAt: "desc" }, }); + + // Получаем заказы поставок, созданные этим фулфилмент-центром для себя + // Показываем только заказы, которые еще не доставлены + const ourSupplyOrders = await prisma.supplyOrder.findMany({ + where: { + organizationId: currentUser.organization.id, // Создали мы + fulfillmentCenterId: currentUser.organization.id, // Получатель - мы + status: { + in: ["PENDING", "CONFIRMED", "IN_TRANSIT"], // Только не доставленные заказы + }, + }, + include: { + partner: true, + items: { + include: { + product: { + include: { + category: true, + }, + }, + }, + }, + }, + orderBy: { createdAt: "desc" }, + }); + + // Преобразуем заказы поставок в формат supply для единообразия + const suppliesFromOrders = ourSupplyOrders.flatMap((order) => + order.items.map((item) => ({ + id: `order-${order.id}-${item.id}`, + name: item.product.name, + description: + item.product.description || `Заказ от ${order.partner.name}`, + price: item.price, + quantity: item.quantity, + unit: "шт", + category: item.product.category?.name || "Расходники", + status: + order.status === "PENDING" + ? "in-transit" + : order.status === "CONFIRMED" + ? "in-transit" + : order.status === "IN_TRANSIT" + ? "in-transit" + : "available", + date: order.createdAt, + supplier: order.partner.name || order.partner.fullName || "Не указан", + minStock: Math.round(item.quantity * 0.1), + currentStock: order.status === "DELIVERED" ? item.quantity : 0, + imageUrl: null, + createdAt: order.createdAt, + updatedAt: order.updatedAt, + organizationId: currentUser.organization.id, + organization: currentUser.organization, + shippedQuantity: 0, + })) + ); + + // Проверяем все заказы для этого фулфилмент-центра для отладки + const allOurOrders = await prisma.supplyOrder.findMany({ + where: { + organizationId: currentUser.organization.id, + fulfillmentCenterId: currentUser.organization.id, + }, + select: { id: true, status: true, createdAt: true }, + }); + + // Логирование для отладки + console.log("🔥🔥🔥 MY_SUPPLIES RESOLVER CALLED 🔥🔥🔥"); + console.log("📊 mySupplies resolver debug:", { + organizationId: currentUser.organization.id, + existingSuppliesCount: existingSupplies.length, + ourSupplyOrdersCount: ourSupplyOrders.length, + suppliesFromOrdersCount: suppliesFromOrders.length, + allOrdersCount: allOurOrders.length, + allOrdersStatuses: allOurOrders.map((o) => ({ + id: o.id, + status: o.status, + createdAt: o.createdAt, + })), + filteredOrdersStatuses: ourSupplyOrders.map((o) => ({ + id: o.id, + status: o.status, + })), + }); + console.log("🔥🔥🔥 END MY_SUPPLIES RESOLVER 🔥🔥🔥"); + + // Объединяем существующие расходники и расходники из заказов + return [...existingSupplies, ...suppliesFromOrders]; }, // Заказы поставок расходников