@tosin2013/kanbn
Version:
A CLI Kanban board with AI-powered task management features
307 lines (270 loc) • 8.48 kB
JavaScript
const humanizeDuration = require('humanize-duration');
const taskUtils = require('./task-utils');
const indexUtils = require('./index-utils');
/**
* Calculate assigned task totals and workloads
* @param {object[]} tasks List of tasks
* @return {object} Object with assigned task statistics
*/
function calculateAssignedTaskStats(tasks) {
return tasks.reduce((a, task) => {
if ("assigned" in task.metadata) {
if (!(task.metadata.assigned in a)) {
a[task.metadata.assigned] = {
total: 0,
workload: 0,
remainingWorkload: 0,
};
}
a[task.metadata.assigned].total++;
a[task.metadata.assigned].workload += task.workload;
a[task.metadata.assigned].remainingWorkload += task.remainingWorkload;
}
return a;
}, {});
}
/**
* Calculate AI interaction metrics
* @param {object[]} tasks List of tasks
* @return {object|null} AI metrics object or null if no AI interactions
*/
function calculateAIMetrics(tasks) {
const aiInteractions = tasks.filter(task =>
task.metadata.tags &&
task.metadata.tags.includes('ai-interaction')
);
if (aiInteractions.length === 0) {
return null;
}
const metrics = {
total: aiInteractions.length,
byType: {}
};
for (let interaction of aiInteractions) {
if (interaction.metadata.tags) {
for (let tag of interaction.metadata.tags) {
if (tag !== 'ai-interaction') {
if (!(tag in metrics.byType)) {
metrics.byType[tag] = 0;
}
metrics.byType[tag]++;
}
}
}
}
return metrics;
}
/**
* Calculate parent-child relationship metrics
* @param {object[]} tasks List of tasks
* @return {object|null} Relationship metrics or null if no relationships
*/
function calculateRelationMetrics(tasks) {
const parentTasks = tasks.filter(task =>
task.relations &&
task.relations.some(relation => relation.type === 'parent-of')
);
const childTasks = tasks.filter(task =>
task.relations &&
task.relations.some(relation => relation.type === 'child-of')
);
if (parentTasks.length === 0 && childTasks.length === 0) {
return null;
}
return {
parentTasks: parentTasks.length,
childTasks: childTasks.length
};
}
/**
* Calculate column workloads
* @param {object[]} tasks List of tasks
* @param {string[]} columnNames List of column names
* @return {object} Object with total and per-column workload statistics
*/
function calculateColumnWorkloads(tasks, columnNames) {
let totalWorkload = 0;
let totalRemainingWorkload = 0;
const columnWorkloads = tasks.reduce(
(a, task) => {
totalWorkload += task.workload;
totalRemainingWorkload += task.remainingWorkload;
a[task.column].workload += task.workload;
a[task.column].remainingWorkload += task.remainingWorkload;
return a;
},
Object.fromEntries(
columnNames.map((columnName) => [
columnName,
{
workload: 0,
remainingWorkload: 0,
},
])
)
);
return {
totalWorkload,
totalRemainingWorkload,
columnWorkloads
};
}
/**
* Calculate task workloads
* @param {object} index The index object
* @param {object[]} tasks List of tasks
* @return {object} Object with task workload statistics
*/
function calculateTaskWorkloads(index, tasks) {
return Object.fromEntries(
tasks.map((task) => [
task.id,
{
workload: task.workload,
progress: task.progress,
remainingWorkload: task.remainingWorkload,
completed: taskUtils.taskCompleted(index, task),
},
])
);
}
/**
* Calculate sprint statistics
* @param {object} index The index object
* @param {object[]} tasks List of tasks
* @param {string|number|null} sprint Sprint name, number, or null for current sprint
* @return {object|null} Sprint statistics or null if no sprints defined
*/
function calculateSprintStats(index, tasks, sprint = null) {
if (!("sprints" in index.options) || !index.options.sprints.length) {
return null;
}
const sprints = index.options.sprints;
const currentSprint = index.options.sprints.length;
let sprintIndex = currentSprint - 1;
if (sprint !== null) {
if (typeof sprint === "number") {
if (sprint < 1 || sprint > sprints.length) {
throw new Error(`Sprint ${sprint} does not exist`);
} else {
sprintIndex = sprint - 1;
}
} else if (typeof sprint === "string") {
sprintIndex = sprints.findIndex((s) => s.name === sprint);
if (sprintIndex === -1) {
throw new Error(`No sprint found with name "${sprint}"`);
}
}
}
const result = {
number: sprintIndex + 1,
name: sprints[sprintIndex].name,
start: sprints[sprintIndex].start,
};
if (currentSprint - 1 !== sprintIndex) {
if (sprintIndex === sprints.length - 1) {
result.end = sprints[sprintIndex + 1].start;
}
result.current = currentSprint;
}
if (sprints[sprintIndex].description) {
result.description = sprints[sprintIndex].description;
}
const sprintStartDate = sprints[sprintIndex].start;
const sprintEndDate = sprintIndex === sprints.length - 1 ? new Date() : sprints[sprintIndex + 1].start;
const duration = sprintEndDate - sprintStartDate;
result.durationDelta = duration;
result.durationMessage = humanizeDuration(duration, {
largest: 3,
round: true,
});
result.created = indexUtils.taskWorkloadInPeriod(tasks, "created", sprintStartDate, sprintEndDate);
result.started = indexUtils.taskWorkloadInPeriod(tasks, "started", sprintStartDate, sprintEndDate);
result.completed = indexUtils.taskWorkloadInPeriod(tasks, "completed", sprintStartDate, sprintEndDate);
result.due = indexUtils.taskWorkloadInPeriod(tasks, "due", sprintStartDate, sprintEndDate);
if ("customFields" in index.options) {
for (let customField of index.options.customFields) {
if (customField.type === "date") {
result[customField.name] = indexUtils.taskWorkloadInPeriod(
tasks,
customField.name,
sprintStartDate,
sprintEndDate
);
}
}
}
return result;
}
/**
* Calculate period statistics for specified dates
* @param {object} index The index object
* @param {object[]} tasks List of tasks
* @param {Date[]} dates Array of dates to calculate statistics for
* @return {object|null} Period statistics or null if no dates specified
*/
function calculatePeriodStats(index, tasks, dates) {
if (!dates || dates.length === 0) {
return null;
}
const result = {};
let periodStart, periodEnd;
if (dates.length === 1) {
periodStart = new Date(+dates[0]);
periodStart.setHours(0, 0, 0, 0);
periodEnd = new Date(+dates[0]);
periodEnd.setHours(23, 59, 59, 999);
result.start = periodStart;
result.end = periodEnd;
} else {
result.start = periodStart = new Date(Math.min(...dates));
result.end = periodEnd = new Date(Math.max(...dates));
}
result.created = indexUtils.taskWorkloadInPeriod(tasks, "created", periodStart, periodEnd);
result.started = indexUtils.taskWorkloadInPeriod(tasks, "started", periodStart, periodEnd);
result.completed = indexUtils.taskWorkloadInPeriod(tasks, "completed", periodStart, periodEnd);
result.due = indexUtils.taskWorkloadInPeriod(tasks, "due", periodStart, periodEnd);
if ("customFields" in index.options) {
for (let customField of index.options.customFields) {
if (customField.type === "date") {
result[customField.name] = indexUtils.taskWorkloadInPeriod(
tasks,
customField.name,
periodStart,
periodEnd
);
}
}
}
return result;
}
/**
* Calculate due tasks information
* @param {object[]} tasks List of tasks
* @return {object[]} Array of due task information
*/
function calculateDueTasks(tasks) {
const dueTasks = [];
tasks.forEach((task) => {
if ("dueData" in task) {
dueTasks.push({
task: task.id,
workload: task.workload,
progress: task.progress,
remainingWorkload: task.remainingWorkload,
...task.dueData,
});
}
});
return dueTasks;
}
module.exports = {
calculateAssignedTaskStats,
calculateAIMetrics,
calculateRelationMetrics,
calculateColumnWorkloads,
calculateTaskWorkloads,
calculateSprintStats,
calculatePeriodStats,
calculateDueTasks
};