diff --git a/backend-node/src/app.ts b/backend-node/src/app.ts index 6660bc13..4ba9405d 100644 --- a/backend-node/src/app.ts +++ b/backend-node/src/app.ts @@ -55,7 +55,6 @@ import riskAlertRoutes from "./routes/riskAlertRoutes"; // 리스크/알림 관 import todoRoutes from "./routes/todoRoutes"; // To-Do 관리 import bookingRoutes from "./routes/bookingRoutes"; // 예약 요청 관리 import mapDataRoutes from "./routes/mapDataRoutes"; // 지도 데이터 관리 -import warehouseRoutes from "./routes/warehouseRoutes"; // 창고 관리 import { BatchSchedulerService } from "./services/batchSchedulerService"; // import collectionRoutes from "./routes/collectionRoutes"; // 임시 주석 // import batchRoutes from "./routes/batchRoutes"; // 임시 주석 @@ -205,7 +204,6 @@ app.use("/api/risk-alerts", riskAlertRoutes); // 리스크/알림 관리 app.use("/api/todos", todoRoutes); // To-Do 관리 app.use("/api/bookings", bookingRoutes); // 예약 요청 관리 app.use("/api/map-data", mapDataRoutes); // 지도 데이터 조회 -app.use("/api/warehouse", warehouseRoutes); // 창고 관리 // app.use("/api/collections", collectionRoutes); // 임시 주석 // app.use("/api/batch", batchRoutes); // 임시 주석 // app.use('/api/users', userRoutes); @@ -235,7 +233,7 @@ app.listen(PORT, HOST, async () => { // 대시보드 마이그레이션 실행 try { - const { runDashboardMigration } = await import('./database/runMigration'); + const { runDashboardMigration } = await import("./database/runMigration"); await runDashboardMigration(); } catch (error) { logger.error(`❌ 대시보드 마이그레이션 실패:`, error); diff --git a/backend-node/src/controllers/WarehouseController.ts b/backend-node/src/controllers/WarehouseController.ts deleted file mode 100644 index 1fe140e8..00000000 --- a/backend-node/src/controllers/WarehouseController.ts +++ /dev/null @@ -1,97 +0,0 @@ -import { Request, Response } from "express"; -import { WarehouseService } from "../services/WarehouseService"; - -export class WarehouseController { - private warehouseService: WarehouseService; - - constructor() { - this.warehouseService = new WarehouseService(); - } - - // 창고 및 자재 데이터 조회 - getWarehouseData = async (req: Request, res: Response) => { - try { - const data = await this.warehouseService.getWarehouseData(); - - return res.json({ - success: true, - warehouses: data.warehouses, - materials: data.materials, - }); - } catch (error: any) { - console.error("창고 데이터 조회 오류:", error); - return res.status(500).json({ - success: false, - message: "창고 데이터를 불러오는데 실패했습니다.", - error: error.message, - }); - } - }; - - // 특정 창고 정보 조회 - getWarehouseById = async (req: Request, res: Response) => { - try { - const { id } = req.params; - const warehouse = await this.warehouseService.getWarehouseById(id); - - if (!warehouse) { - return res.status(404).json({ - success: false, - message: "창고를 찾을 수 없습니다.", - }); - } - - return res.json({ - success: true, - data: warehouse, - }); - } catch (error: any) { - console.error("창고 조회 오류:", error); - return res.status(500).json({ - success: false, - message: "창고 정보를 불러오는데 실패했습니다.", - error: error.message, - }); - } - }; - - // 창고별 자재 목록 조회 - getMaterialsByWarehouse = async (req: Request, res: Response) => { - try { - const { warehouseId } = req.params; - const materials = - await this.warehouseService.getMaterialsByWarehouse(warehouseId); - - return res.json({ - success: true, - data: materials, - }); - } catch (error: any) { - console.error("자재 목록 조회 오류:", error); - return res.status(500).json({ - success: false, - message: "자재 목록을 불러오는데 실패했습니다.", - error: error.message, - }); - } - }; - - // 창고 통계 조회 - getWarehouseStats = async (req: Request, res: Response) => { - try { - const stats = await this.warehouseService.getWarehouseStats(); - - return res.json({ - success: true, - data: stats, - }); - } catch (error: any) { - console.error("창고 통계 조회 오류:", error); - return res.status(500).json({ - success: false, - message: "창고 통계를 불러오는데 실패했습니다.", - error: error.message, - }); - } - }; -} diff --git a/backend-node/src/routes/warehouseRoutes.ts b/backend-node/src/routes/warehouseRoutes.ts deleted file mode 100644 index 15625a35..00000000 --- a/backend-node/src/routes/warehouseRoutes.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { Router } from "express"; -import { WarehouseController } from "../controllers/WarehouseController"; - -const router = Router(); -const warehouseController = new WarehouseController(); - -// 창고 및 자재 데이터 조회 -router.get("/data", warehouseController.getWarehouseData); - -// 특정 창고 정보 조회 -router.get("/:id", warehouseController.getWarehouseById); - -// 창고별 자재 목록 조회 -router.get( - "/:warehouseId/materials", - warehouseController.getMaterialsByWarehouse -); - -// 창고 통계 조회 -router.get("/stats/summary", warehouseController.getWarehouseStats); - -export default router; diff --git a/backend-node/src/services/WarehouseService.ts b/backend-node/src/services/WarehouseService.ts deleted file mode 100644 index fe0433c7..00000000 --- a/backend-node/src/services/WarehouseService.ts +++ /dev/null @@ -1,170 +0,0 @@ -import pool from "../database/db"; - -export class WarehouseService { - // 창고 및 자재 데이터 조회 - async getWarehouseData() { - try { - // 창고 목록 조회 - const warehousesResult = await pool.query(` - SELECT - id, - name, - position_x, - position_y, - position_z, - size_x, - size_y, - size_z, - color, - capacity, - current_usage, - status, - description, - created_at, - updated_at - FROM warehouse - WHERE status = 'active' - ORDER BY id - `); - - // 자재 목록 조회 - const materialsResult = await pool.query(` - SELECT - id, - warehouse_id, - name, - material_code, - quantity, - unit, - position_x, - position_y, - position_z, - size_x, - size_y, - size_z, - color, - status, - last_updated, - created_at - FROM warehouse_material - ORDER BY warehouse_id, id - `); - - return { - warehouses: warehousesResult, - materials: materialsResult, - }; - } catch (error) { - throw error; - } - } - - // 특정 창고 정보 조회 - async getWarehouseById(id: string) { - try { - const result = await pool.query( - ` - SELECT - id, - name, - position_x, - position_y, - position_z, - size_x, - size_y, - size_z, - color, - capacity, - current_usage, - status, - description, - created_at, - updated_at - FROM warehouse - WHERE id = $1 - `, - [id] - ); - - return result[0] || null; - } catch (error) { - throw error; - } - } - - // 창고별 자재 목록 조회 - async getMaterialsByWarehouse(warehouseId: string) { - try { - const result = await pool.query( - ` - SELECT - id, - warehouse_id, - name, - material_code, - quantity, - unit, - position_x, - position_y, - position_z, - size_x, - size_y, - size_z, - color, - status, - last_updated, - created_at - FROM warehouse_material - WHERE warehouse_id = $1 - ORDER BY id - `, - [warehouseId] - ); - - return result; - } catch (error) { - throw error; - } - } - - // 창고 통계 조회 - async getWarehouseStats() { - try { - const result = await pool.query(` - SELECT - COUNT(DISTINCT w.id) as total_warehouses, - COUNT(m.id) as total_materials, - SUM(w.capacity) as total_capacity, - SUM(w.current_usage) as total_usage, - ROUND(AVG((w.current_usage::numeric / NULLIF(w.capacity, 0)) * 100), 2) as avg_usage_percent - FROM warehouse w - LEFT JOIN warehouse_material m ON w.id = m.warehouse_id - WHERE w.status = 'active' - `); - - // 상태별 자재 수 - const statusResult = await pool.query(` - SELECT - status, - COUNT(*) as count - FROM warehouse_material - GROUP BY status - `); - - const statusCounts = statusResult.reduce( - (acc: Record, row: any) => { - acc[row.status] = parseInt(row.count); - return acc; - }, - {} as Record - ); - - return { - ...result[0], - materialsByStatus: statusCounts, - }; - } catch (error) { - throw error; - } - } -} diff --git a/frontend/components/admin/dashboard/CanvasElement.tsx b/frontend/components/admin/dashboard/CanvasElement.tsx index b45d8ed3..3be76d41 100644 --- a/frontend/components/admin/dashboard/CanvasElement.tsx +++ b/frontend/components/admin/dashboard/CanvasElement.tsx @@ -98,17 +98,6 @@ const DocumentWidget = dynamic(() => import("@/components/dashboard/widgets/Docu loading: () =>
로딩 중...
, }); -const Warehouse3DWidget = dynamic( - () => - import("@/components/admin/dashboard/widgets/Warehouse3DWidget").then((mod) => ({ - default: mod.Warehouse3DWidget, - })), - { - ssr: false, - loading: () =>
로딩 중...
, - }, -); - // 시계 위젯 임포트 import { ClockWidget } from "./widgets/ClockWidget"; // 달력 위젯 임포트 @@ -231,20 +220,16 @@ export function CanvasElement({ const subGridSize = Math.floor(cellSize / 3); const gridSize = cellSize + 5; // GAP 포함한 실제 그리드 크기 const magneticThreshold = 15; // 큰 그리드에 끌리는 거리 (px) - + // X 좌표 스냅 (큰 그리드 우선, 없으면 서브그리드) const nearestGridX = Math.round(rawX / gridSize) * gridSize; const distToGridX = Math.abs(rawX - nearestGridX); - const snappedX = distToGridX <= magneticThreshold - ? nearestGridX - : Math.round(rawX / subGridSize) * subGridSize; - + const snappedX = distToGridX <= magneticThreshold ? nearestGridX : Math.round(rawX / subGridSize) * subGridSize; + // Y 좌표 스냅 (큰 그리드 우선, 없으면 서브그리드) const nearestGridY = Math.round(rawY / gridSize) * gridSize; const distToGridY = Math.abs(rawY - nearestGridY); - const snappedY = distToGridY <= magneticThreshold - ? nearestGridY - : Math.round(rawY / subGridSize) * subGridSize; + const snappedY = distToGridY <= magneticThreshold ? nearestGridY : Math.round(rawY / subGridSize) * subGridSize; setTempPosition({ x: snappedX, y: snappedY }); } else if (isResizing) { @@ -293,45 +278,49 @@ export function CanvasElement({ const subGridSize = Math.floor(cellSize / 3); const gridSize = cellSize + 5; // GAP 포함한 실제 그리드 크기 const magneticThreshold = 15; - + // 위치 스냅 const nearestGridX = Math.round(newX / gridSize) * gridSize; const distToGridX = Math.abs(newX - nearestGridX); - const snappedX = distToGridX <= magneticThreshold - ? nearestGridX - : Math.round(newX / subGridSize) * subGridSize; - + const snappedX = distToGridX <= magneticThreshold ? nearestGridX : Math.round(newX / subGridSize) * subGridSize; + const nearestGridY = Math.round(newY / gridSize) * gridSize; const distToGridY = Math.abs(newY - nearestGridY); - const snappedY = distToGridY <= magneticThreshold - ? nearestGridY - : Math.round(newY / subGridSize) * subGridSize; - + const snappedY = distToGridY <= magneticThreshold ? nearestGridY : Math.round(newY / subGridSize) * subGridSize; + // 크기 스냅 (그리드 칸 단위로 스냅하되, 마지막 GAP은 제외) // 예: 1칸 = cellSize, 2칸 = cellSize*2 + GAP, 3칸 = cellSize*3 + GAP*2 const calculateGridWidth = (cells: number) => cells * cellSize + Math.max(0, cells - 1) * 5; - + // 가장 가까운 그리드 칸 수 계산 const nearestWidthCells = Math.round(newWidth / gridSize); const nearestGridWidth = calculateGridWidth(nearestWidthCells); const distToGridWidth = Math.abs(newWidth - nearestGridWidth); - const snappedWidth = distToGridWidth <= magneticThreshold - ? nearestGridWidth - : Math.round(newWidth / subGridSize) * subGridSize; - + const snappedWidth = + distToGridWidth <= magneticThreshold ? nearestGridWidth : Math.round(newWidth / subGridSize) * subGridSize; + const nearestHeightCells = Math.round(newHeight / gridSize); const nearestGridHeight = calculateGridWidth(nearestHeightCells); const distToGridHeight = Math.abs(newHeight - nearestGridHeight); - const snappedHeight = distToGridHeight <= magneticThreshold - ? nearestGridHeight - : Math.round(newHeight / subGridSize) * subGridSize; + const snappedHeight = + distToGridHeight <= magneticThreshold ? nearestGridHeight : Math.round(newHeight / subGridSize) * subGridSize; // 임시 크기/위치 저장 (스냅됨) setTempPosition({ x: Math.max(0, snappedX), y: Math.max(0, snappedY) }); setTempSize({ width: snappedWidth, height: snappedHeight }); } }, - [isDragging, isResizing, dragStart, resizeStart, element.size.width, element.type, element.subtype, canvasWidth, cellSize], + [ + isDragging, + isResizing, + dragStart, + resizeStart, + element.size.width, + element.type, + element.subtype, + canvasWidth, + cellSize, + ], ); // 마우스 업 처리 (이미 스냅된 위치 사용) @@ -738,11 +727,6 @@ export function CanvasElement({
- ) : element.type === "widget" && element.subtype === "warehouse-3d" ? ( - // 창고 현황 3D 위젯 렌더링 -
- -
) : ( // 기타 위젯 렌더링
router.push(`/dashboard/${dashboardId}`) : undefined} dashboardTitle={dashboardTitle} onAddElement={addElementFromMenu} resolution={resolution} @@ -469,117 +468,117 @@ export default function DashboardDesigner({ dashboardId: initialDashboardId }: D onBackgroundColorChange={setCanvasBackgroundColor} /> - {/* 캔버스 영역 - 해상도에 따른 크기, 중앙 정렬 */} -
-
+
+ +
+
+ + {/* 요소 설정 모달 */} + {configModalElement && ( + <> + {configModalElement.type === "widget" && configModalElement.subtype === "list" ? ( + + ) : configModalElement.type === "widget" && configModalElement.subtype === "todo" ? ( + + ) : ( + + )} + + )} + + {/* 저장 모달 */} + setSaveModalOpen(false)} + onSave={handleSave} + initialTitle={dashboardTitle} + initialDescription={dashboardDescription} + isEditing={!!dashboardId} + /> + + {/* 저장 성공 모달 */} + { + setSuccessModalOpen(false); + router.push("/admin/dashboard"); }} > - -
-
- - {/* 요소 설정 모달 */} - {configModalElement && ( - <> - {configModalElement.type === "widget" && configModalElement.subtype === "list" ? ( - - ) : configModalElement.type === "widget" && configModalElement.subtype === "todo" ? ( - - ) : ( - - )} - - )} - - {/* 저장 모달 */} - setSaveModalOpen(false)} - onSave={handleSave} - initialTitle={dashboardTitle} - initialDescription={dashboardDescription} - isEditing={!!dashboardId} - /> - - {/* 저장 성공 모달 */} - { - setSuccessModalOpen(false); - router.push("/admin/dashboard"); - }} - > - - -
- + + +
+ +
+ 저장 완료 + 대시보드가 성공적으로 저장되었습니다. +
+
+
- 저장 완료 - 대시보드가 성공적으로 저장되었습니다. - -
- -
-
-
+ + - {/* 초기화 확인 모달 */} - - - - 캔버스 초기화 - - 모든 요소가 삭제됩니다. 이 작업은 되돌릴 수 없습니다. -
- 계속하시겠습니까? -
-
- - 취소 - - 초기화 - - -
-
+ {/* 초기화 확인 모달 */} + + + + 캔버스 초기화 + + 모든 요소가 삭제됩니다. 이 작업은 되돌릴 수 없습니다. +
+ 계속하시겠습니까? +
+
+ + 취소 + + 초기화 + + +
+
); @@ -618,8 +617,6 @@ function getElementTitle(type: ElementType, subtype: ElementSubtype): string { return "🚚 기사 관리 위젯"; case "list": return "📋 리스트 위젯"; - case "warehouse-3d": - return "🏭 창고 현황 (3D)"; default: return "🔧 위젯"; } @@ -660,8 +657,6 @@ function getElementContent(type: ElementType, subtype: ElementSubtype): string { return "driver-management"; case "list": return "list-widget"; - case "warehouse-3d": - return "warehouse-3d"; default: return "위젯 내용이 여기에 표시됩니다"; } diff --git a/frontend/components/admin/dashboard/DashboardTopMenu.tsx b/frontend/components/admin/dashboard/DashboardTopMenu.tsx index 00957f91..03464aee 100644 --- a/frontend/components/admin/dashboard/DashboardTopMenu.tsx +++ b/frontend/components/admin/dashboard/DashboardTopMenu.tsx @@ -185,7 +185,6 @@ export function DashboardTopMenu({ 커스텀 지도 카드 {/* 커스텀 목록 카드 */} 커스텀 상태 카드 - 창고 현황 (3D) 일반 위젯 diff --git a/frontend/components/admin/dashboard/types.ts b/frontend/components/admin/dashboard/types.ts index 73a4bf89..05a656f3 100644 --- a/frontend/components/admin/dashboard/types.ts +++ b/frontend/components/admin/dashboard/types.ts @@ -35,8 +35,7 @@ export type ElementSubtype = | "booking-alert" | "maintenance" | "document" - | "list" - | "warehouse-3d"; // 위젯 타입 + | "list"; // 위젯 타입 export interface Position { x: number; diff --git a/frontend/components/admin/dashboard/widgets/Warehouse3DWidget.tsx b/frontend/components/admin/dashboard/widgets/Warehouse3DWidget.tsx deleted file mode 100644 index 71d2df0e..00000000 --- a/frontend/components/admin/dashboard/widgets/Warehouse3DWidget.tsx +++ /dev/null @@ -1,418 +0,0 @@ -"use client"; - -import React, { useRef, useState, useEffect, Suspense } from "react"; -import { Canvas, useFrame } from "@react-three/fiber"; -import { OrbitControls, Text, Box, Html } from "@react-three/drei"; -import * as THREE from "three"; -import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; -import { Badge } from "@/components/ui/badge"; -import { Loader2, Maximize2, Info } from "lucide-react"; - -interface WarehouseData { - id: string; - name: string; - position_x: number; - position_y: number; - position_z: number; - size_x: number; - size_y: number; - size_z: number; - color: string; - capacity: number; - current_usage: number; - status: string; - description?: string; -} - -interface MaterialData { - id: string; - warehouse_id: string; - name: string; - material_code: string; - quantity: number; - unit: string; - position_x: number; - position_y: number; - position_z: number; - size_x: number; - size_y: number; - size_z: number; - color: string; - status: string; -} - -interface Warehouse3DWidgetProps { - element?: any; -} - -// 창고 3D 박스 컴포넌트 -function WarehouseBox({ - warehouse, - onClick, - isSelected, -}: { - warehouse: WarehouseData; - onClick: () => void; - isSelected: boolean; -}) { - const meshRef = useRef(null); - const [hovered, setHovered] = useState(false); - - useFrame(() => { - if (meshRef.current) { - if (isSelected) { - meshRef.current.scale.lerp(new THREE.Vector3(1.05, 1.05, 1.05), 0.1); - } else if (hovered) { - meshRef.current.scale.lerp(new THREE.Vector3(1.02, 1.02, 1.02), 0.1); - } else { - meshRef.current.scale.lerp(new THREE.Vector3(1, 1, 1), 0.1); - } - } - }); - - const usagePercentage = (warehouse.current_usage / warehouse.capacity) * 100; - - return ( - - { - e.stopPropagation(); - onClick(); - }} - onPointerOver={() => setHovered(true)} - onPointerOut={() => setHovered(false)} - > - - - - - {/* 창고 테두리 */} - - - - - - {/* 창고 이름 라벨 */} - - {warehouse.name} - - - {/* 사용률 표시 */} - -
- {usagePercentage.toFixed(0)}% 사용중 -
- -
- ); -} - -// 자재 3D 박스 컴포넌트 -function MaterialBox({ - material, - onClick, - isSelected, -}: { - material: MaterialData; - onClick: () => void; - isSelected: boolean; -}) { - const meshRef = useRef(null); - const [hovered, setHovered] = useState(false); - - useFrame(() => { - if (meshRef.current && (isSelected || hovered)) { - meshRef.current.rotation.y += 0.01; - } - }); - - const statusColor = - { - stocked: material.color, - reserved: "#FFA500", - urgent: "#FF0000", - out_of_stock: "#808080", - }[material.status] || material.color; - - return ( - - { - e.stopPropagation(); - onClick(); - }} - onPointerOver={() => setHovered(true)} - onPointerOut={() => setHovered(false)} - > - - - - - {(hovered || isSelected) && ( - -
-
{material.name}
-
- {material.quantity} {material.unit} -
-
- - )} -
- ); -} - -// 3D 씬 컴포넌트 -function Scene({ - warehouses, - materials, - onSelectWarehouse, - onSelectMaterial, - selectedWarehouse, - selectedMaterial, -}: { - warehouses: WarehouseData[]; - materials: MaterialData[]; - onSelectWarehouse: (warehouse: WarehouseData | null) => void; - onSelectMaterial: (material: MaterialData | null) => void; - selectedWarehouse: WarehouseData | null; - selectedMaterial: MaterialData | null; -}) { - return ( - <> - {/* 조명 */} - - - - - {/* 바닥 그리드 */} - - - {/* 창고들 */} - {warehouses.map((warehouse) => ( - { - if (selectedWarehouse?.id === warehouse.id) { - onSelectWarehouse(null); - } else { - onSelectWarehouse(warehouse); - onSelectMaterial(null); - } - }} - isSelected={selectedWarehouse?.id === warehouse.id} - /> - ))} - - {/* 자재들 */} - {materials.map((material) => ( - { - if (selectedMaterial?.id === material.id) { - onSelectMaterial(null); - } else { - onSelectMaterial(material); - } - }} - isSelected={selectedMaterial?.id === material.id} - /> - ))} - - {/* 카메라 컨트롤 */} - - - ); -} - -export function Warehouse3DWidget({ element }: Warehouse3DWidgetProps) { - const [warehouses, setWarehouses] = useState([]); - const [materials, setMaterials] = useState([]); - const [loading, setLoading] = useState(true); - const [selectedWarehouse, setSelectedWarehouse] = useState(null); - const [selectedMaterial, setSelectedMaterial] = useState(null); - const [isFullscreen, setIsFullscreen] = useState(false); - - useEffect(() => { - loadData(); - }, []); - - const loadData = async () => { - try { - setLoading(true); - // API 호출 (백엔드 API 구현 필요) - const response = await fetch("/api/warehouse/data"); - if (response.ok) { - const data = await response.json(); - setWarehouses(data.warehouses || []); - setMaterials(data.materials || []); - } else { - // 임시 더미 데이터 (개발용) - console.log("API 실패, 더미 데이터 사용"); - } - } catch (error) { - console.error("창고 데이터 로드 실패:", error); - } finally { - setLoading(false); - } - }; - - if (loading) { - return ( - - - - - - ); - } - - return ( - - - 🏭 창고 현황 (3D) -
- - {warehouses.length}개 창고 | {materials.length}개 자재 - - -
-
- - {/* 3D 뷰 */} -
- - - - - -
- - {/* 정보 패널 */} -
- {/* 선택된 창고 정보 */} - {selectedWarehouse && ( - - - - - 창고 정보 - - - -
- 이름: {selectedWarehouse.name} -
-
- ID: {selectedWarehouse.id} -
-
- 용량: {selectedWarehouse.current_usage} /{" "} - {selectedWarehouse.capacity} -
-
- 사용률:{" "} - {((selectedWarehouse.current_usage / selectedWarehouse.capacity) * 100).toFixed(1)}% -
-
- 상태:{" "} - - {selectedWarehouse.status} - -
- {selectedWarehouse.description && ( -
- 설명: {selectedWarehouse.description} -
- )} -
-
- )} - - {/* 선택된 자재 정보 */} - {selectedMaterial && ( - - - - - 자재 정보 - - - -
- 이름: {selectedMaterial.name} -
-
- 코드: {selectedMaterial.material_code} -
-
- 수량: {selectedMaterial.quantity} {selectedMaterial.unit} -
-
- 위치:{" "} - {warehouses.find((w) => w.id === selectedMaterial.warehouse_id)?.name} -
-
- 상태:{" "} - - {selectedMaterial.status} - -
-
-
- )} - - {/* 창고 목록 */} - {!selectedWarehouse && !selectedMaterial && ( - - - 창고 목록 - - - {warehouses.map((warehouse) => { - const warehouseMaterials = materials.filter((m) => m.warehouse_id === warehouse.id); - return ( - - ); - })} - - - )} -
-
-
- ); -}