mowing and properties
This commit is contained in:
@@ -11,7 +11,10 @@ import {
|
||||
SwatchIcon,
|
||||
PencilIcon
|
||||
} from '@heroicons/react/24/outline';
|
||||
import { propertiesAPI } from '../../services/api';
|
||||
import { propertiesAPI, applicationsAPI, mowingAPI } from '../../services/api';
|
||||
import ApplicationViewModal from '../../components/Applications/ApplicationViewModal';
|
||||
import MowingSessionViewModal from '../../components/Mowing/MowingSessionViewModal';
|
||||
import { EyeIcon, CalendarIcon, WrenchScrewdriverIcon } from '@heroicons/react/24/outline';
|
||||
import LoadingSpinner from '../../components/UI/LoadingSpinner';
|
||||
import toast from 'react-hot-toast';
|
||||
import 'leaflet/dist/leaflet.css';
|
||||
@@ -302,10 +305,45 @@ const PropertyDetail = () => {
|
||||
const [editingSection, setEditingSection] = useState(null);
|
||||
const [showEditModal, setShowEditModal] = useState(false);
|
||||
|
||||
// Recent history state for this property
|
||||
const [completedApplications, setCompletedApplications] = useState([]);
|
||||
const [applicationLogs, setApplicationLogs] = useState([]);
|
||||
const [mowingLogs, setMowingLogs] = useState([]);
|
||||
const [historyLoading, setHistoryLoading] = useState(false);
|
||||
const [viewingApplication, setViewingApplication] = useState(null);
|
||||
const [viewingMowingSession, setViewingMowingSession] = useState(null);
|
||||
|
||||
useEffect(() => {
|
||||
fetchPropertyDetails();
|
||||
}, [id]); // eslint-disable-line react-hooks/exhaustive-deps
|
||||
|
||||
// Load recent history when property is available
|
||||
useEffect(() => {
|
||||
if (!property?.id) return;
|
||||
const loadHistory = async () => {
|
||||
try {
|
||||
setHistoryLoading(true);
|
||||
const [completedRes, archivedRes, logsRes, mowRes] = await Promise.all([
|
||||
applicationsAPI.getPlans({ status: 'completed', property_id: property.id }),
|
||||
applicationsAPI.getPlans({ status: 'archived', property_id: property.id }),
|
||||
applicationsAPI.getLogs({ property_id: property.id }),
|
||||
mowingAPI.getLogs({ property_id: property.id })
|
||||
]);
|
||||
const completedPlans = completedRes.data?.data?.plans || [];
|
||||
const archivedPlans = archivedRes.data?.data?.plans || [];
|
||||
setCompletedApplications([...(completedPlans||[]), ...(archivedPlans||[])]);
|
||||
setApplicationLogs(logsRes.data?.data?.logs || []);
|
||||
setMowingLogs(mowRes.data?.data?.logs || []);
|
||||
} catch (e) {
|
||||
console.warn('Failed to load property history', e?.response?.data || e.message);
|
||||
setCompletedApplications([]);
|
||||
setApplicationLogs([]);
|
||||
setMowingLogs([]);
|
||||
} finally { setHistoryLoading(false); }
|
||||
};
|
||||
loadHistory();
|
||||
}, [property?.id]);
|
||||
|
||||
// Handle keyboard shortcuts for polygon drawing
|
||||
useEffect(() => {
|
||||
const handleKeyPress = (e) => {
|
||||
@@ -494,6 +532,55 @@ const PropertyDetail = () => {
|
||||
return lawnSections.reduce((total, section) => total + section.area, 0);
|
||||
};
|
||||
|
||||
// History helpers
|
||||
const getApplicationType = (app) => {
|
||||
if (!app.productDetails || app.productDetails.length === 0) return null;
|
||||
return app.productDetails[0].type;
|
||||
};
|
||||
|
||||
const calculateCoverage = (application, log) => {
|
||||
if (!log?.gpsTrack?.points && !log?.gps_track?.points) return 0;
|
||||
const storedMeters = typeof (log.gpsTrack?.totalDistance || log.gps_track?.totalDistance) === 'number' ? (log.gpsTrack?.totalDistance || log.gps_track?.totalDistance) : 0;
|
||||
const totalDistanceFeet = storedMeters * 3.28084;
|
||||
const plannedArea = application.totalSectionArea || application.total_section_area || 0;
|
||||
if (totalDistanceFeet === 0 || plannedArea === 0) return 0;
|
||||
let equipmentWidth = 4;
|
||||
const equipmentName = (application.equipmentName || '').toLowerCase();
|
||||
if (equipmentName.includes('spreader')) equipmentWidth = 12;
|
||||
else if (equipmentName.includes('sprayer')) equipmentWidth = 20;
|
||||
else if (equipmentName.includes('mower')) equipmentWidth = 6;
|
||||
const theoreticalCoverageArea = totalDistanceFeet * equipmentWidth;
|
||||
return Math.min(Math.round((theoreticalCoverageArea / plannedArea) * 100), 100);
|
||||
};
|
||||
|
||||
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);
|
||||
const widthInches = parseFloat(log.cutting_width_inches || 0);
|
||||
const widthFeet = isNaN(widthInches) ? 0 : (widthInches / 12);
|
||||
if (totalDistanceFeet === 0 || plannedArea === 0 || widthFeet === 0) return 0;
|
||||
const theoreticalCoverageArea = totalDistanceFeet * widthFeet;
|
||||
return Math.min(100, Math.round((theoreticalCoverageArea / plannedArea) * 100));
|
||||
};
|
||||
|
||||
const unifiedHistory = (() => {
|
||||
const apps = (completedApplications||[]).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 };
|
||||
});
|
||||
const mows = (mowingLogs||[]).map((log) => {
|
||||
const dateStr = log.session_date || log.created_at;
|
||||
const date = dateStr ? new Date(dateStr) : new Date(0);
|
||||
return { kind: 'mowing', date, log };
|
||||
});
|
||||
const items = [...apps, ...mows];
|
||||
items.sort((a,b)=> b.date - a.date);
|
||||
return items.slice(0, 10);
|
||||
})();
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="p-6">
|
||||
@@ -704,6 +791,95 @@ const PropertyDetail = () => {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Recent History */}
|
||||
<div className="mt-8">
|
||||
<h2 className="text-lg font-semibold mb-3">Recent History</h2>
|
||||
{historyLoading ? (
|
||||
<div className="card p-4"><div className="text-gray-600">Loading…</div></div>
|
||||
) : unifiedHistory.length === 0 ? (
|
||||
<div className="card p-4 text-gray-600">No history yet for this property.</div>
|
||||
) : (
|
||||
<div className="grid gap-4">
|
||||
{unifiedHistory.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">
|
||||
<div className="flex justify-between items-start">
|
||||
<div className="flex-1">
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<div className="flex items-center gap-2">
|
||||
<h3 className="text-lg font-semibold text-gray-900">{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 ${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-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" />{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>
|
||||
{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>
|
||||
)}
|
||||
</div>
|
||||
<button onClick={()=> setViewingApplication(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>
|
||||
);
|
||||
}
|
||||
const log = item.log;
|
||||
const durationMin = Math.round((log.duration_seconds || log.durationSeconds || log.gpsTrack?.duration || log.gps_track?.duration || 0)/60);
|
||||
const distFeet = Math.round(((log.total_distance_meters || log.gpsTrack?.totalDistance || log.gps_track?.totalDistance || 0)*3.28084)||0);
|
||||
return (
|
||||
<div key={`mow-${log.id}`} className="bg-white p-6 rounded-lg shadow">
|
||||
<div className="flex justify-between items-start">
|
||||
<div className="flex-1">
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<div className="flex items-center gap-2">
|
||||
<h3 className="text-lg font-semibold text-gray-900">{log.property_name} - {log.section_names}</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>
|
||||
<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>
|
||||
|
||||
{/* Name Modal */}
|
||||
{showNameModal && (
|
||||
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center" style={{ zIndex: 9999 }}>
|
||||
@@ -806,8 +982,23 @@ const PropertyDetail = () => {
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* View Modals for History */}
|
||||
{viewingApplication && (
|
||||
<ApplicationViewModal
|
||||
application={viewingApplication}
|
||||
propertyDetails={property}
|
||||
onClose={() => setViewingApplication(null)}
|
||||
/>
|
||||
)}
|
||||
{viewingMowingSession && (
|
||||
<MowingSessionViewModal
|
||||
session={viewingMowingSession}
|
||||
onClose={() => setViewingMowingSession(null)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default PropertyDetail;
|
||||
export default PropertyDetail;
|
||||
|
||||
Reference in New Issue
Block a user