claritykit-svelte
Version:
A comprehensive Svelte component library focused on accessibility, ADHD-optimized design, developer experience, and full SSR compatibility
262 lines (261 loc) • 10.4 kB
JavaScript
/**
* Gantt Utilities - ADHD-Optimized Project Management
* Provides therapeutic calculations and algorithms for cognitive-friendly project management
*/
/**
* Calculate task position based on timeline configuration
*/
export function calculateTaskPosition(task, timelineConfig) {
const startDate = timelineConfig.startDate.getTime();
const taskStartMs = task.startDate.getTime();
const taskEndMs = task.endDate.getTime();
const msPerDay = 24 * 60 * 60 * 1000;
const daysFromStart = (taskStartMs - startDate) / msPerDay;
const taskDurationDays = (taskEndMs - taskStartMs) / msPerDay;
return {
left: daysFromStart * timelineConfig.scale,
width: Math.max(taskDurationDays * timelineConfig.scale, 20) // Minimum width for visibility
};
}
/**
* Critical Path Method (CPM) Algorithm
* ADHD-optimized to identify the most important tasks clearly
*/
export function calculateCriticalPath(tasks, dependencies) {
// Create task map for quick lookup
const taskMap = new Map(tasks.map(task => [task.id, task]));
// Calculate Early Start (ES) and Early Finish (EF)
const earlyTimes = new Map();
// Forward pass
const visited = new Set();
function calculateEarlyTimes(taskId) {
if (visited.has(taskId))
return;
const task = taskMap.get(taskId);
if (!task)
return;
visited.add(taskId);
// Find dependencies that end at this task
const predecessors = dependencies.filter(dep => dep.toTaskId === taskId);
let maxEF = 0;
for (const pred of predecessors) {
calculateEarlyTimes(pred.fromTaskId);
const predEarly = earlyTimes.get(pred.fromTaskId);
if (predEarly) {
maxEF = Math.max(maxEF, predEarly.ef + pred.lag);
}
}
const es = maxEF;
const ef = es + task.duration;
earlyTimes.set(taskId, { es, ef });
}
// Calculate early times for all tasks
tasks.forEach(task => calculateEarlyTimes(task.id));
// Find project completion time
const projectFinish = Math.max(...Array.from(earlyTimes.values()).map(t => t.ef));
// Calculate Late Start (LS) and Late Finish (LF) - Backward pass
const lateTimes = new Map();
const visitedBackward = new Set();
function calculateLateTimes(taskId) {
if (visitedBackward.has(taskId))
return;
const task = taskMap.get(taskId);
if (!task)
return;
visitedBackward.add(taskId);
// Find dependencies that start from this task
const successors = dependencies.filter(dep => dep.fromTaskId === taskId);
let minLS = projectFinish;
if (successors.length === 0) {
// End task
minLS = projectFinish - task.duration;
}
else {
for (const succ of successors) {
calculateLateTimes(succ.toTaskId);
const succLate = lateTimes.get(succ.toTaskId);
if (succLate) {
minLS = Math.min(minLS, succLate.ls - succ.lag);
}
}
minLS = minLS - task.duration;
}
const ls = minLS;
const lf = ls + task.duration;
lateTimes.set(taskId, { ls, lf });
}
// Calculate late times for all tasks
tasks.forEach(task => calculateLateTimes(task.id));
// Identify critical tasks (where ES = LS and EF = LF)
const criticalTasks = [];
const criticalDependencies = [];
for (const task of tasks) {
const early = earlyTimes.get(task.id);
const late = lateTimes.get(task.id);
if (early && late && early.es === late.ls && early.ef === late.lf) {
criticalTasks.push(task.id);
}
}
// Find critical dependencies (connecting critical tasks)
for (const dep of dependencies) {
if (criticalTasks.includes(dep.fromTaskId) && criticalTasks.includes(dep.toTaskId)) {
criticalDependencies.push(dep.id);
}
}
return {
criticalTasks,
criticalDependencies,
totalDuration: projectFinish,
latestFinishDate: new Date(Date.now() + projectFinish * 24 * 60 * 60 * 1000)
};
}
/**
* ADHD-Optimized Cognitive Load Assessment
*/
export function assessCognitiveLoad(tasks) {
const totalTasks = tasks.length;
const activeTasks = tasks.filter(t => t.status === 'in-progress').length;
const overdueTasks = tasks.filter(t => t.status !== 'completed' && t.endDate < new Date()).length;
// ADHD-friendly scoring system
let cognitiveScore = 0;
cognitiveScore += activeTasks * 15; // Each active task adds cognitive load
cognitiveScore += overdueTasks * 25; // Overdue tasks add stress
cognitiveScore += Math.max(0, totalTasks - 10) * 5; // Too many total tasks
let recommendation;
if (cognitiveScore <= 30)
recommendation = 'optimal';
else if (cognitiveScore <= 60)
recommendation = 'manageable';
else if (cognitiveScore <= 90)
recommendation = 'warning';
else
recommendation = 'overloaded';
return {
totalTasks,
activeTasks,
overdueTasks,
cognitiveScore: Math.min(100, cognitiveScore),
recommendation
};
}
/**
* Bulk Operations for ADHD-Friendly Project Management
*/
export function performBulkOperation(tasks, dependencies, operation) {
const affectedTasks = [];
const warnings = [];
const recommendations = [];
switch (operation.type) {
case 'reschedule-all': {
const offsetDays = operation.parameters?.offsetDays || 0;
const newSchedule = tasks.map(task => ({
...task,
startDate: new Date(task.startDate.getTime() + offsetDays * 24 * 60 * 60 * 1000),
endDate: new Date(task.endDate.getTime() + offsetDays * 24 * 60 * 60 * 1000)
}));
return {
success: true,
affectedTasks: tasks.map(t => t.id),
newSchedule,
recommendations: ['All tasks have been rescheduled. Review dependencies for conflicts.']
};
}
case 'optimize-timeline': {
// Remove gaps between dependent tasks
const optimizedTasks = [...tasks];
const cognitiveLoad = assessCognitiveLoad(tasks);
if (cognitiveLoad.recommendation === 'overloaded') {
warnings.push('High cognitive load detected. Consider reducing parallel tasks.');
}
recommendations.push('Consider batching similar tasks to reduce context switching.');
recommendations.push('Schedule high-energy tasks during peak focus periods.');
return {
success: true,
affectedTasks: tasks.map(t => t.id),
warnings,
recommendations
};
}
case 'auto-balance': {
// Distribute workload evenly across timeline
const highEnergyTasks = tasks.filter(t => t.energy >= 4);
const lowEnergyTasks = tasks.filter(t => t.energy <= 2);
recommendations.push(`Schedule ${highEnergyTasks.length} high-energy tasks during morning hours.`);
recommendations.push(`Schedule ${lowEnergyTasks.length} low-energy tasks for afternoon periods.`);
if (highEnergyTasks.length > 3) {
warnings.push('Too many high-energy tasks scheduled. Consider spreading them across multiple days.');
}
return {
success: true,
affectedTasks: [...highEnergyTasks, ...lowEnergyTasks].map(t => t.id),
warnings,
recommendations
};
}
case 'compress-timeline': {
// Minimize project duration by parallelizing non-dependent tasks
const criticalPath = calculateCriticalPath(tasks, dependencies);
recommendations.push(`Focus on critical path tasks: ${criticalPath.criticalTasks.length} tasks identified.`);
recommendations.push('Non-critical tasks can be delayed or parallelized.');
return {
success: true,
affectedTasks: criticalPath.criticalTasks,
recommendations
};
}
default:
return {
success: false,
affectedTasks: [],
warnings: ['Unknown operation type']
};
}
}
/**
* Get task pair for dependency visualization
*/
export function getTaskPair(dependency, tasks) {
const fromTask = tasks.find(t => t.id === dependency.fromTaskId);
const toTask = tasks.find(t => t.id === dependency.toTaskId);
return { fromTask, toTask };
}
/**
* ADHD-Friendly Break Scheduling
*/
export function calculateBreakReminders(tasks) {
const breakReminders = [];
const activeTasks = tasks.filter(t => t.status === 'in-progress');
for (const task of activeTasks) {
// Schedule breaks every 2 hours for high-energy tasks
if (task.energy >= 4) {
const taskStart = task.startDate.getTime();
const taskEnd = task.endDate.getTime();
const duration = taskEnd - taskStart;
const breakInterval = 2 * 60 * 60 * 1000; // 2 hours
for (let time = taskStart + breakInterval; time < taskEnd; time += breakInterval) {
breakReminders.push(new Date(time));
}
}
}
return breakReminders.sort((a, b) => a.getTime() - b.getTime());
}
/**
* Check for scheduling conflicts
*/
export function detectConflicts(tasks, dependencies) {
const conflicts = [];
// Check for dependency violations
for (const dep of dependencies) {
const fromTask = tasks.find(t => t.id === dep.fromTaskId);
const toTask = tasks.find(t => t.id === dep.toTaskId);
if (!fromTask || !toTask)
continue;
const fromEnd = fromTask.endDate.getTime();
const toStart = toTask.startDate.getTime();
const lagMs = dep.lag * 24 * 60 * 60 * 1000;
if (dep.type === 'finish-to-start' && fromEnd + lagMs > toStart) {
conflicts.push(`Task "${toTask.name}" starts before "${fromTask.name}" finishes (dependency violation)`);
}
}
return conflicts;
}