From fddb3b9b7f41825c0aeee7ce6ab51afce80a566b Mon Sep 17 00:00:00 2001 From: Mark Turner Date: Wed, 18 Sep 2024 17:17:15 +0900 Subject: [PATCH] additional calculation for the next available date for each RSE --- src/api/rse/services/rse.js | 197 ++++++++++++++---- src/api/timesheet/services/timesheet.js | 10 +- .../1.0.0/full_documentation.json | 2 +- 3 files changed, 157 insertions(+), 52 deletions(-) diff --git a/src/api/rse/services/rse.js b/src/api/rse/services/rse.js index 08223b8..53a3204 100644 --- a/src/api/rse/services/rse.js +++ b/src/api/rse/services/rse.js @@ -5,70 +5,175 @@ */ const { createCoreService } = require("@strapi/strapi").factories +const { DateTime, Interval } = require("luxon") -module.exports = createCoreService('api::rse.rse') -/* -module.exports = createCoreService("api::rse.rse", ({ strapi }) => ({ +module.exports = createCoreService('api::rse.rse', () => ({ async find(...args) { - let { results, pagination } = await super.find(...args) + + let populate = { + assignments: true, + capacities: true + } + + // If populate is not set, populate assignments and capacities + if(args[0].populate && !args[0].populate.isArray && args[0].populate.assignments && args[0].populate.capacities) { + args[0].populate = ['assignments', 'capacities'] + } + else { + if(!args[0] || !args[0].populate.isArray) { + args[0].populate = [] + } + + if(!args[0].populate.includes('assignments')) { + populate.assignments = false + args[0].populate.push('assignments') + } + + if(!args[0].populate.includes('capacities')) { + populate.capacities = false + args[0].populate.push('capacities') + } + } + + const { results, pagination } = await super.find(...args) let rses = results - results = [] + + rses.forEach((rse, index) => { - const year = args[0].filters.year ? args[0].filters.year.$eq : DateTime.now().year + // Sort assignments by start date + rse.assignments = rse.assignments.sort((a, b) => DateTime.fromISO(a.start) - DateTime.fromISO(b.start)) - const startDate = DateTime.fromISO(`${year}-08-01`), - endDate = DateTime.fromISO(`${(Number(year)+1)}-07-31`) + // Only look at assignments that end in the future + const assignments = rse.assignments.filter(assignment => { return DateTime.fromISO(assignment.end) >= DateTime.now() }), + endDates = assignments.reduce((dates, assignment) => { dates.push(DateTime.fromISO(assignment.end)); return dates }, []).sort((a, b) => a - b) - // 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() + if(!rse.active || endDates.length === 0) { + rse.nextAvailableDate = null + rse.nextAvailableFTE = null + } + else { + // Loop over end dates + for(const date of endDates) { + + let assignments = [], + capacities = [] + + // Find assignments that are concurrent with the end date + rse.assignments.forEach((assignment) => { + const period = Interval.fromDateTimes(DateTime.fromISO(assignment.start), DateTime.fromISO(assignment.end)) + period.contains(date.plus({ days: 1})) ? assignments.push(assignment) : null + }) + + // Find capacities that are concurrent with the end date + rse.capacities.forEach((capacity) => { + // If end date is null, set it to 20 years in the future + capacity.end = capacity.end ? capacity.end : DateTime.now().plus({ years: 20}).toISODate() + const period = Interval.fromDateTimes(DateTime.fromISO(capacity.start), DateTime.fromISO(capacity.end)) + period.contains(date.plus({ days: 1})) ? capacities.push(capacity) : null + }) + + // Reduce down total assigned time and capacity time + const assigned = assignments.reduce((total, assignment) => total + assignment.fte, 0), + capacity = capacities.reduce((total, capacity) => total + capacity.capacity, 0) + + // If there is capacity available, set the next available date and FTE + if(capacity - assigned > 0) { + rse.nextAvailableDate = date.plus({ days: 1 }).toISODate() + rse.nextAvailableFTE = capacity - assigned + break } } - ] + } + + // cleanup if assignments and capacities are not requested + if(!populate.assignments) { delete results[index].assignments } + if(!populate.capacities) { delete results[index].capacities } + }) + + return { results, pagination } + }, + async findOne(entityId, params) { + + let populate = { + assignments: true, + capacities: true } - const yearFilter = { - year: { $eq: year } + // If populate is not set, populate assignments and capacities + if(params.populate && !params.populate.isArray && params.populate.assignments && params.populate.capacities) { + params.populate = ['assignments', 'capacities'] } + else { + if(!params.populate || !params.populate.isArray) { + params.populate = [] + } + + if(!params.populate.includes('assignments')) { + populate.assignments = false + params.populate.push('assignments') + } + + if(!params.populate.includes('capacities')) { + populate.capacities = false + params.populate.push('capacities') + } + } + + const result = await super.findOne(entityId, params) + + let rse = result - let assignments = await strapi.service("api::assignment.assignment").find({filters: dateRangeFilter, populate: { rse: { fields: ['id'] }, 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}) + // Sort assignments by start date + rse.assignments = rse.assignments.sort((a, b) => DateTime.fromISO(a.start) - DateTime.fromISO(b.start)) - - for await (const rse of rses) { + // Only look at assignments that end in the future + const assignments = rse.assignments.filter(assignment => { return DateTime.fromISO(assignment.end) >= DateTime.now() }), + endDates = assignments.reduce((dates, assignment) => { dates.push(DateTime.fromISO(assignment.end)); return dates }, []).sort((a, b) => a - b) - rse.calendar = createCalendar(rse, holidays, leave.data, assignments.results, capacities.results, timesheets.data, startDate, endDate) - - const nextAvailable = rse.calendar.find(day => (day.utilisation.unallocated > 0 && DateTime.fromISO(day.date) >= DateTime.now())) + if(!rse.active || endDates.length === 0) { + rse.nextAvailableDate = null + rse.nextAvailableFTE = null + } + else { + // Loop over end dates + for(const date of endDates) { + + let assignments = [], + capacities = [] + + // Find assignments that are concurrent with the end date + rse.assignments.forEach((assignment) => { + const period = Interval.fromDateTimes(DateTime.fromISO(assignment.start), DateTime.fromISO(assignment.end)) + period.contains(date.plus({ days: 1})) ? assignments.push(assignment) : null + }) - rse.nextAvailableDate = nextAvailable ? nextAvailable.date : null - rse.nextAvailableFTE = nextAvailable ? nextAvailable.utilisation.unallocated : 0 + // Find capacities that are concurrent with the end date + rse.capacities.forEach((capacity) => { + // If end date is null, set it to 20 years in the future + capacity.end = capacity.end ? capacity.end : DateTime.now().plus({ years: 20}).toISODate() + const period = Interval.fromDateTimes(DateTime.fromISO(capacity.start), DateTime.fromISO(capacity.end)) + period.contains(date.plus({ days: 1})) ? capacities.push(capacity) : null + }) - results.push(rse) + // Reduce down total assigned time and capacity time + const assigned = assignments.reduce((total, assignment) => total + assignment.fte, 0), + capacity = capacities.reduce((total, capacity) => total + capacity.capacity, 0) + + // If there is capacity available, set the next available date and FTE + if(capacity - assigned > 0) { + rse.nextAvailableDate = date.plus({ days: 1 }).toISODate() + rse.nextAvailableFTE = capacity - assigned + break + } + } } - return { data: results, meta: pagination } + // cleanup if assignments and capacities are not requested + if(!populate.assignments) { delete result.assignments } + if(!populate.capacities) { delete result.capacities } + + return result } -})) -*/ \ No newline at end of file +})) \ No newline at end of file diff --git a/src/api/timesheet/services/timesheet.js b/src/api/timesheet/services/timesheet.js index d02217c..24fdd9f 100644 --- a/src/api/timesheet/services/timesheet.js +++ b/src/api/timesheet/services/timesheet.js @@ -196,7 +196,7 @@ function createCalendar(rse, holidays, leave, assignments, capacities, timesheet leave: leaveDay ? { type: leaveDay.TYPE, durationCode: leaveDay.DURATION, - duration: leaveDay.DURATION === 'Y' ? 7.4 : 3.7, + duration: leaveDay.DURATION === 'Y' ? 7.26 : 3.63, status: leaveDay.STATUS } : null, assignments: currentAssignments.map(({ rse, ...assignment }) => assignment), @@ -476,10 +476,10 @@ module.exports = ({ strapi }) => ({ summary.days.assigned.push(dailyAssignments.reduce((total, assignment) => total + (assignment.fte / 100), 0).toFixed(1)) summary.days.leave.push((dailyTimesheetSummary.leave).toFixed(1)) summary.days.sickness.push((dailyTimesheetSummary.sickness).toFixed(1)) - summary.days.recorded.push((dailyTimesheetSummary.recorded.reduce((total, entry) => total + entry.timeInterval.duration, 0) / 60 / 60 / 7.4).toFixed(1)) - summary.days.billable.push((dailyTimesheetSummary.billable.reduce((total, entry) => total + entry.timeInterval.duration, 0) / 60 / 60 / 7.4).toFixed(1)) - summary.days.nonBillable.push((dailyTimesheetSummary.nonBillable.reduce((total, entry) => total + entry.timeInterval.duration, 0) / 60 / 60 / 7.4).toFixed(1)) - summary.days.volunteered.push((dailyTimesheetSummary.volunteered.reduce((total, entry) => total + entry.timeInterval.duration, 0) / 60 / 60 / 7.4).toFixed(1)) + summary.days.recorded.push((dailyTimesheetSummary.recorded.reduce((total, entry) => total + entry.timeInterval.duration, 0) / 60 / 60 / 7.26).toFixed(1)) + summary.days.billable.push((dailyTimesheetSummary.billable.reduce((total, entry) => total + entry.timeInterval.duration, 0) / 60 / 60 / 7.26).toFixed(1)) + summary.days.nonBillable.push((dailyTimesheetSummary.nonBillable.reduce((total, entry) => total + entry.timeInterval.duration, 0) / 60 / 60 / 7.26).toFixed(1)) + summary.days.volunteered.push((dailyTimesheetSummary.volunteered.reduce((total, entry) => total + entry.timeInterval.duration, 0) / 60 / 60 / 7.26).toFixed(1)) } else { summary.days.capacity.push(null) 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 0950809..0c68b84 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-09-07T22:21:39.958Z" + "x-generation-date": "2024-09-18T08:16:42.272Z" }, "x-strapi-config": { "path": "/documentation",