Skip to content

Commit 3029168

Browse files
Merge pull request #1895 from OneCommunityGlobal/saikrishna_DonutChartForActualCostBreakdown_Backend
SaiKrishna Actual Cost Breakdown Donut Chart Back End
2 parents dde787c + c8df0a8 commit 3029168

4 files changed

Lines changed: 260 additions & 0 deletions

File tree

Lines changed: 203 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,203 @@
1+
const moment = require('moment');
2+
const mongoose = require('mongoose');
3+
const ActualCost = require('../models/actualCost');
4+
const logger = require('../startup/logger');
5+
6+
const actualCostController = function () {
7+
const getActualCostBreakdown = async function (req, res) {
8+
try {
9+
// Check if MongoDB is connected
10+
if (mongoose.connection.readyState !== 1) {
11+
return res.status(503).send({
12+
error: 'Database not connected',
13+
message:
14+
'MongoDB connection is not established. Please check your database configuration.',
15+
});
16+
}
17+
18+
const { id: projectId } = req.params;
19+
const { fromDate, toDate } = req.query;
20+
21+
// Validate projectId
22+
if (!projectId) {
23+
return res.status(400).send({ error: 'Project ID is required' });
24+
}
25+
26+
// Convert projectId to ObjectId
27+
if (!mongoose.Types.ObjectId.isValid(projectId)) {
28+
return res.status(400).send({ error: 'Invalid project ID format' });
29+
}
30+
const projectObjectId = new mongoose.Types.ObjectId(projectId);
31+
32+
// Set default date range to current month if not provided
33+
const startDate = fromDate ? moment(fromDate).startOf('day') : moment().startOf('month');
34+
const endDate = toDate ? moment(toDate).endOf('day') : moment().endOf('month');
35+
36+
// Validate date range
37+
if (!startDate.isValid() || !endDate.isValid()) {
38+
return res.status(400).send({ error: 'Invalid date format. Use YYYY-MM-DD' });
39+
}
40+
41+
if (startDate.isAfter(endDate)) {
42+
return res.status(400).send({ error: 'Start date cannot be after end date' });
43+
}
44+
45+
// Prepare date objects for query
46+
const startDateObj = startDate.toDate();
47+
const endDateObj = endDate.toDate();
48+
const startDateISO = startDate.toISOString();
49+
const endDateISO = endDate.toISOString();
50+
51+
// Get current period cost breakdown
52+
// Handle both Date object and string date formats
53+
const matchStage = {
54+
$and: [
55+
{
56+
$or: [{ projectId: projectObjectId }, { projectId }],
57+
},
58+
{
59+
$or: [
60+
// Try Date object comparison
61+
{
62+
date: {
63+
$gte: startDateObj,
64+
$lte: endDateObj,
65+
},
66+
},
67+
// Try string/ISO comparison (if dates are stored as strings)
68+
{
69+
date: {
70+
$gte: startDateISO,
71+
$lte: endDateISO,
72+
},
73+
},
74+
// Try moment format string
75+
{
76+
date: {
77+
$gte: startDate.format('YYYY-MM-DD'),
78+
$lte: endDate.format('YYYY-MM-DD'),
79+
},
80+
},
81+
],
82+
},
83+
],
84+
};
85+
86+
const currentCosts = await ActualCost.aggregate([
87+
{
88+
$match: matchStage,
89+
},
90+
{
91+
$group: {
92+
_id: '$category',
93+
totalCost: { $sum: '$cost' },
94+
},
95+
},
96+
]);
97+
98+
// Calculate previous month's total for percentage change
99+
const previousMonthStart = moment(startDate).subtract(1, 'month').startOf('month');
100+
const previousMonthEnd = moment(startDate).subtract(1, 'month').endOf('month');
101+
102+
const previousStartDateObj = previousMonthStart.toDate();
103+
const previousEndDateObj = previousMonthEnd.toDate();
104+
const previousStartDateISO = previousMonthStart.toISOString();
105+
const previousEndDateISO = previousMonthEnd.toISOString();
106+
107+
const previousMatchStage = {
108+
$and: [
109+
{
110+
$or: [{ projectId: projectObjectId }, { projectId }],
111+
},
112+
{
113+
$or: [
114+
// Try Date object comparison
115+
{
116+
date: {
117+
$gte: previousStartDateObj,
118+
$lte: previousEndDateObj,
119+
},
120+
},
121+
// Try string/ISO comparison
122+
{
123+
date: {
124+
$gte: previousStartDateISO,
125+
$lte: previousEndDateISO,
126+
},
127+
},
128+
// Try moment format string
129+
{
130+
date: {
131+
$gte: previousMonthStart.format('YYYY-MM-DD'),
132+
$lte: previousMonthEnd.format('YYYY-MM-DD'),
133+
},
134+
},
135+
],
136+
},
137+
],
138+
};
139+
140+
const previousMonthCosts = await ActualCost.aggregate([
141+
{
142+
$match: previousMatchStage,
143+
},
144+
{
145+
$group: {
146+
_id: null,
147+
totalCost: { $sum: '$cost' },
148+
},
149+
},
150+
]);
151+
152+
// Format current costs into the required structure
153+
const current = {
154+
plumbing: 0,
155+
electrical: 0,
156+
structural: 0,
157+
mechanical: 0,
158+
};
159+
160+
currentCosts.forEach((cost) => {
161+
const category = cost._id.toLowerCase();
162+
if (Object.prototype.hasOwnProperty.call(current, category)) {
163+
current[category] = cost.totalCost;
164+
}
165+
});
166+
167+
// Calculate total current cost
168+
const currentTotal = Object.values(current).reduce((sum, cost) => sum + cost, 0);
169+
170+
// Get previous month total
171+
const previousMonthTotal =
172+
previousMonthCosts.length > 0 ? previousMonthCosts[0].totalCost : 0;
173+
174+
// Calculate percentage change
175+
const percentageChange =
176+
previousMonthTotal > 0
177+
? ((currentTotal - previousMonthTotal) / previousMonthTotal) * 100
178+
: 0;
179+
180+
const response = {
181+
current,
182+
previousMonthTotal,
183+
percentageChange: Math.round(percentageChange * 100) / 100, // Round to 2 decimal places
184+
currentTotal,
185+
dateRange: {
186+
from: startDate.format('YYYY-MM-DD'),
187+
to: endDate.format('YYYY-MM-DD'),
188+
},
189+
};
190+
191+
res.status(200).send(response);
192+
} catch (error) {
193+
logger.logException(error);
194+
res.status(500).send({ error: 'Error fetching cost breakdown data' });
195+
}
196+
};
197+
198+
return {
199+
getActualCostBreakdown,
200+
};
201+
};
202+
203+
module.exports = actualCostController;

src/models/actualCost.js

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
const mongoose = require('mongoose');
2+
3+
const { Schema } = mongoose;
4+
5+
const actualCostSchema = new Schema(
6+
{
7+
projectId: {
8+
type: Schema.Types.Mixed, // Accept both ObjectId and String
9+
ref: 'project',
10+
required: true,
11+
},
12+
date: {
13+
type: Date,
14+
required: true,
15+
},
16+
category: {
17+
type: String,
18+
enum: ['Plumbing', 'Electrical', 'Structural', 'Mechanical'],
19+
required: true,
20+
},
21+
cost: {
22+
type: Number,
23+
required: true,
24+
min: 0,
25+
},
26+
// Timestamps are automatically managed by Mongoose
27+
// createdAt and updatedAt are added automatically with timestamps: true
28+
},
29+
{
30+
timestamps: true, // Automatically adds createdAt and updatedAt fields
31+
},
32+
);
33+
34+
// Create compound index for efficient querying
35+
actualCostSchema.index({ projectId: 1, date: 1, category: 1 });
36+
37+
// Explicitly set collection name to match database
38+
module.exports = mongoose.model('ActualCost', actualCostSchema, 'actualcosts');

src/routes/actualCostRouter.js

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
const express = require('express');
2+
3+
const routes = function () {
4+
const controller = require('../controllers/actualCostController')();
5+
const actualCostRouter = express.Router();
6+
7+
// GET /api/projects/:id/actual-cost-breakdown
8+
actualCostRouter
9+
.route('/projects/:id/actual-cost-breakdown')
10+
.get(controller.getActualCostBreakdown);
11+
12+
return actualCostRouter;
13+
};
14+
15+
module.exports = routes;

src/startup/routes.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -366,6 +366,9 @@ const promotionDetailsRouter = require('../routes/promotionDetailsRouter');
366366

367367
const summaryDashboardRouter = require('../routes/summaryDashboard.routes');
368368

369+
// Actual Cost
370+
const actualCostRouter = require('../routes/actualCostRouter')();
371+
369372
module.exports = function (app) {
370373
app.use('/api/bm/summary-dashboard', summaryDashboardRouter);
371374
app.use('/api', forgotPwdRouter);
@@ -520,6 +523,7 @@ module.exports = function (app) {
520523
app.use('/api/bm', bmRentalChart);
521524
app.use('/api', bmToolsDowntimeRouter);
522525
app.use('/api/lb', lbWishlistsRouter);
526+
app.use('/api', actualCostRouter);
523527

524528
app.use('/api', promotionDetailsRouter);
525529
app.use('/api/analytics', analyticsPopularPRsRouter);

0 commit comments

Comments
 (0)