diff --git a/src/controllers/bmdashboard/__tests__/bmMaterialInsightsController.test.js b/src/controllers/bmdashboard/__tests__/bmMaterialInsightsController.test.js new file mode 100644 index 000000000..250aac75f --- /dev/null +++ b/src/controllers/bmdashboard/__tests__/bmMaterialInsightsController.test.js @@ -0,0 +1,384 @@ +/** + * Material Insights Controller Tests + * Tests for all calculation functions and API endpoints + */ + +const bmMaterialInsightsController = require('../bmMaterialInsightsController'); + +describe('Material Insights Controller', () => { + let mockBuildingMaterial; + let controller; + + beforeEach(() => { + // Mock BuildingMaterial model + mockBuildingMaterial = { + find: jest.fn(), + findById: jest.fn(), + }; + + controller = bmMaterialInsightsController(mockBuildingMaterial); + }); + + describe('Calculation Functions', () => { + describe('calculateMaterialInsights', () => { + it('should calculate insights for material with complete data', () => { + const material = { + _id: '123', + stockBought: 100, + stockUsed: 75, + stockAvailable: 30, + stockWasted: 5, + stockHold: 0, + itemType: { name: 'Cement', unit: 'kg' }, + project: { _id: 'proj1', name: 'Project A' }, + }; + + const insights = controller.calculateMaterialInsights(material); + + expect(insights.usagePct).toBe(75); + expect(insights.stockRatio).toBe(0.3); + expect(insights.stockHealth).toBe('low'); + expect(insights.materialName).toBe('Cement'); + expect(insights.unit).toBe('kg'); + expect(insights.hasBoughtData).toBe(true); + }); + + it('should handle material with no purchases', () => { + const material = { + _id: '123', + stockBought: 0, + stockUsed: 0, + stockAvailable: 0, + stockWasted: 0, + stockHold: 0, + itemType: { name: 'Steel', unit: 'tons' }, + project: { _id: 'proj1', name: 'Project B' }, + }; + + const insights = controller.calculateMaterialInsights(material); + + expect(insights.usagePct).toBeNull(); + expect(insights.stockRatio).toBeNull(); + expect(insights.stockHealth).toBe('no-data'); + expect(insights.hasBoughtData).toBe(false); + }); + + it('should classify as critical when stock <= 20%', () => { + const material = { + _id: '123', + stockBought: 100, + stockUsed: 90, + stockAvailable: 10, + stockWasted: 0, + stockHold: 0, + itemType: { name: 'Wood', unit: 'pcs' }, + project: { _id: 'proj1', name: 'Project C' }, + }; + + const insights = controller.calculateMaterialInsights(material); + + expect(insights.stockHealth).toBe('critical'); + expect(insights.stockHealthColor).toBe('red'); + expect(insights.stockHealthLabel).toBe('Critical'); + }); + + it('should classify as healthy when stock > 40%', () => { + const material = { + _id: '123', + stockBought: 100, + stockUsed: 50, + stockAvailable: 50, + stockWasted: 0, + stockHold: 0, + itemType: { name: 'Brick', unit: 'units' }, + project: { _id: 'proj1', name: 'Project D' }, + }; + + const insights = controller.calculateMaterialInsights(material); + + expect(insights.stockHealth).toBe('healthy'); + expect(insights.stockHealthColor).toBe('green'); + expect(insights.stockHealthLabel).toBe('Healthy'); + }); + }); + + describe('calculateUsagePercentage', () => { + it('should calculate usage percentage correctly', () => { + const percentage = controller.calculateUsagePercentage(50, 100); + expect(percentage).toBe(50); + }); + + it('should return null when bought is 0', () => { + const percentage = controller.calculateUsagePercentage(50, 0); + expect(percentage).toBeNull(); + }); + + it('should handle decimal values', () => { + const percentage = controller.calculateUsagePercentage(33.33, 100); + expect(percentage).toBe(33.33); + }); + }); + + describe('calculateStockRatio', () => { + it('should calculate stock ratio correctly', () => { + const ratio = controller.calculateStockRatio(50, 100); + expect(ratio).toBe(0.5); + }); + + it('should return null when bought is 0', () => { + const ratio = controller.calculateStockRatio(50, 0); + expect(ratio).toBeNull(); + }); + + it('should clamp ratio at 1', () => { + const ratio = controller.calculateStockRatio(150, 100); + expect(ratio).toBe(1.5); // Can exceed 1 if available > bought + }); + }); + + describe('getStockHealthStatus', () => { + it('should return critical for ratio <= 0.2', () => { + expect(controller.getStockHealthStatus(0.2)).toBe('critical'); + expect(controller.getStockHealthStatus(0.1)).toBe('critical'); + }); + + it('should return low for ratio between 0.2 and 0.4', () => { + expect(controller.getStockHealthStatus(0.3)).toBe('low'); + expect(controller.getStockHealthStatus(0.4)).toBe('low'); + }); + + it('should return healthy for ratio > 0.4', () => { + expect(controller.getStockHealthStatus(0.5)).toBe('healthy'); + expect(controller.getStockHealthStatus(1.0)).toBe('healthy'); + }); + + it('should return no-data for null/undefined', () => { + expect(controller.getStockHealthStatus(null)).toBe('no-data'); + expect(controller.getStockHealthStatus(undefined)).toBe('no-data'); + }); + }); + }); + + describe('Summary Metrics', () => { + describe('calculateSummaryMetrics', () => { + it('should calculate summary for empty array', () => { + const summary = controller.calculateSummaryMetrics([]); + + expect(summary.totalMaterials).toBe(0); + expect(summary.lowStockCount).toBe(0); + expect(summary.lowStockPercentage).toBe(0); + expect(summary.overUsageCount).toBe(0); + expect(summary.overUsagePercentage).toBe(0); + expect(summary.onHoldCount).toBe(0); + }); + + it('should count low stock items', () => { + const materials = [ + { + _id: '1', + stockBought: 100, + stockUsed: 80, + stockAvailable: 20, + stockWasted: 0, + stockHold: 0, + itemType: { name: 'A', unit: 'kg' }, + project: { _id: 'p1', name: 'P1' }, + }, + { + _id: '2', + stockBought: 100, + stockUsed: 85, + stockAvailable: 15, + stockWasted: 0, + stockHold: 0, + itemType: { name: 'B', unit: 'kg' }, + project: { _id: 'p1', name: 'P1' }, + }, + { + _id: '3', + stockBought: 100, + stockUsed: 50, + stockAvailable: 50, + stockWasted: 0, + stockHold: 0, + itemType: { name: 'C', unit: 'kg' }, + project: { _id: 'p1', name: 'P1' }, + }, + ]; + + const summary = controller.calculateSummaryMetrics(materials); + + expect(summary.totalMaterials).toBe(3); + expect(summary.lowStockCount).toBe(2); + expect(summary.lowStockPercentage).toBe(66.7); + }); + + it('should count high usage items', () => { + const materials = [ + { + _id: '1', + stockBought: 100, + stockUsed: 85, + stockAvailable: 15, + stockWasted: 0, + stockHold: 0, + itemType: { name: 'A', unit: 'kg' }, + project: { _id: 'p1', name: 'P1' }, + }, + { + _id: '2', + stockBought: 100, + stockUsed: 75, + stockAvailable: 25, + stockWasted: 0, + stockHold: 0, + itemType: { name: 'B', unit: 'kg' }, + project: { _id: 'p1', name: 'P1' }, + }, + ]; + + const summary = controller.calculateSummaryMetrics(materials); + + expect(summary.totalMaterials).toBe(2); + expect(summary.overUsageCount).toBe(1); // Only item 1 >= 80% + expect(summary.overUsagePercentage).toBe(50); + }); + + it('should count items on hold', () => { + const materials = [ + { + _id: '1', + stockBought: 100, + stockUsed: 50, + stockAvailable: 40, + stockWasted: 0, + stockHold: 10, + itemType: { name: 'A', unit: 'kg' }, + project: { _id: 'p1', name: 'P1' }, + }, + { + _id: '2', + stockBought: 100, + stockUsed: 50, + stockAvailable: 50, + stockWasted: 0, + stockHold: 0, + itemType: { name: 'B', unit: 'kg' }, + project: { _id: 'p1', name: 'P1' }, + }, + ]; + + const summary = controller.calculateSummaryMetrics(materials); + + expect(summary.onHoldCount).toBe(1); + }); + }); + }); + + describe('API Endpoints', () => { + let mockRes; + let mockReq; + + beforeEach(() => { + mockRes = { + status: jest.fn().mockReturnThis(), + json: jest.fn(), + }; + + mockReq = { + params: {}, + query: {}, + }; + }); + + describe('getMaterialInsightsAll', () => { + it('should return all materials with insights and summary', async () => { + const mockMaterials = [ + { + _id: '1', + stockBought: 100, + stockUsed: 75, + stockAvailable: 25, + stockWasted: 0, + stockHold: 0, + itemType: { name: 'Material 1', unit: 'kg' }, + project: { _id: 'p1', name: 'Project 1' }, + }, + ]; + + mockBuildingMaterial.find = jest.fn().mockReturnThis(); + mockBuildingMaterial.populate = jest.fn().mockReturnThis(); + mockBuildingMaterial.lean = jest.fn().mockReturnThis(); + mockBuildingMaterial.exec = jest.fn().mockResolvedValue(mockMaterials); + + await controller.getMaterialInsightsAll(mockReq, mockRes); + + expect(mockRes.status).toHaveBeenCalledWith(200); + expect(mockRes.json).toHaveBeenCalledWith( + expect.objectContaining({ + success: true, + data: expect.objectContaining({ + materials: expect.any(Array), + summary: expect.any(Object), + timestamp: expect.any(Date), + }), + }), + ); + }); + + it('should handle errors gracefully', async () => { + mockBuildingMaterial.find = jest.fn().mockImplementation(() => { + throw new Error('Database error'); + }); + + await controller.getMaterialInsightsAll(mockReq, mockRes); + + expect(mockRes.status).toHaveBeenCalledWith(500); + expect(mockRes.json).toHaveBeenCalledWith( + expect.objectContaining({ + success: false, + message: 'Internal server error', + }), + ); + }); + }); + + describe('getMaterialInsightsByProject', () => { + it('should reject invalid project ID', async () => { + mockReq.params.projectId = 'invalid-id'; + + await controller.getMaterialInsightsByProject(mockReq, mockRes); + + expect(mockRes.status).toHaveBeenCalledWith(400); + expect(mockRes.json).toHaveBeenCalledWith( + expect.objectContaining({ + success: false, + message: 'Invalid project ID', + }), + ); + }); + }); + + describe('getSummaryMetrics', () => { + it('should return only summary metrics', async () => { + const mockMaterials = []; + + mockBuildingMaterial.find = jest.fn().mockReturnThis(); + mockBuildingMaterial.lean = jest.fn().mockReturnThis(); + mockBuildingMaterial.exec = jest.fn().mockResolvedValue(mockMaterials); + + await controller.getSummaryMetrics(mockReq, mockRes); + + expect(mockRes.status).toHaveBeenCalledWith(200); + expect(mockRes.json).toHaveBeenCalledWith( + expect.objectContaining({ + success: true, + data: expect.objectContaining({ + totalMaterials: 0, + }), + }), + ); + }); + }); + }); +}); diff --git a/src/controllers/bmdashboard/bmMaterialInsightsController.js b/src/controllers/bmdashboard/bmMaterialInsightsController.js new file mode 100644 index 000000000..bb54f195a --- /dev/null +++ b/src/controllers/bmdashboard/bmMaterialInsightsController.js @@ -0,0 +1,28 @@ +/** + * Material Insights Controller + * Factory function that assembles handlers and utilities for material insights endpoints + */ + +const createHandlers = require('./materialInsightsHandlers'); +const { + calculateMaterialInsights, + calculateSummaryMetrics, + calculateUsagePercentage, + calculateStockRatio, + getStockHealthStatus, +} = require('./materialInsightsCalculations'); + +const bmMaterialInsightsController = (BuildingMaterial) => { + const handlers = createHandlers(BuildingMaterial); + + return { + ...handlers, + calculateMaterialInsights, + calculateSummaryMetrics, + calculateUsagePercentage, + calculateStockRatio, + getStockHealthStatus, + }; +}; + +module.exports = bmMaterialInsightsController; diff --git a/src/controllers/bmdashboard/materialInsightsCalculations.js b/src/controllers/bmdashboard/materialInsightsCalculations.js new file mode 100644 index 000000000..314ee36b9 --- /dev/null +++ b/src/controllers/bmdashboard/materialInsightsCalculations.js @@ -0,0 +1,150 @@ +/** + * Material Insights Calculations + * Pure calculation functions for material insights + */ + +const formatNumber = (value, decimals = 2) => { + if (typeof value !== 'number' || Number.isNaN(value)) { + return null; + } + return Math.round(value * 10 ** decimals) / 10 ** decimals; +}; + +const calculateUsagePercentage = (used, bought) => { + if (!bought || bought <= 0) { + return null; + } + const percentage = (used / bought) * 100; + return formatNumber(percentage, 2); +}; + +const calculateStockRatio = (available, bought) => { + if (!bought || bought <= 0) { + return null; + } + const ratio = available / bought; + return formatNumber(ratio, 2); +}; + +const getStockHealthStatus = (stockRatio) => { + if (stockRatio === null || stockRatio === undefined) { + return 'no-data'; + } + if (stockRatio <= 0.2) { + return 'critical'; + } + if (stockRatio <= 0.4) { + return 'low'; + } + return 'healthy'; +}; + +const getStockHealthColor = (status) => { + const colorMap = { + healthy: 'green', + low: 'yellow', + critical: 'red', + }; + return colorMap[status] || 'gray'; +}; + +const getStockHealthLabel = (status) => { + const labelMap = { + healthy: 'Healthy', + low: 'Low', + critical: 'Critical', + }; + return labelMap[status] || 'No Data'; +}; + +const calculateMaterialInsights = (material) => { + const bought = material?.stockBought || 0; + const used = material?.stockUsed || 0; + const available = material?.stockAvailable || 0; + const wasted = material?.stockWasted || 0; + const hold = material?.stockHold || 0; + + const usagePct = calculateUsagePercentage(used, bought); + const stockRatio = calculateStockRatio(available, bought); + const stockHealth = getStockHealthStatus(stockRatio); + const stockHealthColor = getStockHealthColor(stockHealth); + const stockHealthLabel = getStockHealthLabel(stockHealth); + + return { + materialId: material._id?.toString(), + materialName: material.itemType?.name || 'Unknown', + unit: material.itemType?.unit || '', + projectId: material.project?._id?.toString(), + projectName: material.project?.name || 'Unknown', + bought, + used, + available, + wasted, + hold, + usagePct, + stockRatio, + stockHealth, + stockHealthColor, + stockHealthLabel, + hasBoughtData: bought > 0, + }; +}; + +const calculateSummaryMetrics = (materials) => { + if (!materials || materials.length === 0) { + return { + totalMaterials: 0, + lowStockCount: 0, + lowStockPercentage: 0, + overUsageCount: 0, + overUsagePercentage: 0, + onHoldCount: 0, + usageThreshold: 80, + }; + } + + const total = materials.length; + let lowStockCount = 0; + let overUsageCount = 0; + let onHoldCount = 0; + + materials.forEach((material) => { + const insights = calculateMaterialInsights(material); + + if (insights.stockHealth === 'low' || insights.stockHealth === 'critical') { + lowStockCount += 1; + } + + if (insights.usagePct !== null && insights.usagePct >= 80) { + overUsageCount += 1; + } + + if ((material?.stockHold || 0) > 0) { + onHoldCount += 1; + } + }); + + const lowStockPercentage = formatNumber((lowStockCount / total) * 100, 1); + const overUsagePercentage = formatNumber((overUsageCount / total) * 100, 1); + + return { + totalMaterials: total, + lowStockCount, + lowStockPercentage, + overUsageCount, + overUsagePercentage, + onHoldCount, + usageThreshold: 80, + }; +}; + +module.exports = { + formatNumber, + calculateUsagePercentage, + calculateStockRatio, + getStockHealthStatus, + getStockHealthColor, + getStockHealthLabel, + calculateMaterialInsights, + calculateSummaryMetrics, +}; diff --git a/src/controllers/bmdashboard/materialInsightsHandlers.js b/src/controllers/bmdashboard/materialInsightsHandlers.js new file mode 100644 index 000000000..54f2d3811 --- /dev/null +++ b/src/controllers/bmdashboard/materialInsightsHandlers.js @@ -0,0 +1,193 @@ +/** + * Material Insights Handlers + * API endpoint handlers for material insights + */ + +const mongoose = require('mongoose'); +const { + calculateMaterialInsights, + calculateSummaryMetrics, +} = require('./materialInsightsCalculations'); + +const createUtilities = (BuildingMaterial) => { + const fetchMaterials = (query = {}) => + BuildingMaterial.find(query) + .populate([ + { path: 'project', select: '_id name' }, + { path: 'itemType', select: '_id name unit' }, + ]) + .lean() + .exec(); + + const handleError = (res, error, endpoint) => { + console.error(`Error ${endpoint}:`, error); + return res.status(500).json({ + success: false, + message: 'Internal server error', + error: error.message, + }); + }; + + const sendSuccessResponse = (res, data, status = 200) => + res.status(status).json({ success: true, data }); + + const sendErrorResponse = (res, message, status = 400) => + res.status(status).json({ success: false, message }); + + const validateObjectId = (id) => mongoose.Types.ObjectId.isValid(id); + + return { fetchMaterials, handleError, sendSuccessResponse, sendErrorResponse, validateObjectId }; +}; + +const createAllMaterialsHandler = (BuildingMaterial, utils) => async (req, res) => { + try { + const materials = await utils.fetchMaterials(); + const materialInsights = materials.map((m) => calculateMaterialInsights(m)); + const summaryMetrics = calculateSummaryMetrics(materials); + return utils.sendSuccessResponse(res, { + materials: materialInsights, + summary: summaryMetrics, + timestamp: new Date(), + }); + } catch (error) { + return utils.handleError(res, error, 'fetching material insights'); + } +}; + +const createProjectMaterialsHandler = (BuildingMaterial, utils) => async (req, res) => { + try { + const { projectId } = req.params; + if (!utils.validateObjectId(projectId)) { + return utils.sendErrorResponse(res, 'Invalid project ID'); + } + + const materials = await utils.fetchMaterials({ project: projectId }); + const materialInsights = materials.map((m) => calculateMaterialInsights(m)); + const summaryMetrics = calculateSummaryMetrics(materials); + return utils.sendSuccessResponse(res, { + projectId, + materials: materialInsights, + summary: summaryMetrics, + timestamp: new Date(), + }); + } catch (error) { + return utils.handleError(res, error, 'fetching material insights by project'); + } +}; + +const createSummaryHandler = (BuildingMaterial, utils) => async (req, res) => { + try { + const materials = await BuildingMaterial.find().lean().exec(); + const summaryMetrics = calculateSummaryMetrics(materials); + return utils.sendSuccessResponse(res, { + ...summaryMetrics, + timestamp: new Date(), + }); + } catch (error) { + return utils.handleError(res, error, 'fetching summary metrics'); + } +}; + +const createProjectSummaryHandler = (BuildingMaterial, utils) => async (req, res) => { + try { + const { projectId } = req.params; + if (!utils.validateObjectId(projectId)) { + return utils.sendErrorResponse(res, 'Invalid project ID'); + } + + const materials = await BuildingMaterial.find({ project: projectId }).lean().exec(); + const summaryMetrics = calculateSummaryMetrics(materials); + return utils.sendSuccessResponse(res, { + projectId, + ...summaryMetrics, + timestamp: new Date(), + }); + } catch (error) { + return utils.handleError(res, error, 'fetching summary metrics by project'); + } +}; + +const createCriticalItemsHandler = (BuildingMaterial, utils) => async (req, res) => { + try { + const materials = await utils.fetchMaterials(); + const criticalItems = materials + .map((m) => calculateMaterialInsights(m)) + .filter((i) => i.stockHealth === 'critical' || i.stockHealth === 'low') + .sort((a, b) => { + if (a.stockRatio === null) return 1; + if (b.stockRatio === null) return -1; + return a.stockRatio - b.stockRatio; + }); + + return utils.sendSuccessResponse(res, { + criticalItemCount: criticalItems.length, + items: criticalItems, + timestamp: new Date(), + }); + } catch (error) { + return utils.handleError(res, error, 'fetching critical stock items'); + } +}; + +const createHighUsageHandler = (BuildingMaterial, utils) => async (req, res) => { + try { + const materials = await utils.fetchMaterials(); + const highUsageItems = materials + .map((m) => calculateMaterialInsights(m)) + .filter((i) => i.usagePct !== null && i.usagePct >= 80) + .sort((a, b) => (b.usagePct || 0) - (a.usagePct || 0)); + + return utils.sendSuccessResponse(res, { + highUsageItemCount: highUsageItems.length, + items: highUsageItems, + timestamp: new Date(), + }); + } catch (error) { + return utils.handleError(res, error, 'fetching high usage items'); + } +}; + +const createDetailHandler = (BuildingMaterial, utils) => async (req, res) => { + try { + const { materialId } = req.params; + if (!utils.validateObjectId(materialId)) { + return utils.sendErrorResponse(res, 'Invalid material ID'); + } + + const material = await BuildingMaterial.findById(materialId) + .populate([ + { path: 'project', select: '_id name' }, + { path: 'itemType', select: '_id name unit' }, + ]) + .lean() + .exec(); + + if (!material) { + return utils.sendErrorResponse(res, 'Material not found', 404); + } + + const insights = calculateMaterialInsights(material); + return utils.sendSuccessResponse(res, { + ...insights, + timestamp: new Date(), + }); + } catch (error) { + return utils.handleError(res, error, 'fetching material insights detail'); + } +}; + +const createHandlers = (BuildingMaterial) => { + const utils = createUtilities(BuildingMaterial); + + return { + getMaterialInsightsAll: createAllMaterialsHandler(BuildingMaterial, utils), + getMaterialInsightsByProject: createProjectMaterialsHandler(BuildingMaterial, utils), + getSummaryMetrics: createSummaryHandler(BuildingMaterial, utils), + getSummaryMetricsByProject: createProjectSummaryHandler(BuildingMaterial, utils), + getCriticalStockItems: createCriticalItemsHandler(BuildingMaterial, utils), + getHighUsageItems: createHighUsageHandler(BuildingMaterial, utils), + getMaterialInsightsDetail: createDetailHandler(BuildingMaterial, utils), + }; +}; + +module.exports = createHandlers; diff --git a/src/routes/bmdashboard/bmMaterialInsightsRouter.js b/src/routes/bmdashboard/bmMaterialInsightsRouter.js new file mode 100644 index 000000000..e744b1c9c --- /dev/null +++ b/src/routes/bmdashboard/bmMaterialInsightsRouter.js @@ -0,0 +1,71 @@ +/** + * Material Insights Router + * Provides API endpoints for Material Usage Insights & Visual Indicators feature + * All endpoints return structured JSON with success flag, data, and timestamp + */ + +const express = require('express'); + +const routes = function (BuildingMaterial) { + const insightsRouter = express.Router(); + const controller = require('../../controllers/bmdashboard/bmMaterialInsightsController')( + BuildingMaterial, + ); + + /** + * GET /materials/insights/all + * Get insights for all materials with summary metrics + * Response: { success, data: { materials[], summary{}, timestamp } } + */ + insightsRouter.route('/insights/all').get(controller.getMaterialInsightsAll); + + /** + * GET /materials/insights/summary + * Get summary metrics for all materials (lightweight) + * Response: { success, data: { totalMaterials, lowStockCount, lowStockPercentage, ... } } + */ + insightsRouter.route('/insights/summary').get(controller.getSummaryMetrics); + + /** + * GET /materials/insights/by-project/:projectId + * Get insights for materials in a specific project + * Response: { success, data: { projectId, materials[], summary{}, timestamp } } + */ + insightsRouter + .route('/insights/by-project/:projectId') + .get(controller.getMaterialInsightsByProject); + + /** + * GET /materials/insights/summary/by-project/:projectId + * Get summary metrics for a specific project + * Response: { success, data: { projectId, totalMaterials, lowStockCount, ... } } + */ + insightsRouter + .route('/insights/summary/by-project/:projectId') + .get(controller.getSummaryMetricsByProject); + + /** + * GET /materials/insights/critical-items + * Get all materials with critical/low stock levels + * Response: { success, data: { criticalItemCount, items[], timestamp } } + */ + insightsRouter.route('/insights/critical-items').get(controller.getCriticalStockItems); + + /** + * GET /materials/insights/high-usage-items + * Get all materials with high usage (>= 80%) + * Response: { success, data: { highUsageItemCount, items[], timestamp } } + */ + insightsRouter.route('/insights/high-usage-items').get(controller.getHighUsageItems); + + /** + * GET /materials/insights/:materialId + * Get detailed insights for a specific material + * Response: { success, data: { materialId, materialName, unit, projectId, ... } } + */ + insightsRouter.route('/insights/:materialId').get(controller.getMaterialInsightsDetail); + + return insightsRouter; +}; + +module.exports = routes; diff --git a/src/startup/routes.js b/src/startup/routes.js index 56a3ae939..69138ca47 100644 --- a/src/startup/routes.js +++ b/src/startup/routes.js @@ -198,6 +198,9 @@ const materialCostRouter = require('../routes/materialCostRouter')(); // bm dashboard const bmLoginRouter = require('../routes/bmdashboard/bmLoginRouter')(); const bmMaterialsRouter = require('../routes/bmdashboard/bmMaterialsRouter')(buildingMaterial); +const bmMaterialInsightsRouter = require('../routes/bmdashboard/bmMaterialInsightsRouter')( + buildingMaterial, +); const bmReusableRouter = require('../routes/bmdashboard/bmReusableRouter')(buildingReusable); const bmProjectRouter = require('../routes/bmdashboard/bmProjectRouter')(buildingProject); @@ -450,6 +453,7 @@ module.exports = function (app) { // bm dashboard app.use('/api/bm', bmLoginRouter); app.use('/api/bm', bmMaterialsRouter); + app.use('/api/bm', bmMaterialInsightsRouter); app.use('/api/bm', bmReusableRouter); app.use('/api/bm', bmProjectRouter); app.use('/api/bm', bmNewLessonRouter);