Skip to content

Aditya-feat: Side-by-Side Comparison Mode for Actual vs. Planned Expenditure Charts#2073

Open
Aditya-gam wants to merge 7 commits intodevelopmentfrom
Aditya-feat/Add-Side-by-Side-Comparison-Mode-for-Actual-vs-Planned-Expenditure-Charts
Open

Aditya-feat: Side-by-Side Comparison Mode for Actual vs. Planned Expenditure Charts#2073
Aditya-gam wants to merge 7 commits intodevelopmentfrom
Aditya-feat/Add-Side-by-Side-Comparison-Mode-for-Actual-vs-Planned-Expenditure-Charts

Conversation

@Aditya-gam
Copy link
Contributor

@Aditya-gam Aditya-gam commented Feb 26, 2026

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.
IssueDescription

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**
    • getProjectExpensesPie: Validates projectId with mongoose.Types.ObjectId.isValid(projectId); returns 400 and { message: 'Invalid project ID' } when invalid (before any DB call). Uses new mongoose.Types.ObjectId(projectId) in the aggregate $match. Replaces console.error with logger.logException(..., 'expenditureController.getProjectExpensesPie', { projectId }). Adds explicit return before all res.json / res.status(...).json to avoid double-send.
    • getProjectIdsWithExpenditure: Replaces console.error with logger.logException(..., 'expenditureController.getProjectIdsWithExpenditure'). Adds explicit return for success and error responses.
    • No changes to the Expenditure model or aggregation logic; only validation, logging, and response handling.
  • **src/routes/bmdashboard/bmExpenditureRouter.js**
    • Route order fix: Defines the static route before the parameterized one so GET /expenditure/projects is not matched by GET /expenditure/:projectId/pie (which would treat "projects" as projectId and return 400).
    • Order: router.get('/expenditure/projects', getProjectIdsWithExpenditure); then router.get('/expenditure/:projectId/pie', getProjectExpensesPie);.
  • **src/startup/routes.js**
    • Added: const bmExpenditureRouter = require('../routes/bmdashboard/bmExpenditureRouter'); and app.use('/api/bm', bmExpenditureRouter);.
    • Removed: Require and use of plannedCost model and plannedCostRouter (including console.log debug lines and app.use('/api', plannedCostRouter(plannedCost, project))). Removed duplicate/early app.use for toolAvailabilityRouter and projectCostTrackingRouter in the block above BM routes. Removed app.use('/api', projectMaterialRouter) and app.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)
    • getProjectIdsWithExpenditure: 200 with array of project IDs; 200 with empty array; 500 and logger call on DB error.
    • getProjectExpensesPie: 400 for invalid/short non-hex projectId without calling aggregate; 200 with { actual, planned } shape; empty arrays when no data; 500 and logger with projectId on aggregate error.
    • Mocks: Expenditure (distinct, aggregate), logger (logException, logInfo).
  • **src/controllers/bmdashboard/__tests__/bmEquipmentController.test.js**
    • fetchSingleEquipment: success 200 and error 500.
    • fetchBMEquipments: success 200 and error 500.
    • bmPurchaseEquipments: create when equipment does not exist (201).
    • updateEquipmentById: 400 for invalid equipment ID, invalid project ID, invalid enums (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**
    • getLaborHoursDistribution: 403 when user lacks getWeeklySummaries permission; 400 for missing/invalid start_date/end_date (format and calendar date); success 200 with cached/uncached aggregation; 500 and logger on aggregate error.
    • Mocks: LaborHours, logger, nodeCache, permissions.

Deleted files

  • **src/controllers/plannedCostController.js** (-220)
    • Legacy planned cost controller removed; replaced by expenditure-based APIs.
  • **src/models/plannedCost.js** (-40)
    • Legacy planned cost model removed.
  • **src/routes/plannedCostRouter.js** (-25)
    • Legacy planned cost router removed.

Key implementation details

  • Expenditure API (mounted under /api/bm):
  • GET /api/bm/expenditure/projects — Returns a JSON array of distinct projectId values (MongoDB ObjectIds as strings) that have records in the expenditurePie collection. Used by the frontend to populate the project selector for the comparison view.
  • GET /api/bm/expenditure/:projectId/pie — Returns { actual: [{ category, amount }], planned: [{ category, amount }] } from the Expenditure model (collection expenditurePie), aggregated by type (actual/planned) and category (Labor, Equipment, Materials). Used to render Actual and Planned pie charts side-by-side with consistent categories and amounts.
  • Expenditure model (unchanged in this PR): projectId (ObjectId), category (enum: Labor | Equipment | Materials), type (enum: actual | planned), amount (Number); collection expenditurePie.
  • Controller hardening: Invalid projectId is rejected before any DB call; all errors are logged via logger.logException with controller method name and context; every response path uses return to avoid double-sending.
  • Route order: The static path /expenditure/projects must be registered before /expenditure/:projectId/pie in Express so that a request to list projects is not interpreted as a pie request with projectId = "projects".

How to test

  1. Check out the current branch:

git checkout Aditya-feat/Add-Side-by-Side-Comparison-Mode-for-Actual-vs-Planned-Expenditure-Charts
2. Reinstall dependencies and clean cache using rm -rf node_modules package-lock.json && npm cache clean --force
3. Run npm install to 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 Authorization header, same as other /api/bm routes):
- List projects with expenditure data:
GET /api/bm/expenditure/projects
Expected: 200, body is a JSON array of project IDs (or []).
- Get pie data for a project:
GET /api/bm/expenditure/:projectId/pie
Use a valid 24-character hex ObjectId (e.g., from the list above).
Expected: 200, body { actual: [...], planned: [...] } with category and amount per item.
Use an invalid ID (e.g. invalid-id or 12345): 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)"

Verify that `expenditureController.test.js`, `bmEquipmentController.test.js`, and `laborHoursDistributionController.test.js` pass (e.g., 8 + 14 + multiple labor-hours cases).
  1. Verify:
    No remaining references to plannedCost or plannedCostRouter in src/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

  • Test Coverage:
TestCoverageReport
  • Test Video:
TestVideo.mov

Note

  • Breaking changes: Removal of the /api routes previously provided by plannedCostRouter. 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.
  • Validation: projectId in getProjectExpensesPie is validated with mongoose.Types.ObjectId.isValid; invalid or short/non-hex values receive 400 without hitting the DB.
  • Not backward compatible with callers of the removed planned cost API; they must use the new expenditure endpoints under /api/bm/expenditure/.

Aditya-gam and others added 6 commits February 21, 2026 11:57
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>
@Aditya-gam Aditya-gam changed the title Backend: Side-by-Side Comparison Mode for Actual vs. Planned Expenditure Charts Aditya-feat: Side-by-Side Comparison Mode for Actual vs. Planned Expenditure Charts Feb 26, 2026
…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
@sonarqubecloud
Copy link

@one-community one-community added the High Priority - Please Review First This is an important PR we'd like to get merged as soon as possible label Feb 26, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

High Priority - Please Review First This is an important PR we'd like to get merged as soon as possible

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants