Aditya-feat: Side-by-Side Comparison Mode for Actual vs. Planned Expenditure Charts#2073
Open
Aditya-gam wants to merge 7 commits intodevelopmentfrom
Conversation
Expose expenditure endpoints under /api/bm for Financials Tracking (Actual vs Planned pie charts): - GET /api/bm/expenditure/projects — list project IDs with expenditure data - GET /api/bm/expenditure/:projectId/pie — actual and planned by category Register bmExpenditureRouter in the BM dashboard block alongside bmActualVsPlannedCostRouter. Co-authored-by: Cursor <cursoragent@cursor.com>
- Validate projectId with mongoose.Types.ObjectId.isValid() before DB access; return 400 for invalid IDs to avoid unhandled throws. - Use new mongoose.Types.ObjectId(projectId) only after validation. - Replace console.error with logger.logException including transaction name and projectId (for pie handler) as extra data. - Rename unused req to _req in getProjectIdsWithExpenditure. - Use explicit return on all branches to prevent fall-through. Co-authored-by: Cursor <cursoragent@cursor.com>
Add expenditureController.test.js with full coverage:
getProjectIdsWithExpenditure:
- 200 with array of project IDs on success
- 200 with empty array when no records exist
- 500 and logger.logException on DB error
getProjectExpensesPie:
- 400 for invalid projectId (non-hex, short) without calling aggregate
- 200 with { actual, planned } shape on success
- 200 with { actual: [], planned: [] } when no matching records
- 500 and logger.logException with projectId on DB error
Co-authored-by: Cursor <cursoragent@cursor.com>
Cover the new updateEquipmentById handler and validation in bmEquipmentController: - 400 for invalid equipment ID or project ID - 400 for invalid enum values (purchaseStatus, currentUsage, condition) - 400 when no valid fields provided to update - 200 with updated equipment on success - 404 when equipment not found after update - 500 on updateOne error Add updateOne to the mock BuildingEquipment model. Co-authored-by: Cursor <cursoragent@cursor.com>
…controller Add laborHoursDistributionController.test.js for getLaborHoursDistribution: - 403 when user lacks getWeeklySummaries permission - 400 for missing or invalid start_date/end_date (format, calendar date, range) - 200 with cached response on cache hit - 200 with total_hours and distribution (with percentages) on success - 200 with empty distribution when no data - Optional category filter in cache key and aggregation pipeline - 500 and logger.logException on aggregate error Mocks: LaborHours, logger, nodeCache, hasPermission. Co-authored-by: Cursor <cursoragent@cursor.com>
…s with it.each Replace 6 near-duplicate it() blocks (validation tests) with a single it.each() data table of [description, queryOverrides, expectedError]. Reduces file from 241 to 174 lines while preserving all 12 tests and identical assertions. Test names unchanged (e.g. 'returns 400 when start_date format is invalid'). Made-with: Cursor
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.



Description
This PR adds backend support for the Side-by-Side Comparison Mode for Actual vs. Planned Expenditure Charts on the BM (Building Management) dashboard. It exposes expenditure APIs under

/api/bm, hardens the expenditure controller with validation and structured logging, fixes route registration so the list-projects endpoint is reachable, and removes the legacy planned-cost flow in favor of the expenditure-based pie data. It also adds unit tests for the expenditure controller, the BM equipment controller (updateEquipmentById), and the labor hours distribution controller.Location: Dashboard → Reports → Total Construction Summary → Financials Tracking. Users can toggle “Comparison View (Side-by-Side)” so Actual and Planned expenditure pie charts appear next to each other (desktop) or as a swipeable/tabbed view (mobile), with consistent colors (Labor = blue, Equipment = green, Materials = yellow). This backend provides the data endpoints the frontend needs for that comparison view.
Related PRs (if any)
Main changes explained
Created/Updated files
**src/controllers/bmdashboard/expenditureController.js**projectIdwithmongoose.Types.ObjectId.isValid(projectId); returns 400 and{ message: 'Invalid project ID' }when invalid (before any DB call). Usesnew mongoose.Types.ObjectId(projectId)in the aggregate$match. Replacesconsole.errorwithlogger.logException(..., 'expenditureController.getProjectExpensesPie', { projectId }). Adds explicitreturnbefore allres.json/res.status(...).jsonto avoid double-send.console.errorwithlogger.logException(..., 'expenditureController.getProjectIdsWithExpenditure'). Adds explicitreturnfor success and error responses.**src/routes/bmdashboard/bmExpenditureRouter.js**GET /expenditure/projectsis not matched byGET /expenditure/:projectId/pie(which would treat"projects"asprojectIdand return 400).router.get('/expenditure/projects', getProjectIdsWithExpenditure);thenrouter.get('/expenditure/:projectId/pie', getProjectExpensesPie);.**src/startup/routes.js**const bmExpenditureRouter = require('../routes/bmdashboard/bmExpenditureRouter');andapp.use('/api/bm', bmExpenditureRouter);.plannedCostmodel andplannedCostRouter(includingconsole.logdebug lines andapp.use('/api', plannedCostRouter(plannedCost, project))). Removed duplicate/earlyapp.usefortoolAvailabilityRouterandprojectCostTrackingRouterin the block above BM routes. Removedapp.use('/api', projectMaterialRouter)andapp.use('/api', plannedCostRouter(...))from the latter block (routers may still be mounted elsewhere in the file as applicable).**src/controllers/bmdashboard/__tests__/expenditureController.test.js** (+148, new)projectIdwithout calling aggregate; 200 with{ actual, planned }shape; empty arrays when no data; 500 and logger withprojectIdon aggregate error.Expenditure(distinct, aggregate),logger(logException, logInfo).**src/controllers/bmdashboard/__tests__/bmEquipmentController.test.js**purchaseStatus,currentUsage,condition), no valid fields; 200 with updated equipment; 404 when equipment not found after update; 500 on DB error.**src/controllers/summaryDashboard/__tests__/laborHoursDistributionController.test.js**getWeeklySummariespermission; 400 for missing/invalidstart_date/end_date(format and calendar date); success 200 with cached/uncached aggregation; 500 and logger on aggregate error.Deleted files
**src/controllers/plannedCostController.js** (-220)**src/models/plannedCost.js** (-40)**src/routes/plannedCostRouter.js**(-25)Key implementation details
/api/bm):/api/bm/expenditure/projects— Returns a JSON array of distinctprojectIdvalues (MongoDB ObjectIds as strings) that have records in theexpenditurePiecollection. Used by the frontend to populate the project selector for the comparison view./api/bm/expenditure/:projectId/pie— Returns{ actual: [{ category, amount }], planned: [{ category, amount }] }from theExpendituremodel (collectionexpenditurePie), aggregated bytype(actual/planned) andcategory(Labor, Equipment, Materials). Used to render Actual and Planned pie charts side-by-side with consistent categories and amounts.projectId(ObjectId),category(enum: Labor | Equipment | Materials),type(enum: actual | planned),amount(Number); collectionexpenditurePie.projectIdis rejected before any DB call; all errors are logged vialogger.logExceptionwith controller method name and context; every response path usesreturnto avoid double-sending./expenditure/projectsmust be registered before/expenditure/:projectId/piein Express so that a request to list projects is not interpreted as a pie request withprojectId = "projects".How to test
git checkout Aditya-feat/Add-Side-by-Side-Comparison-Mode-for-Actual-vs-Planned-Expenditure-Charts2. Reinstall dependencies and clean cache using
rm -rf node_modules package-lock.json && npm cache clean --force3. Run
npm installto install dependencies, then start the backend locally (npm run dev)4. Use Admin/Owner login.
5. Test expenditure endpoints (use an auth token in the
Authorizationheader, same as other/api/bmroutes):- List projects with expenditure data:
GET /api/bm/expenditure/projectsExpected: 200, body is a JSON array of project IDs (or
[]).- Get pie data for a project:
GET /api/bm/expenditure/:projectId/pieUse a valid 24-character hex ObjectId (e.g., from the list above).
Expected: 200, body
{ actual: [...], planned: [...] }withcategoryandamountper item.Use an invalid ID (e.g.
invalid-idor12345): Expected 400,{ message: 'Invalid project ID' }.6. Run unit tests:
npm test -- --coverage --collectCoverageFrom=src/controllers/bmdashboard/expenditureController.js --collectCoverageFrom=src/controllers/bmdashboard/bmEquipmentController.js --collectCoverageFrom=src/controllers/summaryDashboard/laborHoursDistributionController.js --testPathPattern="(expenditureController|bmEquipmentController|laborHoursDistributionController)"No remaining references to
plannedCostorplannedCostRouterinsrc/startup/routes.js.When the frontend Comparison View is enabled, it can call the above endpoints to load the project list and pie data for side-by-side Actual vs. Planned charts.
Screenshots or videos of changes
TestVideo.mov
Note
/apiroutes previously provided byplannedCostRouter. Any client that relied on the old planned cost API must switch to GET /api/bm/expenditure/projects and GET /api/bm/expenditure/:projectId/pie.projectIdingetProjectExpensesPieis validated withmongoose.Types.ObjectId.isValid; invalid or short/non-hex values receive 400 without hitting the DB./api/bm/expenditure/.