425 lines
10 KiB
JavaScript
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);
|
|
}
|
|
|
|
} |