Skip to content

Commit c381366

Browse files
Xiaolei Implement Loss Tracking API (#1351)
* prototype * add parameters in query and store in schema * loss tracking api finished * fix: added validation for input fields- startDate, endDate, materialId --------- Co-authored-by: ALISHA WALUNJ <walunjalisha@gmail.com>
1 parent 3755a28 commit c381366

4 files changed

Lines changed: 279 additions & 4 deletions

File tree

Lines changed: 245 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,245 @@
1+
/* eslint-disable no-restricted-globals */
2+
const mongoose = require('mongoose');
3+
const { buildingMaterial } = require('../models/bmdashboard/buildingInventoryItem');
4+
5+
module.exports = function (MaterialLoss) {
6+
const isValidDate = (dateStr) => {
7+
const regex = /^\d{4}-\d{2}-\d{2}$/;
8+
if (!regex.test(dateStr)) return false;
9+
const date = new Date(dateStr);
10+
return !isNaN(date.getTime());
11+
};
12+
13+
const monthNumberToName = (monthNum) =>
14+
['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'][
15+
monthNum - 1
16+
];
17+
18+
const getMonthStartEnd = (year, month) => {
19+
const monthStart = new Date(Date.UTC(year, month - 1, 1, 0, 0, 0));
20+
const monthEnd = new Date(Date.UTC(year, month, 1, 0, 0, 0));
21+
monthEnd.setSeconds(monthEnd.getSeconds() - 1);
22+
return { monthStart, monthEnd };
23+
};
24+
25+
async function computeMonthLoss(materialId, year, month) {
26+
const { monthStart, monthEnd } = getMonthStartEnd(year, month);
27+
const matchStage = { __t: 'material_item', updateRecord: { $exists: true, $ne: [] } };
28+
if (materialId) matchStage.itemType = new mongoose.Types.ObjectId(materialId);
29+
30+
const pipeline = [
31+
{ $match: matchStage },
32+
{ $unwind: '$updateRecord' },
33+
{ $match: { 'updateRecord.date': { $gte: monthStart, $lte: monthEnd } } },
34+
{
35+
$group: {
36+
_id: null,
37+
totalUsed: { $sum: '$updateRecord.quantityUsed' },
38+
totalWasted: { $sum: '$updateRecord.quantityWasted' },
39+
},
40+
},
41+
];
42+
43+
const result = await buildingMaterial.aggregate(pipeline);
44+
const totalUsed = result[0]?.totalUsed || 0;
45+
const totalWasted = result[0]?.totalWasted || 0;
46+
const lossPerc =
47+
totalUsed + totalWasted ? ((totalWasted / (totalUsed + totalWasted)) * 100).toFixed(2) : 0;
48+
49+
return {
50+
lossPercentage: parseFloat(lossPerc),
51+
year,
52+
month: monthNumberToName(month),
53+
};
54+
}
55+
56+
async function computeAndStoreHistoricalLoss(materialId) {
57+
// Error handling for invalid input
58+
if (materialId && !mongoose.Types.ObjectId.isValid(materialId)) {
59+
throw new Error(`Invalid materialId provided: ${materialId}`);
60+
}
61+
const matchStage = { __t: 'material_item', updateRecord: { $exists: true, $ne: [] } };
62+
if (materialId) matchStage.itemType = new mongoose.Types.ObjectId(materialId);
63+
64+
const minDateAgg = await buildingMaterial.aggregate([
65+
{ $match: matchStage },
66+
{ $unwind: '$updateRecord' },
67+
{
68+
$group: {
69+
_id: null,
70+
minDate: { $min: '$updateRecord.date' },
71+
},
72+
},
73+
]);
74+
75+
const startDate = minDateAgg[0]?.minDate ? new Date(minDateAgg[0].minDate) : null;
76+
if (!startDate) return;
77+
78+
const now = new Date();
79+
const startYear = startDate.getUTCFullYear();
80+
const startMonth = startDate.getUTCMonth() + 1;
81+
const endYear = now.getUTCFullYear();
82+
const endMonth = now.getUTCMonth() + 1;
83+
const currentMonthName = monthNumberToName(endMonth);
84+
85+
const monthList = [];
86+
for (let y = startYear; y <= endYear; y += 1) {
87+
const mStart = y === startYear ? startMonth : 1;
88+
const mEnd = y === endYear ? endMonth : 12;
89+
for (let m = mStart; m <= mEnd; m += 1) {
90+
monthList.push({ year: y, month: m });
91+
}
92+
}
93+
94+
await Promise.all(
95+
monthList.map(async ({ year, month }) => {
96+
const monthName = monthNumberToName(month);
97+
const existing = await MaterialLoss.findOne({ materialId, year, month: monthName });
98+
const isCurrentMonth = year === endYear && monthName === currentMonthName;
99+
100+
if (!existing || isCurrentMonth) {
101+
let lossPercentage = 0;
102+
const matchDate = getMonthStartEnd(year, month).monthStart;
103+
104+
const hasData = await buildingMaterial.exists({
105+
itemType: materialId,
106+
updateRecord: {
107+
$elemMatch: {
108+
date: { $gte: matchDate },
109+
},
110+
},
111+
});
112+
113+
if (hasData) {
114+
const computed = await computeMonthLoss(materialId, year, month);
115+
lossPercentage = computed.lossPercentage;
116+
}
117+
118+
const materialDoc = await buildingMaterial
119+
.findOne({ itemType: materialId })
120+
.populate('itemType', 'name');
121+
const materialName = materialDoc?.itemType?.name || 'Unknown Material';
122+
123+
await MaterialLoss.findOneAndUpdate(
124+
{ materialId, year, month: monthName },
125+
{
126+
materialId,
127+
materialName,
128+
year,
129+
month: monthName,
130+
lossPercentage,
131+
updatedAt: new Date(),
132+
},
133+
{ upsert: true, new: true },
134+
);
135+
}
136+
}),
137+
);
138+
}
139+
140+
const getMaterialLossData = async (req, res) => {
141+
const { materialId, year, startDate, endDate } = req.query;
142+
// Error handling for invalid input
143+
if (materialId && !mongoose.Types.ObjectId.isValid(materialId)) {
144+
return res.status(400).json({ error: 'Invalid materialId' });
145+
}
146+
147+
// Date format validation for startDate and endDate
148+
if ((startDate && !isValidDate(startDate)) || (endDate && !isValidDate(endDate))) {
149+
return res.status(400).json({ error: 'Invalid date format. Use YYYY-MM-DD.' });
150+
}
151+
152+
if (materialId) {
153+
await computeAndStoreHistoricalLoss(materialId);
154+
} else {
155+
const materialIds = await buildingMaterial.distinct('itemType', { __t: 'material_item' });
156+
await Promise.all(materialIds.map((id) => computeAndStoreHistoricalLoss(id.toString())));
157+
}
158+
159+
const filter = {};
160+
if (materialId) filter.materialId = materialId;
161+
// if (year) filter.year = parseInt(year, 10);
162+
const parsedYear = parseInt(year, 10);
163+
if (!isNaN(parsedYear)) {
164+
filter.year = parsedYear;
165+
}
166+
167+
if (startDate || endDate) {
168+
const start = startDate ? new Date(startDate) : null;
169+
const end = endDate ? new Date(endDate) : null;
170+
171+
const startYear = start?.getUTCFullYear();
172+
const startMonth = start ? start.getUTCMonth() + 1 : null;
173+
const endYear = end?.getUTCFullYear();
174+
const endMonth = end ? end.getUTCMonth() + 1 : null;
175+
176+
filter.$and = [];
177+
178+
if (start) {
179+
filter.$and.push({
180+
$or: [
181+
{ year: { $gt: startYear } },
182+
{
183+
year: startYear,
184+
month: {
185+
$in: [
186+
'Jan',
187+
'Feb',
188+
'Mar',
189+
'Apr',
190+
'May',
191+
'Jun',
192+
'Jul',
193+
'Aug',
194+
'Sep',
195+
'Oct',
196+
'Nov',
197+
'Dec',
198+
].slice(startMonth - 1),
199+
},
200+
},
201+
],
202+
});
203+
}
204+
205+
if (end) {
206+
filter.$and.push({
207+
$or: [
208+
{ year: { $lt: endYear } },
209+
{
210+
year: endYear,
211+
month: {
212+
$in: [
213+
'Jan',
214+
'Feb',
215+
'Mar',
216+
'Apr',
217+
'May',
218+
'Jun',
219+
'Jul',
220+
'Aug',
221+
'Sep',
222+
'Oct',
223+
'Nov',
224+
'Dec',
225+
].slice(0, endMonth),
226+
},
227+
},
228+
],
229+
});
230+
}
231+
}
232+
233+
const result = await MaterialLoss.find(filter, {
234+
_id: 0,
235+
materialId: 1,
236+
month: 1,
237+
lossPercentage: 1,
238+
year: 1,
239+
});
240+
241+
res.status(200).json({ data: result });
242+
};
243+
244+
return { getMaterialLossData };
245+
};

src/models/materialLoss.js

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
const mongoose = require('mongoose');
2+
3+
const materialLossModel = new mongoose.Schema({
4+
materialId: { type: String, required: true, index: true },
5+
materialName: { type: String, required: true },
6+
year: { type: Number, required: true, index: true },
7+
month: { type: String, required: true, index: true },
8+
lossPercentage: { type: Number, required: true },
9+
updatedAt: { type: Date, default: Date.now, required: true}
10+
});
11+
12+
materialLossModel.index({ materialId: 1, year: 1, month: 1 });
13+
14+
module.exports = mongoose.model('MaterialLoss', materialLossModel, 'materialLoss');

src/routes/materialLossRouter.js

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
const express = require('express');
2+
3+
const routes = function (materialLossModel) {
4+
const router = express.Router();
5+
const controller = require('../controllers/materialLossController')(materialLossModel);
6+
7+
router.route('/loss-tracking')
8+
.get(controller.getMaterialLossData);
9+
10+
return router;
11+
};
12+
13+
module.exports = routes;

src/startup/routes.js

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,7 @@ const bmTimeLog = require('../models/bmdashboard/buildingTimeLogger');
6868
const timeOffRequest = require('../models/timeOffRequest');
6969
const followUp = require('../models/followUp');
7070
const tag = require('../models/tag');
71+
const materialLoss = require('../models/materialLoss');
7172

7273
const bidoverview_Listing = require('../models/lbdashboard/bidoverview/Listing');
7374
const bidoverview_Bid = require('../models/lbdashboard/bidoverview/Bid');
@@ -141,11 +142,12 @@ const timeOffRequestRouter = require('../routes/timeOffRequestRouter')(
141142
userProfile,
142143
);
143144
const followUpRouter = require('../routes/followUpRouter')(followUp);
144-
const form = require('../models/forms');
145-
const formResponse = require('../models/formResponse');
146-
const formRouter = require('../routes/formRouter')(form, formResponse);
147-
145+
const form=require('../models/forms')
146+
const formResponse=require('../models/formResponse')
147+
const formRouter=require('../routes/formRouter')(form,formResponse);
148+
const materialLossRouter = require('../routes/materialLossRouter')(materialLoss);
148149
const wastedMaterialRouter = require('../routes/mostWastedRouter');
150+
149151
// bm dashboard
150152
const bmLoginRouter = require('../routes/bmdashboard/bmLoginRouter')();
151153
const bmMaterialsRouter = require('../routes/bmdashboard/bmMaterialsRouter')(buildingMaterial);
@@ -279,6 +281,7 @@ module.exports = function (app) {
279281

280282
app.use('/api/help-categories', helpCategoryRouter);
281283
app.use('/api', tagRouter);
284+
app.use('/api/materials', materialLossRouter);
282285

283286
// bm dashboard
284287
app.use('/api/bm', bmLoginRouter);

0 commit comments

Comments
 (0)