mowing history
This commit is contained in:
@@ -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 } });
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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)} />
|
||||||
|
|||||||
Reference in New Issue
Block a user