diff --git a/src/controllers/WeeklySummaryEmailAssignmentController.js b/src/controllers/WeeklySummaryEmailAssignmentController.js index 795e56a3e..2e7cb0e0a 100644 --- a/src/controllers/WeeklySummaryEmailAssignmentController.js +++ b/src/controllers/WeeklySummaryEmailAssignmentController.js @@ -1,108 +1,111 @@ -const WeeklySummaryEmailAssignmentController = function (WeeklySummaryEmailAssignment, userProfile) { - const getWeeklySummaryEmailAssignment = async function (req, res) { - try { - const assignments = await WeeklySummaryEmailAssignment.find().populate('assignedTo').exec(); - res.status(200).send(assignments); - } catch (error) { - res.status(500).send(error); +const WeeklySummaryEmailAssignmentController = function ( + WeeklySummaryEmailAssignment, + userProfile, +) { + const getWeeklySummaryEmailAssignment = async function (req, res) { + try { + const assignments = await WeeklySummaryEmailAssignment.find().populate('assignedTo').exec(); + res.status(200).send(assignments); + } catch (error) { + res.status(500).send(error); + } + }; + + const setWeeklySummaryEmailAssignment = async function (req, res) { + try { + const { email } = req.body; + + if (!email) { + res.status(400).send('bad request'); + return; } - }; - - const setWeeklySummaryEmailAssignment = async function (req, res) { - try { - const { email } = req.body; - - if (!email) { - res.status(400).send('bad request'); - return; - } - - const user = await userProfile.findOne({ email }); - if (!user) { - return res.status(400).send('User profile not found'); - } - - const newAssignment = new WeeklySummaryEmailAssignment({ - email, - assignedTo: user._id, - }); - - await newAssignment.save(); - const assignment = await WeeklySummaryEmailAssignment.find({ email }).populate('assignedTo').exec(); - - res.status(200).send(assignment[0]); - } catch (error) { - res.status(500).send(error); + + const user = await userProfile.findOne({ email }); + if (!user) { + return res.status(400).send('User profile not found'); } - }; - - const deleteWeeklySummaryEmailAssignment = async function (req, res) { - try { - const { id } = req.params; - - if (!id) { - res.status(400).send('bad request'); - return; - } - - const deletedAssignment = await WeeklySummaryEmailAssignment.findOneAndDelete({ _id: id }); - if (!deletedAssignment) { - res.status(404).send('Assignment not found'); - return; - } - - res.status(200).send({ id }); - } catch (error) { - res.status(500).send(error); + + const newAssignment = new WeeklySummaryEmailAssignment({ + email, + assignedTo: user._id, + }); + + await newAssignment.save(); + const assignment = await WeeklySummaryEmailAssignment.find({ email }) + .populate('assignedTo') + .exec(); + + res.status(200).send(assignment[0]); + } catch (error) { + res.status(500).send(error); + } + }; + + const deleteWeeklySummaryEmailAssignment = async function (req, res) { + try { + const { id } = req.params; + + if (!id) { + res.status(400).send('bad request'); + return; } - }; - - const updateWeeklySummaryEmailAssignment = async function (req, res) { - try{ - const { id } = req.params; - const { email } = req.body; - - if (!id || (!email)) { - res.status(400).send('bad request'); - return; - } - - const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; - if (!emailRegex.test(email)) { - return res.status(400).json({ - error: 'Invalid email format' - }); - } - - const updateAssignment = await WeeklySummaryEmailAssignment.findOneAndUpdate( - { _id: id }, - { - email, - }, - { - new: true - } - ); - - if (!updateAssignment) { - res.status(404).send('Assignment not found'); - return; - } - - res.status(200).send(updateAssignment); - - } catch (error) { - res.status(500).send(error); + + const deletedAssignment = await WeeklySummaryEmailAssignment.findOneAndDelete({ _id: id }); + if (!deletedAssignment) { + res.status(404).send('Assignment not found'); + return; } - }; - - return { - getWeeklySummaryEmailAssignment, - setWeeklySummaryEmailAssignment, - deleteWeeklySummaryEmailAssignment, - updateWeeklySummaryEmailAssignment - }; + + res.status(200).send({ id }); + } catch (error) { + res.status(500).send(error); + } }; - - module.exports = WeeklySummaryEmailAssignmentController; - \ No newline at end of file + + const updateWeeklySummaryEmailAssignment = async function (req, res) { + try { + const { id } = req.params; + const { email } = req.body; + + if (!id || !email) { + res.status(400).send('bad request'); + return; + } + + const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; + if (!emailRegex.test(email)) { + return res.status(400).json({ + error: 'Invalid email format', + }); + } + + const updateAssignment = await WeeklySummaryEmailAssignment.findOneAndUpdate( + { _id: id }, + { + email, + }, + { + new: true, + }, + ); + + if (!updateAssignment) { + res.status(404).send('Assignment not found'); + return; + } + + res.status(200).send(updateAssignment); + } catch (error) { + res.status(500).send(error); + } + }; + + return { + getWeeklySummaryEmailAssignment, + setWeeklySummaryEmailAssignment, + deleteWeeklySummaryEmailAssignment, + updateWeeklySummaryEmailAssignment, + }; +}; + +module.exports = WeeklySummaryEmailAssignmentController; diff --git a/src/controllers/automation/sentryController.js b/src/controllers/automation/sentryController.js index 9f407114d..3b3fd09a0 100644 --- a/src/controllers/automation/sentryController.js +++ b/src/controllers/automation/sentryController.js @@ -3,7 +3,6 @@ const sentryService = require('../../services/automation/sentryService'); const appAccessService = require('../../services/automation/appAccessService'); const { checkAppAccess } = require('./utils'); - // Controller function to invite a user async function inviteUser(req, res) { const { targetUser } = req.body; @@ -15,11 +14,16 @@ async function inviteUser(req, res) { if (!checkAppAccess(requestor.role)) { res.status(403).send({ message: 'Unauthorized request' }); return; - } + } try { const invitation = await sentryService.inviteUser(targetUser.email); - await appAccessService.upsertAppAccess(targetUser.targetUserId, 'sentry', 'invited', targetUser.email); + await appAccessService.upsertAppAccess( + targetUser.targetUserId, + 'sentry', + 'invited', + targetUser.email, + ); res.status(201).json({ message: 'Invitation sent', data: invitation }); } catch (error) { res.status(500).json({ message: error.message }); @@ -50,8 +54,7 @@ async function removeUser(req, res) { } } - module.exports = { inviteUser, removeUser, -}; \ No newline at end of file +}; diff --git a/src/controllers/bmdashboard/bmEquipmentController.js b/src/controllers/bmdashboard/bmEquipmentController.js index 81feb1d23..729367748 100644 --- a/src/controllers/bmdashboard/bmEquipmentController.js +++ b/src/controllers/bmdashboard/bmEquipmentController.js @@ -148,7 +148,7 @@ const bmEquipmentController = (BuildingEquipment) => { return res.status(400).send({ error: 'Request body must be a non-empty array.' }); } - const invalid = updates.some(item => { + const invalid = updates.some((item) => { if (!item.equipmentId || !mongoose.Types.ObjectId.isValid(item.equipmentId)) { res.status(400).send({ error: 'Invalid or missing equipmentId.' }); return true; diff --git a/src/controllers/bmdashboard/projectCostTrackingController.js b/src/controllers/bmdashboard/projectCostTrackingController.js new file mode 100644 index 000000000..17dbf993a --- /dev/null +++ b/src/controllers/bmdashboard/projectCostTrackingController.js @@ -0,0 +1,191 @@ +const projectCostTrackingController = function (ProjectCostTracking) { + // Simple linear regression class compatible with older Node.js versions + class SimpleLinearRegression { + constructor(x, y) { + if (x.length !== y.length) { + throw new Error('X and Y arrays must have the same length'); + } + + const n = x.length; + const sumX = x.reduce((a, b) => a + b, 0); + const sumY = y.reduce((a, b) => a + b, 0); + const sumXY = x.reduce((acc, xi, i) => acc + xi * y[i], 0); + const sumXX = x.reduce((a, b) => a + b * b, 0); + + // Calculate slope and intercept + this.slope = (n * sumXY - sumX * sumY) / (n * sumXX - sumX * sumX); + this.intercept = (sumY - this.slope * sumX) / n; + } + + predict(x) { + return this.slope * x + this.intercept; + } + } + const getProjectCosts = async (req, res) => { + try { + const { id } = req.params; + const { categories, fromDate, toDate } = req.query; + + // Build query + const query = { projectId: id }; + + // Add category filter if provided + if (categories) { + const categoryList = categories.split(','); + query.category = { $in: categoryList }; + } + + // Add date range filter if provided + if (fromDate || toDate) { + query.date = {}; + if (fromDate) query.date.$gte = new Date(fromDate); + if (toDate) query.date.$lte = new Date(toDate); + } + + // Get actual cost data + const costData = await ProjectCostTracking.find(query).sort({ date: 1 }); + + // Process data for response + const result = { + actual: {}, + predicted: {}, + plannedBudget: 10000, // This would typically come from project data + }; + + // Group by category + const categorizedData = {}; + costData.forEach((entry) => { + if (!categorizedData[entry.category]) { + categorizedData[entry.category] = []; + } + categorizedData[entry.category].push({ + date: entry.date, + cost: entry.cost, + }); + }); + + // Calculate cumulative costs for each category + Object.keys(categorizedData).forEach((category) => { + let cumulativeCost = 0; + categorizedData[category] = categorizedData[category].map((item) => { + cumulativeCost += item.cost; + return { + date: item.date, + cost: cumulativeCost, + }; + }); + }); + + // Calculate total costs + const dateMap = new Map(); + + costData.forEach((entry) => { + const dateStr = entry.date.toISOString().split('T')[0]; + if (!dateMap.has(dateStr)) { + dateMap.set(dateStr, { date: entry.date, cost: 0 }); + } + dateMap.get(dateStr).cost += entry.cost; + }); + + // Convert to array and calculate cumulative total + let totalCumulative = 0; + const totalCosts = Array.from(dateMap.values()) + .sort((a, b) => a.date - b.date) + .map((item) => { + totalCumulative += item.cost; + return { + date: item.date, + cost: totalCumulative, + }; + }); + + if (totalCosts.length > 0) { + categorizedData.Total = totalCosts; + } + + // Format actual data + result.actual = categorizedData; + + // Generate prediction data using linear regression + if (costData.length > 0) { + const predictedData = {}; + + // For each category, perform linear regression + Object.keys(categorizedData).forEach((category) => { + if (categorizedData[category].length > 0) { + const categoryData = categorizedData[category]; + + // Get the last actual data point + const lastEntry = categoryData[categoryData.length - 1]; + + // Prepare data for linear regression + const xValues = categoryData.map((item, index) => index); // Use indices as x values + const yValues = categoryData.map((item) => item.cost); + + // Linear regression using ml-regression library + const regression = new SimpleLinearRegression(xValues, yValues); + + // Function to predict value + const predict = (x) => regression.predict(x); + + // Generate predictions for next 3 months + predictedData[category] = []; + + // Get the last date + const lastDate = new Date(lastEntry.date); + const lastValue = lastEntry.cost; + + // Calculate the final predicted value for 3 months ahead + // This ensures we have a perfect linear growth between the last actual point + // and the final prediction point + const finalPredictedValue = predict(xValues.length + 2); // +2 for 3 months ahead (0-indexed) + + // Calculate the monthly growth rate for a perfect straight line + const monthlyGrowth = (finalPredictedValue - lastValue) / 3; + + // Generate predictions for the next 3 months with perfect linear growth + for (let i = 1; i <= 3; i++) { + const predictedDate = new Date(lastDate); + predictedDate.setMonth(lastDate.getMonth() + i); + + // Apply perfect linear growth + const predictedCost = lastValue + monthlyGrowth * i; + + // Ensure predicted value is not less than the last actual value + // This prevents negative growth which doesn't make sense for cumulative costs + const finalCost = Math.max(predictedCost, lastValue); + + predictedData[category].push({ + date: predictedDate, + cost: finalCost, + }); + } + } + }); + + result.predicted = predictedData; + } + + res.status(200).json(result); + } catch (error) { + res.status(500).json({ error: error.message }); + } + }; + + const getAllProjectIds = async (req, res) => { + try { + // Using MongoDB's distinct to get unique project IDs + const projectIds = await ProjectCostTracking.distinct('projectId'); + res.status(200).json({ projectIds }); + } catch (error) { + res.status(500).json({ error: error.message }); + } + }; + + return { + getProjectCosts, + getAllProjectIds, + }; +}; + +module.exports = projectCostTrackingController; diff --git a/src/controllers/dashBoardController.js b/src/controllers/dashBoardController.js index 0cb9415e5..b18b2acaa 100644 --- a/src/controllers/dashBoardController.js +++ b/src/controllers/dashBoardController.js @@ -1,19 +1,11 @@ /* eslint-disable quotes */ -const path = require('path'); -const fs = require('fs/promises'); const mongoose = require('mongoose'); const userProfile = require('../models/userProfile'); -const actionItem = require('../models/actionItem'); const dashboardHelperClosure = require('../helpers/dashboardhelper'); const emailSender = require('../utilities/emailSender'); const AIPrompt = require('../models/weeklySummaryAIPrompt'); const User = require('../models/userProfile'); -// Configuration constants to prevent conflicts -const EMAIL_CONFIG = { - SUPPORT_EMAIL: 'onecommunityglobal@gmail.com', -}; - const dashboardcontroller = function () { const dashboardhelper = dashboardHelperClosure(); const dashboarddata = function (req, res) { @@ -141,20 +133,17 @@ const dashboardcontroller = function () { }); } }) - .catch(error => res.status(400).send(error)); + .catch((error) => res.status(400).send(error)); }; // 6th month and yearly anniversaries const postTrophyIcon = function (req, res) { - console.log("API called with params:", req.params); + console.log('API called with params:', req.params); const userId = mongoose.Types.ObjectId(req.params.userId); const trophyFollowedUp = req.params.trophyFollowedUp === 'true'; - userProfile.findByIdAndUpdate( - userId, - { trophyFollowedUp }, - { new: true } - ) + userProfile + .findByIdAndUpdate(userId, { trophyFollowedUp }, { new: true }) .then((updatedRecord) => { if (!updatedRecord) { return res.status(404).send('No valid records found'); @@ -162,7 +151,7 @@ const dashboardcontroller = function () { res.status(200).send(updatedRecord); }) .catch((error) => { - console.error("Error updating trophy icon:", error); + console.error('Error updating trophy icon:', error); res.status(500).send(error); }); }; @@ -233,7 +222,7 @@ const dashboardcontroller = function () { visual, severity, ); - + try { await emailSender.sendEmail( 'onecommunityglobal@gmail.com', @@ -243,7 +232,7 @@ const dashboardcontroller = function () { ); res.status(200).send('Success'); } catch (error) { - res.status(500).send("Failed to send email"); + res.status(500).send('Failed to send email'); } }; @@ -312,7 +301,7 @@ const dashboardcontroller = function () { ); res.status(200).send('Success'); } catch (error) { - res.status(500).send("Failed to send email"); + res.status(500).send('Failed to send email'); } }; @@ -356,7 +345,7 @@ const dashboardcontroller = function () { } }; const requestFeedbackModal = async function (req, res) { - /** request structure - pass with userId fetched from initial load response. + /** request structure - pass with userId fetched from initial load response. { "haveYouRecievedHelpLastWeek": "Yes", //no @@ -367,7 +356,7 @@ const dashboardcontroller = function () { "daterequestedFeedback": "2025-04-20T04:04:40.189Z", "foundHelpSomeWhereClosePermanently": false, "userId": "5baac381e16814009017678c" - }*/ + } */ try { const savingRequestFeedbackData = await dashboardhelper.requestFeedback(req); return res.status(200).json({ savingRequestFeedbackData }); @@ -375,7 +364,7 @@ const dashboardcontroller = function () { return res.status(500).send({ msg: 'Error occured while fetching data. Please try again!' }); } }; - + const getUserNames = async function (req, res) { /** Call this api once and show in frontend. * this will be the response structure @@ -391,19 +380,19 @@ const dashboardcontroller = function () { */ try { const usersList = await dashboardhelper.getNamesFromProfiles(); - return res.status(200).json({ users : usersList }); + return res.status(200).json({ users: usersList }); } catch (err) { return res.status(500).send({ msg: 'Error occured while fetching data. Please try again!' }); } }; const checkUserFoundHelpSomewhere = async function (req, res) { -/** request structure - pass with userId fetched from initial load response. + /** request structure - pass with userId fetched from initial load response. Only call this api, when clicking found help permanentely { "foundHelpSomeWhereClosePermanently": true, "userId": "5baac381e16814009017678c" -}*/ +} */ try { const foundHelp = await dashboardhelper.checkQuestionaireFeedback(req); return res.status(200).json({ foundHelp }); @@ -413,7 +402,6 @@ const dashboardcontroller = function () { } }; - return { dashboarddata, getAIPrompt, @@ -431,8 +419,8 @@ const dashboardcontroller = function () { postTrophyIcon, requestFeedbackModal, getUserNames, - checkUserFoundHelpSomewhere + checkUserFoundHelpSomewhere, }; }; -module.exports = dashboardcontroller; \ No newline at end of file +module.exports = dashboardcontroller; diff --git a/src/controllers/laborCostController.js b/src/controllers/laborCostController.js index 352c9e0aa..376a55b71 100644 --- a/src/controllers/laborCostController.js +++ b/src/controllers/laborCostController.js @@ -1,4 +1,3 @@ -const mongoose = require('mongoose'); const Labour = require('../models/laborCost'); const createLabourCost = async (req, res) => { @@ -55,15 +54,15 @@ const getLabourCostByDate = async (req, res) => { }; const getLabourCostByProject = async (req, res) => { - const { project_name } = req.query; + const { projectName } = req.query; - if (!project_name) { + if (!projectName) { res.status(500).json({ success: false, message: 'Project Name not provided' }); } try { const filteredDatabyProject = await Labour.find({ - project_name: { $regex: project_name, $options: 'i' }, + project_name: { $regex: projectName, $options: 'i' }, }); res.status(200).json({ success: true, data: filteredDatabyProject }); } catch (error) { diff --git a/src/controllers/titleController.js b/src/controllers/titleController.js index 3724e9808..b29ad1a17 100644 --- a/src/controllers/titleController.js +++ b/src/controllers/titleController.js @@ -1,12 +1,11 @@ -const Team = require('../models/team'); const Project = require('../models/project'); const cacheClosure = require('../utilities/nodeCache'); const userProfileController = require('./userProfileController'); const userProfile = require('../models/userProfile'); -const project = require('../models/project'); +const projectModel = require('../models/project'); -const controller = userProfileController(userProfile, project); -const getAllTeamCodeHelper = controller.getAllTeamCodeHelper; +const controller = userProfileController(userProfile, projectModel); +const { getAllTeamCodeHelper } = controller; const titlecontroller = function (Title) { const cache = cacheClosure(); @@ -122,13 +121,9 @@ const titlecontroller = function (Title) { try { const savedTitle = await title.save(); - - - await userProfile.updateMany( - {}, - { $addToSet: { teamCodes: title.teamCode } } - ); - + + await userProfile.updateMany({}, { $addToSet: { teamCodes: title.teamCode } }); + res.status(200).send(savedTitle); } catch (error) { res.status(500).send(error); @@ -140,7 +135,7 @@ const titlecontroller = function (Title) { const { orderData } = req.body; console.log('Received order data:', orderData); - const updates = await Promise.all( + await Promise.all( orderData.map(async ({ id, order }) => { const updated = await Title.findByIdAndUpdate(id, { order }, { new: true }); console.log('Updated title:', updated); @@ -265,30 +260,6 @@ const titlecontroller = function (Title) { res.status(500).send(error); }); }; - // Update: Confirmed with Jae. Team code is not related to the Team data model. But the team code field within the UserProfile data model. - async function checkTeamCodeExists(teamCode) { - try { - if (cache.getCache('teamCodes')) { - const teamCodes = JSON.parse(cache.getCache('teamCodes')); - return teamCodes.includes(teamCode); - } - const teamCodes = await getAllTeamCodeHelper(); - return teamCodes.includes(teamCode); - } catch (error) { - console.error('Error checking if team code exists:', error); - throw error; - } - } - - async function checkProjectExists(projectID) { - try { - const project = await Project.findOne({ _id: projectID }).exec(); - return !!project; - } catch (error) { - console.error('Error checking if project exists:', error); - throw error; - } - } return { getAllTitles, diff --git a/src/helpers/reporthelper.js b/src/helpers/reporthelper.js index 820633e78..e7101e934 100644 --- a/src/helpers/reporthelper.js +++ b/src/helpers/reporthelper.js @@ -23,7 +23,6 @@ const reporthelper = function () { * @param {integer} endWeekIndex The end week index, eg. 1 for last week. */ const weeklySummaries = async (startWeekIndex, endWeekIndex) => { - const pstStart = moment() .tz('America/Los_Angeles') .startOf('week') diff --git a/src/jobs/dailyMessageEmailNotification.js b/src/jobs/dailyMessageEmailNotification.js index 97a20ff4e..e91ec3688 100644 --- a/src/jobs/dailyMessageEmailNotification.js +++ b/src/jobs/dailyMessageEmailNotification.js @@ -9,44 +9,52 @@ const TEST_MODE = true; // Set to false to disable test mode // Schedule the job to run daily at midnight cron.schedule('0 0 * * *', async () => { - - try { - const userPreferences = await UserPreferences.find().populate('user users.userNotifyingFor'); - - for (const preference of userPreferences) { - const { user, users } = preference; - - let summary = ''; - for (const { userNotifyingFor, notifyEmail } of users) { - if (notifyEmail) { - // Fetch unread messages from the specific sender - const unreadMessages = await Message.find({ - receiver: user._id, - sender: userNotifyingFor._id, - status: { $ne: 'read' }, - }); - - const userNotifyingForProfile = await UserProfile.findById(userNotifyingFor._id).select('firstName lastName'); - - if (unreadMessages.length > 0) { - if (unreadMessages.length > 5) { - summary += `
  • ${unreadMessages.length} messages from ${userNotifyingForProfile.firstName} ${userNotifyingForProfile.lastName}
  • `; - } else { - const messageList = unreadMessages - .map((msg) => `
  • ${msg.content} (Sent: ${msg.timestamp.toLocaleString()})
  • `) - .join(''); - summary += `
  • ${unreadMessages.length} messages from ${userNotifyingForProfile.firstName} ${userNotifyingForProfile.lastName}
  • `; - } - } - } - } - - if (summary) { - const recipientEmail = TEST_MODE ? 'test@example.com' : user.email; - await emailSender.sendSummaryNotification(recipientEmail, summary); + try { + const userPreferences = await UserPreferences.find().populate('user users.userNotifyingFor'); + + await Promise.all( + userPreferences.map(async (preference) => { + const { user, users } = preference; + + const summaryPromises = users.map(async ({ userNotifyingFor, notifyEmail }) => { + if (!notifyEmail) return ''; + + // Fetch unread messages from the specific sender + const unreadMessages = await Message.find({ + receiver: user._id, + sender: userNotifyingFor._id, + status: { $ne: 'read' }, + }); + + const userNotifyingForProfile = await UserProfile.findById(userNotifyingFor._id).select( + 'firstName lastName', + ); + + if (unreadMessages.length > 0) { + if (unreadMessages.length > 5) { + return `
  • ${unreadMessages.length} messages from ${userNotifyingForProfile.firstName} ${userNotifyingForProfile.lastName}
  • `; } + const messageList = unreadMessages + .map( + (msg) => + `
  • ${msg.content} (Sent: ${msg.timestamp.toLocaleString()})
  • `, + ) + .join(''); + return `
  • ${unreadMessages.length} messages from ${userNotifyingForProfile.firstName} ${userNotifyingForProfile.lastName}
  • `; + } + return ''; + }); + + const summaries = await Promise.all(summaryPromises); + const summary = summaries.join(''); + + if (summary) { + const recipientEmail = TEST_MODE ? 'test@example.com' : user.email; + await emailSender.sendSummaryNotification(recipientEmail, summary); } - } catch (error) { - console.error('❌ Error running daily email notification job:', error); - } + }), + ); + } catch (error) { + console.error('❌ Error running daily email notification job:', error); + } }); diff --git a/src/models/WeeklySummaryEmailAssignment.js b/src/models/WeeklySummaryEmailAssignment.js index 4d8bb37d1..ab7c80dfc 100644 --- a/src/models/WeeklySummaryEmailAssignment.js +++ b/src/models/WeeklySummaryEmailAssignment.js @@ -1,14 +1,14 @@ -const mongoose = require("mongoose"); +const mongoose = require('mongoose'); const { Schema } = mongoose; const WeeklySummaryEmailAssignmentSchema = new Schema({ email: { type: String, required: true, unique: true }, - assignedTo: { type: mongoose.SchemaTypes.ObjectId, ref: 'userProfile', required: true } + assignedTo: { type: mongoose.SchemaTypes.ObjectId, ref: 'userProfile', required: true }, }); module.exports = mongoose.model( - "WeeklySummaryEmailAssignment", + 'WeeklySummaryEmailAssignment', WeeklySummaryEmailAssignmentSchema, - "WeeklySummaryEmailAssignments" // 晚点检查数据库中是否正确创建 + 'WeeklySummaryEmailAssignments', // 晚点检查数据库中是否正确创建 ); diff --git a/src/models/bmdashboard/projectCostTracking.js b/src/models/bmdashboard/projectCostTracking.js new file mode 100644 index 000000000..c56f93e8c --- /dev/null +++ b/src/models/bmdashboard/projectCostTracking.js @@ -0,0 +1,48 @@ +const mongoose = require('mongoose'); + +const { Schema } = mongoose; + +const projectCostTrackingSchema = new Schema({ + projectId: { + type: mongoose.Schema.Types.ObjectId, + required: true, + ref: 'Project', + }, + date: { + type: Date, + required: true, + }, + category: { + type: String, + required: true, + enum: ['Labor', 'Materials', 'Equipment'], + }, + cost: { + type: Number, + required: true, + min: 0, + }, + createdAt: { + type: Date, + default: Date.now, + }, + updatedAt: { + type: Date, + default: Date.now, + }, +}); + +// Index for efficient querying +projectCostTrackingSchema.index({ projectId: 1, date: 1, category: 1 }); + +// Update updatedAt on save +projectCostTrackingSchema.pre('save', function (next) { + this.updatedAt = Date.now(); + next(); +}); + +module.exports = mongoose.model( + 'ProjectCostTracking', + projectCostTrackingSchema, + 'projectCostTracking', +); diff --git a/src/routes/WeeklySummaryEmailAssignmentRoute.js b/src/routes/WeeklySummaryEmailAssignmentRoute.js index 4d723155b..809f49368 100644 --- a/src/routes/WeeklySummaryEmailAssignmentRoute.js +++ b/src/routes/WeeklySummaryEmailAssignmentRoute.js @@ -4,17 +4,19 @@ const routes = function (WeeklySummaryEmailAssignment, userProfile) { const WeeklySummaryEmailAssignmentRouter = express.Router(); const controller = require('../controllers/WeeklySummaryEmailAssignmentController')( WeeklySummaryEmailAssignment, - userProfile + userProfile, ); WeeklySummaryEmailAssignmentRouter.route('/AssignWeeklySummaryEmail') .get(controller.getWeeklySummaryEmailAssignment) .post(controller.setWeeklySummaryEmailAssignment); - WeeklySummaryEmailAssignmentRouter.route('/AssignWeeklySummaryEmail/:id') - .delete(controller.deleteWeeklySummaryEmailAssignment); - WeeklySummaryEmailAssignmentRouter.route('/AssignWeeklySummaryEmail/:id') - .put(controller.updateWeeklySummaryEmailAssignment); + WeeklySummaryEmailAssignmentRouter.route('/AssignWeeklySummaryEmail/:id').delete( + controller.deleteWeeklySummaryEmailAssignment, + ); + WeeklySummaryEmailAssignmentRouter.route('/AssignWeeklySummaryEmail/:id').put( + controller.updateWeeklySummaryEmailAssignment, + ); return WeeklySummaryEmailAssignmentRouter; }; diff --git a/src/routes/bmdashboard/projectCostTrackingRouter.js b/src/routes/bmdashboard/projectCostTrackingRouter.js new file mode 100644 index 000000000..c95e78b81 --- /dev/null +++ b/src/routes/bmdashboard/projectCostTrackingRouter.js @@ -0,0 +1,18 @@ +const express = require('express'); + +const routes = function (ProjectCostTracking) { + const projectCostTrackingRouter = express.Router(); + const controller = require('../../controllers/bmdashboard/projectCostTrackingController')( + ProjectCostTracking, + ); + + // GET /api/bm/projects/:id/costs + projectCostTrackingRouter.route('/bm/projects/:id/costs').get(controller.getProjectCosts); + + // GET /api/bm/projects-cost/ids + projectCostTrackingRouter.route('/bm/projects-cost/ids').get(controller.getAllProjectIds); + + return projectCostTrackingRouter; +}; + +module.exports = routes; diff --git a/src/routes/projectMaterialroutes.js b/src/routes/projectMaterialroutes.js index ffd970e99..3dbf03015 100644 --- a/src/routes/projectMaterialroutes.js +++ b/src/routes/projectMaterialroutes.js @@ -1,4 +1,4 @@ -const express = require("express"); +const express = require('express'); const controller = require('../controllers/materialSusceptibleController'); @@ -12,4 +12,4 @@ router.get('/projectMaterial/byDate', controller.getProjectMaterialByDate); router.get('/projectMaterial/byProjectName', controller.getProjectMaterialByProject); -module.exports = router; \ No newline at end of file +module.exports = router; diff --git a/src/routes/userProfileRouter.js b/src/routes/userProfileRouter.js index 2f2f32b91..a0314d572 100644 --- a/src/routes/userProfileRouter.js +++ b/src/routes/userProfileRouter.js @@ -23,11 +23,11 @@ const routes = function (userProfile, project) { controller.postUserProfile, ); - userProfileRouter - .route('/users/search') - .get(param('name').exists(), controller.searchUsersByName); + userProfileRouter + .route('/users/search') + .get(param('name').exists(), controller.searchUsersByName); - userProfileRouter.route('/userProfile/update').patch(controller.updateUserInformation); + userProfileRouter.route('/userProfile/update').patch(controller.updateUserInformation); // Endpoint to retrieve basic user profile information userProfileRouter.route('/userProfile/basicInfo').get(controller.getUserProfileBasicInfo); userProfileRouter @@ -123,19 +123,23 @@ const routes = function (userProfile, project) { userProfileRouter.route('/userProfile/projects/:name').get(controller.getProjectsByPerson); userProfileRouter.route('/userProfile/teamCode/list').get(controller.getAllTeamCode); - + userProfileRouter.route('/userProfile/profileImage/remove').put(controller.removeProfileImage); - userProfileRouter.route('/userProfile/profileImage/imagefromwebsite').put(controller.updateProfileImageFromWebsite); + userProfileRouter + .route('/userProfile/profileImage/imagefromwebsite') + .put(controller.updateProfileImageFromWebsite); userProfileRouter .route('/userProfile/autocomplete/:searchText') .get(controller.getUserByAutocomplete); - userProfileRouter.route('/userProfile/:userId/toggleBio').patch( controller.toggleUserBioPosted); - + userProfileRouter.route('/userProfile/:userId/toggleBio').patch(controller.toggleUserBioPosted); + userProfileRouter.route('/userProfile/replaceTeamCode').post(controller.replaceTeamCodeForUsers); - userProfileRouter.route('/userProfile/skills/:skill').get(controller.getAllMembersSkillsAndContact) + userProfileRouter + .route('/userProfile/skills/:skill') + .get(controller.getAllMembersSkillsAndContact); return userProfileRouter; }; diff --git a/src/services/automation/dropboxService.js b/src/services/automation/dropboxService.js index a6a24c7bb..2440ed6c1 100644 --- a/src/services/automation/dropboxService.js +++ b/src/services/automation/dropboxService.js @@ -31,21 +31,30 @@ async function ensureFolderExists(path) { * Polls an async share-folder job until complete, using the correct endpoint. */ async function waitForShareCompletion(asyncJobId, maxAttempts = 10) { - let attempts = 0; - while (attempts < maxAttempts) { + const checkStatus = async (attempt) => { + if (attempt >= maxAttempts) { + throw new Error('Timeout waiting for share to complete'); + } + const status = await dbx.sharingCheckShareJobStatus({ async_job_id: asyncJobId }); const tag = status.result['.tag']; + if (tag === 'complete') { return status.result; } if (tag === 'failed') { throw new Error(`Share job failed: ${JSON.stringify(status.result.failed)}`); } - // in_progress - await new Promise((res) => setTimeout(res, 1000)); - attempts += 1; - } - throw new Error('Timeout waiting for share to complete'); + + // in_progress - wait and try again + await new Promise((resolve) => { + setTimeout(resolve, 1000); + }); + + return checkStatus(attempt + 1); + }; + + return checkStatus(0); } /** @@ -104,9 +113,7 @@ async function createFolderAndInvite(email, projectName) { // 3. Invite user const inviteResponse = await dbx.sharingAddFolderMember({ shared_folder_id: sharedFolderId, - members: [ - { member: { '.tag': 'email', email }, access_level: { '.tag': 'editor' } }, - ], + members: [{ member: { '.tag': 'email', email }, access_level: { '.tag': 'editor' } }], quiet: false, }); diff --git a/src/services/automation/sentryService.js b/src/services/automation/sentryService.js index 5009d08f5..2f310cc77 100644 --- a/src/services/automation/sentryService.js +++ b/src/services/automation/sentryService.js @@ -1,11 +1,11 @@ -const axios = require('axios'); // Use gaxios instead of axios +const axios = require('axios'); // Use gaxios instead of axios require('dotenv').config(); -const sentryApiToken = process.env.SENTRY_API_TOKEN; // Sentry API Token from .env file -const organizationSlug = process.env.SENTRY_ORG_SLUG; // Organization slug from .env file +const sentryApiToken = process.env.SENTRY_API_TOKEN; // Sentry API Token from .env file +const organizationSlug = process.env.SENTRY_ORG_SLUG; // Organization slug from .env file const headers = { - 'Authorization': `Bearer ${sentryApiToken}`, + Authorization: `Bearer ${sentryApiToken}`, 'Content-Type': 'application/json', }; @@ -18,10 +18,11 @@ async function getMembers() { try { if (nextUrl) { const response = await axios({ url: nextUrl, headers }); - members.push(...response.data); // Add members to the array - nextUrl = response.headers.link && response.headers.link.includes('rel="next"') - ? response.headers.link.match(/<([^>]+)>; rel="next"/)[1] - : null; // Extract next URL from 'link' header if available + members.push(...response.data); // Add members to the array + nextUrl = + response.headers.link && response.headers.link.includes('rel="next"') + ? response.headers.link.match(/<([^>]+)>; rel="next"/)[1] + : null; // Extract next URL from 'link' header if available } return members; } catch (error) { @@ -29,15 +30,13 @@ async function getMembers() { } } - - // Function to invite a user to the Sentry organization async function inviteUser(email, role = 'member') { const url = `https://sentry.io/api/0/organizations/${organizationSlug}/members/`; const data = { email, - role, // Default to 'member', can also be 'admin' + role, // Default to 'member', can also be 'admin' }; try { @@ -47,7 +46,7 @@ async function inviteUser(email, role = 'member') { headers, data, }); - return response.data; // Return the invitation details + return response.data; // Return the invitation details } catch (error) { throw new Error(`Error sending invitation: ${error.message}`); } @@ -63,8 +62,8 @@ async function removeUser(email, members) { members = await getMembers(); } - const existingMember = members.find(member => - member.email.toLowerCase() === email.toLowerCase() + const existingMember = members.find( + (member) => member.email.toLowerCase() === email.toLowerCase(), ); if (!existingMember) { @@ -90,4 +89,4 @@ module.exports = { getMembers, inviteUser, removeUser, -}; \ No newline at end of file +}; diff --git a/src/startup/routes.js b/src/startup/routes.js index c9c28eb3b..4aba75cdc 100644 --- a/src/startup/routes.js +++ b/src/startup/routes.js @@ -212,6 +212,11 @@ const toolAvailabilityRouter = require('../routes/bmdashboard/toolAvailabilityRo toolAvailability, ); +const projectCostTracking = require('../models/bmdashboard/projectCostTracking'); +const projectCostTrackingRouter = require('../routes/bmdashboard/projectCostTrackingRouter')( + projectCostTracking, +); + const blueSquareEmailAssignmentRouter = require('../routes/BlueSquareEmailAssignmentRouter')( blueSquareEmailAssignment, userProfile, @@ -299,6 +304,7 @@ module.exports = function (app) { app.use('/api', followUpRouter); app.use('/api', blueSquareEmailAssignmentRouter); app.use('/api', weeklySummaryEmailAssignmentRouter); + app.use('/api', formRouter); app.use('/api', collaborationRouter); app.use('/api', userSkillsProfileRouter); @@ -343,12 +349,14 @@ module.exports = function (app) { app.use('/api/bm', bmExternalTeam); app.use('/api', bmProjectRiskProfileRouter); - - app.use('/api/bm', bmTimeLoggerRouter); + app.use('/api/bm', bmTimeLoggerRouter); app.use('/api/bm/injuries', injuryCategoryRoutes); app.use('/api', toolAvailabilityRouter); // lb dashboard + app.use('/api', toolAvailabilityRouter); + app.use('/api', projectCostTrackingRouter); + app.use('/api/bm', bmIssueRouter); app.use('/api/bm', bmDashboardRouter); app.use('/api/bm', bmActualVsPlannedCostRouter);