Files
ALwrity/frontend/src/services/TaskNavigationService.ts

470 lines
12 KiB
TypeScript

import {
TodayTask,
DailyWorkflow,
NavigationState,
WorkflowError
} from '../types/workflow';
interface NavigationConfig {
autoNavigate: boolean;
delayBeforeNavigation: number; // milliseconds
showNavigationConfirmation: boolean;
enableBackNavigation: boolean;
persistNavigationState: boolean;
}
interface NavigationEvent {
type: 'task_started' | 'task_completed' | 'task_skipped' | 'navigation_requested';
taskId: string;
workflowId: string;
timestamp: Date;
metadata?: Record<string, any>;
}
class TaskNavigationService {
private config: NavigationConfig;
private navigationHistory: NavigationEvent[] = [];
private currentNavigationState: NavigationState | null = null;
private navigationListeners: Array<(event: NavigationEvent) => void> = [];
constructor(config: NavigationConfig = {
autoNavigate: true,
delayBeforeNavigation: 2000,
showNavigationConfirmation: false,
enableBackNavigation: true,
persistNavigationState: true
}) {
this.config = config;
this.loadNavigationHistory();
}
/**
* Navigate to a specific task
*/
async navigateToTask(
task: TodayTask,
workflow: DailyWorkflow,
options: {
skipConfirmation?: boolean;
trackNavigation?: boolean;
} = {}
): Promise<boolean> {
try {
// Validate task and workflow
if (!this.validateTaskForNavigation(task, workflow)) {
throw new WorkflowError({
code: 'INVALID_NAVIGATION_TARGET',
message: `Cannot navigate to task ${task.id}`,
timestamp: new Date(),
recoverable: true,
suggestedAction: 'Check task dependencies and status'
});
}
// Show confirmation if required
if (this.config.showNavigationConfirmation && !options.skipConfirmation) {
const confirmed = await this.showNavigationConfirmation(task);
if (!confirmed) {
return false;
}
}
// Execute navigation based on action type
const navigationSuccess = await this.executeNavigation(task);
if (navigationSuccess) {
// Update navigation state
this.updateNavigationState(task, workflow);
// Track navigation event
if (options.trackNavigation !== false) {
this.trackNavigationEvent({
type: 'navigation_requested',
taskId: task.id,
workflowId: workflow.id,
timestamp: new Date(),
metadata: { actionType: task.actionType, actionUrl: task.actionUrl }
});
}
// Auto-navigate to next task if enabled
if (this.config.autoNavigate && task.status === 'completed') {
setTimeout(() => {
this.autoNavigateToNextTask(workflow);
}, this.config.delayBeforeNavigation);
}
}
return navigationSuccess;
} catch (error) {
console.error('Navigation failed:', error);
throw error;
}
}
/**
* Auto-navigate to the next available task
*/
async autoNavigateToNextTask(workflow: DailyWorkflow): Promise<TodayTask | null> {
try {
const nextTask = this.getNextAvailableTask(workflow);
if (nextTask) {
await this.navigateToTask(nextTask, workflow, {
skipConfirmation: true,
trackNavigation: true
});
return nextTask;
}
return null;
} catch (error) {
console.error('Auto-navigation failed:', error);
return null;
}
}
/**
* Navigate back to previous task
*/
async navigateBack(workflow: DailyWorkflow): Promise<TodayTask | null> {
if (!this.config.enableBackNavigation) {
throw new WorkflowError({
code: 'BACK_NAVIGATION_DISABLED',
message: 'Back navigation is disabled',
timestamp: new Date(),
recoverable: false
});
}
try {
const previousTask = this.getPreviousTask(workflow);
if (previousTask) {
await this.navigateToTask(previousTask, workflow, {
skipConfirmation: true,
trackNavigation: true
});
return previousTask;
}
return null;
} catch (error) {
console.error('Back navigation failed:', error);
throw error;
}
}
/**
* Get the next available task in the workflow
*/
getNextAvailableTask(workflow: DailyWorkflow): TodayTask | null {
const currentIndex = workflow.currentTaskIndex;
const remainingTasks = workflow.tasks.slice(currentIndex + 1);
// Find next task that's not completed and has dependencies satisfied
for (const task of remainingTasks) {
if (task.status === 'pending' && this.areDependenciesSatisfied(task, workflow)) {
return task;
}
}
return null;
}
/**
* Get the previous task in the workflow
*/
getPreviousTask(workflow: DailyWorkflow): TodayTask | null {
const currentIndex = workflow.currentTaskIndex;
if (currentIndex > 0) {
return workflow.tasks[currentIndex - 1];
}
return null;
}
/**
* Check if task dependencies are satisfied
*/
areDependenciesSatisfied(task: TodayTask, workflow: DailyWorkflow): boolean {
if (!task.dependencies || task.dependencies.length === 0) {
return true;
}
return task.dependencies.every(depId => {
const depTask = workflow.tasks.find(t => t.id === depId);
return depTask && depTask.status === 'completed';
});
}
/**
* Execute the actual navigation based on task action type
*/
private async executeNavigation(task: TodayTask): Promise<boolean> {
try {
switch (task.actionType) {
case 'navigate':
return await this.navigateToInternalPage(task);
case 'modal':
return await this.openModal(task);
case 'external':
return await this.navigateToExternalUrl(task);
default:
throw new WorkflowError({
code: 'UNKNOWN_ACTION_TYPE',
message: `Unknown action type: ${task.actionType}`,
timestamp: new Date(),
recoverable: true,
suggestedAction: 'Check task configuration'
});
}
} catch (error) {
console.error(`Navigation execution failed for task ${task.id}:`, error);
return false;
}
}
/**
* Navigate to internal ALwrity page
*/
private async navigateToInternalPage(task: TodayTask): Promise<boolean> {
if (!task.actionUrl) {
throw new WorkflowError({
code: 'MISSING_ACTION_URL',
message: `Task ${task.id} is missing action URL`,
timestamp: new Date(),
recoverable: true,
suggestedAction: 'Configure action URL for the task'
});
}
try {
// Use React Router navigation
if (typeof window !== 'undefined' && window.history) {
window.history.pushState(null, '', task.actionUrl);
// Dispatch custom event for React Router to handle
window.dispatchEvent(new PopStateEvent('popstate'));
return true;
}
return false;
} catch (error) {
console.error('Internal navigation failed:', error);
return false;
}
}
/**
* Open modal for task
*/
private async openModal(task: TodayTask): Promise<boolean> {
try {
// Dispatch custom event to open modal
const modalEvent = new CustomEvent('openTaskModal', {
detail: { task }
});
if (typeof window !== 'undefined') {
window.dispatchEvent(modalEvent);
return true;
}
return false;
} catch (error) {
console.error('Modal opening failed:', error);
return false;
}
}
/**
* Navigate to external URL
*/
private async navigateToExternalUrl(task: TodayTask): Promise<boolean> {
if (!task.actionUrl) {
throw new WorkflowError({
code: 'MISSING_ACTION_URL',
message: `Task ${task.id} is missing external URL`,
timestamp: new Date(),
recoverable: true,
suggestedAction: 'Configure external URL for the task'
});
}
try {
if (typeof window !== 'undefined') {
window.open(task.actionUrl, '_blank', 'noopener,noreferrer');
return true;
}
return false;
} catch (error) {
console.error('External navigation failed:', error);
return false;
}
}
/**
* Validate if task can be navigated to
*/
private validateTaskForNavigation(task: TodayTask, workflow: DailyWorkflow): boolean {
// Check if task exists in workflow
const workflowTask = workflow.tasks.find(t => t.id === task.id);
if (!workflowTask) {
return false;
}
// Check if task is enabled
if (!task.enabled) {
return false;
}
// Check dependencies
if (!this.areDependenciesSatisfied(task, workflow)) {
return false;
}
return true;
}
/**
* Show navigation confirmation dialog
*/
private async showNavigationConfirmation(task: TodayTask): Promise<boolean> {
return new Promise((resolve) => {
// In a real implementation, this would show a confirmation dialog
// For now, we'll use a simple confirm dialog
const confirmed = window.confirm(
`Navigate to: ${task.title}\n\n${task.description}\n\nContinue?`
);
resolve(confirmed);
});
}
/**
* Update navigation state
*/
private updateNavigationState(task: TodayTask, workflow: DailyWorkflow): void {
const currentIndex = workflow.tasks.findIndex(t => t.id === task.id);
const previousTask = currentIndex > 0 ? workflow.tasks[currentIndex - 1] : null;
const nextTask = currentIndex < workflow.tasks.length - 1 ? workflow.tasks[currentIndex + 1] : null;
this.currentNavigationState = {
currentTask: task,
previousTask,
nextTask,
canGoBack: currentIndex > 0,
canGoForward: currentIndex < workflow.tasks.length - 1
};
}
/**
* Track navigation event
*/
private trackNavigationEvent(event: NavigationEvent): void {
this.navigationHistory.push(event);
// Notify listeners
this.navigationListeners.forEach(listener => {
try {
listener(event);
} catch (error) {
console.error('Navigation listener error:', error);
}
});
// Persist navigation history
if (this.config.persistNavigationState) {
this.persistNavigationHistory();
}
}
/**
* Add navigation event listener
*/
addNavigationListener(listener: (event: NavigationEvent) => void): void {
this.navigationListeners.push(listener);
}
/**
* Remove navigation event listener
*/
removeNavigationListener(listener: (event: NavigationEvent) => void): void {
const index = this.navigationListeners.indexOf(listener);
if (index > -1) {
this.navigationListeners.splice(index, 1);
}
}
/**
* Get current navigation state
*/
getCurrentNavigationState(): NavigationState | null {
return this.currentNavigationState;
}
/**
* Get navigation history
*/
getNavigationHistory(): NavigationEvent[] {
return [...this.navigationHistory];
}
/**
* Clear navigation history
*/
clearNavigationHistory(): void {
this.navigationHistory = [];
this.persistNavigationHistory();
}
/**
* Persist navigation history to localStorage
*/
private persistNavigationHistory(): void {
try {
localStorage.setItem('task-navigation-history', JSON.stringify(this.navigationHistory));
} catch (error) {
console.warn('Failed to persist navigation history:', error);
}
}
/**
* Load navigation history from localStorage
*/
private loadNavigationHistory(): void {
try {
const stored = localStorage.getItem('task-navigation-history');
if (stored) {
this.navigationHistory = JSON.parse(stored).map((event: any) => ({
...event,
timestamp: new Date(event.timestamp)
}));
}
} catch (error) {
console.warn('Failed to load navigation history:', error);
}
}
/**
* Update navigation configuration
*/
updateConfig(newConfig: Partial<NavigationConfig>): void {
this.config = { ...this.config, ...newConfig };
}
/**
* Get current configuration
*/
getConfig(): NavigationConfig {
return { ...this.config };
}
}
// Export singleton instance
export const taskNavigationService = new TaskNavigationService();
export default TaskNavigationService;