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