Files
remoteturtle/client/src/components/TaskPanel.jsx

430 lines
14 KiB
JavaScript
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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();
// Server returns flat array of formatted tasks
setTasks(Array.isArray(data) ? data : (data.tasks || []));
}
} 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;