feat: Add TaskPanel component for managing tasks with create, update, and delete functionalities
This commit is contained in:
428
client/src/components/TaskPanel.jsx
Normal file
428
client/src/components/TaskPanel.jsx
Normal file
@@ -0,0 +1,428 @@
|
|||||||
|
import { useState, useEffect } from 'react';
|
||||||
|
import './TaskPanel.css';
|
||||||
|
|
||||||
|
const TaskPanel = ({ turtles, apiUrl }) => {
|
||||||
|
const [tasks, setTasks] = useState([]);
|
||||||
|
const [filter, setFilter] = useState('all'); // all, pending, in_progress, completed, failed
|
||||||
|
const [showCreateForm, setShowCreateForm] = useState(false);
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
const [message, setMessage] = useState(null);
|
||||||
|
|
||||||
|
// Form state
|
||||||
|
const [taskType, setTaskType] = useState('mine_area');
|
||||||
|
const [priority, setPriority] = useState(5);
|
||||||
|
const [assignedTurtleId, setAssignedTurtleId] = useState('');
|
||||||
|
const [parameters, setParameters] = useState({
|
||||||
|
x1: '', y1: '', z1: '',
|
||||||
|
x2: '', y2: '', z2: ''
|
||||||
|
});
|
||||||
|
|
||||||
|
const taskTypes = [
|
||||||
|
{ value: 'mine_area', label: '⛏️ Mine Area', icon: '⛏️' },
|
||||||
|
{ value: 'explore', label: '🔍 Explore', icon: '🔍' },
|
||||||
|
{ value: 'gather', label: '📦 Gather Resources', icon: '📦' },
|
||||||
|
{ value: 'build', label: '🏗️ Build Structure', icon: '🏗️' },
|
||||||
|
{ value: 'transport', label: '🚚 Transport Items', icon: '🚚' },
|
||||||
|
{ value: 'clear_area', label: '🧹 Clear Area', icon: '🧹' },
|
||||||
|
];
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
loadTasks();
|
||||||
|
|
||||||
|
// Refresh tasks every 10 seconds
|
||||||
|
const interval = setInterval(loadTasks, 10000);
|
||||||
|
return () => clearInterval(interval);
|
||||||
|
}, [filter]);
|
||||||
|
|
||||||
|
const loadTasks = async () => {
|
||||||
|
setLoading(true);
|
||||||
|
try {
|
||||||
|
const url = filter === 'all'
|
||||||
|
? `${apiUrl}/api/tasks`
|
||||||
|
: `${apiUrl}/api/tasks?status=${filter}`;
|
||||||
|
|
||||||
|
const response = await fetch(url);
|
||||||
|
if (response.ok) {
|
||||||
|
const data = await response.json();
|
||||||
|
setTasks(data);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to load tasks:', error);
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const createTask = async (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
|
||||||
|
// Validate coordinates
|
||||||
|
const coords = ['x1', 'y1', 'z1', 'x2', 'y2', 'z2'];
|
||||||
|
const hasCoords = coords.some(key => parameters[key] !== '');
|
||||||
|
|
||||||
|
if (hasCoords && !coords.every(key => parameters[key] !== '')) {
|
||||||
|
showMessage('Please fill all coordinates or leave them empty', 'error');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const taskData = {
|
||||||
|
taskType,
|
||||||
|
priority: parseInt(priority),
|
||||||
|
assignedTurtleId: assignedTurtleId ? parseInt(assignedTurtleId) : null,
|
||||||
|
parameters: hasCoords ? {
|
||||||
|
x1: parseInt(parameters.x1),
|
||||||
|
y1: parseInt(parameters.y1),
|
||||||
|
z1: parseInt(parameters.z1),
|
||||||
|
x2: parseInt(parameters.x2),
|
||||||
|
y2: parseInt(parameters.y2),
|
||||||
|
z2: parseInt(parameters.z2)
|
||||||
|
} : {}
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch(`${apiUrl}/api/tasks`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify(taskData)
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.ok) {
|
||||||
|
showMessage('Task created successfully!', 'success');
|
||||||
|
setShowCreateForm(false);
|
||||||
|
resetForm();
|
||||||
|
loadTasks();
|
||||||
|
} else {
|
||||||
|
showMessage('Failed to create task', 'error');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
showMessage('Error creating task', 'error');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const updateTaskStatus = async (taskId, status, result = null) => {
|
||||||
|
try {
|
||||||
|
const response = await fetch(`${apiUrl}/api/tasks/${taskId}`, {
|
||||||
|
method: 'PUT',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ status, result })
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.ok) {
|
||||||
|
showMessage('Task updated', 'success');
|
||||||
|
loadTasks();
|
||||||
|
} else {
|
||||||
|
showMessage('Failed to update task', 'error');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
showMessage('Error updating task', 'error');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const deleteTask = async (taskId) => {
|
||||||
|
if (!confirm('Are you sure you want to delete this task?')) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch(`${apiUrl}/api/tasks/${taskId}`, {
|
||||||
|
method: 'DELETE'
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.ok) {
|
||||||
|
showMessage('Task deleted', 'success');
|
||||||
|
loadTasks();
|
||||||
|
} else {
|
||||||
|
showMessage('Failed to delete task', 'error');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
showMessage('Error deleting task', 'error');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const resetForm = () => {
|
||||||
|
setTaskType('mine_area');
|
||||||
|
setPriority(5);
|
||||||
|
setAssignedTurtleId('');
|
||||||
|
setParameters({ x1: '', y1: '', z1: '', x2: '', y2: '', z2: '' });
|
||||||
|
};
|
||||||
|
|
||||||
|
const showMessage = (text, type) => {
|
||||||
|
setMessage({ text, type });
|
||||||
|
setTimeout(() => setMessage(null), 3000);
|
||||||
|
};
|
||||||
|
|
||||||
|
const getTaskIcon = (type) => {
|
||||||
|
const taskType = taskTypes.find(t => t.value === type);
|
||||||
|
return taskType ? taskType.icon : '📋';
|
||||||
|
};
|
||||||
|
|
||||||
|
const getStatusColor = (status) => {
|
||||||
|
switch (status) {
|
||||||
|
case 'pending': return '#94a3b8';
|
||||||
|
case 'in_progress': return '#3b82f6';
|
||||||
|
case 'completed': return '#10b981';
|
||||||
|
case 'failed': return '#ef4444';
|
||||||
|
default: return '#94a3b8';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const getPriorityLabel = (priority) => {
|
||||||
|
if (priority >= 8) return { label: 'Critical', color: '#ef4444' };
|
||||||
|
if (priority >= 6) return { label: 'High', color: '#f59e0b' };
|
||||||
|
if (priority >= 4) return { label: 'Medium', color: '#3b82f6' };
|
||||||
|
return { label: 'Low', color: '#94a3b8' };
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="task-panel">
|
||||||
|
<div className="task-header">
|
||||||
|
<h2>📋 Task Queue</h2>
|
||||||
|
<button
|
||||||
|
className="create-task-btn"
|
||||||
|
onClick={() => setShowCreateForm(!showCreateForm)}
|
||||||
|
>
|
||||||
|
{showCreateForm ? '✕ Cancel' : '➕ New Task'}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{message && (
|
||||||
|
<div className={`message ${message.type}`}>
|
||||||
|
{message.text}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Create Task Form */}
|
||||||
|
{showCreateForm && (
|
||||||
|
<div className="create-task-form">
|
||||||
|
<h3>Create New Task</h3>
|
||||||
|
<form onSubmit={createTask}>
|
||||||
|
<div className="form-row">
|
||||||
|
<label>
|
||||||
|
Task Type:
|
||||||
|
<select value={taskType} onChange={(e) => setTaskType(e.target.value)}>
|
||||||
|
{taskTypes.map(type => (
|
||||||
|
<option key={type.value} value={type.value}>
|
||||||
|
{type.label}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<label>
|
||||||
|
Priority (1-10):
|
||||||
|
<input
|
||||||
|
type="range"
|
||||||
|
min="1"
|
||||||
|
max="10"
|
||||||
|
value={priority}
|
||||||
|
onChange={(e) => setPriority(e.target.value)}
|
||||||
|
/>
|
||||||
|
<span className="priority-value" style={{ color: getPriorityLabel(priority).color }}>
|
||||||
|
{priority} - {getPriorityLabel(priority).label}
|
||||||
|
</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<label>
|
||||||
|
Assign to Turtle (optional):
|
||||||
|
<select value={assignedTurtleId} onChange={(e) => setAssignedTurtleId(e.target.value)}>
|
||||||
|
<option value="">Any available turtle</option>
|
||||||
|
{turtles.map(turtle => (
|
||||||
|
<option key={turtle.turtleID} value={turtle.turtleID}>
|
||||||
|
🐢 {turtle.name || `Turtle ${turtle.turtleID}`}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<div className="coordinates-section">
|
||||||
|
<h4>Coordinates (optional):</h4>
|
||||||
|
<div className="coordinates-grid">
|
||||||
|
<div className="coord-group">
|
||||||
|
<span>Start:</span>
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
placeholder="X1"
|
||||||
|
value={parameters.x1}
|
||||||
|
onChange={(e) => setParameters({...parameters, x1: e.target.value})}
|
||||||
|
/>
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
placeholder="Y1"
|
||||||
|
value={parameters.y1}
|
||||||
|
onChange={(e) => setParameters({...parameters, y1: e.target.value})}
|
||||||
|
/>
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
placeholder="Z1"
|
||||||
|
value={parameters.z1}
|
||||||
|
onChange={(e) => setParameters({...parameters, z1: e.target.value})}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="coord-group">
|
||||||
|
<span>End:</span>
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
placeholder="X2"
|
||||||
|
value={parameters.x2}
|
||||||
|
onChange={(e) => setParameters({...parameters, x2: e.target.value})}
|
||||||
|
/>
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
placeholder="Y2"
|
||||||
|
value={parameters.y2}
|
||||||
|
onChange={(e) => setParameters({...parameters, y2: e.target.value})}
|
||||||
|
/>
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
placeholder="Z2"
|
||||||
|
value={parameters.z2}
|
||||||
|
onChange={(e) => setParameters({...parameters, z2: e.target.value})}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button type="submit" className="submit-btn">
|
||||||
|
Create Task
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Filter Tabs */}
|
||||||
|
<div className="task-filters">
|
||||||
|
<button
|
||||||
|
className={filter === 'all' ? 'active' : ''}
|
||||||
|
onClick={() => setFilter('all')}
|
||||||
|
>
|
||||||
|
All ({tasks.length})
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
className={filter === 'pending' ? 'active' : ''}
|
||||||
|
onClick={() => setFilter('pending')}
|
||||||
|
>
|
||||||
|
Pending
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
className={filter === 'in_progress' ? 'active' : ''}
|
||||||
|
onClick={() => setFilter('in_progress')}
|
||||||
|
>
|
||||||
|
In Progress
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
className={filter === 'completed' ? 'active' : ''}
|
||||||
|
onClick={() => setFilter('completed')}
|
||||||
|
>
|
||||||
|
Completed
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
className={filter === 'failed' ? 'active' : ''}
|
||||||
|
onClick={() => setFilter('failed')}
|
||||||
|
>
|
||||||
|
Failed
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Tasks List */}
|
||||||
|
<div className="tasks-list">
|
||||||
|
{loading && <div className="loading">Loading tasks...</div>}
|
||||||
|
|
||||||
|
{!loading && tasks.length === 0 && (
|
||||||
|
<div className="empty-state">
|
||||||
|
<div className="empty-icon">📋</div>
|
||||||
|
<div className="empty-title">No Tasks Found</div>
|
||||||
|
<div className="empty-text">
|
||||||
|
{filter === 'all'
|
||||||
|
? 'Create a task to get started!'
|
||||||
|
: `No ${filter.replace('_', ' ')} tasks`}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{tasks.map(task => {
|
||||||
|
const priorityInfo = getPriorityLabel(task.priority);
|
||||||
|
const turtle = turtles.find(t => t.turtleID === task.assignedTurtleId);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div key={task.taskId} className="task-card">
|
||||||
|
<div className="task-card-header">
|
||||||
|
<div className="task-title">
|
||||||
|
<span className="task-icon">{getTaskIcon(task.taskType)}</span>
|
||||||
|
<span className="task-type">
|
||||||
|
{taskTypes.find(t => t.value === task.taskType)?.label || task.taskType}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className="task-badges">
|
||||||
|
<span
|
||||||
|
className="priority-badge"
|
||||||
|
style={{ backgroundColor: priorityInfo.color }}
|
||||||
|
>
|
||||||
|
{priorityInfo.label}
|
||||||
|
</span>
|
||||||
|
<span
|
||||||
|
className="status-badge"
|
||||||
|
style={{ backgroundColor: getStatusColor(task.status) }}
|
||||||
|
>
|
||||||
|
{task.status.replace('_', ' ')}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{task.assignedTurtleId && (
|
||||||
|
<div className="task-assignment">
|
||||||
|
🐢 {turtle?.name || `Turtle ${task.assignedTurtleId}`}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{task.parameters && Object.keys(task.parameters).length > 0 && (
|
||||||
|
<div className="task-parameters">
|
||||||
|
<strong>Area:</strong> ({task.parameters.x1}, {task.parameters.y1}, {task.parameters.z1}) →
|
||||||
|
({task.parameters.x2}, {task.parameters.y2}, {task.parameters.z2})
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{task.result && (
|
||||||
|
<div className="task-result">
|
||||||
|
<strong>Result:</strong> {task.result}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className="task-actions">
|
||||||
|
{task.status === 'pending' && (
|
||||||
|
<>
|
||||||
|
<button onClick={() => updateTaskStatus(task.taskId, 'in_progress')}>
|
||||||
|
▶️ Start
|
||||||
|
</button>
|
||||||
|
<button onClick={() => deleteTask(task.taskId)}>
|
||||||
|
🗑️ Delete
|
||||||
|
</button>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
{task.status === 'in_progress' && (
|
||||||
|
<>
|
||||||
|
<button onClick={() => updateTaskStatus(task.taskId, 'completed', 'Task completed')}>
|
||||||
|
✅ Complete
|
||||||
|
</button>
|
||||||
|
<button onClick={() => updateTaskStatus(task.taskId, 'failed', 'Task failed')}>
|
||||||
|
❌ Fail
|
||||||
|
</button>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
{(task.status === 'completed' || task.status === 'failed') && (
|
||||||
|
<button onClick={() => deleteTask(task.taskId)}>
|
||||||
|
🗑️ Delete
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="task-timestamp">
|
||||||
|
Created: {new Date(task.createdAt).toLocaleString()}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default TaskPanel;
|
||||||
Reference in New Issue
Block a user