447 lines
14 KiB
JavaScript
447 lines
14 KiB
JavaScript
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>
|
||
);
|
||
}
|