The endpoint /resourceAnalysis
will return the estimated and actual efforts of users in tasks. It is similar to the existing /resourceCapacity
but v2, admitting POST or URLEncoded Get (as we do with /search)
These are the main concepts we will use throughout the document.
Properties between < >
denote optional
Entity
: A project or a service. Has the properties{Id, Name, <Program:{Id, Name}>, <Client:{Id, Name}>,<xx:{Id, Name}>,<StartDate>, <EndDate>}
WorkItem
: A task or activity. Has the properties{Id, Name, {Entity}Id, <StartDate>, <EndDate>}
Category
: Professional category a user has{Id, Name}
Estimated effort
, in minutes: It can apply to a user in a task ("assigned effort"), or to a category in a task or a project ("unassigned effort").Capacity
, in minutes: working time per user per day.Interval
: Accumulates efforts and capacities in time periods (day, week, month, or quarter)-
ActualEffort
refers to the reported effort, which can be by intervals
AcceptedEffort
refers to the total accepted effort for the user in the task. It can be the same asActualEffort
or different.
Interval example:
{
"analysisMode": "intervals",
"intervals": {
"startDate": "2024-01-01",
"intervalType": "week",
"noOfIntervals": 5
},
"filter": {
"projects": {
"Program.Id": { "$in": [12, 23] }
},
"services": {
"Program.Id": { "$in": [12, 23] }
}
}
}
Totals example:
This is an example of "liveBetween" filter. We're requesting that the StartDate is before the range end date and the EndDate is after the range start date.
{
"analysisMode": "totals",
"totals": {
"StartDate": { "$lte": "2023-11-30" },
"EndDate": { "$gte": "2023-09-01" }
}
}
Must be intervals
or totals
.
Mandatory if analysisMode
is intervals
.
In this example, we are requesting five weeks starting on 2024-01-01. The response will give us the efforts and capacity summarized for weeks 1 to 5.
{"intervals": {
"startDate": "2024-01-01",
"intervalType": "week",
"noOfIntervals":5
}
}
noOfIntervals
limits:
- 31 days
- 12 weeks
- 12 months
- 4 quarters
Error code: If any of the limits are breached, the response will be a 400 error with the message including the limit breached.
👉🏼 Note: In the existing
/resourceCapacity
we use numbers forintervalType
which is misleading.
👉🏼 Note: Copy the current way of calculating intervals of
/resourceCapacity
: weeks are 7 days, months are sensitive to whether they have 28, 29, 03 or 31 days.
Mandatory if analysisMode
is totals
. The dates correspond to the entities (projects or services) start and end dates (not the effort dates)
"totals": {
"StartDate": { "$lte": "2023-11-30" },
"EndDate": { "$gte": "2023-09-01" }
}
Date range limit: Maximum date range is one year. Error code: If the date range is breached, the response will be a 400 error with the message including the limit breached.
filter:{project, service, user}
Optional. Determines which entities and users are requested. Filters can apply to entities (projects
, services
) and users (users
).
- The general form is:
filter:{projects, services, users}
- When a filter is applied, it will return the main property. For example,
{projects: "ExternalClient.Id":{"$in":[*]}}
will return the project'sExternalClient:{Id, Name}
property.⚠️ Confirm the format"$in":[*]
works. This is the way to include properties in the response without filtering. - If no filters are present, it will return all active entities (inactive must not be present).
Because we need to map the filters to the database, we will limit the filters to the following properties:
projects
and services
filters:
[
"Name",
"PerformingUnit.Id",
"PerformingUnit.Name",
"ApprovalStatus.Id",
"ApprovalStatus.Name",
"Status.Id",
"Status.Name",
"Priority.Id",
"Priority.Name",
"Type.Id",
"Type.Name",
"Duration",
"Id",
"No",
"Code",
"BusinessGoal.Id",
"BusinessGoal.Name",
"Category.Id",
"Category.Name",
"Sponsor.Id",
"Sponsor.Name",
"InternalClient.Id",
"InternalClient.Name",
"ExternalClient.Id",
"ExternalClient.Name",
"Program.Id",
"Program.Name",
"ProcessAffected.Id",
"ProcessAffected.Name",
"Asset.Id",
"Asset.Name",
"IsActive",
"IsService"
]
StartDate
and EndDate
will not be part of the allowed filters at all, neither for projects or services, intervals or totals since they are part of the interval or total request.
Filter examples
All projects and services of the program Ids 12 and 23
"filter":{
"projects":{
"Program.Id":{"$in":[12, 23]}
},
"services":{
"Program.Id":{"$in":[12, 23]}
}
Projects whose start date is within a range and are assigned to clients 21 and 223
"filter":{
"projects":{
"Client.Id":{"$in":[21, 223]}
}}
Tasks with a start date between some values and only users of category 21
"filter":{
"users":{
"Category.Id": {"$in":[21]}}
}
👉🏼 Note on filter and
analysisMode
:totals
. The UI will force to filter entities by dates to prevent server overload. Depending on the final performance tests, we will decide whether to make filter mandatory and limited in the API.
The response structure will be the like so. You can see a full example below.
{
Intervals?: [{ IntervalId, IntervalName, StartDate, EndDate }],
Entities: [
{
Id, Name, EntityType: 'project' | 'service',
EntitySubType?:'waterfall' | 'agile',
RequestedProperty?: { Id, Name },
WorkItems: [
{
Id, Name,
AssignedEfforts: [
{
UserId,
Intervals?: [{ IntervalId, EstimatedEffort, ActualEffort, Capacity }],
TotalUserWorkItemEffort?: { EstimatedEffort, AcceptedEffort }
},
],
},
],
},
],
Users: [
{ Id, Name, UserImageUrl, CategoryId,
UserTotals: { TotalEstimatedEffort, TotalActualEffort, TotalCapacity } },
],
Categories: [
{ Id, Name },
]
}
Explanation:
Intervals
will be present depending onanalysisMode
. StartDate and EndDate include time, although the time is always 00:00:00 for the start and 23:59:59 for the end.- Totals (such as
UserWorkItemTotals
,WorkItemTotals
, andEntityTotals
) will not consider intervals; they are the totals. (beware of double calculations)
Note: we removed the unassigned efforts from the response. UnassignedEfforts: { CategoryN: Integer, ...others},
If needed, we can add them back
The response must pass the validation. Use the Postman testing to validate the response.
Otherwise you can use the local JS. Simplified example:
const payload = {"analysisMode": "totals"};
const result = await fetch('http://localhost:3000/resourceAnalysis', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify(payload)
});
const response = await result.json();
resourceAnalysisValidator.validateResponse(response);
We have to use the effort tables as primary sources:
- Daily Estimated:
[tblWorkHourDistribute] - [intTaskId] ,[intUserId] ,[dtsDate] ,[intWorkMin]
⚠️ No AccountID - Daily Actuals:
[tblTaskTime] - [intTaskTimeId] ,[intAccountId] ,[intProjectId] ,[intTaskId] ,[intUserId], ,[dtsWorkdate], ,[intWorkHour], ,[intWorkMin]
⚠️ No AccountID. Must convert HH:MM to minutes.
Total Estimated and Accepted: [tblTaskUser] [intTaskUserId] ,[intTaskId] ,[intProjectUserId], ,[intEstimatedHours], ,[intEstimatedMins] ,[intActualEffortAcceptedHours] ,[intActualEffortAcceptedMins]
We need to decide if filtering will happen before, after or it will be a join.
❓Are services in v2? If not, we can remove the
services
filter and offer the feature only for projects in the first version.
In this example we requested two week intervals.
{
Intervals: [
{
IntervalId: 1,
IntervalName: "01-01-2024",
StartDate: "2024-01-01T00:00:00",
EndDate: "2024-01-07T23:59:59"
},
{
IntervalId: 2,
IntervalName: "08-01-2024",
StartDate: "2024-01-08T00:00:00",
EndDate: "2024-01-14T23:59:59"
}
],
Entities: [
{
EntityType: "project",
EntitySubType: "waterfall",
Id: "project1",
Name: "Project name 1",
Client: {
Id: "client1",
Name: "Client 1"
},
Program: {
Id: "program1",
Name: "Program 1"
},
WorkItems: [
{
Id: "task1",
Name: "Task name 1",
AssignedEfforts: [
{
UserId: "user1",
Intervals: [
{
IntervalId: 1,
EstimatedEffort: 600,
ActualEffort: 540,
Capacity: 2400
},
{
IntervalId: 2,
EstimatedEffort: 600,
ActualEffort: 540,
Capacity: 720
}
]
},
{
UserId: "user2",
Intervals: [
{
IntervalId: 1,
EstimatedEffort: 720,
ActualEffort: 780,
Capacity: 2400
},
{
IntervalId: 2,
EstimatedEffort: 720,
ActualEffort: 780,
Capacity: 2400
}
]
}
],
UnassignedEfforts: {
Category1: 60,
Category2: 60
}
},
{
Id: "task2",
Name: "Task name 2 with a longer name that exceeds usual length",
AssignedEfforts: [
{
UserId: "user1",
Intervals: [
{
IntervalId: 1,
EstimatedEffort: 1200,
ActualEffort: 720,
Capacity: 2400
},
{
IntervalId: 2,
EstimatedEffort: 1200,
ActualEffort: 720,
Capacity: 720
}
]
}
],
UnassignedEfforts: {
Category1: 60,
Category2: 60
}
}
]
},
{
EntityType: "project",
EntitySubType: "waterfall",
Id: "project2",
Name: "Project name 2",
Client: {
Id: "client1",
Name: "Client 1"
},
Program: {
Id: "program1",
Name: "Program 1"
},
WorkItems: [
{
Id: "task3",
Name: "Task name 3",
AssignedEfforts: [
{
UserId: "user2",
Intervals: [
{
IntervalId: 1,
EstimatedEffort: 780,
ActualEffort: 1560,
Capacity: 2400
},
{
IntervalId: 2,
EstimatedEffort: 780,
ActualEffort: 1560,
Capacity: 2400
}
],
},
{
UserId: "user3",
Intervals: [
{
IntervalId: 1,
EstimatedEffort: 1800,
ActualEffort: 600,
Capacity: 2400
},
{
IntervalId: 2,
EstimatedEffort: 1800,
ActualEffort: 600,
Capacity: 2400
}
],
}
],
UnassignedEfforts: {
Category1: 60,
Category2: 60
}
}
]
}
],
Users: [
{
Id: "user1",
Name: "User name 1",
UserImageUrl: "UploadData/PHOTO/16by16_30168.jpg",
CategoryId: "category1",
},
{
Id: "user2",
Name: "User name 2",
UserImageUrl: "UploadData/PHOTO/16by16_30169.jpg",
CategoryId: "category1",
},
{
Id: "user3",
Name: "User name 3",
UserImageUrl: "UploadData/PHOTO/16by16_30170.jpg",
CategoryId: "category2",
}
],
Categories: [
{
Id: "category1",
Name: "Category 1",
}
]
}
In this example we requested totals.
{
Entities: [
{
EntityType: "project",
EntitySubType: "waterfall",
Id: "project1",
Name: "Project name 1",
WorkItems: [
{
Id: "task1",
Name: "Task name 1",
AssignedEfforts: [
{
UserId: "user1",
TotalUserWorkItemEffort: {
EstimatedEffort: 8400,
AcceptedEffort: 9645
}
},
{
UserId: "user2",
TotalUserWorkItemEffort: {
EstimatedEffort: 5910,
AcceptedEffort: 5298
}
}
],
UnassignedEfforts: {
Category1: 60,
Category2: 60
}
},
{
Id: "task2",
Name: "Task name 2 with a longer name that exceeds usual length",
AssignedEfforts: [
{
UserId: "user1",
TotalUserWorkItemEffort: {
EstimatedEffort: 3900,
AcceptedEffort: 2550
}
}
],
UnassignedEfforts: {
Category1: 60,
Category2: 60
}
}
]
},
{
EntityType: "project",
EntitySubType: "waterfall",
Id: "project2",
Name: "Project name 2",
Client: {
Id: "client1",
Name: "Client 1"
},
Program: {
Id: "program1",
Name: "Program 1"
},
WorkItems: [
{
Id: "task3",
Name: "Task name 3",
AssignedEfforts: [
{
UserId: "user2",
TotalUserWorkItemEffort: {
EstimatedEffort: 7800,
AcceptedEffort: 15600
}
},
{
UserId: "user3",
TotalUserWorkItemEffort: {
EstimatedEffort: 18000,
AcceptedEffort: 6000
}
}
],
UnassignedEfforts: {
Category1: 60,
Category2: 60
}
}
]
}
],
Users: [
{
Id: "user1",
Name: "User name 1",
UserImageUrl: "UploadData/PHOTO/16by16_30168.jpg",
CategoryId: "category1",
},
{
Id: "user2",
Name: "User name 2",
UserImageUrl: "UploadData/PHOTO/16by16_30169.jpg",
CategoryId: "category1",
},
{
Id: "user3",
Name: "User name 3",
UserImageUrl: "UploadData/PHOTO/16by16_30170.jpg",
CategoryId: "category2",
}
],
Categories: [
{
Id: "category1",
Name: "Category 1",
}
]
}