diff --git a/src/controllers/timeEntryController.js b/src/controllers/timeEntryController.js index 962f30170..9c4d7c5e3 100644 --- a/src/controllers/timeEntryController.js +++ b/src/controllers/timeEntryController.js @@ -1,17 +1,17 @@ -const moment = require('moment-timezone'); -const mongoose = require('mongoose'); -const { getInfringementEmailBody } = require('../helpers/userHelper')(); -const userProfile = require('../models/userProfile'); -const task = require('../models/task'); -const emailSender = require('../utilities/emailSender'); -const { hasPermission } = require('../utilities/permissions'); +const moment = require("moment-timezone"); +const mongoose = require("mongoose"); +const { getInfringementEmailBody } = require("../helpers/userHelper")(); +const userProfile = require("../models/userProfile"); +const task = require("../models/task"); +const emailSender = require("../utilities/emailSender"); +const { hasPermission } = require("../utilities/permissions"); const formatSeconds = function (seconds) { const formattedseconds = parseInt(seconds, 10); const values = `${Math.floor( - moment.duration(formattedseconds, 'seconds').asHours(), - )}:${moment.duration(formattedseconds, 'seconds').minutes()}`; - return values.split(':'); + moment.duration(formattedseconds, "seconds").asHours() + )}:${moment.duration(formattedseconds, "seconds").minutes()}`; + return values.split(":"); }; /** @@ -24,9 +24,20 @@ const formatSeconds = function (seconds) { * @param {*} requestor The userProfile object of the person that modified the time entry * @returns {String} */ -const getEditedTimeEntryEmailBody = (firstName, lastName, email, originalTime, finalTime, requestor) => { - const formattedOriginal = moment.utc(originalTime * 1000).format('HH[ hours ]mm[ minutes]'); - const formattedFinal = moment.utc(finalTime * 1000).format('HH[ hours ]mm[ minutes]'); +const getEditedTimeEntryEmailBody = ( + firstName, + lastName, + email, + originalTime, + finalTime, + requestor +) => { + const formattedOriginal = moment + .utc(originalTime * 1000) + .format("HH[ hours ]mm[ minutes]"); + const formattedFinal = moment + .utc(finalTime * 1000) + .format("HH[ hours ]mm[ minutes]"); return ` A time entry belonging to ${firstName} ${lastName} (${email}) was modified by ${requestor.firstName} ${requestor.lastName} (${requestor.email}). The entry's duration was changed from [${formattedOriginal}] to [${formattedFinal}] @@ -45,18 +56,39 @@ const notifyEditByEmail = async (personId, original, finalTime, final) => { try { const originalTime = original.totalSeconds; const record = await userProfile.findById(personId); - const requestor = (personId !== final.requestor.requestorId) ? await userProfile.findById(final.requestor.requestorId) : record; - const emailBody = getEditedTimeEntryEmailBody(record.firstName, record.lastName, record.email, originalTime, finalTime, requestor); - emailSender('onecommunityglobal@gmail.com', `A Time Entry was Edited for ${record.firstName} ${record.lastName}`, emailBody); + const requestor = + personId !== final.requestor.requestorId + ? await userProfile.findById(final.requestor.requestorId) + : record; + const emailBody = getEditedTimeEntryEmailBody( + record.firstName, + record.lastName, + record.email, + originalTime, + finalTime, + requestor + ); + emailSender( + "onecommunityglobal@gmail.com", + `A Time Entry was Edited for ${record.firstName} ${record.lastName}`, + emailBody + ); } catch (error) { - throw new Error(`Failed to send email notification about the modification of time entry belonging to user with id ${personId}`); + throw new Error( + `Failed to send email notification about the modification of time entry belonging to user with id ${personId}` + ); } }; -const notifyTaskOvertimeEmailBody = async (personId, taskName, estimatedHours, hoursLogged) => { +const notifyTaskOvertimeEmailBody = async ( + personId, + taskName, + estimatedHours, + hoursLogged +) => { try { - const record = await userProfile.findById(personId); - const text = `Dear ${record.firstName}${record.lastName}, + const record = await userProfile.findById(personId); + const text = `Dear ${record.firstName}${record.lastName},

Oops, it looks like you have logged more hours than estimated for a task

Task Name : ${taskName}

Time Estimated : ${estimatedHours}

@@ -64,24 +96,37 @@ const notifyTaskOvertimeEmailBody = async (personId, taskName, estimatedHours, h

Please connect with your manager to explain what happened and submit a new hours estimation for completion.

Thank you,

One Community

`; - emailSender( - record.email, - 'Logged more hours than estimated for a task', - text, - 'onecommunityglobal@gmail.com', - null, + emailSender( + record.email, + "Logged more hours than estimated for a task", + text, + "onecommunityglobal@gmail.com", + null, + record.email, + null ); } catch (error) { - console.log(`Failed to send email notification about the overtime for a task belonging to user with id ${personId}`); + console.log( + `Failed to send email notification about the overtime for a task belonging to user with id ${personId}` + ); } }; const checkTaskOvertime = async (timeentry, record, currentTask) => { try { // send email notification if logged in hours exceeds estiamted hours for a task - if (currentTask.hoursLogged > currentTask.estimatedHours) { notifyTaskOvertimeEmailBody(timeentry.personId.toString(), currentTask.taskName, currentTask.estimatedHours, currentTask.hoursLogged); } + if (currentTask.hoursLogged > currentTask.estimatedHours) { + notifyTaskOvertimeEmailBody( + timeentry.personId.toString(), + currentTask.taskName, + currentTask.estimatedHours, + currentTask.hoursLogged + ); + } } catch (error) { - console.log(`Failed to find task whose logged-in hours are more than estimated hours ${record.email}`); + console.log( + `Failed to find task whose logged-in hours are more than estimated hours ${record.email}` + ); } }; @@ -92,31 +137,58 @@ const timeEntrycontroller = function (TimeEntry) { try { if (!req.params.timeEntryId) { - return res.status(400).send({ error: 'ObjectId in request param is not in correct format' }); + return res + .status(400) + .send({ + error: "ObjectId in request param is not in correct format", + }); } - if (!mongoose.Types.ObjectId.isValid(req.params.timeEntryId) || !mongoose.Types.ObjectId.isValid(req.body.projectId)) { - return res.status(400).send({ error: 'ObjectIds are not correctly formed' }); + if ( + !mongoose.Types.ObjectId.isValid(req.params.timeEntryId) || + !mongoose.Types.ObjectId.isValid(req.body.projectId) + ) { + return res + .status(400) + .send({ error: "ObjectIds are not correctly formed" }); } // Get initial timeEntry by timeEntryId const timeEntry = await TimeEntry.findById(req.params.timeEntryId); if (!timeEntry) { - return res.status(400).send({ error: `No valid records found for ${req.params.timeEntryId}` }); + return res + .status(400) + .send({ + error: `No valid records found for ${req.params.timeEntryId}`, + }); } - if (!(await hasPermission(req.body.requestor, 'editTimeEntry') || timeEntry.personId.toString() === req.body.requestor.requestorId.toString())) { - return res.status(403).send({ error: 'Unauthorized request' }); + if ( + !( + (await hasPermission(req.body.requestor, "editTimeEntry")) || + timeEntry.personId.toString() === + req.body.requestor.requestorId.toString() + ) + ) { + return res.status(403).send({ error: "Unauthorized request" }); } - const hours = req.body.hours ? req.body.hours : '00'; - const minutes = req.body.minutes ? req.body.minutes : '00'; + const hours = req.body.hours ? req.body.hours : "00"; + const minutes = req.body.minutes ? req.body.minutes : "00"; const totalSeconds = moment.duration(`${hours}:${minutes}`).asSeconds(); - if (timeEntry.isTangible === true && totalSeconds !== timeEntry.totalSeconds) { - notifyEditByEmail(timeEntry.personId.toString(), timeEntry, totalSeconds, req.body); + if ( + timeEntry.isTangible === true && + totalSeconds !== timeEntry.totalSeconds + ) { + notifyEditByEmail( + timeEntry.personId.toString(), + timeEntry, + totalSeconds, + req.body + ); } const initialSeconds = timeEntry.totalSeconds; @@ -130,7 +202,7 @@ const timeEntrycontroller = function (TimeEntry) { timeEntry.isTangible = req.body.isTangible; timeEntry.lastModifiedDateTime = moment().utc().toISOString(); timeEntry.projectId = mongoose.Types.ObjectId(req.body.projectId); - timeEntry.dateOfWork = moment(req.body.dateOfWork).format('YYYY-MM-DD'); + timeEntry.dateOfWork = moment(req.body.dateOfWork).format("YYYY-MM-DD"); // Update the hoursLogged field of related tasks based on before and after timeEntries // initialIsTangible is a bealoon value, req.body.isTangible is a string @@ -138,11 +210,11 @@ const timeEntrycontroller = function (TimeEntry) { try { if (findTask) { if (initialIsTangible === true) { - findTask.hoursLogged -= (initialSeconds / 3600); + findTask.hoursLogged -= initialSeconds / 3600; } if (req.body.isTangible === true) { - findTask.hoursLogged += (totalSeconds / 3600); + findTask.hoursLogged += totalSeconds / 3600; } await findTask.save(); @@ -152,14 +224,17 @@ const timeEntrycontroller = function (TimeEntry) { } // Update edit history - if (initialSeconds !== totalSeconds - && timeEntry.isTangible - && req.body.requestor.requestorId === timeEntry.personId.toString() - && !await hasPermission(req.body.requestor, 'editTimeEntry') - ) { - const requestor = await userProfile.findById(req.body.requestor.requestorId); + if ( + initialSeconds !== totalSeconds && + timeEntry.isTangible && + req.body.requestor.requestorId === timeEntry.personId.toString() && + !(await hasPermission(req.body.requestor, "editTimeEntry")) + ) { + const requestor = await userProfile.findById( + req.body.requestor.requestorId + ); requestor.timeEntryEditHistory.push({ - date: moment().tz('America/Los_Angeles').toDate(), + date: moment().tz("America/Los_Angeles").toDate(), initialSeconds, newSeconds: totalSeconds, }); @@ -168,18 +243,23 @@ const timeEntrycontroller = function (TimeEntry) { let totalRecentEdits = 0; requestor.timeEntryEditHistory.forEach((edit) => { - if (moment().tz('America/Los_Angeles').diff(edit.date, 'days') <= 365) { + if ( + moment().tz("America/Los_Angeles").diff(edit.date, "days") <= 365 + ) { totalRecentEdits += 1; } }); if (totalRecentEdits >= 5) { requestor.infringements.push({ - date: moment().tz('America/Los_Angeles'), + date: moment().tz("America/Los_Angeles"), description: `${totalRecentEdits} time entry edits in the last calendar year`, }); - emailSender('onecommunityglobal@gmail.com', `${requestor.firstName} ${requestor.lastName} was issued a blue square for for editing a time entry ${totalRecentEdits} times`, ` + emailSender( + "onecommunityglobal@gmail.com", + `${requestor.firstName} ${requestor.lastName} was issued a blue square for for editing a time entry ${totalRecentEdits} times`, + `

${requestor.firstName} ${requestor.lastName} (${requestor.email}) was issued a blue square for editing their time entries ${totalRecentEdits} times within the last calendar year. @@ -187,28 +267,39 @@ const timeEntrycontroller = function (TimeEntry) {

This is the ${totalRecentEdits}th edit within the past 365 days.

- `); + ` + ); const emailInfringement = { - date: moment().tz('America/Los_Angeles').format('MMMM-DD-YY'), + date: moment().tz("America/Los_Angeles").format("MMMM-DD-YY"), description: `You edited your time entries ${totalRecentEdits} times within the last 365 days, exceeding the limit of 4 times per year you can edit them without penalty.`, }; - emailSender(requestor.email, 'You\'ve been issued a blue square for editing your time entry', getInfringementEmailBody(requestor.firstName, requestor.lastName, emailInfringement, requestor.infringements.length)); + emailSender( + requestor.email, + "You've been issued a blue square for editing your time entry", + getInfringementEmailBody( + requestor.firstName, + requestor.lastName, + emailInfringement, + requestor.infringements.length + ) + ); } await requestor.save(); } - await timeEntry.save(); - res.status(200).send({ message: 'Successfully updated time entry' }); + res.status(200).send({ message: "Successfully updated time entry" }); // If the time entry isn't related to a task (i.e. it's a project), then don't check for overtime (Most likely pr team) if (findTask) { // checking if logged in hours exceed estimated time after timeentry edit for a task - const record = await userProfile.findById(timeEntry.personId.toString()); + const record = await userProfile.findById( + timeEntry.personId.toString() + ); const currentTask = await task.findById(req.body.projectId); checkTaskOvertime(timeEntry, record, currentTask); } @@ -233,9 +324,9 @@ const timeEntrycontroller = function (TimeEntry) { timeentry.personId = element.personId; timeentry.projectId = element.projectId; timeentry.dateOfWork = element.dateOfWork; - timeentry.timeSpent = moment('1900-01-01 00:00:00') - .add(element.totalSeconds, 'seconds') - .format('HH:mm:ss'); + timeentry.timeSpent = moment("1900-01-01 00:00:00") + .add(element.totalSeconds, "seconds") + .format("HH:mm:ss"); timeentry.notes = element.notes; timeentry.isTangible = element.isTangible; items.push(timeentry); @@ -246,21 +337,21 @@ const timeEntrycontroller = function (TimeEntry) { const postTimeEntry = async function (req, res) { if ( - !mongoose.Types.ObjectId.isValid(req.body.personId) - || !mongoose.Types.ObjectId.isValid(req.body.projectId) - || !req.body.dateOfWork - || !moment(req.body.dateOfWork).isValid() - || !req.body.timeSpent - || !req.body.isTangible + !mongoose.Types.ObjectId.isValid(req.body.personId) || + !mongoose.Types.ObjectId.isValid(req.body.projectId) || + !req.body.dateOfWork || + !moment(req.body.dateOfWork).isValid() || + !req.body.timeSpent || + !req.body.isTangible ) { - res.status(400).send({ error: 'Bad request' }); + res.status(400).send({ error: "Bad request" }); return; } const timeentry = new TimeEntry(); const { dateOfWork, timeSpent } = req.body; timeentry.personId = req.body.personId; timeentry.projectId = req.body.projectId; - timeentry.dateOfWork = moment(dateOfWork).format('YYYY-MM-DD'); + timeentry.dateOfWork = moment(dateOfWork).format("YYYY-MM-DD"); timeentry.totalSeconds = moment.duration(timeSpent).asSeconds(); timeentry.notes = req.body.notes; timeentry.isTangible = req.body.isTangible; @@ -274,12 +365,14 @@ const timeEntrycontroller = function (TimeEntry) { .status(200) .send({ message: `Time Entry saved with id as ${results._id}` }); }) - .catch(error => res.status(400).send(error)); + .catch((error) => res.status(400).send(error)); - // Get the task related to this time entry, if not found, then it's a project sets to null - const currentTask = await task.findById(req.body.projectId).catch(() => null); + // Get the task related to this time entry, if not found, then it's a project sets to null + const currentTask = await task + .findById(req.body.projectId) + .catch(() => null); - // Add this tangbile time entry to related task's hoursLogged and checks if timeEntry is related to a task + // Add this tangbile time entry to related task's hoursLogged and checks if timeEntry is related to a task if (timeentry.isTangible === true && currentTask) { try { currentTask.hoursLogged += timeentry.totalSeconds / 3600; @@ -292,7 +385,9 @@ const timeEntrycontroller = function (TimeEntry) { // checking if logged in hours exceed estimated time after timeentry for a task, only if the time entry is related to a task (It might not be, if it's a project) if (currentTask) { try { - const record = await userProfile.findById(timeentry.personId.toString()); + const record = await userProfile.findById( + timeentry.personId.toString() + ); checkTaskOvertime(timeentry, record, currentTask); } catch (error) { throw new Error(error); @@ -302,19 +397,23 @@ const timeEntrycontroller = function (TimeEntry) { const getTimeEntriesForSpecifiedPeriod = function (req, res) { if ( - !req.params - || !req.params.fromdate - || !req.params.todate - || !req.params.userId - || !moment(req.params.fromdate).isValid() - || !moment(req.params.toDate).isValid() + !req.params || + !req.params.fromdate || + !req.params.todate || + !req.params.userId || + !moment(req.params.fromdate).isValid() || + !moment(req.params.toDate).isValid() ) { - res.status(400).send({ error: 'Invalid request' }); + res.status(400).send({ error: "Invalid request" }); return; } - const fromdate = moment(req.params.fromdate).tz('America/Los_Angeles').format('YYYY-MM-DD'); - const todate = moment(req.params.todate).tz('America/Los_Angeles').format('YYYY-MM-DD'); + const fromdate = moment(req.params.fromdate) + .tz("America/Los_Angeles") + .format("YYYY-MM-DD"); + const todate = moment(req.params.todate) + .tz("America/Los_Angeles") + .format("YYYY-MM-DD"); const { userId } = req.params; TimeEntry.aggregate([ @@ -326,18 +425,18 @@ const timeEntrycontroller = function (TimeEntry) { }, { $lookup: { - from: 'projects', - localField: 'projectId', - foreignField: '_id', - as: 'project', + from: "projects", + localField: "projectId", + foreignField: "_id", + as: "project", }, }, { $lookup: { - from: 'tasks', - localField: 'projectId', - foreignField: '_id', - as: 'task', + from: "tasks", + localField: "projectId", + foreignField: "_id", + as: "task", }, }, { @@ -349,41 +448,26 @@ const timeEntrycontroller = function (TimeEntry) { projectId: 1, lastModifiedDateTime: 1, projectName: { - $arrayElemAt: [ - '$project.projectName', - 0, - ], + $arrayElemAt: ["$project.projectName", 0], }, taskName: { - $arrayElemAt: [ - '$task.taskName', - 0, - ], + $arrayElemAt: ["$task.taskName", 0], }, category: { - $arrayElemAt: [ - '$project.category', - 0, - ], + $arrayElemAt: ["$project.category", 0], }, classification: { - $arrayElemAt: [ - '$task.classification', - 0, - ], + $arrayElemAt: ["$task.classification", 0], }, dateOfWork: 1, hours: { $floor: { - $divide: ['$totalSeconds', 3600], + $divide: ["$totalSeconds", 3600], }, }, minutes: { $floor: { - $divide: [ - { $mod: ['$totalSeconds', 3600] }, - 60, - ], + $divide: [{ $mod: ["$totalSeconds", 3600] }, 60], }, }, }, @@ -393,9 +477,11 @@ const timeEntrycontroller = function (TimeEntry) { lastModifiedDateTime: -1, }, }, - ]).then((results) => { - res.status(200).send(results); - }).catch(error => res.status(400).send(error)); + ]) + .then((results) => { + res.status(200).send(results); + }) + .catch((error) => res.status(400).send(error)); }; const getTimeEntriesForUsersList = function (req, res) { @@ -406,9 +492,9 @@ const timeEntrycontroller = function (TimeEntry) { personId: { $in: users }, dateOfWork: { $gte: fromDate, $lte: toDate }, }, - ' -createdDateTime', + " -createdDateTime" ) - .populate('projectId') + .populate("projectId") .sort({ lastModifiedDateTime: -1 }) .then((results) => { const data = []; @@ -419,72 +505,73 @@ const timeEntrycontroller = function (TimeEntry) { record.notes = element.notes; record.isTangible = element.isTangible; record.personId = element.personId; - record.projectId = element.projectId ? element.projectId._id : ''; + record.projectId = element.projectId ? element.projectId._id : ""; record.projectName = element.projectId ? element.projectId.projectName - : ''; + : ""; record.dateOfWork = element.dateOfWork; [record.hours, record.minutes] = formatSeconds(element.totalSeconds); data.push(record); }); res.status(200).send(data); }) - .catch(error => res.status(400).send(error)); + .catch((error) => res.status(400).send(error)); }; const getTimeEntriesForSpecifiedProject = function (req, res) { if ( - !req.params - || !req.params.fromDate - || !req.params.toDate - || !req.params.projectId + !req.params || + !req.params.fromDate || + !req.params.toDate || + !req.params.projectId ) { - res.status(400).send({ error: 'Invalid request' }); + res.status(400).send({ error: "Invalid request" }); return; } - const todate = moment(req.params.toDate).format('YYYY-MM-DD'); - const fromDate = moment(req.params.fromDate).format('YYYY-MM-DD'); + const todate = moment(req.params.toDate).format("YYYY-MM-DD"); + const fromDate = moment(req.params.fromDate).format("YYYY-MM-DD"); const { projectId } = req.params; TimeEntry.find( { projectId, dateOfWork: { $gte: fromDate, $lte: todate }, }, - '-createdDateTime -lastModifiedDateTime', + "-createdDateTime -lastModifiedDateTime" ) - .populate('userId') + .populate("userId") .sort({ dateOfWork: -1 }) .then((results) => { res.status(200).send(results); }) - .catch(error => res.status(400).send(error)); + .catch((error) => res.status(400).send(error)); }; const deleteTimeEntry = async function (req, res) { if (!req.params.timeEntryId) { - res.status(400).send({ error: 'Bad request' }); + res.status(400).send({ error: "Bad request" }); return; } TimeEntry.findById(req.params.timeEntryId) .then(async (record) => { if (!record) { - res.status(400).send({ message: 'No valid record found' }); + res.status(400).send({ message: "No valid record found" }); return; } if ( - record.personId.toString() - === req.body.requestor.requestorId.toString() - || await hasPermission(req.body.requestor, 'deleteTimeEntry') + record.personId.toString() === + req.body.requestor.requestorId.toString() || + (await hasPermission(req.body.requestor, "deleteTimeEntry")) ) { // Revert this tangible timeEntry of related task's hoursLogged if (record.isTangible === true) { - task.findById(record.projectId) + task + .findById(record.projectId) .then((currentTask) => { // If the time entry isn't related to a task (i.e. it's a project), then don't revert hours (Most likely pr team) if (currentTask) { - currentTask.hoursLogged -= (record.totalSeconds / 3600); + currentTask.hoursLogged -= record.totalSeconds / 3600; currentTask.save(); } }) @@ -496,13 +583,13 @@ const timeEntrycontroller = function (TimeEntry) { record .remove() .then(() => { - res.status(200).send({ message: 'Successfully deleted' }); + res.status(200).send({ message: "Successfully deleted" }); }) .catch((error) => { res.status(500).send(error); }); } else { - res.status(403).send({ error: 'Unauthorized request' }); + res.status(403).send({ error: "Unauthorized request" }); } }) .catch((error) => { @@ -510,7 +597,6 @@ const timeEntrycontroller = function (TimeEntry) { }); }; - return { getAllTimeEnteries, postTimeEntry, diff --git a/src/helpers/userHelper.js b/src/helpers/userHelper.js index fa0115271..addd2e2e4 100644 --- a/src/helpers/userHelper.js +++ b/src/helpers/userHelper.js @@ -484,8 +484,8 @@ const userHelper = function () { emailBody, null, "onecommunityglobal@gmail.com", - null, - status.email + status.email, + null ); const categories = await dashboardHelper.laborThisWeekByCategory( diff --git a/src/models/bmdashboard/buildingInventoryItem.js b/src/models/bmdashboard/buildingInventoryItem.js new file mode 100644 index 000000000..5e8b24916 --- /dev/null +++ b/src/models/bmdashboard/buildingInventoryItem.js @@ -0,0 +1,148 @@ +const mongoose = require('mongoose'); + +//----------------------- +// BASE INVENTORY SCHEMAS +//----------------------- + +// TODO: purchaseRecord subdocs may be changed to purchaseRequests. A new purchaseRecord subdoc may be added to track purchases and costs for the item. + +// SMALL ITEMS BASE +// base schema for Consumable, Material, Reusable +// documents stored in 'buildingInventoryItems' collection + +const smallItemBaseSchema = mongoose.Schema({ + itemType: { type: mongoose.SchemaTypes.ObjectId, ref: 'buildingInventoryType' }, + project: { type: mongoose.SchemaTypes.ObjectId, ref: 'buildingProject' }, + stockBought: { type: Number, default: 0 }, // total amount of item bought for use in the project + // TODO: can stockAvailable default be a function? + stockAvailable: { type: Number, default: 0 }, // available = bought - (used + wasted/destroyed) + purchaseRecord: [{ + _id: false, // do not add _id field to subdocument + date: { type: Date, default: Date.now() }, + requestedBy: { type: mongoose.SchemaTypes.ObjectId, ref: 'userProfile' }, + quantity: { type: Number, required: true, default: 1 }, // default 1 for tool or equipment purchases + priority: { type: String, enum: ['Low', 'Medium', 'High'], required: true }, + brandPref: String, + status: { type: String, default: 'Pending', enum: ['Approved', 'Pending', 'Rejected'] }, + }], + updateRecord: [{ + _id: false, + date: { type: Date, required: true }, + createdBy: { type: mongoose.SchemaTypes.ObjectId, ref: 'userProfile' }, + quantityUsed: { type: Number, required: true }, + quantityWasted: { type: Number, required: true }, + }], +}); + +const smallItemBase = mongoose.model('smallItemBase', smallItemBaseSchema, 'buildingInventoryItems'); + +// LARGE ITEMS BASE +// base schema for Tool, Equipment +// documents stored in 'buildingInventoryItems' collection + +const largeItemBaseSchema = mongoose.Schema({ + itemType: { type: mongoose.SchemaTypes.ObjectId, ref: 'buildingInventoryType' }, + project: { type: mongoose.SchemaTypes.ObjectId, ref: 'buildingProject' }, + purchaseStatus: { type: String, enum: ['Rental', 'Purchase'], required: true }, + // rental fields are required if purchaseStatus = "Rental" (hopefully correct syntax) + rentedOnDate: { type: Date, required: () => this.purchaseStatus === 'Rental' }, + rentalDueDate: { type: Date, required: () => this.purchaseStatus === 'Rental' }, + imageUrl: String, + purchaseRecord: [{ + _id: false, // do not add _id field to subdocument + date: { type: Date, default: Date.now() }, + requestedBy: { type: mongoose.SchemaTypes.ObjectId, ref: 'userProfile' }, + priority: { type: String, enum: ['Low', 'Medium', 'High'], required: true }, + makeModelPref: String, + estTimeRequired: { type: Number, required: true }, // estimated time required on site + status: { type: String, default: 'Pending', enum: ['Approved', 'Pending', 'Rejected'] }, + }], + updateRecord: [{ // track tool condition updates + _id: false, + date: { type: Date, default: Date.now() }, + createdBy: { type: mongoose.SchemaTypes.ObjectId, ref: 'userProfile' }, + condition: { type: String, enum: ['Good', 'Needs Repair', 'Out of Order'] }, + }], + logRecord: [{ // track tool daily check in/out and responsible user + _id: false, + date: { type: Date, default: Date.now() }, + createdBy: { type: mongoose.SchemaTypes.ObjectId, ref: 'userProfile' }, + responsibleUser: { type: mongoose.SchemaTypes.ObjectId, ref: 'userProfile' }, + type: { type: String, enum: ['Check In', 'Check Out'] }, + }], +}); + +const largeItemBase = mongoose.model('largeItemBase', largeItemBaseSchema, 'buildingInventoryItems'); + +//----------------- +// MATERIALS SCHEMA +//----------------- + +// inherits all properties of smallItemBaseSchema +// each document derived from this schema includes key field { __t: "material" } +// ex: sand, stone, bricks, lumber, insulation + +const buildingMaterial = smallItemBase.discriminator('material', new mongoose.Schema({ + stockUsed: { type: Number, default: 0 }, // stock that has been used up and cannot be reused + stockWasted: { type: Number, default: 0 }, // ruined or destroyed stock +})); + +//------------------ +// CONSUMABLE SCHEMA +//------------------ + +// inherits all properties of smallItemBaseSchema +// each document derived from this schema includes key field { __t: "consumable" } +// ex: screws, nails, staples + +const buildingConsumable = smallItemBase.discriminator('consumable', new mongoose.Schema({ + stockUsed: { type: Number, default: 0 }, // stock that has been used up and cannot be reused + stockWasted: { type: Number, default: 0 }, // ruined or destroyed stock +})); + +//---------------- +// REUSABLE SCHEMA +//---------------- + +// inherits all properties of smallItemBaseSchema +// each document derived from this schema includes key field { __t: "reusable" } +// ex: hammers, screwdrivers, mallets, brushes, gloves + +const buildingReusable = smallItemBase.discriminator('reusable', new mongoose.Schema({ + stockDestroyed: { type: Number, default: 0 }, +})); + +//------------ +// TOOL SCHEMA +//------------ + +// inherits all properties of largeItemBaseSchema +// each document derived from this schema includes key field { __t: "tool" } +// ex: power drills, wheelbarrows, shovels, jackhammers + +const buildingTool = largeItemBase.discriminator('tool', new mongoose.Schema({ + code: { type: Number, required: true }, // TODO: add function to create simple numeric code for on-site tool tracking +})); + + +//----------------- +// EQUIPMENT SCHEMA +//----------------- + +// inherits all properties of largeItemBaseSchema +// each document derived from this schema includes key field { __t: "equipment" } +// items in this category are assumed to be rented +// ex: tractors, excavators, bulldozers + +const buildingEquipment = largeItemBase.discriminator('equipment', new mongoose.Schema({ + isTracked: { type: Boolean, required: true }, // has asset tracker + assetTracker: { type: String, required: () => this.isTracked }, // required if isTracked = true (syntax?) +})); + +module.exports = { + buildingMaterial, + buildingConsumable, + buildingReusable, + buildingTool, + buildingEquipment, +};