tln-pm
Version:
Project Management as Code
300 lines (282 loc) • 8.82 kB
JavaScript
;
const { compareVersions } = require ('compare-versions');
const ems = require('enhanced-ms');
const { check } = require('yargs');
const { isAfter, intervalToDuration } = require('date-fns');
module.exports.timelineTasks = (tasks, timeline, team, defaultEstimate) => {
// TOSO: think about order attribute to control order of tasks older -> first in list
//
// find all leaf tasks
const leafs = [];
const findLeafs = (component) => {
const checkTasks = (tasks, {assignees, tags, deadline}) => {
tasks.forEach(t => {
const taskTags = t.tags.concat(tags);
const severity = ['high', 'medium', 'low'].findIndex(s => taskTags.includes(s));
const options = {
status: {'+': 0, '>': 1, '!': 2, '-': 3}[t.status],
severity: severity === -1 ? 1: severity,
assignees: t.assignees.concat(assignees),
tags: taskTags,
deadline: t.deadline || deadline,
estimate: t.estimate || defaultEstimate
};
if (t.tasks.length) {
checkTasks(t.tasks, options);
} else {
leafs.push({
attributes: options,
task: t
});
}
});
};
//console.log('component:', component);
checkTasks(component.tasks, {assignees: [], tags: [], deadline: ''});
component.components.forEach(c => findLeafs(c));
};
findLeafs(tasks);
//
// sort them by priority, status and deadlines
leafs.sort((a, b) => {
if (a.attributes.status >= b.attributes.status) {
if (a.attributes.severity >= b.attributes.severity) {
try {
return compareVersions(a.attributes.deadline, b.attributes.deadline);
} catch (e) {
if (a.attributes.deadline) {
return -1;
}
if (b.attributes.deadline) {
return 1;
}
return 0;
}
}
}
return -1;
});
//
// order them by assignee(s) calculating duration using fte(s)
let offset = 0;
leafs.forEach(l => {
l.task.start = offset;
l.task.end = l.task.start + l.attributes.estimate;
offset = l.task.end;
});
//
// move from bottom to top and calculate estimate for parent tasks and components
const updateInterval = (mainI, subI) => {
const r = {};
if (typeof mainI.start !== 'undefined') {
if (mainI.start > subI.start) {
r.start = subI.start;
} else {
r.start = mainI.start;
}
} else {
r.start = subI.start;
}
if (typeof mainI.end !== 'undefined') {
if (mainI.end < subI.end) {
r.end = subI.end;
} else {
r.end = mainI.end;
}
} else {
r.end = subI.end;
}
return r;
}
const normiliseComponent = (component) => {
let componentInterval = {};
const normaliseTasks = (tasks) => {
let totalInterval = {};
tasks.forEach(t => {
if (t.tasks.length) {
const taskInterval = normaliseTasks(t.tasks);
t.start = taskInterval.start;
t.end = taskInterval.end;
totalInterval = updateInterval(totalInterval, taskInterval);
} else {
totalInterval = updateInterval(totalInterval, {start: t.start, end: t.end});
}
});
return totalInterval;
};
componentInterval = updateInterval(componentInterval, normaliseTasks(component.tasks));
component.components.forEach(c => { componentInterval = updateInterval(componentInterval, normiliseComponent(c)); });
component.start = componentInterval.start;
component.end = componentInterval.end;
return componentInterval;
}
normiliseComponent(tasks);
//
// console.log('leafs:', JSON.stringify(leafs));
}
module.exports.parseTask = (desc) => {
let status = '-';
let id = ''
let title = '';
let estimate = 0;
let deadline = '';
let assignees = [];
let tags = [];
let links = [];
let foundHeader = false;
const matches = desc.matchAll(/^\[([\-,>,+,?,!,+,x]):?(\d*)?:?([\w,\.]+)?\]/g);
for (const match of matches) {
if (match[1]) {
status = match[1];
foundHeader = true;
}
if (match[2]) {
id = match[2];
foundHeader = true;
}
if (match[3]) {
deadline = match[3];
foundHeader = true;
}
}
if (foundHeader) {
const next = desc.replace(/^\[([\-,>,+,?,!,+,x]):?(\d*)?:?([\w,\.]+)?\]/g, '').trim();
// extract assignees(s)
const aees = next.matchAll(/@[a-z,\.,-,_]+/gi);
for (const aee of aees) {
assignees.push(aee[0].substring(1));
}
const next2 = next.replace(/@[a-z,\.,-,_]+/gi, '').trim();
// extract tag(s)
const ts = next2.matchAll(/#[a-z,0-9,\.,-,_]+/gi);
for (const t of ts) {
const tag = t[0].substring(1);
tags.push(tag);
const e = ems(tag);
if (e) {
estimate = e;
}
}
const next3 = next2.replace(/#[a-z,0-9,\.,-,_]+/gi, '').trim();
// ectratc link(s)
title = next3;
}
return { status, id, title, estimate, deadline, assignees, tags, links };
}
module.exports.mergeTwoTeams = (t1, t2) => {
const ids = [...new Set([...t1.map(v => v.id), ...t2.map(v => v.id)])];
return ids.map( i => {
let id = i;
let name = null;
let bandwidth = [];
const status = { todo: 0, dev: 0, blocked: 0, done: 0, total: 0 };
//
const m1 = t1.find(v => v.id === id);
if (m1) {
name = m1.name;
bandwidth.push(...m1.bandwidth);
Object.keys(status).forEach( s => {
status[s] += m1.status[s];
});
}
const m2 = t2.find(v => v.id === id);
if (m2) {
name = m2.name;
bandwidth.push(...m2.bandwidth);
Object.keys(status).forEach( s => {
status[s] += m2.status[s];
});
}
return {id, name, bandwidth, status};
});
}
function getStringFromInterval(interval) {
let diff = '?';
if (interval) {
if (interval.years) {
diff = `${interval.years}yr`;
} else if (interval.months) {
diff = `${interval.months}mo`;
} else if (interval.days) {
diff = `${interval.days}d`;
} else if (interval.hours) {
diff = `${interval.hours}h`;
} else if (interval.minutes) {
diff = `${interval.minutes}m`;
} else if (interval.seconds) {
diff = `${interval.seconds}s`;
}
}
return diff;
}
module.exports.getDurationToDate = (date, now) => {
const t1 = new Date(date);
const t2 = now ? new Date(now) : new Date();
if (isAfter(t1, t2)) {
return getStringFromInterval(intervalToDuration({start: t2, end: t1}));
}
return getStringFromInterval(intervalToDuration({start: t1, end: t2}));
}
/*
const getMillisecondsFromDuration = (duraction) => {
if (duraction) {
const tb = { 'years': 31536000000, 'months': 2592000000, 'days': 86400000, 'hours': 3600000, 'minutes': 60000, 'seconds': 1000};
return Object.keys(tb).reduce(
function(acc, key){
if (duraction[key]) {
return acc + tb[key] * duraction[key];
}
return acc;
},
0
);
}
}
const formatDuration = (duration) => {
return Object.keys(duration).map(
(key) => {
if (duration[key]) {
return `${duration[key]}${key[0]}`;
}
return '';
}
).filter(v => !!v).slice(0, 2).join(' ');
}
const getLocalISOString = (date) =>{
if (date !== 'n/a') {
const offset = date.getTimezoneOffset();
const offsetAbs = Math.abs(offset)
const isoString = new Date(date.getTime() - offset * 60 * 1000).toISOString()
return `${isoString.slice(0, -1)}${offset > 0 ? '-' : '+'}${String(Math.floor(offsetAbs / 60)).padStart(2, '0')}:${String(offsetAbs % 60).padStart(2, '0')}`
}
return date;
}
const getClosestRelease = (timeline, format = ['years', 'months', 'days', 'hours']) => {
let releaseName = 'n/a';
let releaseDate = 'n/a';
let timeToRelease = 'n/a';
let releaseFeatures = 'n/a';
if (timeline.length) {
let minDuration = Number.MAX_SAFE_INTEGER;
const currentDateTime = new Date();
timeline.forEach(function(t) {
const deadline = new Date(t.deadline);
const duration = intervalToDuration({start: currentDateTime, end: deadline});
const ms = getMillisecondsFromDuration(duration);
if (ms > 0 && ms < minDuration) {
minDuration = ms;
releaseName = t.id;
releaseDate = deadline;
timeToRelease = formatDuration(duration);
//timeToRelease = dateFns.fp.formatDuration(duration, { format, delimiter: ', ', zero: false });
releaseFeatures = t.features;
}
});
}
return {releaseName, releaseDate, timeToRelease, releaseFeatures};
};
const getLastUpdateTime = (summary) => {
const lut = intervalToDuration({start: new Date(summary.lastCommit), end: new Date() });
return getStringFromInterval(lut);
}
*/