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

447 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.
import React, { useState, useEffect } from 'react';
import './MiningAreasPanel.css';
export default function MiningAreasPanel({ turtles, selectedTurtle, apiUrl }) {
const [areas, setAreas] = useState([]);
const [showCreateForm, setShowCreateForm] = useState(false);
const [newArea, setNewArea] = useState({
areaName: '',
color: '#4a8c2a',
startX: '',
startY: '',
startZ: '',
endX: '',
endY: '',
endZ: '',
turtleID: ''
});
const [filterStatus, setFilterStatus] = useState('all'); // all, planned, mining, completed
// Load mining areas
useEffect(() => {
loadAreas();
const interval = setInterval(loadAreas, 5000);
return () => clearInterval(interval);
}, [apiUrl]);
const loadAreas = async () => {
try {
const response = await fetch(`${apiUrl}/api/mining-areas`);
if (response.ok) {
const data = await response.json();
// Server returns a flat array of formatted areas
setAreas(Array.isArray(data) ? data : (data.areas || []));
}
} catch (error) {
console.error('Failed to load mining areas:', error);
}
};
// Create new mining area
const handleCreateArea = async (e) => {
e.preventDefault();
if (!newArea.areaName || !newArea.turtleID) {
alert('Please provide area name and assign a turtle');
return;
}
// Validate coordinates
const coords = ['startX', 'startY', 'startZ', 'endX', 'endY', 'endZ'];
for (const coord of coords) {
if (newArea[coord] === '' || isNaN(Number(newArea[coord]))) {
alert(`Please provide valid ${coord} coordinate`);
return;
}
}
try {
const response = await fetch(`${apiUrl}/api/mining-areas`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
...newArea,
startX: Number(newArea.startX),
startY: Number(newArea.startY),
startZ: Number(newArea.startZ),
endX: Number(newArea.endX),
endY: Number(newArea.endY),
endZ: Number(newArea.endZ),
color: newArea.color,
status: 'planned'
})
});
if (response.ok) {
setShowCreateForm(false);
setNewArea({
areaName: '',
startX: '',
startY: '',
startZ: '',
endX: '',
endY: '',
endZ: '',
turtleID: ''
});
loadAreas();
} else {
const error = await response.json();
alert(`Failed to create area: ${error.error || 'Unknown error'}`);
}
} catch (error) {
console.error('Failed to create mining area:', error);
alert('Failed to create mining area');
}
};
// Delete mining area
const handleDeleteArea = async (areaID) => {
if (!confirm('Are you sure you want to delete this mining area?')) return;
try {
const response = await fetch(`${apiUrl}/api/mining-areas/${areaID}`, {
method: 'DELETE'
});
if (response.ok) {
loadAreas();
} else {
alert('Failed to delete area');
}
} catch (error) {
console.error('Failed to delete area:', error);
alert('Failed to delete area');
}
};
// Update area status
const handleUpdateStatus = async (areaID, newStatus) => {
try {
const response = await fetch(`${apiUrl}/api/mining-areas/${areaID}`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ status: newStatus })
});
if (response.ok) {
loadAreas();
} else {
alert('Failed to update status');
}
} catch (error) {
console.error('Failed to update status:', error);
alert('Failed to update status');
}
};
// Auto-fill from selected turtle position
const handleUseCurrentPosition = () => {
if (!selectedTurtle || !selectedTurtle.position) {
alert('No turtle selected or turtle has no position');
return;
}
const pos = selectedTurtle.position;
setNewArea(prev => ({
...prev,
startX: pos.x.toString(),
startY: pos.y.toString(),
startZ: pos.z.toString(),
turtleID: selectedTurtle.turtleID.toString()
}));
};
// Calculate volume
const calculateVolume = (area) => {
const width = Math.abs(area.endX - area.startX) + 1;
const height = Math.abs(area.endY - area.startY) + 1;
const depth = Math.abs(area.endZ - area.startZ) + 1;
return width * height * depth;
};
// Check for overlapping areas
const checkOverlap = (area1, area2) => {
const overlapX = Math.max(
Math.min(area1.endX, area2.endX) - Math.max(area1.startX, area2.startX) + 1,
0
);
const overlapY = Math.max(
Math.min(area1.endY, area2.endY) - Math.max(area1.startY, area2.startY) + 1,
0
);
const overlapZ = Math.max(
Math.min(area1.endZ, area2.endZ) - Math.max(area1.startZ, area2.startZ) + 1,
0
);
return overlapX > 0 && overlapY > 0 && overlapZ > 0;
};
// Find conflicts
const findConflicts = (area) => {
return areas.filter(other =>
other.areaID !== area.areaID &&
checkOverlap(area, other)
);
};
// Filter areas
const filteredAreas = areas.filter(area => {
if (filterStatus === 'all') return true;
return area.status === filterStatus;
});
// Status badge component
const StatusBadge = ({ status }) => {
const colors = {
planned: '#345ec3',
mining: '#ffaa00',
completed: '#4a8c2a'
};
return (
<span className="status-badge" style={{ backgroundColor: colors[status] || '#6b6b6b' }}>
{status}
</span>
);
};
return (
<div className="mining-areas-panel">
<div className="panel-header">
<h2> Mining Areas</h2>
<button
className="btn-create"
onClick={() => setShowCreateForm(!showCreateForm)}
>
{showCreateForm ? '✕ Cancel' : '+ New Area'}
</button>
</div>
{/* Filter buttons */}
<div className="filter-buttons">
<button
className={`filter-btn ${filterStatus === 'all' ? 'active' : ''}`}
onClick={() => setFilterStatus('all')}
>
All ({areas.length})
</button>
<button
className={`filter-btn ${filterStatus === 'planned' ? 'active' : ''}`}
onClick={() => setFilterStatus('planned')}
>
📋 Planned ({areas.filter(a => a.status === 'planned').length})
</button>
<button
className={`filter-btn ${filterStatus === 'mining' ? 'active' : ''}`}
onClick={() => setFilterStatus('mining')}
>
Mining ({areas.filter(a => a.status === 'mining').length})
</button>
<button
className={`filter-btn ${filterStatus === 'completed' ? 'active' : ''}`}
onClick={() => setFilterStatus('completed')}
>
Done ({areas.filter(a => a.status === 'completed').length})
</button>
</div>
{/* Create form */}
{showCreateForm && (
<div className="create-form">
<h3>Create Mining Area</h3>
<form onSubmit={handleCreateArea}>
<div className="form-group">
<label>Area Name:</label>
<input
type="text"
value={newArea.areaName}
onChange={(e) => setNewArea({ ...newArea, areaName: e.target.value })}
placeholder="e.g., Diamond Mine #1"
required
/>
</div>
<div className="form-group">
<label>Assign Turtle:</label>
<select
value={newArea.turtleID}
onChange={(e) => setNewArea({ ...newArea, turtleID: e.target.value })}
required
>
<option value="">Select turtle...</option>
{turtles.map(turtle => (
<option key={turtle.turtleID} value={turtle.turtleID}>
🐢 {turtle.turtleID} {turtle.label ? `- ${turtle.label}` : ''}
</option>
))}
</select>
</div>
<div className="coordinates-section">
<div className="coordinates-header">
<label>Start Position:</label>
<button
type="button"
className="btn-use-position"
onClick={handleUseCurrentPosition}
disabled={!selectedTurtle}
>
Use Current Position
</button>
</div>
<div className="coordinate-inputs">
<input
type="number"
placeholder="X"
value={newArea.startX}
onChange={(e) => setNewArea({ ...newArea, startX: e.target.value })}
required
/>
<input
type="number"
placeholder="Y"
value={newArea.startY}
onChange={(e) => setNewArea({ ...newArea, startY: e.target.value })}
required
/>
<input
type="number"
placeholder="Z"
value={newArea.startZ}
onChange={(e) => setNewArea({ ...newArea, startZ: e.target.value })}
required
/>
</div>
</div>
<div className="coordinates-section">
<label>End Position:</label>
<div className="coordinate-inputs">
<input
type="number"
placeholder="X"
value={newArea.endX}
onChange={(e) => setNewArea({ ...newArea, endX: e.target.value })}
required
/>
<input
type="number"
placeholder="Y"
value={newArea.endY}
onChange={(e) => setNewArea({ ...newArea, endY: e.target.value })}
required
/>
<input
type="number"
placeholder="Z"
value={newArea.endZ}
onChange={(e) => setNewArea({ ...newArea, endZ: e.target.value })}
required
/>
</div>
</div>
<button type="submit" className="btn-submit">
Create Area
</button>
</form>
</div>
)}
{/* Areas list */}
<div className="areas-list">
{filteredAreas.length === 0 ? (
<div className="empty-state">
<p>No mining areas {filterStatus !== 'all' ? `with status "${filterStatus}"` : ''}</p>
{filterStatus === 'all' && (
<p>Create a new area to get started</p>
)}
</div>
) : (
filteredAreas.map(area => {
const turtle = turtles.find(t => t.turtleID === area.turtleID);
const conflicts = findConflicts(area);
const volume = calculateVolume(area);
return (
<div key={area.areaID} className={`area-card ${conflicts.length > 0 ? 'has-conflict' : ''}`}>
<div className="area-header">
<h3>{area.areaName}</h3>
<StatusBadge status={area.status} />
</div>
<div className="area-info">
<div className="info-row">
<span className="info-label">Turtle:</span>
<span className="info-value">
🐢 {turtle ? `${turtle.turtleID} ${turtle.label || ''}` : area.turtleID}
</span>
</div>
<div className="info-row">
<span className="info-label">Coordinates:</span>
<span className="info-value coordinate-value">
({area.startX}, {area.startY}, {area.startZ})
({area.endX}, {area.endY}, {area.endZ})
</span>
</div>
<div className="info-row">
<span className="info-label">Volume:</span>
<span className="info-value">{volume.toLocaleString()} blocks</span>
</div>
{area.createdAt && (
<div className="info-row">
<span className="info-label">Created:</span>
<span className="info-value">
{new Date(area.createdAt).toLocaleDateString()}
</span>
</div>
)}
</div>
{conflicts.length > 0 && (
<div className="conflict-warning">
Overlaps with {conflicts.length} other area(s):
<ul>
{conflicts.map(c => (
<li key={c.areaID}>{c.areaName}</li>
))}
</ul>
</div>
)}
<div className="area-actions">
{area.status === 'planned' && (
<button
className="btn-action btn-start"
onClick={() => handleUpdateStatus(area.areaID, 'mining')}
>
Start Mining
</button>
)}
{area.status === 'mining' && (
<button
className="btn-action btn-complete"
onClick={() => handleUpdateStatus(area.areaID, 'completed')}
>
Mark Complete
</button>
)}
<button
className="btn-action btn-delete"
onClick={() => handleDeleteArea(area.areaID)}
>
🗑 Delete
</button>
</div>
</div>
);
})
)}
</div>
</div>
);
}