history and mowing changes

This commit is contained in:
Jake Kasper
2025-09-03 10:04:34 -04:00
parent 93c697dd12
commit 7561d32171
2 changed files with 187 additions and 142 deletions

View File

@@ -31,7 +31,7 @@ const History = () => {
const [statusFilter, setStatusFilter] = useState('all'); // all, completed, archived const [statusFilter, setStatusFilter] = useState('all'); // all, completed, archived
const [propertyFilter, setPropertyFilter] = useState('all'); const [propertyFilter, setPropertyFilter] = useState('all');
const [selectedProducts, setSelectedProducts] = useState([]); const [selectedProducts, setSelectedProducts] = useState([]);
const [applicationTypeFilter, setApplicationTypeFilter] = useState('all'); // all, granular, liquid const [applicationTypeFilter, setApplicationTypeFilter] = useState('all'); // all, granular, liquid, mowing
const [showFilters, setShowFilters] = useState(false); const [showFilters, setShowFilters] = useState(false);
const [showProductDropdown, setShowProductDropdown] = useState(false); const [showProductDropdown, setShowProductDropdown] = useState(false);
@@ -252,7 +252,8 @@ const History = () => {
} }
// Application type filter // Application type filter
if (applicationTypeFilter !== 'all' && getApplicationType(app) !== applicationTypeFilter) return false; if (applicationTypeFilter === 'mowing') return false; // hide app items when filtering for mowing only
if (applicationTypeFilter !== 'all' && applicationTypeFilter !== 'mowing' && getApplicationType(app) !== applicationTypeFilter) return false;
return true; return true;
}); });
@@ -330,6 +331,43 @@ const History = () => {
const totalMowingSessions = sortedMowingLogs.length; const totalMowingSessions = sortedMowingLogs.length;
// Build unified history list (applications + mowing) sorted by date
const unifiedHistoryItems = (() => {
// Application items
const apps = [...filteredApplications].map((application) => {
const log = applicationLogs.find((l) => l.planId === application.id);
const dateStr = log?.applicationDate || application.plannedDate || application.updatedAt || application.createdAt;
const date = dateStr ? new Date(dateStr) : new Date(0);
return { kind: 'application', date, application, log };
});
// Mowing items (already date-filtered and property-filtered above)
const mows = [...sortedMowingLogs].map((log) => {
const dateStr = log.session_date || log.created_at;
const date = dateStr ? new Date(dateStr) : new Date(0);
return { kind: 'mowing', date, log };
});
let items = [...apps, ...mows];
// Status filter: mowing sessions are treated as completed
if (statusFilter === 'archived') {
items = items.filter((it) => it.kind === 'application' && it.application.status === 'archived');
} else if (statusFilter === 'completed') {
items = items.filter((it) => (it.kind === 'application' && it.application.status === 'completed') || it.kind === 'mowing');
}
// Application Type filter across unified list
if (applicationTypeFilter === 'granular' || applicationTypeFilter === 'liquid') {
items = items.filter((it) => it.kind === 'application' && getApplicationType(it.application) === applicationTypeFilter);
} else if (applicationTypeFilter === 'mowing') {
items = items.filter((it) => it.kind === 'mowing');
}
// Sort newest first
items.sort((a, b) => b.date - a.date);
return items;
})();
if (loading) { if (loading) {
return ( return (
<div className="p-6"> <div className="p-6">
@@ -557,6 +595,7 @@ const History = () => {
<option value="all">All Types</option> <option value="all">All Types</option>
<option value="granular">Granular</option> <option value="granular">Granular</option>
<option value="liquid">Liquid</option> <option value="liquid">Liquid</option>
<option value="mowing">Mowing</option>
</select> </select>
</div> </div>
@@ -598,168 +637,145 @@ const History = () => {
)} )}
</div> </div>
{/* Applications List */} {/* Unified History List */}
{sortedApplications.length === 0 ? ( {unifiedHistoryItems.length === 0 ? (
<div className="text-center py-12"> <div className="text-center py-12">
<CalendarIcon className="h-12 w-12 text-gray-400 mx-auto mb-4" /> <CalendarIcon className="h-12 w-12 text-gray-400 mx-auto mb-4" />
<h3 className="text-lg font-medium text-gray-900 mb-2"> <h3 className="text-lg font-medium text-gray-900 mb-2">No history items</h3>
{statusFilter === 'completed' ? 'No completed applications' : <p className="text-gray-500">Try adjusting your filters or date range.</p>
statusFilter === 'archived' ? 'No archived applications' :
'No completed or archived applications'}
</h3>
<p className="text-gray-500">
{statusFilter === 'completed' ? 'Complete some applications to see them here.' :
statusFilter === 'archived' ? 'Archive some completed applications to see them here.' :
'Complete and archive applications to see them here.'}
</p>
</div> </div>
) : ( ) : (
<div className="grid gap-4"> <div className="grid gap-4">
{sortedApplications.map((application) => { {unifiedHistoryItems.map((item) => {
const log = applicationLogs.find(log => log.planId === application.id); if (item.kind === 'application') {
const application = item.application;
return ( const log = item.log;
<div key={application.id} className="bg-white p-6 rounded-lg shadow hover:shadow-lg transition-shadow"> return (
<div className="flex justify-between items-start"> <div key={`app-${application.id}`} className="bg-white p-6 rounded-lg shadow hover:shadow-lg transition-shadow">
<div className="flex-1"> <div className="flex justify-between items-start">
<div className="flex items-center justify-between mb-4"> <div className="flex-1">
<h3 className="text-lg font-semibold text-gray-900"> <div className="flex items-center justify-between mb-4">
{application.propertyName} - {application.sectionNames} <h3 className="text-lg font-semibold text-gray-900">
</h3> {application.propertyName} - {application.sectionNames}
<span className={`px-3 py-1 text-sm font-medium rounded-full ${ </h3>
application.status === 'archived' <span className={`px-3 py-1 text-sm font-medium rounded-full ${
? 'bg-gray-100 text-gray-800' application.status === 'archived'
: 'bg-green-100 text-green-800' ? 'bg-gray-100 text-gray-800'
}`}> : 'bg-green-100 text-green-800'
{application.status === 'archived' ? 'Archived' : 'Completed'} }`}>
</span> {application.status === 'archived' ? 'Archived' : 'Completed'}
</div> </span>
</div>
<div className="grid grid-cols-2 md:grid-cols-4 gap-4 mb-4"> <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(application.plannedDate).toLocaleDateString()}
</div>
<div className="flex items-center text-sm text-gray-600">
<MapPinIcon className="h-4 w-4 mr-2" />
{(application.totalSectionArea || 0).toLocaleString()} sq ft
</div>
<div className="flex items-center text-sm text-gray-600">
<WrenchScrewdriverIcon className="h-4 w-4 mr-2" />
{application.equipmentName}
</div>
{log && (
<div className="flex items-center text-sm text-gray-600"> <div className="flex items-center text-sm text-gray-600">
<ClockIcon className="h-4 w-4 mr-2" /> <CalendarIcon className="h-4 w-4 mr-2" />
{Math.round((log.gpsTrack?.duration || 0) / 60)} min {new Date(item.date).toLocaleString()}
</div> </div>
<div className="flex items-center text-sm text-gray-600">
<MapPinIcon className="h-4 w-4 mr-2" />
{application.propertyName}
</div>
<div className="flex items-center text-sm text-gray-600">
<WrenchScrewdriverIcon className="h-4 w-4 mr-2" />
{application.equipmentName}
</div>
</div>
{/* GPS Tracking Stats */}
{log?.gpsTrack && (
<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">
{Math.round((log.gpsTrack?.duration || 0) / 60)} 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 || 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">
{Math.round(log.gpsTrack?.totalDistance || 0)} 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">
{calculateCoverage(application, log)}%
</div>
</div>
</div>
)}
{/* Products */}
{application.productDetails && application.productDetails.length > 0 && (
<div className="mb-4">
<div className="flex items-center text-sm text-gray-600 mb-2">
<BeakerIcon className="h-4 w-4 mr-2" />
Products Applied:
</div>
<div className="flex flex-wrap gap-2">
{application.productDetails.map((product, index) => (
<span
key={index}
className="px-2 py-1 bg-gray-100 text-gray-700 rounded text-xs"
>
{product.name} ({product.rateAmount} {product.rateUnit})
</span>
))}
</div>
</div>
)}
{application.notes && (
<p className="text-sm text-gray-600 mb-4">
<strong>Notes:</strong> {application.notes}
</p>
)} )}
</div> </div>
{/* GPS Tracking Stats */} <div className="ml-4">
{log && ( <button
<div className="grid grid-cols-2 md:grid-cols-4 gap-2 mb-4"> onClick={() => handleViewApplication(application)}
<div className="bg-blue-50 p-2 rounded text-center"> className="p-2 text-indigo-600 hover:text-indigo-800 hover:bg-indigo-50 rounded"
<div className="text-xs text-blue-600 font-medium">Avg Speed</div> title="View details"
<div className="text-sm font-bold text-blue-900"> >
{log.averageSpeed?.toFixed(1) || 0} mph <EyeIcon className="h-5 w-5" />
</div> </button>
</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 || 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">
{Math.round(log.gpsTrack?.totalDistance || 0)} 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">
{calculateCoverage(application, log)}%
</div>
</div>
</div>
)}
{/* Products */}
{application.productDetails && application.productDetails.length > 0 && (
<div className="mb-4">
<div className="flex items-center text-sm text-gray-600 mb-2">
<BeakerIcon className="h-4 w-4 mr-2" />
Products Applied:
</div>
<div className="flex flex-wrap gap-2">
{application.productDetails.map((product, index) => (
<span
key={index}
className="px-2 py-1 bg-gray-100 text-gray-700 rounded text-xs"
>
{product.name} ({product.rateAmount} {product.rateUnit})
</span>
))}
</div>
</div>
)}
{application.notes && (
<p className="text-sm text-gray-600 mb-4">
<strong>Notes:</strong> {application.notes}
</p>
)}
</div>
<div className="ml-4">
<button
onClick={() => handleViewApplication(application)}
className="p-2 text-indigo-600 hover:text-indigo-800 hover:bg-indigo-50 rounded"
title="View details"
>
<EyeIcon className="h-5 w-5" />
</button>
</div> </div>
</div> </div>
</div> );
); }
})}
</div>
)}
{/* Mowing History */} // Mowing entry
{mowingLogs.length > 0 && ( const log = item.log;
<div className="bg-white rounded-lg shadow mb-8"> const durationMin = Math.round((log.duration_seconds || log.durationSeconds || log.gpsTrack?.duration || 0) / 60);
<div className="p-4 border-b border-gray-200"> const avg = (log.average_speed_mph || log.averageSpeed || 0).toFixed?.(1) || Number(log.averageSpeed || 0).toFixed(1);
<h3 className="text-lg font-medium text-gray-900">Mowing History</h3> const distFeet = Math.round(((log.total_distance_meters || log.gpsTrack?.totalDistance || 0) * 3.28084) || 0);
<p className="text-sm text-gray-500">{totalMowingSessions} session{totalMowingSessions!==1?'s':''}</p> return (
</div> <div key={`mow-${log.id}`} className="bg-white p-6 rounded-lg shadow hover:shadow-lg transition-shadow">
<div className="divide-y"> <div className="flex items-center justify-between">
{sortedMowingLogs.map((log) => {
const durationMin = Math.round((log.duration_seconds || log.durationSeconds || log.gpsTrack?.duration || 0) / 60);
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);
return (
<div key={log.id} className="flex items-center justify-between p-4">
<div> <div>
<div className="font-medium text-gray-900">{log.property_name || 'Property'}</div> <div className="font-medium text-gray-900">{log.property_name || 'Property'}</div>
<div className="text-sm text-gray-600">{log.equipment_name || 'Mower'} {avg} mph {distFeet} ft {durationMin} min</div> <div className="text-sm text-gray-600">{new Date(item.date).toLocaleString()} {log.equipment_name || 'Mower'} {avg} mph {distFeet} ft {durationMin} min</div>
</div> </div>
<button <button
onClick={() => setViewingMowingSession(log)} onClick={() => setViewingMowingSession(log)}
className="p-2 text-indigo-600 hover:text-indigo-800 hover:bg-indigo-50 rounded" 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" /> <EyeIcon className="h-5 w-5" />
</button> </button>
</div> </div>
); </div>
})} );
</div> })}
</div> </div>
)} )}

View File

@@ -75,6 +75,29 @@ 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>
@@ -125,8 +148,14 @@ const Mowing = () => {
<div>{Number(s.cut_height_inches || 0).toFixed(2)}"</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>{(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>{Math.round(((s.total_distance_meters || s.gpsTrack?.totalDistance || 0) * 3.28084) || 0)} ft</div>
<div> <div className="flex justify-end">
<button className="btn-secondary" onClick={()=> setViewSession(s)}>View</button> <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>
))} ))}