asdfsadfdsa
This commit is contained in:
246
frontend/src/components/Applications/ApplicationViewModal.js
Normal file
246
frontend/src/components/Applications/ApplicationViewModal.js
Normal file
@@ -0,0 +1,246 @@
|
|||||||
|
import React, { useState, useEffect } from 'react';
|
||||||
|
import { applicationsAPI } from '../../services/api';
|
||||||
|
import PropertyMap from '../Maps/PropertyMap';
|
||||||
|
import { XMarkIcon, ClockIcon, MapPinIcon, WrenchScrewdriverIcon, BeakerIcon } from '@heroicons/react/24/outline';
|
||||||
|
|
||||||
|
const ApplicationViewModal = ({ application, propertyDetails, onClose }) => {
|
||||||
|
const [applicationLog, setApplicationLog] = useState(null);
|
||||||
|
const [sections, setSections] = useState([]);
|
||||||
|
const [planDetails, setPlanDetails] = useState(null);
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const fetchApplicationData = async () => {
|
||||||
|
if (!application?.id) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
setLoading(true);
|
||||||
|
|
||||||
|
// Fetch the plan details to get section geometry
|
||||||
|
const planResponse = await applicationsAPI.getPlan(application.id);
|
||||||
|
const fetchedPlanDetails = planResponse.data.data.plan;
|
||||||
|
setPlanDetails(fetchedPlanDetails);
|
||||||
|
|
||||||
|
if (fetchedPlanDetails.sections) {
|
||||||
|
setSections(fetchedPlanDetails.sections);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try to fetch application logs to get GPS tracking data
|
||||||
|
try {
|
||||||
|
const logsResponse = await applicationsAPI.getLogs({ planId: application.id });
|
||||||
|
const logs = logsResponse.data.data.logs;
|
||||||
|
if (logs && logs.length > 0) {
|
||||||
|
setApplicationLog(logs[0]); // Get the most recent log
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.log('No application logs found:', error);
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to fetch application data:', error);
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
fetchApplicationData();
|
||||||
|
}, [application?.id]);
|
||||||
|
|
||||||
|
// Calculate map center from sections
|
||||||
|
const mapCenter = React.useMemo(() => {
|
||||||
|
if (sections.length === 0) return null;
|
||||||
|
|
||||||
|
let totalLat = 0;
|
||||||
|
let totalLng = 0;
|
||||||
|
let pointCount = 0;
|
||||||
|
|
||||||
|
sections.forEach(section => {
|
||||||
|
let polygonData = section.polygonData;
|
||||||
|
if (typeof polygonData === 'string') {
|
||||||
|
try {
|
||||||
|
polygonData = JSON.parse(polygonData);
|
||||||
|
} catch (e) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (polygonData?.coordinates?.[0]) {
|
||||||
|
polygonData.coordinates[0].forEach(([lat, lng]) => {
|
||||||
|
totalLat += lat;
|
||||||
|
totalLng += lng;
|
||||||
|
pointCount++;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (pointCount > 0) {
|
||||||
|
return [totalLat / pointCount, totalLng / pointCount];
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}, [sections]);
|
||||||
|
|
||||||
|
if (loading) {
|
||||||
|
return (
|
||||||
|
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
|
||||||
|
<div className="bg-white rounded-lg p-6 w-full max-w-4xl mx-4 max-h-[90vh] overflow-y-auto">
|
||||||
|
<div className="flex items-center justify-center py-8">
|
||||||
|
<div className="text-gray-500">Loading application details...</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
|
||||||
|
<div className="bg-white rounded-lg p-6 w-full max-w-6xl mx-4 max-h-[90vh] overflow-y-auto">
|
||||||
|
{/* Header */}
|
||||||
|
<div className="flex justify-between items-center mb-6">
|
||||||
|
<h2 className="text-2xl font-bold text-gray-900">
|
||||||
|
Application Details
|
||||||
|
</h2>
|
||||||
|
<button
|
||||||
|
onClick={onClose}
|
||||||
|
className="text-gray-400 hover:text-gray-600"
|
||||||
|
>
|
||||||
|
<XMarkIcon className="h-6 w-6" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Application Overview */}
|
||||||
|
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6 mb-6">
|
||||||
|
<div className="bg-gray-50 p-4 rounded-lg">
|
||||||
|
<h3 className="font-semibold text-gray-900 mb-3 flex items-center">
|
||||||
|
<MapPinIcon className="h-5 w-5 mr-2" />
|
||||||
|
Property & Areas
|
||||||
|
</h3>
|
||||||
|
<div className="space-y-2">
|
||||||
|
<p><span className="font-medium">Property:</span> {application.propertyName}</p>
|
||||||
|
<p><span className="font-medium">Areas:</span> {application.sectionNames}</p>
|
||||||
|
<p><span className="font-medium">Total Area:</span> {application.totalSectionArea?.toLocaleString()} sq ft</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="bg-gray-50 p-4 rounded-lg">
|
||||||
|
<h3 className="font-semibold text-gray-900 mb-3 flex items-center">
|
||||||
|
<WrenchScrewdriverIcon className="h-5 w-5 mr-2" />
|
||||||
|
Equipment & Status
|
||||||
|
</h3>
|
||||||
|
<div className="space-y-2">
|
||||||
|
<p><span className="font-medium">Equipment:</span> {application.equipmentName}</p>
|
||||||
|
<p><span className="font-medium">Status:</span>
|
||||||
|
<span className={`ml-2 px-2 py-1 text-xs font-medium rounded-full ${
|
||||||
|
application.status === 'completed' ? 'bg-green-100 text-green-800' :
|
||||||
|
application.status === 'planned' ? 'bg-blue-100 text-blue-800' :
|
||||||
|
application.status === 'in_progress' ? 'bg-yellow-100 text-yellow-800' :
|
||||||
|
'bg-gray-100 text-gray-800'
|
||||||
|
}`}>
|
||||||
|
{application.status}
|
||||||
|
</span>
|
||||||
|
</p>
|
||||||
|
<p><span className="font-medium">Planned Date:</span> {new Date(application.plannedDate).toLocaleDateString()}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Products */}
|
||||||
|
{planDetails?.products && planDetails.products.length > 0 && (
|
||||||
|
<div className="mb-6">
|
||||||
|
<h3 className="font-semibold text-gray-900 mb-3 flex items-center">
|
||||||
|
<BeakerIcon className="h-5 w-5 mr-2" />
|
||||||
|
Products Applied
|
||||||
|
</h3>
|
||||||
|
<div className="bg-gray-50 rounded-lg overflow-hidden">
|
||||||
|
<div className="grid grid-cols-4 gap-4 p-4 border-b text-sm font-medium text-gray-600">
|
||||||
|
<div>Product</div>
|
||||||
|
<div>Rate</div>
|
||||||
|
<div>Amount</div>
|
||||||
|
<div>Water</div>
|
||||||
|
</div>
|
||||||
|
{planDetails.products.map((product, index) => (
|
||||||
|
<div key={index} className="grid grid-cols-4 gap-4 p-4 border-b last:border-b-0">
|
||||||
|
<div className="font-medium">{product.productName}</div>
|
||||||
|
<div>{product.rateAmount} {product.rateUnit}</div>
|
||||||
|
<div>{product.calculatedProductAmount?.toFixed(2)} lbs</div>
|
||||||
|
<div>{product.calculatedWaterAmount?.toFixed(2)} gal</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* GPS Tracking Information */}
|
||||||
|
{applicationLog && (
|
||||||
|
<div className="mb-6">
|
||||||
|
<h3 className="font-semibold text-gray-900 mb-3 flex items-center">
|
||||||
|
<ClockIcon className="h-5 w-5 mr-2" />
|
||||||
|
Tracking Information
|
||||||
|
</h3>
|
||||||
|
<div className="grid grid-cols-2 lg:grid-cols-4 gap-4">
|
||||||
|
<div className="bg-blue-50 p-3 rounded-lg">
|
||||||
|
<div className="text-sm text-blue-600 font-medium">Average Speed</div>
|
||||||
|
<div className="text-xl font-bold text-blue-900">{applicationLog.averageSpeed?.toFixed(1)} mph</div>
|
||||||
|
</div>
|
||||||
|
<div className="bg-green-50 p-3 rounded-lg">
|
||||||
|
<div className="text-sm text-green-600 font-medium">GPS Points</div>
|
||||||
|
<div className="text-xl font-bold text-green-900">{applicationLog.gpsTrack?.points?.length || 0}</div>
|
||||||
|
</div>
|
||||||
|
<div className="bg-purple-50 p-3 rounded-lg">
|
||||||
|
<div className="text-sm text-purple-600 font-medium">Duration</div>
|
||||||
|
<div className="text-xl font-bold text-purple-900">
|
||||||
|
{applicationLog.gpsTrack?.duration ? Math.round(applicationLog.gpsTrack.duration / 60) : 0} min
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="bg-orange-50 p-3 rounded-lg">
|
||||||
|
<div className="text-sm text-orange-600 font-medium">Distance</div>
|
||||||
|
<div className="text-xl font-bold text-orange-900">
|
||||||
|
{applicationLog.gpsTrack?.totalDistance?.toFixed(0) || 0} ft
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Map with GPS Track */}
|
||||||
|
<div className="mb-6">
|
||||||
|
<h4 className="font-medium mb-2">Application Area & GPS Track</h4>
|
||||||
|
<div className="h-96 border rounded-lg overflow-hidden">
|
||||||
|
<PropertyMap
|
||||||
|
property={propertyDetails}
|
||||||
|
sections={sections}
|
||||||
|
selectedSections={sections.map(s => s.id) || []}
|
||||||
|
mode="view"
|
||||||
|
gpsTrack={applicationLog?.gpsTrack?.points || []}
|
||||||
|
currentLocation={null}
|
||||||
|
center={mapCenter}
|
||||||
|
zoom={mapCenter ? 16 : 15}
|
||||||
|
editable={false}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Notes */}
|
||||||
|
{(application.notes || applicationLog?.notes) && (
|
||||||
|
<div className="mb-6">
|
||||||
|
<h3 className="font-semibold text-gray-900 mb-2">Notes</h3>
|
||||||
|
<div className="bg-gray-50 p-4 rounded-lg">
|
||||||
|
<p className="text-gray-700">{application.notes || applicationLog?.notes}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Close Button */}
|
||||||
|
<div className="flex justify-end">
|
||||||
|
<button
|
||||||
|
onClick={onClose}
|
||||||
|
className="px-4 py-2 bg-gray-500 text-white rounded hover:bg-gray-600"
|
||||||
|
>
|
||||||
|
Close
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ApplicationViewModal;
|
||||||
@@ -7,13 +7,15 @@ import {
|
|||||||
CalculatorIcon,
|
CalculatorIcon,
|
||||||
PencilIcon,
|
PencilIcon,
|
||||||
TrashIcon,
|
TrashIcon,
|
||||||
PlayIcon
|
PlayIcon,
|
||||||
|
EyeIcon
|
||||||
} from '@heroicons/react/24/outline';
|
} from '@heroicons/react/24/outline';
|
||||||
import { propertiesAPI, productsAPI, equipmentAPI, applicationsAPI, nozzlesAPI } from '../../services/api';
|
import { propertiesAPI, productsAPI, equipmentAPI, applicationsAPI, nozzlesAPI } from '../../services/api';
|
||||||
import LoadingSpinner from '../../components/UI/LoadingSpinner';
|
import LoadingSpinner from '../../components/UI/LoadingSpinner';
|
||||||
import PropertyMap from '../../components/Maps/PropertyMap';
|
import PropertyMap from '../../components/Maps/PropertyMap';
|
||||||
import ApplicationExecutionModal from '../../components/Applications/ApplicationExecutionModal';
|
import ApplicationExecutionModal from '../../components/Applications/ApplicationExecutionModal';
|
||||||
import ApplicationPlanModal from '../../components/Applications/ApplicationPlanModal';
|
import ApplicationPlanModal from '../../components/Applications/ApplicationPlanModal';
|
||||||
|
import ApplicationViewModal from '../../components/Applications/ApplicationViewModal';
|
||||||
import toast from 'react-hot-toast';
|
import toast from 'react-hot-toast';
|
||||||
|
|
||||||
const Applications = () => {
|
const Applications = () => {
|
||||||
@@ -31,6 +33,8 @@ const Applications = () => {
|
|||||||
const [loadingRecommendation, setLoadingRecommendation] = useState(false);
|
const [loadingRecommendation, setLoadingRecommendation] = useState(false);
|
||||||
const [executingApplication, setExecutingApplication] = useState(null);
|
const [executingApplication, setExecutingApplication] = useState(null);
|
||||||
const [showExecutionModal, setShowExecutionModal] = useState(false);
|
const [showExecutionModal, setShowExecutionModal] = useState(false);
|
||||||
|
const [showViewModal, setShowViewModal] = useState(false);
|
||||||
|
const [viewingApplication, setViewingApplication] = useState(null);
|
||||||
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -167,6 +171,30 @@ const Applications = () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleViewApplication = async (application) => {
|
||||||
|
try {
|
||||||
|
// Set the viewing application and show the modal
|
||||||
|
setViewingApplication(application);
|
||||||
|
|
||||||
|
// Get the property ID from the application
|
||||||
|
const propertyId = application.property?.id || application.section?.propertyId;
|
||||||
|
|
||||||
|
// Try to fetch property details if we have a valid property ID
|
||||||
|
if (propertyId && (!selectedPropertyDetails || selectedPropertyDetails.id !== propertyId)) {
|
||||||
|
await fetchPropertyDetails(propertyId);
|
||||||
|
} else if (!propertyId) {
|
||||||
|
console.warn('No property ID found for application:', application);
|
||||||
|
// Clear any existing property details since this application doesn't have property info
|
||||||
|
setSelectedPropertyDetails(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
setShowViewModal(true);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to load application details:', error);
|
||||||
|
toast.error('Failed to load application details');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
if (loading) {
|
if (loading) {
|
||||||
return (
|
return (
|
||||||
<div className="p-6">
|
<div className="p-6">
|
||||||
@@ -403,28 +431,39 @@ const Applications = () => {
|
|||||||
</p>
|
</p>
|
||||||
<div className="flex gap-2 mt-2">
|
<div className="flex gap-2 mt-2">
|
||||||
{application.status === 'planned' && (
|
{application.status === 'planned' && (
|
||||||
|
<>
|
||||||
|
<button
|
||||||
|
onClick={() => handleExecuteApplication(application)}
|
||||||
|
className="p-1 text-green-600 hover:text-green-800 hover:bg-green-50 rounded"
|
||||||
|
title="Execute application"
|
||||||
|
>
|
||||||
|
<PlayIcon className="h-4 w-4" />
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => handleEditPlan(application.id)}
|
||||||
|
className="p-1 text-blue-600 hover:text-blue-800 hover:bg-blue-50 rounded"
|
||||||
|
title="Edit plan"
|
||||||
|
>
|
||||||
|
<PencilIcon className="h-4 w-4" />
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => handleDeletePlan(application.id, `${application.propertyName} - ${application.sectionName}`)}
|
||||||
|
className="p-1 text-red-600 hover:text-red-800 hover:bg-red-50 rounded"
|
||||||
|
title="Delete plan"
|
||||||
|
>
|
||||||
|
<TrashIcon className="h-4 w-4" />
|
||||||
|
</button>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
{application.status === 'completed' && (
|
||||||
<button
|
<button
|
||||||
onClick={() => handleExecuteApplication(application)}
|
onClick={() => handleViewApplication(application)}
|
||||||
className="p-1 text-green-600 hover:text-green-800 hover:bg-green-50 rounded"
|
className="p-1 text-indigo-600 hover:text-indigo-800 hover:bg-indigo-50 rounded"
|
||||||
title="Execute application"
|
title="View completed application"
|
||||||
>
|
>
|
||||||
<PlayIcon className="h-4 w-4" />
|
<EyeIcon className="h-4 w-4" />
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
<button
|
|
||||||
onClick={() => handleEditPlan(application.id)}
|
|
||||||
className="p-1 text-blue-600 hover:text-blue-800 hover:bg-blue-50 rounded"
|
|
||||||
title="Edit plan"
|
|
||||||
>
|
|
||||||
<PencilIcon className="h-4 w-4" />
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
onClick={() => handleDeletePlan(application.id, `${application.propertyName} - ${application.sectionName}`)}
|
|
||||||
className="p-1 text-red-600 hover:text-red-800 hover:bg-red-50 rounded"
|
|
||||||
title="Delete plan"
|
|
||||||
>
|
|
||||||
<TrashIcon className="h-4 w-4" />
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -593,6 +632,18 @@ const Applications = () => {
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{/* Application View Modal */}
|
||||||
|
{showViewModal && viewingApplication && (
|
||||||
|
<ApplicationViewModal
|
||||||
|
application={viewingApplication}
|
||||||
|
propertyDetails={selectedPropertyDetails}
|
||||||
|
onClose={() => {
|
||||||
|
setShowViewModal(false);
|
||||||
|
setViewingApplication(null);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,12 +1,331 @@
|
|||||||
import React from 'react';
|
import React, { useState, useEffect } from 'react';
|
||||||
|
import {
|
||||||
|
ClockIcon,
|
||||||
|
MapPinIcon,
|
||||||
|
WrenchScrewdriverIcon,
|
||||||
|
BeakerIcon,
|
||||||
|
EyeIcon,
|
||||||
|
CalendarIcon,
|
||||||
|
ChartBarIcon
|
||||||
|
} from '@heroicons/react/24/outline';
|
||||||
|
import { applicationsAPI } from '../../services/api';
|
||||||
|
import LoadingSpinner from '../../components/UI/LoadingSpinner';
|
||||||
|
import ApplicationViewModal from '../../components/Applications/ApplicationViewModal';
|
||||||
|
import toast from 'react-hot-toast';
|
||||||
|
|
||||||
const History = () => {
|
const History = () => {
|
||||||
|
const [completedApplications, setCompletedApplications] = useState([]);
|
||||||
|
const [applicationLogs, setApplicationLogs] = useState([]);
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
|
const [showViewModal, setShowViewModal] = useState(false);
|
||||||
|
const [viewingApplication, setViewingApplication] = useState(null);
|
||||||
|
const [selectedPropertyDetails, setSelectedPropertyDetails] = useState(null);
|
||||||
|
const [dateFilter, setDateFilter] = useState('all'); // all, today, week, month
|
||||||
|
const [sortBy, setSortBy] = useState('date'); // date, area, duration
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
fetchHistoryData();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const fetchHistoryData = async () => {
|
||||||
|
try {
|
||||||
|
setLoading(true);
|
||||||
|
|
||||||
|
// Fetch completed applications (plans with completed status)
|
||||||
|
const plansResponse = await applicationsAPI.getPlans({ status: 'completed' });
|
||||||
|
const completedPlans = plansResponse.data.data.plans || [];
|
||||||
|
|
||||||
|
// Fetch application logs for additional details
|
||||||
|
const logsResponse = await applicationsAPI.getLogs();
|
||||||
|
const logs = logsResponse.data.data.logs || [];
|
||||||
|
|
||||||
|
setCompletedApplications(completedPlans);
|
||||||
|
setApplicationLogs(logs);
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to fetch history data:', error);
|
||||||
|
toast.error('Failed to load application history');
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleViewApplication = async (application) => {
|
||||||
|
try {
|
||||||
|
setViewingApplication(application);
|
||||||
|
setShowViewModal(true);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to load application details:', error);
|
||||||
|
toast.error('Failed to load application details');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Filter applications based on date filter
|
||||||
|
const filteredApplications = completedApplications.filter(app => {
|
||||||
|
if (dateFilter === 'all') return true;
|
||||||
|
|
||||||
|
const appDate = new Date(app.plannedDate);
|
||||||
|
const now = new Date();
|
||||||
|
|
||||||
|
switch (dateFilter) {
|
||||||
|
case 'today':
|
||||||
|
return appDate.toDateString() === now.toDateString();
|
||||||
|
case 'week':
|
||||||
|
const weekAgo = new Date(now.getTime() - 7 * 24 * 60 * 60 * 1000);
|
||||||
|
return appDate >= weekAgo;
|
||||||
|
case 'month':
|
||||||
|
const monthAgo = new Date(now.getTime() - 30 * 24 * 60 * 60 * 1000);
|
||||||
|
return appDate >= monthAgo;
|
||||||
|
default:
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Sort applications
|
||||||
|
const sortedApplications = [...filteredApplications].sort((a, b) => {
|
||||||
|
switch (sortBy) {
|
||||||
|
case 'date':
|
||||||
|
return new Date(b.plannedDate) - new Date(a.plannedDate);
|
||||||
|
case 'area':
|
||||||
|
return (b.totalSectionArea || 0) - (a.totalSectionArea || 0);
|
||||||
|
case 'duration':
|
||||||
|
const logA = applicationLogs.find(log => log.planId === a.id);
|
||||||
|
const logB = applicationLogs.find(log => log.planId === b.id);
|
||||||
|
return (logB?.gpsTrack?.duration || 0) - (logA?.gpsTrack?.duration || 0);
|
||||||
|
default:
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Calculate summary statistics
|
||||||
|
const totalApplications = completedApplications.length;
|
||||||
|
const totalAreaTreated = completedApplications.reduce((sum, app) => sum + (app.totalSectionArea || 0), 0);
|
||||||
|
const totalDuration = applicationLogs.reduce((sum, log) => sum + (log.gpsTrack?.duration || 0), 0);
|
||||||
|
|
||||||
|
if (loading) {
|
||||||
|
return (
|
||||||
|
<div className="p-6">
|
||||||
|
<LoadingSpinner />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="p-6">
|
<div className="p-6">
|
||||||
<h1 className="text-2xl font-bold text-gray-900 mb-6">History</h1>
|
<div className="flex justify-between items-center mb-6">
|
||||||
<div className="card">
|
<h1 className="text-2xl font-bold text-gray-900">Application History</h1>
|
||||||
<p className="text-gray-600">Application history coming soon...</p>
|
<button
|
||||||
|
onClick={fetchHistoryData}
|
||||||
|
className="px-4 py-2 bg-blue-600 text-white rounded hover:bg-blue-700"
|
||||||
|
>
|
||||||
|
Refresh
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Summary Statistics */}
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-3 gap-4 mb-6">
|
||||||
|
<div className="bg-blue-50 p-4 rounded-lg">
|
||||||
|
<div className="flex items-center">
|
||||||
|
<ChartBarIcon className="h-8 w-8 text-blue-600" />
|
||||||
|
<div className="ml-3">
|
||||||
|
<p className="text-sm text-blue-600 font-medium">Total Applications</p>
|
||||||
|
<p className="text-2xl font-bold text-blue-900">{totalApplications}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="bg-green-50 p-4 rounded-lg">
|
||||||
|
<div className="flex items-center">
|
||||||
|
<MapPinIcon className="h-8 w-8 text-green-600" />
|
||||||
|
<div className="ml-3">
|
||||||
|
<p className="text-sm text-green-600 font-medium">Total Area Treated</p>
|
||||||
|
<p className="text-2xl font-bold text-green-900">
|
||||||
|
{Math.round(totalAreaTreated / 1000)}k sq ft
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="bg-purple-50 p-4 rounded-lg">
|
||||||
|
<div className="flex items-center">
|
||||||
|
<ClockIcon className="h-8 w-8 text-purple-600" />
|
||||||
|
<div className="ml-3">
|
||||||
|
<p className="text-sm text-purple-600 font-medium">Total Time</p>
|
||||||
|
<p className="text-2xl font-bold text-purple-900">
|
||||||
|
{Math.round(totalDuration / 3600)}h
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Filters and Sort */}
|
||||||
|
<div className="bg-white p-4 rounded-lg shadow mb-6">
|
||||||
|
<div className="flex flex-wrap gap-4">
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||||
|
Time Period
|
||||||
|
</label>
|
||||||
|
<select
|
||||||
|
value={dateFilter}
|
||||||
|
onChange={(e) => setDateFilter(e.target.value)}
|
||||||
|
className="border border-gray-300 rounded px-3 py-2 text-sm"
|
||||||
|
>
|
||||||
|
<option value="all">All Time</option>
|
||||||
|
<option value="today">Today</option>
|
||||||
|
<option value="week">Last Week</option>
|
||||||
|
<option value="month">Last Month</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||||
|
Sort By
|
||||||
|
</label>
|
||||||
|
<select
|
||||||
|
value={sortBy}
|
||||||
|
onChange={(e) => setSortBy(e.target.value)}
|
||||||
|
className="border border-gray-300 rounded px-3 py-2 text-sm"
|
||||||
|
>
|
||||||
|
<option value="date">Date</option>
|
||||||
|
<option value="area">Area Size</option>
|
||||||
|
<option value="duration">Duration</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Applications List */}
|
||||||
|
{sortedApplications.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">No completed applications</h3>
|
||||||
|
<p className="text-gray-500">Complete some applications to see them here.</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 bg-green-100 text-green-800">
|
||||||
|
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="flex items-center text-sm text-gray-600">
|
||||||
|
<ClockIcon className="h-4 w-4 mr-2" />
|
||||||
|
{Math.round((log.gpsTrack?.duration || 0) / 60)} min
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</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">100%</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Products */}
|
||||||
|
{application.products && application.products.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.products.map((product, index) => (
|
||||||
|
<span
|
||||||
|
key={index}
|
||||||
|
className="px-2 py-1 bg-gray-100 text-gray-700 rounded text-xs"
|
||||||
|
>
|
||||||
|
{product.productName} ({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>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Application View Modal */}
|
||||||
|
{showViewModal && viewingApplication && (
|
||||||
|
<ApplicationViewModal
|
||||||
|
application={viewingApplication}
|
||||||
|
propertyDetails={selectedPropertyDetails}
|
||||||
|
onClose={() => {
|
||||||
|
setShowViewModal(false);
|
||||||
|
setViewingApplication(null);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user