history and mowing changes
This commit is contained in:
@@ -31,7 +31,7 @@ const History = () => {
|
||||
const [statusFilter, setStatusFilter] = useState('all'); // all, completed, archived
|
||||
const [propertyFilter, setPropertyFilter] = useState('all');
|
||||
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 [showProductDropdown, setShowProductDropdown] = useState(false);
|
||||
|
||||
@@ -252,7 +252,8 @@ const History = () => {
|
||||
}
|
||||
|
||||
// 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;
|
||||
});
|
||||
@@ -330,6 +331,43 @@ const History = () => {
|
||||
|
||||
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) {
|
||||
return (
|
||||
<div className="p-6">
|
||||
@@ -557,6 +595,7 @@ const History = () => {
|
||||
<option value="all">All Types</option>
|
||||
<option value="granular">Granular</option>
|
||||
<option value="liquid">Liquid</option>
|
||||
<option value="mowing">Mowing</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
@@ -598,168 +637,145 @@ const History = () => {
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Applications List */}
|
||||
{sortedApplications.length === 0 ? (
|
||||
{/* Unified History List */}
|
||||
{unifiedHistoryItems.length === 0 ? (
|
||||
<div className="text-center py-12">
|
||||
<CalendarIcon className="h-12 w-12 text-gray-400 mx-auto mb-4" />
|
||||
<h3 className="text-lg font-medium text-gray-900 mb-2">
|
||||
{statusFilter === 'completed' ? 'No completed applications' :
|
||||
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>
|
||||
<h3 className="text-lg font-medium text-gray-900 mb-2">No history items</h3>
|
||||
<p className="text-gray-500">Try adjusting your filters or date range.</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="grid gap-4">
|
||||
{sortedApplications.map((application) => {
|
||||
const log = applicationLogs.find(log => log.planId === application.id);
|
||||
|
||||
return (
|
||||
<div key={application.id} className="bg-white p-6 rounded-lg shadow hover:shadow-lg transition-shadow">
|
||||
<div className="flex justify-between items-start">
|
||||
<div className="flex-1">
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<h3 className="text-lg font-semibold text-gray-900">
|
||||
{application.propertyName} - {application.sectionNames}
|
||||
</h3>
|
||||
<span className={`px-3 py-1 text-sm font-medium rounded-full ${
|
||||
application.status === 'archived'
|
||||
? 'bg-gray-100 text-gray-800'
|
||||
: 'bg-green-100 text-green-800'
|
||||
}`}>
|
||||
{application.status === 'archived' ? 'Archived' : 'Completed'}
|
||||
</span>
|
||||
</div>
|
||||
{unifiedHistoryItems.map((item) => {
|
||||
if (item.kind === 'application') {
|
||||
const application = item.application;
|
||||
const log = item.log;
|
||||
return (
|
||||
<div key={`app-${application.id}`} className="bg-white p-6 rounded-lg shadow hover:shadow-lg transition-shadow">
|
||||
<div className="flex justify-between items-start">
|
||||
<div className="flex-1">
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<h3 className="text-lg font-semibold text-gray-900">
|
||||
{application.propertyName} - {application.sectionNames}
|
||||
</h3>
|
||||
<span className={`px-3 py-1 text-sm font-medium rounded-full ${
|
||||
application.status === 'archived'
|
||||
? 'bg-gray-100 text-gray-800'
|
||||
: 'bg-green-100 text-green-800'
|
||||
}`}>
|
||||
{application.status === 'archived' ? 'Archived' : 'Completed'}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 md:grid-cols-4 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="grid grid-cols-1 md:grid-cols-3 gap-4 mb-4">
|
||||
<div className="flex items-center text-sm text-gray-600">
|
||||
<ClockIcon className="h-4 w-4 mr-2" />
|
||||
{Math.round((log.gpsTrack?.duration || 0) / 60)} min
|
||||
<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" />
|
||||
{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>
|
||||
|
||||
{/* GPS Tracking Stats */}
|
||||
{log && (
|
||||
<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">Avg Speed</div>
|
||||
<div className="text-sm font-bold text-blue-900">
|
||||
{log.averageSpeed?.toFixed(1) || 0} mph
|
||||
</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 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>
|
||||
)}
|
||||
);
|
||||
}
|
||||
|
||||
{/* Mowing History */}
|
||||
{mowingLogs.length > 0 && (
|
||||
<div className="bg-white rounded-lg shadow mb-8">
|
||||
<div className="p-4 border-b border-gray-200">
|
||||
<h3 className="text-lg font-medium text-gray-900">Mowing History</h3>
|
||||
<p className="text-sm text-gray-500">{totalMowingSessions} session{totalMowingSessions!==1?'s':''}</p>
|
||||
</div>
|
||||
<div className="divide-y">
|
||||
{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">
|
||||
// Mowing entry
|
||||
const log = item.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={`mow-${log.id}`} className="bg-white p-6 rounded-lg shadow hover:shadow-lg transition-shadow">
|
||||
<div className="flex items-center justify-between">
|
||||
<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>
|
||||
<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>
|
||||
)}
|
||||
|
||||
|
||||
@@ -75,6 +75,29 @@ const Mowing = () => {
|
||||
{p.notes && (
|
||||
<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 className="text-right">
|
||||
<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>{(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 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>
|
||||
))}
|
||||
|
||||
Reference in New Issue
Block a user