From d0c52eecd3cf3e75629e5876cfcc721737cdfae9 Mon Sep 17 00:00:00 2001 From: Mark Turner Date: Tue, 27 Aug 2024 20:54:10 +0100 Subject: [PATCH] embed calendar data as part of rse response object --- src/api/rse/controllers/rse.js | 20 +- src/api/rse/routes/calendar.js | 9 - src/api/rse/services/rse.js | 488 +++++++----------- src/api/timesheet/services/timesheet.js | 5 +- .../1.0.0/full_documentation.json | 2 +- 5 files changed, 214 insertions(+), 310 deletions(-) delete mode 100644 src/api/rse/routes/calendar.js diff --git a/src/api/rse/controllers/rse.js b/src/api/rse/controllers/rse.js index 1cd72a4..05d7225 100644 --- a/src/api/rse/controllers/rse.js +++ b/src/api/rse/controllers/rse.js @@ -7,9 +7,25 @@ const { createCoreController } = require('@strapi/strapi').factories module.exports = createCoreController('api::rse.rse', ({ strapi }) => ({ - calendar: async (ctx, next) => { + find: async (ctx) => { try { - return await strapi.service('api::rse.rse').calendar(ctx.params.id, ctx.request.query) + return await strapi.service('api::rse.rse').find(ctx.request.query) + } catch (err) { + console.error(err) + return err + } + }, + // Override findOne to use a filter on the find method for service code reuse + findOne: async (ctx) => { + try { + ctx.request.query.filters = { id: ctx.params.id } + const response = await strapi.service('api::rse.rse').find(ctx.request.query) + if (response.results.length === 1) { + return { + data: response.results[0], + meta: {} + } + } } catch (err) { console.error(err) return err diff --git a/src/api/rse/routes/calendar.js b/src/api/rse/routes/calendar.js deleted file mode 100644 index 1961113..0000000 --- a/src/api/rse/routes/calendar.js +++ /dev/null @@ -1,9 +0,0 @@ -module.exports = { - routes: [ - { - method: 'GET', - path: '/rse/:id/calendar', - handler: 'rse.calendar', - } - ] - } \ No newline at end of file diff --git a/src/api/rse/services/rse.js b/src/api/rse/services/rse.js index e5bac64..1f2d260 100644 --- a/src/api/rse/services/rse.js +++ b/src/api/rse/services/rse.js @@ -1,115 +1,197 @@ -"use strict"; +"use strict" /** * rse service. */ -const { DateTime, Interval } = require("luxon"); -const timesheet = require("../../timesheet/services/timesheet"); -const { createCoreService } = require("@strapi/strapi").factories; +const { DateTime, Interval } = require("luxon") +const { createCoreService } = require("@strapi/strapi").factories + +function createCalendar(rse, holidays, leave, assignments, capacities, timesheets, startDate, endDate) { + const dates = [] + + let date = startDate + + while(date <= endDate) { + + const holiday = holidays.find(holiday => holiday.date === date.toISODate()), + leaveDay = leave.find(leave => leave.DATE === date.toISODate() && leave.ID === rse.username), + currentAssignments = assignments.filter(assignment => { + const start = DateTime.fromISO(assignment.start), + end = DateTime.fromISO(assignment.end) + return date >= start && date <= end && assignment.rse === rse.id + }) + + let dateCapacity = 0 + + capacities.forEach(capacity => { + + capacity.end = capacity.end ? capacity.end : endDate.toISODate() + + // Build interval for capacity period + const period = Interval.fromDateTimes(DateTime.fromISO(capacity.start), DateTime.fromISO(capacity.end)) + + // Is current date in loop within the capacity period + if(period.contains(date)) { + dateCapacity = capacity.capacity + } + }) + + const timesheetReport = timesheets.dates[date.toISODate()], + timesheetSummary = [] + + if(timesheetReport) { + timesheetReport.filter(timesheet => timesheet.userId === rse.clockifyID).forEach(timesheet => { + timesheetSummary.push({ + start: timesheet.timeInterval.start, + end: timesheet.timeInterval.end, + duration: timesheet.timeInterval.duration, + billable: timesheet.billable, + project: timesheet.projectName, + }) + }) + } + + let day = { + date: date.toISODate(), + metadata: { + day: date.day, + month: date.month, + year: date.year, + dayOfWeek: date.weekday, + isWeekend: date.weekday > 5, + isWorkingDay: date.weekday < 6 && !holiday + }, + utilisation: { + capacity: dateCapacity, + allocated: currentAssignments.reduce((total, assignment) => total + assignment.fte, 0), + unallocated: dateCapacity - currentAssignments.reduce((total, assignment) => total + assignment.fte, 0), + recorded: { + billable: timesheetSummary.reduce((total, timesheet) => total + (timesheet.billable ? timesheet.duration : 0), 0), + nonBillable: timesheetSummary.reduce((total, timesheet) => total + (timesheet.billable ? 0 : timesheet.duration), 0) + } + }, + holiday: holiday ? holiday : null, + leave: leaveDay ? { + type: leaveDay.TYPE, + durationCode: leaveDay.DURATION, + duration: leaveDay.DURATION === 'Y' ? 7.4 : 3.7, + status: leaveDay.STATUS + } : null, + assignments: currentAssignments, + timesheet: timesheetSummary + } + + dates.push(day) + + date = date.plus({days: 1}) + } + + return dates +} function getAvailability(rse, assignments, capacities) { // Initialize years - let contractStart = DateTime.fromISO(rse.contractStart); - let contractEnd = DateTime.fromISO(rse.contractEnd); + let contractStart = DateTime.fromISO(rse.contractStart) + let contractEnd = DateTime.fromISO(rse.contractEnd) let lastAssignment = new Date( Math.max(...assignments.map((e) => new Date(e.end))) - ); + ) let lastAssignmentEnd = assignments.length > 0 ? DateTime.fromISO(lastAssignment.toISOString()) - : DateTime.now(); - let assignmentsEnd; + : DateTime.now() + let assignmentsEnd // RSE has fixed term contract and end is later than last assignment if (contractEnd && contractEnd > lastAssignmentEnd) { - assignmentsEnd = contractEnd; + assignmentsEnd = contractEnd // console.log(`${rse.firstname} ${rse.lastname}: Contract Ends ${assignmentsEnd}`) } // RSE has fixed term contract and end is earlier than last assignment else if (contractEnd && contractEnd < lastAssignment) { - assignmentsEnd = lastAssignmentEnd; + assignmentsEnd = lastAssignmentEnd // console.log(`${rse.firstname} ${rse.lastname}: Assignment Ends ${assignmentsEnd}`) } // Contract is open-ended, extend 24 months into the future past last assignment end date else { - assignmentsEnd = lastAssignmentEnd.plus({ years: 2 }); + assignmentsEnd = lastAssignmentEnd.plus({ years: 2 }) // console.log(`${rse.firstname} ${rse.lastname}: Open Ended ${assignmentsEnd}`) } - let availability = {}; + let availability = {} // Create outer availability object - let year = contractStart.year; + let year = contractStart.year while (year <= assignmentsEnd.year) { // Default to 100 percent availability each month - availability[year] = new Array(12).fill(100); - year++; + availability[year] = new Array(12).fill(100) + year++ } // Set months in first year before contract start to null - let startMonth = 1; + let startMonth = 1 while (startMonth < contractStart.month) { - availability[contractStart.year][startMonth - 1] = null; - startMonth++; + availability[contractStart.year][startMonth - 1] = null + startMonth++ } if (rse.contractEnd) { // Set months in final year before contract end to null - let endMonth = 12; + let endMonth = 12 while (endMonth > contractEnd.month) { - availability[contractEnd.year][endMonth - 1] = null; - endMonth--; + availability[contractEnd.year][endMonth - 1] = null + endMonth-- } } else { // Set months in final year of assignments end to null - let endMonth = 12; + let endMonth = 12 while (endMonth > assignmentsEnd.month) { - availability[assignmentsEnd.year][endMonth - 1] = null; - endMonth--; + availability[assignmentsEnd.year][endMonth - 1] = null + endMonth-- } } // Use each capacity to update availability capacities.forEach((capacity) => { - let start = DateTime.fromISO(new Date(capacity.start).toISOString()); - let end = assignmentsEnd; + let start = DateTime.fromISO(new Date(capacity.start).toISOString()) + let end = assignmentsEnd if (capacity.end) { - end = DateTime.fromISO(new Date(capacity.end).toISOString()); + end = DateTime.fromISO(new Date(capacity.end).toISOString()) } // Loop over each year in the capacity - let year = start.year; + let year = start.year while (year <= end.year) { // Loop over each month in the year the capacity is valid for - let month = year === start.year ? start.month : 1; - let endMonth = year === end.year ? end.month : 12; + let month = year === start.year ? start.month : 1 + let endMonth = year === end.year ? end.month : 12 while (month <= endMonth) { // capacity spans new year so needs to be added as key if (!availability.hasOwnProperty(year)) { - availability[year] = []; + availability[year] = [] } // Set assignment FTE from that months capacity - availability[year][month - 1] = capacity.capacity; - month++; + availability[year][month - 1] = capacity.capacity + month++ } - year++; + year++ } - }); + }) // Use each assignment to update availability assignments.forEach((assignment) => { - let start = DateTime.fromISO(new Date(assignment.start).toISOString()); - let end = DateTime.fromISO(new Date(assignment.end).toISOString()); + let start = DateTime.fromISO(new Date(assignment.start).toISOString()) + let end = DateTime.fromISO(new Date(assignment.end).toISOString()) // Loop over each year in the assignment - let year = start.year; + let year = start.year while (year <= end.year) { // Loop over each month in the year the assignment is valid for - let month = year === start.year ? start.month : 1; - let endMonth = year === end.year ? end.month : 12; + let month = year === start.year ? start.month : 1 + let endMonth = year === end.year ? end.month : 12 while (month <= endMonth) { try { // Subtract assignment FTE from that months availability @@ -124,13 +206,13 @@ function getAvailability(rse, assignments, capacities) { // console.log(year) // console.log(month) } - month++; + month++ } - year++; + year++ } - }); + }) - return availability; + return availability } async function fetchBankHolidays(year) { @@ -184,80 +266,106 @@ async function fetchBankHolidays(year) { module.exports = createCoreService("api::rse.rse", ({ strapi }) => ({ async find(...args) { - let { results, pagination } = await super.find(...args); + let { results, pagination } = await super.find(...args) - let rses = results; - results = []; + let rses = results + results = [] - for await (const rse of rses) { - // Get availability - const assignments = await strapi - .service("api::assignment.assignment") - .find({ - populate: ["rse", "project"], - filters: { - rse: rse.id, - }, - }); + const year = args[0].filters.year ? args[0].filters.year.$eq : DateTime.now().year - const capacities = await strapi.service("api::capacity.capacity").find({ - populate: ["rse"], - filters: { - rse: rse.id, + const startDate = DateTime.fromISO(`${year}-08-01`), + endDate = DateTime.fromISO(`${(Number(year)+1)}-07-31`) + + // Filter for use when checking if an object with a date range overlaps with the year + const dateRangeFilter = { + $or: [ + { + start: { + $between: [startDate.toISODate(), endDate.toISODate() ] + } }, - }); + { + end: { + $between: [startDate.toISODate(), endDate.toISODate() ] + } + }, + { + start: { + $lt: startDate.toISODate() + }, + end: { + $gt: endDate.toISODate() + } + } + ] + } + + const yearFilter = { + year: { $eq: year } + } + + let assignments = await strapi.service("api::assignment.assignment").find({filters: dateRangeFilter, populate: { project: { fields: ['name'] } } }), + capacities = await strapi.service("api::capacity.capacity").find({filters: dateRangeFilter}), + holidays = await fetchBankHolidays(year), + leave = await strapi.service("api::timesheet.timesheet").findLeave({filters: yearFilter}), + timesheets = await strapi.service("api::timesheet.timesheet").find({filters: yearFilter}) + + + for await (const rse of rses) { + + rse.calendar = createCalendar(rse, holidays, leave.data, assignments.results, capacities.results, timesheets.data, startDate, endDate) let availability = getAvailability( rse, - assignments.results, - capacities.results + assignments.results.filter(assignment => assignment.rse === rse.id), + capacities.results.filter(assignment => assignment.rse === rse.id) ), currentDate = DateTime.now(), contractEndDate = rse.contractEnd ? DateTime.fromISO(rse.contractEnd) : null, - nextAvailableDate = null; + nextAvailableDate = null let year = contractEndDate ? contractEndDate.year : currentDate.year, - month = null; + month = null // Loop over years starting from contract end year or current year while (year < Math.max(...Object.keys(availability).map(Number))) { - let i = 0; + let i = 0 // If current year set start of next month if (year === currentDate.year) { - i = currentDate.month - 1; + i = currentDate.month - 1 } // Loop over months in year for (i; i < availability[year].length; i++) { // If availability found set month from index and break out of loop if (availability[year][i] > 0) { - month = i + 1; - break; + month = i + 1 + break } } // If month has been set break out of loop if (month) { - break; + break } - year++; + year++ } // Availability found, create date object if (month) { - let day = 1; + let day = 1 if (currentDate.year === year && currentDate.month === month) { - day = currentDate.day; + day = currentDate.day } - nextAvailableDate = DateTime.utc(year, month, day); + nextAvailableDate = DateTime.utc(year, month, day) // console.log(`${rse.firstname} ${rse.lastname} ${nextAvailableDate.toISODate()}`) } // RSE has no availability else { if (contractEndDate > currentDate) { - console.error(`${rse.firstname} ${rse.lastname} has no availability`); + console.error(`${rse.firstname} ${rse.lastname} has no availability`) } } @@ -268,229 +376,19 @@ module.exports = createCoreService("api::rse.rse", ({ strapi }) => ({ DateTime.fromJSDate(new Date(e.end)).toISODate() ) ) - ); + ) rse.nextAvailableDate = nextAvailableDate ? nextAvailableDate.toISODate() - : null; - rse.nextAvailableFTE = availability[year][month - 1]; - rse.availability = availability; + : null + rse.nextAvailableFTE = availability[year][month - 1] + rse.availability = availability } catch (err) { - console.log(rse); - } - - results.push(rse); - } - - return { results, pagination }; - }, - async findOne(entryId, ...args) { - // Get availability - const assignments = await strapi - .service("api::assignment.assignment") - .find({ - populate: ["rse"], - filters: { - rse: { - id: { - $eq: entryId, - }, - }, - }, - }); - - const capacities = await strapi.service("api::capacity.capacity").find({ - populate: ["rse"], - filters: { - rse: { - id: { - $eq: entryId, - }, - }, - }, - }); - - let result = await super.findOne(entryId, ...args); - let availability = getAvailability( - result, - assignments.results, - capacities.results - ); - - let date = DateTime.now(); - - let month = availability[date.year].findIndex(function (availability) { - return availability > 0; - }); - - result.nextAvailableDate = new Date(Date.UTC(date.year, month, 1)); - result.nextAvailableFTE = availability[date.year][month]; - result.availability = availability; - - return result; - }, - async calendar(entryId, ...args) { - - const year = args[0].filters.year.$eq - - const startDate = DateTime.fromISO(`${year}-08-01`), - endDate = DateTime.fromISO(`${(Number(year)+1)}-07-31`) - - let rse = await this.findOne(entryId, ...args) - - // Find assignments for RSE where assignment period overlaps with the year - const assignmentFilter = { - populate: { - project: { - fields: ['name'] - } - }, - filters: { - rse: { $eq: entryId }, - $or: [ - { - start: { - $between: [startDate.toISODate(), endDate.toISODate() ] - } - }, - { - end: { - $between: [startDate.toISODate(), endDate.toISODate() ] - } - }, - { - start: { - $lt: startDate.toISODate() - }, - end: { - $gt: endDate.toISODate() - } - } - ] + console.log(err) } - } - - // Find capacity for RSE where capacity period overlaps with the year - const capacityFilter = { - filters: { - rse: { $eq: entryId }, - $or: [ - { - start: { - $between: [startDate.toISODate(), endDate.toISODate() ] - } - }, - { - $or: [ - { - end: { - $between: [startDate.toISODate(), endDate.toISODate() ] - }, - }, - { - end: { - $eq: null } - - } - ] - } - ] - } - } - - // Find timesheets for RSE in the year - const timesheetFilter = { - filters: { - year: { $eq: year } - } - } - - // Find leave for RSE in the year - const leaveFilter = { - filters: { - username: { $eq: rse.username }, - year: { $eq: year } - } - } - - let assignments = await strapi.service("api::assignment.assignment").find(assignmentFilter), - capacities = await strapi.service("api::capacity.capacity").find(capacityFilter), - holidays = await fetchBankHolidays(year), - leave = await strapi.service("api::timesheet.timesheet").findLeave(leaveFilter), - timesheets = await strapi.service("api::timesheet.timesheet").findOne(rse.clockifyID, timesheetFilter) - - const calendar = [] - - let date = startDate - - while(date <= endDate) { - - const holiday = holidays.find(holiday => holiday.date === date.toISODate()), - leaveDay = leave.data.find(leave => leave.DATE === date.toISODate()), - currentAssignments = assignments.results.filter(assignment => { - const start = DateTime.fromISO(assignment.start), - end = DateTime.fromISO(assignment.end) - return date >= start && date <= end - }) - - let dateCapacity = 0 - - capacities.results.forEach(capacity => { - - capacity.end = capacity.end ? capacity.end : endDate.toISODate() - - // Build interval for capacity period - const period = Interval.fromDateTimes(DateTime.fromISO(capacity.start), DateTime.fromISO(capacity.end)) - - // Is current date in loop within the capacity period - if(period.contains(date)) { - dateCapacity = capacity.capacity - } - }) - - const timesheetReport = timesheets.data.dates[date.toISODate()], - timesheetSummary = [] - - if(timesheetReport) { - timesheetReport.forEach(timesheet => { - timesheetSummary.push({ - start: timesheet.timeInterval.start, - end: timesheet.timeInterval.end, - duration: timesheet.timeInterval.duration, - billable: timesheet.billable, - project: timesheet.projectName, - }) - }) - } - - let day = { - date: date.toISODate(), - metadata: { - day: date.day, - month: date.month, - year: date.year, - dayOfWeek: date.weekday, - isWeekend: date.weekday > 5 - }, - utilisation: { - capacity: dateCapacity, - allocated: currentAssignments.reduce((total, assignment) => total + assignment.fte, 0), - unallocated: dateCapacity - currentAssignments.reduce((total, assignment) => total + assignment.fte, 0), - recorded: { - billable: timesheetSummary.reduce((total, timesheet) => total + (timesheet.billable ? timesheet.duration : 0), 0), - nonBillable: timesheetSummary.reduce((total, timesheet) => total + (timesheet.billable ? 0 : timesheet.duration), 0) - } - }, - holiday: holiday ? holiday : null, - leave: leaveDay ? { type: leaveDay.TYPE, duration: leaveDay.DURATION, status: leaveDay.STATUS } : null, - assignments: currentAssignments, - timesheet: timesheetSummary - } - - calendar.push(day) - date = date.plus({days: 1}) + results.push(rse) } - return calendar + return { results, pagination } } -})); +})) diff --git a/src/api/timesheet/services/timesheet.js b/src/api/timesheet/services/timesheet.js index 2430cc2..d42d762 100644 --- a/src/api/timesheet/services/timesheet.js +++ b/src/api/timesheet/services/timesheet.js @@ -81,8 +81,8 @@ module.exports = { const query = args[0] const year = query ? Number(query.filters.year.$eq) : null, - userIDs = query ? query.filters.userIDs.$in : null, - projectIDs = query ? query.filters.projectIDs.$in : null + userIDs = query.filters.userIDs ? query.filters.userIDs.$in : null, + projectIDs = query.filters.projectIDs ? query.filters.projectIDs.$in : null const response = await fetchDetailedReport(year, userIDs, projectIDs) @@ -150,7 +150,6 @@ module.exports = { const period = Interval.fromDateTimes(startDate.startOf('day'), endDate.endOf('day')) try { - // Due to the FY not being the same as the leave year, get the previous year too and combine the two const [response1, response2] = await Promise.all([ axios.get(`/turner?YEAR=${startDate.year}-${endDate.year}`, leaveConfig), diff --git a/src/extensions/documentation/documentation/1.0.0/full_documentation.json b/src/extensions/documentation/documentation/1.0.0/full_documentation.json index 3fe024c..228e78b 100644 --- a/src/extensions/documentation/documentation/1.0.0/full_documentation.json +++ b/src/extensions/documentation/documentation/1.0.0/full_documentation.json @@ -14,7 +14,7 @@ "name": "Apache 2.0", "url": "https://www.apache.org/licenses/LICENSE-2.0.html" }, - "x-generation-date": "2024-08-26T12:43:40.106Z" + "x-generation-date": "2024-08-27T19:53:43.872Z" }, "x-strapi-config": { "path": "/documentation",