UNPKG

@boundless-oss/atlas

Version:

Atlas - MCP Server for comprehensive startup project management

374 lines 15.7 kB
export class TimelineGenerator { generateQuarterlyView(roadmap, startQuarter, endQuarter) { const items = []; const startDate = this.parseQuarter(startQuarter); const endDate = this.parseQuarter(endQuarter); // Add themes for (const theme of roadmap.themes) { const themeStart = this.parseQuarter(theme.timeframe.startQuarter); const themeEnd = this.parseQuarter(theme.timeframe.endQuarter); // Only include themes that overlap with the view period if (themeEnd >= startDate && themeStart <= endDate) { items.push({ id: theme.id, type: 'theme', name: theme.name, startDate: themeStart, endDate: themeEnd, status: theme.status, dependencies: [], progress: theme.metrics.progressPercentage }); } } // Add milestones for (const milestone of roadmap.milestones) { if (milestone.date >= startDate && milestone.date <= endDate) { items.push({ id: milestone.id, type: 'milestone', name: milestone.name, startDate: milestone.date, endDate: milestone.date, status: milestone.status, dependencies: milestone.dependencies }); } } // Sort by start date items.sort((a, b) => a.startDate.getTime() - b.startDate.getTime()); return { type: 'quarterly', startDate, endDate, items }; } generateMonthlyView(roadmap, initiatives, features, startMonth, months) { const items = []; const endDate = new Date(startMonth); endDate.setMonth(endDate.getMonth() + months); // Add initiatives with estimated timelines for (const initiative of initiatives) { const estimatedStart = this.estimateInitiativeStart(initiative, roadmap); const estimatedEnd = this.estimateInitiativeEnd(initiative, estimatedStart); if (estimatedEnd >= startMonth && estimatedStart <= endDate) { items.push({ id: initiative.id, type: 'initiative', name: initiative.title, startDate: estimatedStart, endDate: estimatedEnd, status: initiative.status, dependencies: initiative.dependencies.map(d => d.targetId || '').filter(Boolean), progress: this.calculateInitiativeProgress(initiative, features) }); } } // Add features for (const feature of features) { const featureStart = this.estimateFeatureStart(feature, initiatives); const featureEnd = this.estimateFeatureEnd(feature, featureStart); if (featureEnd >= startMonth && featureStart <= endDate) { items.push({ id: feature.id, type: 'feature', name: feature.name, startDate: featureStart, endDate: featureEnd, status: feature.status, dependencies: [], progress: feature.status === 'completed' ? 100 : feature.status === 'in-progress' ? 50 : 0 }); } } // Add releases for (const release of roadmap.releases) { if (release.date >= startMonth && release.date <= endDate) { items.push({ id: release.id, type: 'release', name: `v${release.version}: ${release.name}`, startDate: release.date, endDate: release.date, status: release.status, dependencies: [] }); } } // Sort by start date items.sort((a, b) => a.startDate.getTime() - b.startDate.getTime()); return { type: 'monthly', startDate: startMonth, endDate, items }; } generateReleaseView(roadmap, features) { const items = []; const now = new Date(); // Group features by release const releaseGroups = new Map(); for (const [_, feature] of features) { if (feature.targetRelease) { if (!releaseGroups.has(feature.targetRelease)) { releaseGroups.set(feature.targetRelease, []); } releaseGroups.get(feature.targetRelease).push(feature); } } // Add releases with their features for (const release of roadmap.releases) { items.push({ id: release.id, type: 'release', name: `v${release.version}: ${release.name}`, startDate: release.date, endDate: release.date, status: release.status, dependencies: [], progress: this.calculateReleaseProgress(release, releaseGroups.get(release.id) || []) }); // Add features for this release const releaseFeatures = releaseGroups.get(release.id) || []; for (const feature of releaseFeatures) { const featureStart = new Date(release.date); featureStart.setMonth(featureStart.getMonth() - 3); // Assume 3 months development items.push({ id: feature.id, type: 'feature', name: feature.name, startDate: featureStart, endDate: release.date, status: feature.status, dependencies: [], progress: feature.status === 'completed' ? 100 : feature.status === 'in-progress' ? 50 : 0 }); } } // Sort by date items.sort((a, b) => a.startDate.getTime() - b.startDate.getTime()); const startDate = items[0]?.startDate || now; const endDate = items[items.length - 1]?.endDate || now; return { type: 'release', startDate, endDate, items }; } generateNowNextLaterView(roadmap, themes, initiatives, features) { const now = new Date(); const nextQuarter = new Date(now); nextQuarter.setMonth(nextQuarter.getMonth() + 3); const laterDate = new Date(now); laterDate.setMonth(laterDate.getMonth() + 9); const items = []; // Categorize items into Now, Next, Later const categorizeByTiming = (startDate) => { if (startDate <= now) return 'now'; if (startDate <= nextQuarter) return 'next'; return 'later'; }; // Process themes for (const theme of roadmap.themes) { const themeStart = this.parseQuarter(theme.timeframe.startQuarter); const timing = categorizeByTiming(themeStart); items.push({ id: theme.id, type: 'theme', name: `[${timing.toUpperCase()}] ${theme.name}`, startDate: timing === 'now' ? now : timing === 'next' ? nextQuarter : laterDate, endDate: this.parseQuarter(theme.timeframe.endQuarter), status: theme.status, dependencies: [], progress: theme.metrics.progressPercentage }); } // Process initiatives for (const [_, initiative] of initiatives) { const initiativeStart = this.estimateInitiativeStart(initiative, roadmap); const timing = categorizeByTiming(initiativeStart); items.push({ id: initiative.id, type: 'initiative', name: `[${timing.toUpperCase()}] ${initiative.title}`, startDate: timing === 'now' ? now : timing === 'next' ? nextQuarter : laterDate, endDate: this.estimateInitiativeEnd(initiative, initiativeStart), status: initiative.status, dependencies: initiative.dependencies.map(d => d.targetId || '').filter(Boolean), progress: 0 }); } // Group by timing for better visualization items.sort((a, b) => { // First sort by timing category const aCategory = a.name.startsWith('[NOW]') ? 0 : a.name.startsWith('[NEXT]') ? 1 : 2; const bCategory = b.name.startsWith('[NOW]') ? 0 : b.name.startsWith('[NEXT]') ? 1 : 2; if (aCategory !== bCategory) return aCategory - bCategory; // Then by type (themes first, then initiatives, then features) const typeOrder = { theme: 0, initiative: 1, feature: 2, milestone: 3, release: 4 }; return typeOrder[a.type] - typeOrder[b.type]; }); return { type: 'now-next-later', startDate: now, endDate: laterDate, items }; } // Helper methods parseQuarter(quarter) { const [q, year] = quarter.split(' '); const quarterNum = parseInt(q.substring(1)); const yearNum = parseInt(year); const month = (quarterNum - 1) * 3; return new Date(yearNum, month, 1); } estimateInitiativeStart(initiative, roadmap) { // Find the theme this initiative belongs to const theme = roadmap.themes.find(t => t.initiatives.some(i => (typeof i === 'string' ? i : i.id) === initiative.id)); if (theme) { return this.parseQuarter(theme.timeframe.startQuarter); } // Default to current date return new Date(); } estimateInitiativeEnd(initiative, startDate) { const endDate = new Date(startDate); // Estimate based on effort const totalWeeks = initiative.effort.developmentWeeks + initiative.effort.designWeeks + initiative.effort.qaWeeks; endDate.setDate(endDate.getDate() + (totalWeeks * 7)); return endDate; } estimateFeatureStart(feature, initiatives) { // Find the initiative this feature belongs to const initiative = initiatives.find(i => i.features.some(f => (typeof f === 'string' ? f : f.id) === feature.id)); if (initiative) { // Return current date as we don't have the full roadmap context here return new Date(); } return new Date(); } estimateFeatureEnd(feature, startDate) { const endDate = new Date(startDate); // Estimate based on complexity const complexityWeeks = { 'low': 2, 'medium': 4, 'high': 8, 'very-high': 12 }; const weeks = complexityWeeks[feature.technicalComplexity]; endDate.setDate(endDate.getDate() + (weeks * 7)); return endDate; } calculateInitiativeProgress(initiative, features) { const initiativeFeatures = features.filter(f => initiative.features.some(initiativeFeature => (typeof initiativeFeature === 'string' ? initiativeFeature : initiativeFeature.id) === f.id)); if (initiativeFeatures.length === 0) return 0; const completedCount = initiativeFeatures.filter(f => f.status === 'completed').length; return Math.round((completedCount / initiativeFeatures.length) * 100); } calculateReleaseProgress(release, features) { if (features.length === 0) return 0; const completedCount = features.filter(f => f.status === 'completed').length; return Math.round((completedCount / features.length) * 100); } // Generate Gantt chart data generateGanttData(timelineView) { return { tasks: timelineView.items.map((item, index) => ({ id: item.id, name: item.name, start: item.startDate.toISOString(), end: item.endDate.toISOString(), progress: item.progress || 0, dependencies: item.dependencies, type: item.type, status: item.status, row: index })), viewStart: timelineView.startDate.toISOString(), viewEnd: timelineView.endDate.toISOString() }; } // Critical path analysis findCriticalPath(items) { // Build dependency graph const graph = new Map(); const itemMap = new Map(); for (const item of items) { itemMap.set(item.id, item); graph.set(item.id, new Set(item.dependencies)); } // Find items with no dependencies (start nodes) const startNodes = items.filter(item => item.dependencies.length === 0); // Calculate earliest start/finish times const earliestTimes = new Map(); const calculateEarliestTimes = (itemId) => { if (earliestTimes.has(itemId)) { return earliestTimes.get(itemId); } const item = itemMap.get(itemId); const dependencies = Array.from(graph.get(itemId) || []); let earliestStart = item.startDate.getTime(); if (dependencies.length > 0) { const depFinishTimes = dependencies.map(depId => { const depTimes = calculateEarliestTimes(depId); return depTimes.finish; }); earliestStart = Math.max(...depFinishTimes); } const duration = item.endDate.getTime() - item.startDate.getTime(); const earliestFinish = earliestStart + duration; earliestTimes.set(itemId, { start: earliestStart, finish: earliestFinish }); return { start: earliestStart, finish: earliestFinish }; }; // Calculate for all items for (const item of items) { calculateEarliestTimes(item.id); } // Find the critical path (items with no slack) const criticalPath = []; // Implementation simplified - in production would need full CPM algorithm // For now, return the longest dependency chain let maxDuration = 0; let longestPath = []; const findLongestPath = (itemId, currentPath, currentDuration) => { currentPath.push(itemId); const item = itemMap.get(itemId); currentDuration += item.endDate.getTime() - item.startDate.getTime(); const dependents = items.filter(i => i.dependencies.includes(itemId)); if (dependents.length === 0) { if (currentDuration > maxDuration) { maxDuration = currentDuration; longestPath = [...currentPath]; } } else { for (const dependent of dependents) { findLongestPath(dependent.id, [...currentPath], currentDuration); } } }; for (const startNode of startNodes) { findLongestPath(startNode.id, [], 0); } return longestPath; } } //# sourceMappingURL=timeline-generator.js.map