rays-javascript-extension/project/project.js

425 lines
10 KiB
JavaScript

class RyzProject {
constructor(options) {
const a = this;
const _options = Object.assign(a.newProject, options);
a.Project = _options;
a.Tasks = [];
a.initialiseComponents();
}
initialiseComponents() {
const a = this;
}
get NewTask() {
return {
// Order: null,
ID: null,
Name: "",
Description: "",
Tag: null,
// StartDate: null, // new Date(),
// FinishDate: null, // new Date(),
StartDelay: 0, // Days
Duration: 1, // Days
PredecessorTaskID: null,
IsCollated: false,
CollatedTaskID: null,
// CalcWorkHours: 0,
ActuWorkHours: null,
Progress: 0,
Resources: []
};
}
get StartDate() {
const a = this;
return new Date(a.Project.StartDate);
}
get FinishDate() {
const a = this;
let result = new Date(a.Project.StartDate);
a.ExportTasks().forEach(e => {
if (e.FinishDate == null) {
return;
}
result = Date.max(result, e.FinishDate);
});
return result;
}
get Duration() {
const a = this;
return Date.diffDays(a.StartDate, a.FinishDate);
}
get newProject() {
return {
Name: "",
Description: "",
StartDate: Date.today(),
Tag: null,
StartOfWeek: 1, // Monday
WorkHours: [0, 7.5, 7.5, 7.5, 7.5, 7.5, 0] // 0 = Sunday
};
}
get newTaskNode() {
return {
Order: null,
ID: null,
Name: "",
Description: "",
Tag: null,
StartDate: null, // new Date(),
FinishDate: null, // new Date(),
StartDelay: 0, // Days
Duration: 1, // Days
PredecessorTaskID: null,
IsCollated: false,
CollatedTaskID: null,
CalcWorkHours: 0,
ActuWorkHours: null,
Progress: 0,
Resources: [],
Level: 0,
PredecessorTaskNo: null,
Tasks: []
};
}
AddTask(task) {
const a = this;
const newTask = Object.assign(a.NewTask, task);
const newTaskNode = Object.assign(a.newTaskNode, newTask);
if ((newTaskNode.PredecessorTaskID == null) && (newTaskNode.CollatedTaskID == null)) {
a.Tasks.push(newTaskNode);
} else if (newTaskNode.PredecessorTaskID != null) {
const node = a.FindTask(newTaskNode.PredecessorTaskID);
if (node != null) {
node.Tasks.push(newTaskNode);
} else {
a.log("Task not found (" + newTaskNode.PredecessorTaskID + ")");
}
} else if (newTaskNode.CollatedTaskID != null) {
const node = a.FindTask(newTaskNode.CollatedTaskID);
if (node != null) {
node.Tasks.push(newTaskNode);
} else {
a.log("Task not found (" + newTaskNode.CollatedTaskID + ")");
}
} else {
a.log("Task not found (" + newTaskNode.ID + ")");
}
}
ClearTasks() {
const a = this;
a.Tasks = [];
}
ExportTasks() {
const a = this;
let result = a.Tasks.copy().flatten("Tasks");
for (var i=0; i<result.length; i++) {
result[i].Order = (i + 1);
delete result[i].Tasks;
}
return result;
}
FindTask(id) {
const a = this;
let result = null;
a.Tasks.forEachTree("Tasks", function(e) {
if (e.ID == id) {
result = e;
return false;
}
return true;
});
return result;
}
/**
* Get node type.
* @returns {int} enum
*
* 0 = orphan
* 1 = task no-parent/root
* 2 = task no-parent/root, collated/group
* 3 = task with parent
* 4 = task with parent, collated/group
* 5 = sub-task
* 6 = sub-task, collated/group
*
*/
GetNodeType(node) {
if (node == null) {
// 0 = orphan
return 0;
}
if ((node.PredecessorTaskID == null) && (node.CollatedTaskID == null) && (node.IsCollated == false)) {
// 1 = task no-parent/root
return 1;
}
if ((node.PredecessorTaskID == null) && (node.CollatedTaskID == null) && (node.IsCollated == true)) {
// 2 = task no-parent/root, collated/group
return 2;
}
if ((node.PredecessorTaskID != null) && (node.CollatedTaskID == null) && (node.IsCollated == false)) {
// 3 = task with parent
return 3;
}
if ((node.PredecessorTaskID != null) && (node.CollatedTaskID == null) && (node.IsCollated == true)) {
// 4 = task with parent, collated/group
return 4;
}
if ((node.CollatedTaskID != null) && (node.IsCollated == false)) {
// 5 = sub-task
return 5;
}
if ((node.CollatedTaskID != null) && (node.IsCollated == true)) {
// 6 = sub-task, collated/group
return 6;
}
return 0;
}
Invalidate() {
const a = this;
let isSuccess = false;
if (!a.Recalculate()) {
isSuccess = false;
}
return isSuccess;
}
Recalculate() {
const a = this;
let isSuccess = false;
a.Sort();
// Get flat references
let result = a.Tasks.flatten("Tasks");
// Reset calculated values
for (var i=0; i<result.length; i++) {
result[i].Order = (i + 1);
result[i].StartDate = null;
result[i].FinishDate = null;
result[i].CalcWorkHours = null;
}
// Maximum 128 rounds
for (var x=0; x<128; x++) {
let pendingCount = result.copy().countMany(
{ propName: "StartDate", value: null },
{ propName: "FinishDate", value: null }
);
// Done
if (pendingCount <= 0) {
isSuccess = true;
break;
}
a.log("Round " + (x + 1));
for (var i=0; i<result.length; i++) {
const nodeType = a.GetNodeType(result[i]);
switch (nodeType) {
case 1: // task no-parent/root
result[i].StartDate = Date.addDays(a.Project.StartDate, result[i].StartDelay);
result[i].FinishDate = Date.addDays(result[i].StartDate, result[i].Duration);
break;
case 2: // task no-parent/root, collated/group
result[i].StartDate = Date.addDays(a.Project.StartDate, result[i].StartDelay);
result[i].Progress = 0;
// update finish date, if possible
a.recalculateCollatedTask(result, i);
break;
case 3: // task with parent
const node3 = result.first("ID", result[i].PredecessorTaskID);
if (node3 != null) {
if (node3.FinishDate != null) {
result[i].StartDate = Date.addDays(node3.FinishDate, result[i].StartDelay);
result[i].FinishDate = Date.addDays(result[i].StartDate, result[i].Duration);
}
}
break;
case 4: // task with parent, collated/group
const node4 = result.first("ID", result[i].PredecessorTaskID);
if (node4 != null) {
if (node4.FinishDate != null) {
result[i].StartDate = Date.addDays(node4.FinishDate, result[i].StartDelay);
result[i].Progress = 0;
// update finish date, if possible
a.recalculateCollatedTask(result, i);
}
}
break;
case 5: // sub-task
const node5 = result.first("ID", result[i].CollatedTaskID);
if (node5 != null) {
if (node5.StartDate != null) {
result[i].StartDate = Date.addDays(node5.StartDate, result[i].StartDelay);
result[i].FinishDate = Date.addDays(result[i].StartDate, result[i].Duration);
result[i].Level = (node5.Level + 1);
}
}
break;
case 6: // sub-task, collated/group
const node6 = result.first("ID", result[i].CollatedTaskID);
if (node6 != null) {
if (node6.StartDate != null) {
result[i].StartDate = Date.addDays(node6.StartDate, result[i].StartDelay);
result[i].Progress = 0;
result[i].Level = (node6.Level + 1);
// update finish date, if possible
a.recalculateCollatedTask(result, i);
}
}
break;
default: // orphan
break;
}
}
}
// Calculate work-hours
a.recalculateWorkHours(result);
// Calculate predecessor
a.recalculatePredecessorTaskNo(result);
return isSuccess;
}
Sort() {
const a = this;
a.Tasks.sortTree("Tasks", "StartDelay");
}
log(message) {
console.log(message);
}
recalculateWorkHours(array) {
const a = this;
for (var i=0; i<array.length; i++) {
if (array[i].StartDate == null) {
continue;
}
if (array[i].FinishDate == null) {
continue;
}
if (array[i].Duration <= 0) {
continue;
}
let workHours = 0;
let date = new Date(array[i].StartDate);
for (let x=0; x<array[i].Duration; x++) {
const d = date.getDay();
workHours += a.Project.WorkHours[d];
}
array[i].CalcWorkHours = workHours;
}
}
recalculatePredecessorTaskNo(array) {
const a = this;
for (var i=0; i<array.length; i++) {
if (array[i].PredecessorTaskID == null) {
continue;
}
const predecessor = a.FindTask(array[i].PredecessorTaskID);
if (predecessor == null) {
continue;
}
array[i].PredecessorTaskNo = predecessor.Order;
}
}
recalculateCollatedTask(array, index) {
let node2 = array.select("CollatedTaskID", array[index].ID);
if (node2.length <= 0) {
// No children
array[index].Duration = 0;
return new Date(array[index].StartDate);
}
// Not ready, calculation pending
if (node2.any("FinishDate", null)) {
array[index].Duration = 0;
return null;
}
let node2FinishDate = new Date(array[index].StartDate);
node2.forEach(e => {
if (e.FinishDate > node2FinishDate) {
node2FinishDate = e.FinishDate;
}
});
array[index].Progress = Math.average(node2.toList("Progress"));
array[index].FinishDate = new Date(node2FinishDate);
array[index].Duration = Date.diffDays(array[index].StartDate, array[index].FinishDate);
}
}