119 lines
4.9 KiB
JavaScript
119 lines
4.9 KiB
JavaScript
import React, { useEffect, useState } from 'react';
|
|
import { mowingAPI } from '../../services/api';
|
|
import LoadingSpinner from '../../components/UI/LoadingSpinner';
|
|
import MowingPlanModal from '../../components/Mowing/MowingPlanModal';
|
|
import MowingExecutionModal from '../../components/Mowing/MowingExecutionModal';
|
|
import MowingSessionViewModal from '../../components/Mowing/MowingSessionViewModal';
|
|
import toast from 'react-hot-toast';
|
|
|
|
const Mowing = () => {
|
|
const [plans, setPlans] = useState([]);
|
|
const [loading, setLoading] = useState(true);
|
|
const [showPlanModal, setShowPlanModal] = useState(false);
|
|
const [execPlan, setExecPlan] = useState(null);
|
|
const [sessions, setSessions] = useState([]);
|
|
const [viewSession, setViewSession] = useState(null);
|
|
|
|
const fetchPlans = async () => {
|
|
try {
|
|
setLoading(true);
|
|
const r = await mowingAPI.getPlans();
|
|
setPlans(r.data.data.plans || []);
|
|
// also load recent sessions
|
|
try {
|
|
const s = await mowingAPI.getLogs();
|
|
setSessions(s.data?.data?.logs || []);
|
|
} catch {}
|
|
} catch (e) {
|
|
toast.error('Failed to load mowing plans');
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
};
|
|
|
|
useEffect(() => { fetchPlans(); }, []);
|
|
|
|
if (loading) return (<div className="p-6"><LoadingSpinner /></div>);
|
|
|
|
return (
|
|
<div className="p-6">
|
|
<div className="flex justify-between items-center mb-6">
|
|
<h1 className="text-2xl font-bold text-gray-900">Mowing</h1>
|
|
<button className="btn-primary" onClick={()=> setShowPlanModal(true)}>New Plan</button>
|
|
</div>
|
|
|
|
<div className="bg-white rounded-lg shadow overflow-hidden">
|
|
<div className="grid grid-cols-7 gap-4 p-4 border-b text-sm font-medium text-gray-600">
|
|
<div>Property</div>
|
|
<div>Areas</div>
|
|
<div>Date</div>
|
|
<div>Mower</div>
|
|
<div>Cut Height</div>
|
|
<div>Status</div>
|
|
<div>Actions</div>
|
|
</div>
|
|
{plans.length === 0 ? (
|
|
<div className="p-4 text-gray-600">No mowing plans yet.</div>
|
|
) : plans.map((p) => (
|
|
<div key={p.id} className="grid grid-cols-7 gap-4 p-4 border-b text-sm items-center">
|
|
<div className="font-medium">{p.property_name}</div>
|
|
<div className="truncate" title={p.section_names}>{p.section_names}</div>
|
|
<div>{new Date(p.planned_date).toLocaleDateString()}</div>
|
|
<div>{p.equipment_name || '—'}</div>
|
|
<div>{p.cut_height_inches}"</div>
|
|
<div><span className={`px-2 py-1 rounded text-xs ${p.status==='completed'?'bg-green-100 text-green-800': p.status==='planned'?'bg-blue-100 text-blue-800': p.status==='archived'?'bg-gray-100 text-gray-800':'bg-yellow-100 text-yellow-800'}`}>{p.status}</span></div>
|
|
<div className="flex gap-2">
|
|
<button className="btn-secondary" onClick={()=> setExecPlan(p)}>Execute</button>
|
|
{p.status !== 'archived' && (
|
|
<button className="px-3 py-1 text-xs rounded border" onClick={async ()=>{ await mowingAPI.updatePlanStatus(p.id, 'archived'); fetchPlans(); }}>Archive</button>
|
|
)}
|
|
</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
|
|
{showPlanModal && (
|
|
<MowingPlanModal onClose={()=> setShowPlanModal(false)} onCreated={fetchPlans} />
|
|
)}
|
|
{execPlan && (
|
|
<MowingExecutionModal plan={execPlan} onClose={()=> setExecPlan(null)} onComplete={fetchPlans} />
|
|
)}
|
|
|
|
{/* Recent Sessions */}
|
|
<div className="mt-8">
|
|
<h2 className="text-lg font-semibold mb-3">Recent Sessions</h2>
|
|
<div className="bg-white rounded-lg shadow overflow-hidden">
|
|
<div className="grid grid-cols-6 gap-4 p-4 border-b text-sm font-medium text-gray-600">
|
|
<div>Property</div>
|
|
<div>Mower</div>
|
|
<div>Cut Height</div>
|
|
<div>Avg Speed</div>
|
|
<div>Distance</div>
|
|
<div>Actions</div>
|
|
</div>
|
|
{sessions.length === 0 ? (
|
|
<div className="p-4 text-gray-600">No sessions yet.</div>
|
|
) : sessions.map((s) => (
|
|
<div key={s.id} className="grid grid-cols-6 gap-4 p-4 border-b text-sm items-center">
|
|
<div className="font-medium">{s.property_name}</div>
|
|
<div>{s.equipment_name || '—'}</div>
|
|
<div>{Number(s.cut_height_inches || 0).toFixed(2)}"</div>
|
|
<div>{(s.average_speed_mph || s.averageSpeed || 0).toFixed?.(1) || Number(s.averageSpeed || 0).toFixed(1)} mph</div>
|
|
<div>{Math.round(((s.total_distance_meters || s.gpsTrack?.totalDistance || 0) * 3.28084) || 0)} ft</div>
|
|
<div>
|
|
<button className="btn-secondary" onClick={()=> setViewSession(s)}>View</button>
|
|
</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
|
|
{viewSession && (
|
|
<MowingSessionViewModal session={viewSession} onClose={()=> setViewSession(null)} />
|
|
)}
|
|
</div>
|
|
);
|
|
};
|
|
|
|
export default Mowing;
|