Files
archon/archon-ui-main/src/components/project-tasks/TasksTab.tsx

850 lines
32 KiB
TypeScript

import React, { useState, useEffect, useCallback, useMemo } from 'react';
import { Table, LayoutGrid, Plus, Wifi, WifiOff, List, Trash2 } from 'lucide-react';
import { DndProvider } from 'react-dnd';
import { HTML5Backend } from 'react-dnd-html5-backend';
import { Toggle } from '../ui/Toggle';
import { projectService } from '../../services/projectService';
import { getGlobalOperationTracker } from '../../utils/operationTracker';
import { Card } from '../ui/card';
import { useTaskSocket } from '../../hooks/useTaskSocket';
import type { CreateTaskRequest, UpdateTaskRequest, DatabaseTaskStatus } from '../../types/project';
import { WebSocketState } from '../../services/socketIOService';
import { TaskTableView, Task } from './TaskTableView';
import { TaskBoardView } from './TaskBoardView';
import { EditTaskModal } from './EditTaskModal';
// Assignee utilities
const ASSIGNEE_OPTIONS = ['User', 'Archon', 'AI IDE Agent'] as const;
// Delete confirmation modal component
interface DeleteConfirmModalProps {
onConfirm: () => void;
onCancel: () => void;
title: string;
message: string;
confirmText?: string;
}
const DeleteConfirmModal = ({
onConfirm,
onCancel,
title,
message,
confirmText = 'Archive'
}: DeleteConfirmModalProps) => {
return (
<div className="fixed inset-0 bg-black/50 backdrop-blur-sm flex items-center justify-center z-50">
<div className="relative p-6 rounded-md backdrop-blur-md w-full max-w-md
bg-gradient-to-b from-white/80 to-white/60 dark:from-white/10 dark:to-black/30
border border-gray-200 dark:border-zinc-800/50
shadow-[0_10px_30px_-15px_rgba(0,0,0,0.1)] dark:shadow-[0_10px_30px_-15px_rgba(0,0,0,0.7)]
before:content-[''] before:absolute before:top-0 before:left-0 before:right-0 before:h-[2px]
before:rounded-t-[4px] before:bg-red-500
before:shadow-[0_0_10px_2px_rgba(239,68,68,0.4)] dark:before:shadow-[0_0_20px_5px_rgba(239,68,68,0.7)]">
<div className="relative z-10">
<div className="flex items-center gap-3 mb-4">
<div className="w-12 h-12 rounded-full bg-red-100 dark:bg-red-900/30 flex items-center justify-center">
<Trash2 className="w-6 h-6 text-red-600 dark:text-red-400" />
</div>
<div>
<h3 className="text-lg font-semibold text-gray-800 dark:text-white">
{title}
</h3>
<p className="text-sm text-gray-600 dark:text-gray-400">
This action cannot be undone
</p>
</div>
</div>
<p className="text-gray-700 dark:text-gray-300 mb-6">
{message}
</p>
<div className="flex justify-end gap-3">
<button
onClick={onCancel}
className="px-4 py-2 text-gray-600 dark:text-gray-400 hover:text-gray-800 dark:hover:text-gray-200 transition-colors"
>
Cancel
</button>
<button
onClick={onConfirm}
className="px-4 py-2 bg-red-600 hover:bg-red-700 text-white rounded-lg transition-colors shadow-lg shadow-red-600/25 hover:shadow-red-700/25"
>
{confirmText}
</button>
</div>
</div>
</div>
</div>
);
};
// Mapping functions for status conversion
const mapUIStatusToDBStatus = (uiStatus: Task['status']): DatabaseTaskStatus => {
switch (uiStatus) {
case 'backlog': return 'todo';
case 'in-progress': return 'doing';
case 'review': return 'review'; // Map UI 'review' to database 'review'
case 'complete': return 'done';
default: return 'todo';
}
};
const mapDBStatusToUIStatus = (dbStatus: DatabaseTaskStatus): Task['status'] => {
switch (dbStatus) {
case 'todo': return 'backlog';
case 'doing': return 'in-progress';
case 'review': return 'review'; // Map database 'review' to UI 'review'
case 'done': return 'complete';
default: return 'backlog';
}
};
// Helper function to map database task format to UI task format
const mapDatabaseTaskToUITask = (dbTask: any): Task => {
return {
id: dbTask.id,
title: dbTask.title,
description: dbTask.description || '',
status: mapDBStatusToUIStatus(dbTask.status),
assignee: {
name: dbTask.assignee || 'User',
avatar: ''
},
feature: dbTask.feature || 'General',
featureColor: '#3b82f6', // Default blue color
task_order: dbTask.task_order || 0,
};
};
export const TasksTab = ({
initialTasks,
onTasksChange,
projectId
}: {
initialTasks: Task[];
onTasksChange: (tasks: Task[]) => void;
projectId: string;
}) => {
const [viewMode, setViewMode] = useState<'table' | 'board'>('board');
const [tasks, setTasks] = useState<Task[]>([]);
const [editingTask, setEditingTask] = useState<Task | null>(null);
const [isModalOpen, setIsModalOpen] = useState(false);
const [projectFeatures, setProjectFeatures] = useState<import('../types/jsonb').ProjectFeature[]>([]);
const [isLoadingFeatures, setIsLoadingFeatures] = useState(false);
const [isSavingTask, setIsSavingTask] = useState<boolean>(false);
const [isWebSocketConnected, setIsWebSocketConnected] = useState(false);
const [taskToDelete, setTaskToDelete] = useState<Task | null>(null);
const [showDeleteConfirm, setShowDeleteConfirm] = useState(false);
// Track recently deleted tasks to prevent race conditions
const [recentlyDeletedIds, setRecentlyDeletedIds] = useState<Set<string>>(new Set());
// Initialize tasks
useEffect(() => {
setTasks(initialTasks);
}, [initialTasks]);
// Load project features on component mount
useEffect(() => {
loadProjectFeatures();
}, [projectId]);
// Optimized socket handlers with conflict resolution
const handleTaskUpdated = useCallback((message: any) => {
const updatedTask = message.data || message;
const mappedTask = mapDatabaseTaskToUITask(updatedTask);
// Skip updates for recently deleted tasks (race condition prevention)
if (recentlyDeletedIds.has(updatedTask.id)) {
console.log('[Socket] Ignoring update for recently deleted task:', updatedTask.id);
return;
}
// Skip updates while modal is open for the same task to prevent conflicts
if (isModalOpen && editingTask?.id === updatedTask.id) {
console.log('[Socket] Skipping update for task being edited:', updatedTask.id);
return;
}
setTasks(prev => {
// Use server timestamp for conflict resolution
const existingTask = prev.find(task => task.id === updatedTask.id);
// Skip if we already have this task (prevent duplicate additions)
if (!existingTask) {
console.log('[Socket] Task not found locally, adding:', updatedTask.id);
}
const updated = prev.map(task =>
task.id === updatedTask.id
? { ...mappedTask }
: task
);
// Notify parent after state settles
setTimeout(() => onTasksChange(updated), 0);
return updated;
});
}, [onTasksChange, isModalOpen, editingTask?.id, recentlyDeletedIds]);
const handleTaskCreated = useCallback((message: any) => {
const newTask = message.data || message;
console.log('🆕 Real-time task created:', newTask);
const mappedTask = mapDatabaseTaskToUITask(newTask);
setTasks(prev => {
// Check if this is replacing a temporary task from optimistic update
const hasTempTask = prev.some(task => task.id.startsWith('temp-') && task.title === mappedTask.title);
if (hasTempTask) {
// Replace temporary task with real task
const updated = prev.map(task =>
task.id.startsWith('temp-') && task.title === mappedTask.title
? mappedTask
: task
);
setTimeout(() => onTasksChange(updated), 0);
console.log('Replaced temporary task with real task:', mappedTask.id);
return updated;
}
// Check if task already exists to prevent duplicates
if (prev.some(task => task.id === newTask.id)) {
console.log('Task already exists, skipping create');
return prev;
}
const updated = [...prev, mappedTask];
setTimeout(() => onTasksChange(updated), 0);
return updated;
});
}, [onTasksChange]);
const handleTaskDeleted = useCallback((message: any) => {
const deletedTask = message.data || message;
console.log('🗑️ Real-time task deleted:', deletedTask);
// Remove from recently deleted cache when deletion is confirmed
setRecentlyDeletedIds(prev => {
const newSet = new Set(prev);
newSet.delete(deletedTask.id);
return newSet;
});
setTasks(prev => {
const updated = prev.filter(task => task.id !== deletedTask.id);
setTimeout(() => onTasksChange(updated), 0);
return updated;
});
}, [onTasksChange]);
const handleTaskArchived = useCallback((message: any) => {
const archivedTask = message.data || message;
console.log('📦 Real-time task archived:', archivedTask);
setTasks(prev => {
const updated = prev.filter(task => task.id !== archivedTask.id);
setTimeout(() => onTasksChange(updated), 0);
return updated;
});
}, [onTasksChange]);
const handleTasksReordered = useCallback((message: any) => {
const reorderData = message.data || message;
console.log('🔄 Real-time tasks reordered:', reorderData);
// Handle bulk task reordering from server
if (reorderData.tasks && Array.isArray(reorderData.tasks)) {
const uiTasks: Task[] = reorderData.tasks.map(mapDatabaseTaskToUITask);
setTasks(uiTasks);
setTimeout(() => onTasksChange(uiTasks), 0);
}
}, [onTasksChange]);
const handleInitialTasks = useCallback((message: any) => {
const initialWebSocketTasks = message.data || message;
const uiTasks: Task[] = initialWebSocketTasks.map(mapDatabaseTaskToUITask);
setTasks(uiTasks);
onTasksChange(uiTasks);
}, [onTasksChange]);
// Simplified socket connection with better lifecycle management
const { isConnected, connectionState } = useTaskSocket({
projectId,
onTaskCreated: handleTaskCreated,
onTaskUpdated: handleTaskUpdated,
onTaskDeleted: handleTaskDeleted,
onTaskArchived: handleTaskArchived,
onTasksReordered: handleTasksReordered,
onInitialTasks: handleInitialTasks,
onConnectionStateChange: (state) => {
setIsWebSocketConnected(state === WebSocketState.CONNECTED);
}
});
// Update connection state when hook state changes
useEffect(() => {
setIsWebSocketConnected(isConnected);
}, [isConnected]);
const loadProjectFeatures = async () => {
if (!projectId) return;
setIsLoadingFeatures(true);
try {
const response = await projectService.getProjectFeatures(projectId);
setProjectFeatures(response.features || []);
} catch (error) {
console.error('Failed to load project features:', error);
setProjectFeatures([]);
} finally {
setIsLoadingFeatures(false);
}
};
// Modal management functions
const openEditModal = async (task: Task) => {
setEditingTask(task);
setIsModalOpen(true);
};
const closeModal = () => {
setIsModalOpen(false);
setEditingTask(null);
};
const saveTask = async (task: Task) => {
setEditingTask(task);
setIsSavingTask(true);
try {
let parentTaskId = task.id;
if (task.id) {
// Update existing task
const updateData: UpdateTaskRequest = {
title: task.title,
description: task.description,
status: mapUIStatusToDBStatus(task.status),
assignee: task.assignee?.name || 'User',
task_order: task.task_order,
...(task.feature && { feature: task.feature }),
...(task.featureColor && { featureColor: task.featureColor })
};
await projectService.updateTask(task.id, updateData);
} else {
// Create new task first to get UUID
const createData: CreateTaskRequest = {
project_id: projectId,
title: task.title,
description: task.description,
status: mapUIStatusToDBStatus(task.status),
assignee: task.assignee?.name || 'User',
task_order: task.task_order,
...(task.feature && { feature: task.feature }),
...(task.featureColor && { featureColor: task.featureColor })
};
const createdTask = await projectService.createTask(createData);
parentTaskId = createdTask.id;
}
// Don't reload tasks - let socket updates handle synchronization
closeModal();
} catch (error) {
console.error('Failed to save task:', error);
alert(`Failed to save task: ${error instanceof Error ? error.message : 'Unknown error'}`);
} finally {
setIsSavingTask(false);
}
};
// Update tasks helper
const updateTasks = (newTasks: Task[]) => {
setTasks(newTasks);
onTasksChange(newTasks);
};
// Helper function to reorder tasks by status to ensure no gaps (1,2,3...)
const reorderTasksByStatus = async (status: Task['status']) => {
const tasksInStatus = tasks
.filter(task => task.status === status)
.sort((a, b) => a.task_order - b.task_order);
const updatePromises = tasksInStatus.map((task, index) =>
projectService.updateTask(task.id, { task_order: index + 1 })
);
await Promise.all(updatePromises);
};
// Helper function to get next available order number for a status
const getNextOrderForStatus = (status: Task['status']): number => {
const tasksInStatus = tasks.filter(task =>
task.status === status
);
if (tasksInStatus.length === 0) return 1;
const maxOrder = Math.max(...tasksInStatus.map(task => task.task_order));
return maxOrder + 1;
};
// Simple debounce function
const debounce = (func: Function, delay: number) => {
let timeoutId: NodeJS.Timeout;
return (...args: any[]) => {
clearTimeout(timeoutId);
timeoutId = setTimeout(() => func(...args), delay);
};
};
// Batch reorder persistence for efficient updates
const debouncedPersistBatchReorder = useMemo(
() => debounce(async (tasksToUpdate: Task[]) => {
try {
console.log(`REORDER: Persisting batch update for ${tasksToUpdate.length} tasks`);
// Send batch update request to backend
// For now, update tasks individually (backend can be optimized later for batch endpoint)
const updatePromises = tasksToUpdate.map(task =>
projectService.updateTask(task.id, {
task_order: task.task_order
})
);
await Promise.all(updatePromises);
console.log('REORDER: Batch reorder persisted successfully');
} catch (error) {
console.error('REORDER: Failed to persist batch reorder:', error);
// Socket will handle state recovery
console.log('REORDER: Socket will handle state recovery');
}
}, 500), // Shorter delay for batch updates
[projectId]
);
// Single task persistence (still used for other operations)
const debouncedPersistSingleTask = useMemo(
() => debounce(async (task: Task) => {
try {
console.log('REORDER: Persisting position change for task:', task.title, 'new position:', task.task_order);
// Update only the moved task
await projectService.updateTask(task.id, {
task_order: task.task_order
});
console.log('REORDER: Single task position persisted successfully');
} catch (error) {
console.error('REORDER: Failed to persist task position:', error);
console.log('REORDER: Socket will handle state recovery');
}
}, 800),
[projectId]
);
// Standard drag-and-drop reordering with sequential integers (like Jira/Trello/Linear)
const handleTaskReorder = useCallback((taskId: string, targetIndex: number, status: Task['status']) => {
console.log('REORDER: Moving task', taskId, 'to index', targetIndex, 'in status', status);
// Get all tasks in the target status, sorted by current order
const statusTasks = tasks
.filter(task => task.status === status)
.sort((a, b) => a.task_order - b.task_order);
const otherTasks = tasks.filter(task => task.status !== status);
// Find the moving task
const movingTaskIndex = statusTasks.findIndex(task => task.id === taskId);
if (movingTaskIndex === -1) {
console.log('REORDER: Task not found in status');
return;
}
// Prevent invalid moves
if (targetIndex < 0 || targetIndex >= statusTasks.length) {
console.log('REORDER: Invalid target index', targetIndex);
return;
}
// Skip if moving to same position
if (movingTaskIndex === targetIndex) {
console.log('REORDER: Task already in target position');
return;
}
console.log('REORDER: Moving task from position', movingTaskIndex, 'to', targetIndex);
// Remove the task from its current position and insert at target position
const reorderedTasks = [...statusTasks];
const [movedTask] = reorderedTasks.splice(movingTaskIndex, 1);
reorderedTasks.splice(targetIndex, 0, movedTask);
// Assign sequential order numbers (1, 2, 3, etc.) to all tasks in this status
const updatedStatusTasks = reorderedTasks.map((task, index) => ({
...task,
task_order: index + 1,
lastUpdate: Date.now()
}));
console.log('REORDER: New order:', updatedStatusTasks.map(t => `${t.title}:${t.task_order}`));
// Update UI immediately with all reordered tasks
const allUpdatedTasks = [...otherTasks, ...updatedStatusTasks];
updateTasks(allUpdatedTasks);
// Batch update to backend - only update tasks that changed position
const tasksToUpdate = updatedStatusTasks.filter((task, index) => {
const originalTask = statusTasks.find(t => t.id === task.id);
return originalTask && originalTask.task_order !== task.task_order;
});
console.log(`REORDER: Updating ${tasksToUpdate.length} tasks in backend`);
// Send batch update to backend (debounced)
debouncedPersistBatchReorder(tasksToUpdate);
}, [tasks, updateTasks, debouncedPersistBatchReorder]);
// Task move function (for board view) with optimistic UI update
const moveTask = async (taskId: string, newStatus: Task['status']) => {
console.log(`[TasksTab] Attempting to move task ${taskId} to new status: ${newStatus}`);
const movingTask = tasks.find(task => task.id === taskId);
if (!movingTask) {
console.warn(`[TasksTab] Task ${taskId} not found for move operation.`);
return;
}
const oldStatus = movingTask.status;
const newOrder = getNextOrderForStatus(newStatus);
const updatedTask = { ...movingTask, status: newStatus, task_order: newOrder };
console.log(`[TasksTab] Moving task ${movingTask.title} from ${oldStatus} to ${newStatus} with order ${newOrder}`);
// OPTIMISTIC UPDATE: Update UI immediately
setTasks(prev => {
const updated = prev.map(task => task.id === taskId ? updatedTask : task);
setTimeout(() => onTasksChange(updated), 0);
return updated;
});
console.log(`[TasksTab] Optimistically updated UI for task ${taskId}`);
try {
// Then update the backend
await projectService.updateTask(taskId, {
status: mapUIStatusToDBStatus(newStatus),
task_order: newOrder
});
console.log(`[TasksTab] Successfully updated task ${taskId} status in backend.`);
// Socket will confirm the update, but UI is already updated
} catch (error) {
console.error(`[TasksTab] Failed to move task ${taskId}, rolling back:`, error);
// ROLLBACK on error - restore original task
setTasks(prev => {
const updated = prev.map(task => task.id === taskId ? movingTask : task);
setTimeout(() => onTasksChange(updated), 0);
return updated;
});
alert(`Failed to move task: ${error instanceof Error ? error.message : 'Unknown error'}`);
}
};
const completeTask = (taskId: string) => {
console.log(`[TasksTab] Calling completeTask for ${taskId}`);
moveTask(taskId, 'complete');
};
const deleteTask = async (task: Task) => {
// Set the task to delete and show confirmation modal
setTaskToDelete(task);
setShowDeleteConfirm(true);
};
const confirmDeleteTask = async () => {
if (!taskToDelete) return;
try {
// Add to recently deleted cache to prevent race conditions
setRecentlyDeletedIds(prev => new Set(prev).add(taskToDelete.id));
// OPTIMISTIC UPDATE: Remove task from UI immediately
setTasks(prev => {
const updated = prev.filter(t => t.id !== taskToDelete.id);
setTimeout(() => onTasksChange(updated), 0);
return updated;
});
console.log(`[TasksTab] Optimistically removed task ${taskToDelete.id} from UI`);
// Then delete from backend
await projectService.deleteTask(taskToDelete.id);
console.log(`[TasksTab] Task ${taskToDelete.id} deletion confirmed by backend`);
// Clear from recently deleted cache after a delay (to catch any lingering socket events)
setTimeout(() => {
setRecentlyDeletedIds(prev => {
const newSet = new Set(prev);
newSet.delete(taskToDelete.id);
return newSet;
});
}, 3000); // 3 second window to ignore stale socket events
} catch (error) {
console.error('Failed to delete task:', error);
// Remove from recently deleted cache on error
setRecentlyDeletedIds(prev => {
const newSet = new Set(prev);
newSet.delete(taskToDelete.id);
return newSet;
});
// ROLLBACK on error - restore the task
setTasks(prev => {
const updated = [...prev, taskToDelete].sort((a, b) => a.task_order - b.task_order);
setTimeout(() => onTasksChange(updated), 0);
return updated;
});
console.log(`[TasksTab] Rolled back task deletion for ${taskToDelete.id}`);
// Re-throw to let the calling component handle the error display
throw error;
} finally {
setTaskToDelete(null);
setShowDeleteConfirm(false);
}
};
// Inline task creation function
const createTaskInline = async (newTask: Omit<Task, 'id'>) => {
try {
// Auto-assign next order number if not provided
const nextOrder = newTask.task_order || getNextOrderForStatus(newTask.status);
const createData: CreateTaskRequest = {
project_id: projectId,
title: newTask.title,
description: newTask.description,
status: mapUIStatusToDBStatus(newTask.status),
assignee: newTask.assignee?.name || 'User',
task_order: nextOrder,
...(newTask.feature && { feature: newTask.feature }),
...(newTask.featureColor && { featureColor: newTask.featureColor })
};
await projectService.createTask(createData);
// Don't reload tasks - let socket updates handle synchronization
console.log('[TasksTab] Task creation sent to backend, waiting for socket update');
} catch (error) {
console.error('Failed to create task:', error);
throw error;
}
};
// Inline task update function
const updateTaskInline = async (taskId: string, updates: Partial<Task>) => {
console.log(`[TasksTab] Inline update for task ${taskId} with updates:`, updates);
try {
const updateData: Partial<UpdateTaskRequest> = {};
if (updates.title !== undefined) updateData.title = updates.title;
if (updates.description !== undefined) updateData.description = updates.description;
if (updates.status !== undefined) {
console.log(`[TasksTab] Mapping UI status ${updates.status} to DB status.`);
updateData.status = mapUIStatusToDBStatus(updates.status);
console.log(`[TasksTab] Mapped status for ${taskId}: ${updates.status} -> ${updateData.status}`);
}
if (updates.assignee !== undefined) updateData.assignee = updates.assignee.name;
if (updates.task_order !== undefined) updateData.task_order = updates.task_order;
if (updates.feature !== undefined) updateData.feature = updates.feature;
if (updates.featureColor !== undefined) updateData.featureColor = updates.featureColor;
console.log(`[TasksTab] Sending update request for task ${taskId} to projectService:`, updateData);
await projectService.updateTask(taskId, updateData);
console.log(`[TasksTab] projectService.updateTask successful for ${taskId}.`);
// Don't update local state optimistically - let socket handle it
console.log(`[TasksTab] Waiting for socket update for task ${taskId}.`);
} catch (error) {
console.error(`[TasksTab] Failed to update task ${taskId} inline:`, error);
alert(`Failed to update task: ${error instanceof Error ? error.message : 'Unknown error'}`);
throw error;
}
};
// Get tasks for priority selection with descriptive labels
const getTasksForPrioritySelection = (status: Task['status']): Array<{value: number, label: string}> => {
const tasksInStatus = tasks
.filter(task => task.status === status && task.id !== editingTask?.id) // Exclude current task if editing
.sort((a, b) => a.task_order - b.task_order);
const options: Array<{value: number, label: string}> = [];
if (tasksInStatus.length === 0) {
// No tasks in this status
options.push({ value: 1, label: "1 - First task in this status" });
} else {
// Add option to be first
options.push({
value: 1,
label: `1 - Before "${tasksInStatus[0].title.substring(0, 30)}${tasksInStatus[0].title.length > 30 ? '...' : ''}"`
});
// Add options between existing tasks
for (let i = 0; i < tasksInStatus.length - 1; i++) {
const currentTask = tasksInStatus[i];
const nextTask = tasksInStatus[i + 1];
options.push({
value: i + 2,
label: `${i + 2} - After "${currentTask.title.substring(0, 20)}${currentTask.title.length > 20 ? '...' : ''}", Before "${nextTask.title.substring(0, 20)}${nextTask.title.length > 20 ? '...' : ''}"`
});
}
// Add option to be last
const lastTask = tasksInStatus[tasksInStatus.length - 1];
options.push({
value: tasksInStatus.length + 1,
label: `${tasksInStatus.length + 1} - After "${lastTask.title.substring(0, 30)}${lastTask.title.length > 30 ? '...' : ''}"`
});
}
return options;
};
// Memoized version of getTasksForPrioritySelection to prevent recalculation on every render
const memoizedGetTasksForPrioritySelection = useMemo(
() => getTasksForPrioritySelection,
[tasks, editingTask?.id]
);
return (
<DndProvider backend={HTML5Backend}>
<div className="min-h-[70vh] relative">
{/* Main content - Table or Board view */}
<div className="relative h-[calc(100vh-220px)] overflow-auto">
{viewMode === 'table' ? (
<TaskTableView
tasks={tasks}
onTaskView={openEditModal}
onTaskComplete={completeTask}
onTaskDelete={deleteTask}
onTaskReorder={handleTaskReorder}
onTaskCreate={createTaskInline}
onTaskUpdate={updateTaskInline}
/>
) : (
<TaskBoardView
tasks={tasks}
onTaskView={openEditModal}
onTaskComplete={completeTask}
onTaskDelete={deleteTask}
onTaskMove={moveTask}
onTaskReorder={handleTaskReorder}
/>
)}
</div>
{/* Fixed View Controls */}
<div className="fixed bottom-6 left-0 right-0 flex justify-center z-50 pointer-events-none">
<div className="flex items-center gap-4">
{/* WebSocket Status Indicator */}
<div className="flex items-center gap-2 px-3 py-2 bg-white/80 dark:bg-black/90 border border-gray-200 dark:border-gray-800 rounded-lg shadow-[0_0_20px_rgba(0,0,0,0.1)] dark:shadow-[0_0_20px_rgba(0,0,0,0.5)] backdrop-blur-md pointer-events-auto">
{isWebSocketConnected ? (
<>
<Wifi className="w-4 h-4 text-green-500" />
<span className="text-xs text-green-600 dark:text-green-400">Live</span>
</>
) : (
<>
<WifiOff className="w-4 h-4 text-red-500" />
<span className="text-xs text-red-600 dark:text-red-400">Offline</span>
</>
)}
</div>
{/* Add Task Button with Luminous Style */}
<button
onClick={() => {
const defaultOrder = getTasksForPrioritySelection('backlog')[0]?.value || 1;
setEditingTask({
id: '',
title: '',
description: '',
status: 'backlog',
assignee: { name: 'AI IDE Agent', avatar: '' },
feature: '',
featureColor: '#3b82f6',
task_order: defaultOrder
});
setIsModalOpen(true);
}}
className="relative px-5 py-2.5 flex items-center gap-2 bg-white/80 dark:bg-black/90 border border-gray-200 dark:border-gray-800 rounded-lg shadow-[0_0_20px_rgba(0,0,0,0.1)] dark:shadow-[0_0_20px_rgba(0,0,0,0.5)] backdrop-blur-md pointer-events-auto text-cyan-600 dark:text-cyan-400 hover:text-cyan-700 dark:hover:text-cyan-300 transition-all duration-300"
>
<Plus className="w-4 h-4 mr-1" />
<span>Add Task</span>
<span className="absolute bottom-0 left-[0%] right-[0%] w-[95%] mx-auto h-[2px] bg-cyan-500 shadow-[0_0_10px_2px_rgba(34,211,238,0.4)] dark:shadow-[0_0_20px_5px_rgba(34,211,238,0.7)]"></span>
</button>
{/* View Toggle Controls */}
<div className="flex items-center bg-white/80 dark:bg-black/90 border border-gray-200 dark:border-gray-800 rounded-lg overflow-hidden shadow-[0_0_20px_rgba(0,0,0,0.1)] dark:shadow-[0_0_20px_rgba(0,0,0,0.5)] backdrop-blur-md pointer-events-auto">
<button
onClick={() => setViewMode('table')}
className={`px-5 py-2.5 flex items-center gap-2 relative transition-all duration-300 ${viewMode === 'table' ? 'text-cyan-600 dark:text-cyan-400' : 'text-gray-600 dark:text-gray-400 hover:text-gray-800 dark:hover:text-gray-300'}`}
>
<Table className="w-4 h-4" />
<span>Table</span>
{viewMode === 'table' && <span className="absolute bottom-0 left-[15%] right-[15%] w-[70%] mx-auto h-[2px] bg-cyan-500 shadow-[0_0_10px_2px_rgba(34,211,238,0.4)] dark:shadow-[0_0_20px_5px_rgba(34,211,238,0.7)]"></span>}
</button>
<button
onClick={() => setViewMode('board')}
className={`px-5 py-2.5 flex items-center gap-2 relative transition-all duration-300 ${viewMode === 'board' ? 'text-purple-600 dark:text-purple-400' : 'text-gray-600 dark:text-gray-400 hover:text-gray-800 dark:hover:text-gray-300'}`}
>
<LayoutGrid className="w-4 h-4" />
<span>Board</span>
{viewMode === 'board' && <span className="absolute bottom-0 left-[15%] right-[15%] w-[70%] mx-auto h-[2px] bg-purple-500 shadow-[0_0_10px_2px_rgba(168,85,247,0.4)] dark:shadow-[0_0_20px_5px_rgba(168,85,247,0.7)]"></span>}
</button>
</div>
</div>
</div>
{/* Edit Task Modal */}
<EditTaskModal
isModalOpen={isModalOpen}
editingTask={editingTask}
projectFeatures={projectFeatures}
isLoadingFeatures={isLoadingFeatures}
isSavingTask={isSavingTask}
onClose={closeModal}
onSave={saveTask}
getTasksForPrioritySelection={memoizedGetTasksForPrioritySelection}
/>
{/* Delete Confirmation Modal */}
{showDeleteConfirm && taskToDelete && (
<DeleteConfirmModal
onConfirm={confirmDeleteTask}
onCancel={() => {
setTaskToDelete(null);
setShowDeleteConfirm(false);
}}
title="Archive Task"
message={`Are you sure you want to archive the task "${taskToDelete.title}"? You can restore it from the archived tasks view.`}
confirmText="Archive Task"
/>
)}
</div>
</DndProvider>
);
};