mowing history

This commit is contained in:
Jake Kasper
2025-09-03 10:16:32 -04:00
parent 7561d32171
commit 30875cb125
3 changed files with 88 additions and 81 deletions

View File

@@ -84,11 +84,16 @@ router.post('/sessions', validateRequest(mowingSessionSchema), async (req, res,
router.get('/sessions', async (req, res, next) => { router.get('/sessions', async (req, res, next) => {
try { try {
const result = await pool.query( const result = await pool.query(
`SELECT ms.*, p.name as property_name, ue.custom_name as equipment_name `SELECT ms.*, p.name as property_name, ue.custom_name as equipment_name,
STRING_AGG(ls.name, ', ') as section_names,
SUM(ls.area) as total_area
FROM mowing_sessions ms FROM mowing_sessions ms
JOIN properties p ON ms.property_id=p.id JOIN properties p ON ms.property_id=p.id
LEFT JOIN user_equipment ue ON ms.equipment_id=ue.id LEFT JOIN user_equipment ue ON ms.equipment_id=ue.id
JOIN mowing_session_sections lss ON lss.session_id = ms.id
JOIN lawn_sections ls ON lss.lawn_section_id = ls.id
WHERE ms.user_id=$1 WHERE ms.user_id=$1
GROUP BY ms.id, p.name, ue.custom_name
ORDER BY ms.created_at DESC ORDER BY ms.created_at DESC
LIMIT 200`, LIMIT 200`,
[req.user.id] [req.user.id]
@@ -214,11 +219,16 @@ router.put('/plans/:id/status', async (req, res, next) => {
router.get('/logs', async (req, res, next) => { router.get('/logs', async (req, res, next) => {
try { try {
const rs = await pool.query( const rs = await pool.query(
`SELECT ms.*, p.name as property_name, ue.custom_name as equipment_name `SELECT ms.*, p.name as property_name, ue.custom_name as equipment_name,
STRING_AGG(ls.name, ', ') as section_names,
SUM(ls.area) as total_area
FROM mowing_sessions ms FROM mowing_sessions ms
JOIN properties p ON ms.property_id=p.id JOIN properties p ON ms.property_id=p.id
LEFT JOIN user_equipment ue ON ms.equipment_id=ue.id LEFT JOIN user_equipment ue ON ms.equipment_id=ue.id
JOIN mowing_session_sections lss ON lss.session_id = ms.id
JOIN lawn_sections ls ON lss.lawn_section_id = ls.id
WHERE ms.user_id=$1 WHERE ms.user_id=$1
GROUP BY ms.id, p.name, ue.custom_name
ORDER BY ms.created_at DESC ORDER BY ms.created_at DESC
LIMIT 200`, [req.user.id]); LIMIT 200`, [req.user.id]);
res.json({ success: true, data: { logs: rs.rows } }); res.json({ success: true, data: { logs: rs.rows } });

View File

@@ -368,6 +368,19 @@ const History = () => {
return items; return items;
})(); })();
// Coverage calculation for mowing when shown in unified list
const calculateMowingCoverage = (log) => {
const meters = (log.total_distance_meters || log.gpsTrack?.totalDistance || log.gps_track?.totalDistance || 0) || 0;
const totalDistanceFeet = meters * 3.28084;
const plannedArea = Number(log.total_area || 0);
if (totalDistanceFeet === 0 || plannedArea === 0) return 0;
let equipmentWidth = 6; // default mower deck width in ft
const name = (log.equipment_name || '').toLowerCase();
if (name.includes('30"') || name.includes('30 in')) equipmentWidth = 2.5;
const theoreticalCoverageArea = totalDistanceFeet * equipmentWidth;
return Math.min(100, Math.round((theoreticalCoverageArea / plannedArea) * 100));
};
if (loading) { if (loading) {
return ( return (
<div className="p-6"> <div className="p-6">
@@ -655,9 +668,12 @@ const History = () => {
<div className="flex justify-between items-start"> <div className="flex justify-between items-start">
<div className="flex-1"> <div className="flex-1">
<div className="flex items-center justify-between mb-4"> <div className="flex items-center justify-between mb-4">
<h3 className="text-lg font-semibold text-gray-900"> <div className="flex items-center gap-2">
{application.propertyName} - {application.sectionNames} <h3 className="text-lg font-semibold text-gray-900">
</h3> {application.propertyName} - {application.sectionNames}
</h3>
<span className="px-2 py-0.5 text-xs rounded-full bg-indigo-100 text-indigo-800">Application</span>
</div>
<span className={`px-3 py-1 text-sm font-medium rounded-full ${ <span className={`px-3 py-1 text-sm font-medium rounded-full ${
application.status === 'archived' application.status === 'archived'
? 'bg-gray-100 text-gray-800' ? 'bg-gray-100 text-gray-800'
@@ -755,23 +771,66 @@ const History = () => {
// Mowing entry // Mowing entry
const log = item.log; const log = item.log;
const durationMin = Math.round((log.duration_seconds || log.durationSeconds || log.gpsTrack?.duration || 0) / 60); const durationMin = Math.round((log.duration_seconds || log.durationSeconds || log.gpsTrack?.duration || log.gps_track?.duration || 0) / 60);
const avg = (log.average_speed_mph || log.averageSpeed || 0).toFixed?.(1) || Number(log.averageSpeed || 0).toFixed(1); const avg = (log.average_speed_mph || log.averageSpeed || 0).toFixed?.(1) || Number(log.averageSpeed || 0).toFixed(1);
const distFeet = Math.round(((log.total_distance_meters || log.gpsTrack?.totalDistance || 0) * 3.28084) || 0); const distFeet = Math.round(((log.total_distance_meters || log.gpsTrack?.totalDistance || log.gps_track?.totalDistance || 0) * 3.28084) || 0);
return ( return (
<div key={`mow-${log.id}`} className="bg-white p-6 rounded-lg shadow hover:shadow-lg transition-shadow"> <div key={`mow-${log.id}`} className="bg-white p-6 rounded-lg shadow hover:shadow-lg transition-shadow">
<div className="flex items-center justify-between"> <div className="flex justify-between items-start">
<div> <div className="flex-1">
<div className="font-medium text-gray-900">{log.property_name || 'Property'}</div> <div className="flex items-center justify-between mb-4">
<div className="text-sm text-gray-600">{new Date(item.date).toLocaleString()} {log.equipment_name || 'Mower'} {avg} mph {distFeet} ft {durationMin} min</div> <div className="flex items-center gap-2">
<h3 className="text-lg font-semibold text-gray-900">
{(log.property_name || 'Property')} - {(log.section_names || 'Areas')}
</h3>
<span className="px-2 py-0.5 text-xs rounded-full bg-green-100 text-green-800">Mowing</span>
</div>
<span className="px-3 py-1 text-sm font-medium rounded-full bg-green-100 text-green-800">Completed</span>
</div>
<div className="grid grid-cols-1 md:grid-cols-3 gap-4 mb-4">
<div className="flex items-center text-sm text-gray-600">
<CalendarIcon className="h-4 w-4 mr-2" />
{new Date(item.date).toLocaleString()}
</div>
<div className="flex items-center text-sm text-gray-600">
<MapPinIcon className="h-4 w-4 mr-2" />
{log.property_name}
</div>
<div className="flex items-center text-sm text-gray-600">
<WrenchScrewdriverIcon className="h-4 w-4 mr-2" />
{log.equipment_name || 'Mower'}
</div>
</div>
<div className="grid grid-cols-2 md:grid-cols-4 gap-2 mb-4">
<div className="bg-blue-50 p-2 rounded text-center">
<div className="text-xs text-blue-600 font-medium">Duration</div>
<div className="text-sm font-bold text-blue-900">{durationMin} min</div>
</div>
<div className="bg-green-50 p-2 rounded text-center">
<div className="text-xs text-green-600 font-medium">GPS Points</div>
<div className="text-sm font-bold text-green-900">{log.gpsTrack?.points?.length || log.gps_track?.points?.length || 0}</div>
</div>
<div className="bg-purple-50 p-2 rounded text-center">
<div className="text-xs text-purple-600 font-medium">Distance</div>
<div className="text-sm font-bold text-purple-900">{distFeet} ft</div>
</div>
<div className="bg-orange-50 p-2 rounded text-center">
<div className="text-xs text-orange-600 font-medium">Coverage</div>
<div className="text-sm font-bold text-orange-900">{calculateMowingCoverage(log)}%</div>
</div>
</div>
</div>
<div className="ml-4">
<button
onClick={() => setViewingMowingSession(log)}
className="p-2 text-indigo-600 hover:text-indigo-800 hover:bg-indigo-50 rounded"
title="View mowing session"
>
<EyeIcon className="h-5 w-5" />
</button>
</div> </div>
<button
onClick={() => setViewingMowingSession(log)}
className="p-2 text-indigo-600 hover:text-indigo-800 hover:bg-indigo-50 rounded"
title="View mowing session"
>
<EyeIcon className="h-5 w-5" />
</button>
</div> </div>
</div> </div>
); );

View File

@@ -12,7 +12,6 @@ const Mowing = () => {
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
const [showPlanModal, setShowPlanModal] = useState(false); const [showPlanModal, setShowPlanModal] = useState(false);
const [execPlan, setExecPlan] = useState(null); const [execPlan, setExecPlan] = useState(null);
const [sessions, setSessions] = useState([]);
const [viewSession, setViewSession] = useState(null); const [viewSession, setViewSession] = useState(null);
const fetchPlans = async () => { const fetchPlans = async () => {
@@ -20,11 +19,6 @@ const Mowing = () => {
setLoading(true); setLoading(true);
const r = await mowingAPI.getPlans(); const r = await mowingAPI.getPlans();
setPlans(r.data.data.plans || []); setPlans(r.data.data.plans || []);
// also load recent sessions
try {
const s = await mowingAPI.getLogs();
setSessions(s.data?.data?.logs || []);
} catch {}
} catch (e) { } catch (e) {
toast.error('Failed to load mowing plans'); toast.error('Failed to load mowing plans');
} finally { } finally {
@@ -75,29 +69,6 @@ const Mowing = () => {
{p.notes && ( {p.notes && (
<p className="text-sm text-gray-500 mt-1 italic">"{p.notes}"</p> <p className="text-sm text-gray-500 mt-1 italic">"{p.notes}"</p>
)} )}
{/* Recent sessions for this property */}
{sessions.filter(s => s.property_id === p.property_id).length > 0 && (
<div className="mt-3">
<h4 className="text-sm font-semibold text-gray-800 mb-2">Recent Sessions</h4>
<div className="divide-y border rounded-md">
{sessions.filter(s => s.property_id === p.property_id).slice(0,3).map(s => (
<div key={s.id} className="flex items-center justify-between p-2 text-sm">
<div className="text-gray-700">
{new Date(s.created_at || s.session_date).toLocaleString()} • {(s.average_speed_mph || s.averageSpeed || 0).toFixed?.(1) || Number(s.averageSpeed || 0).toFixed(1)} mph • {Math.round(((s.total_distance_meters || s.gpsTrack?.totalDistance || 0) * 3.28084) || 0)} ft
</div>
<button
className="p-1 text-indigo-600 hover:text-indigo-800 hover:bg-indigo-50 rounded"
title="View session"
onClick={()=> setViewSession(s)}
>
<EyeIcon className="h-4 w-4" />
</button>
</div>
))}
</div>
</div>
)}
</div> </div>
<div className="text-right"> <div className="text-right">
<p className="text-sm font-medium text-gray-900">{p.planned_date ? new Date(p.planned_date).toLocaleDateString() : 'No date'}</p> <p className="text-sm font-medium text-gray-900">{p.planned_date ? new Date(p.planned_date).toLocaleDateString() : 'No date'}</p>
@@ -127,40 +98,7 @@ const Mowing = () => {
<MowingExecutionModal plan={execPlan} onClose={()=> setExecPlan(null)} onComplete={fetchPlans} /> <MowingExecutionModal plan={execPlan} onClose={()=> setExecPlan(null)} onComplete={fetchPlans} />
)} )}
{/* Recent Sessions */} {/* Recent Sessions section removed as requested */}
<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 className="flex justify-end">
<button
className="p-1 text-indigo-600 hover:text-indigo-800 hover:bg-indigo-50 rounded"
title="View session"
onClick={()=> setViewSession(s)}
>
<EyeIcon className="h-5 w-5" />
</button>
</div>
</div>
))}
</div>
</div>
{viewSession && ( {viewSession && (
<MowingSessionViewModal session={viewSession} onClose={()=> setViewSession(null)} /> <MowingSessionViewModal session={viewSession} onClose={()=> setViewSession(null)} />