diff --git a/frontend/src/components/Applications/ApplicationViewModal.js b/frontend/src/components/Applications/ApplicationViewModal.js
new file mode 100644
index 0000000..670d9e3
--- /dev/null
+++ b/frontend/src/components/Applications/ApplicationViewModal.js
@@ -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 (
+
+
+
+
Loading application details...
+
+
+
+ );
+ }
+
+ return (
+
+
+ {/* Header */}
+
+
+ Application Details
+
+
+
+
+ {/* Application Overview */}
+
+
+
+
+ Property & Areas
+
+
+
Property: {application.propertyName}
+
Areas: {application.sectionNames}
+
Total Area: {application.totalSectionArea?.toLocaleString()} sq ft
+
+
+
+
+
+
+ Equipment & Status
+
+
+
Equipment: {application.equipmentName}
+
Status:
+
+ {application.status}
+
+
+
Planned Date: {new Date(application.plannedDate).toLocaleDateString()}
+
+
+
+
+ {/* Products */}
+ {planDetails?.products && planDetails.products.length > 0 && (
+
+
+
+ Products Applied
+
+
+
+
Product
+
Rate
+
Amount
+
Water
+
+ {planDetails.products.map((product, index) => (
+
+
{product.productName}
+
{product.rateAmount} {product.rateUnit}
+
{product.calculatedProductAmount?.toFixed(2)} lbs
+
{product.calculatedWaterAmount?.toFixed(2)} gal
+
+ ))}
+
+
+ )}
+
+ {/* GPS Tracking Information */}
+ {applicationLog && (
+
+
+
+ Tracking Information
+
+
+
+
Average Speed
+
{applicationLog.averageSpeed?.toFixed(1)} mph
+
+
+
GPS Points
+
{applicationLog.gpsTrack?.points?.length || 0}
+
+
+
Duration
+
+ {applicationLog.gpsTrack?.duration ? Math.round(applicationLog.gpsTrack.duration / 60) : 0} min
+
+
+
+
Distance
+
+ {applicationLog.gpsTrack?.totalDistance?.toFixed(0) || 0} ft
+
+
+
+
+ )}
+
+ {/* Map with GPS Track */}
+
+
Application Area & GPS Track
+
+
s.id) || []}
+ mode="view"
+ gpsTrack={applicationLog?.gpsTrack?.points || []}
+ currentLocation={null}
+ center={mapCenter}
+ zoom={mapCenter ? 16 : 15}
+ editable={false}
+ />
+
+
+
+ {/* Notes */}
+ {(application.notes || applicationLog?.notes) && (
+
+
Notes
+
+
{application.notes || applicationLog?.notes}
+
+
+ )}
+
+ {/* Close Button */}
+
+
+
+
+
+ );
+};
+
+export default ApplicationViewModal;
\ No newline at end of file
diff --git a/frontend/src/pages/Applications/Applications.js b/frontend/src/pages/Applications/Applications.js
index 925fb41..3d52af7 100644
--- a/frontend/src/pages/Applications/Applications.js
+++ b/frontend/src/pages/Applications/Applications.js
@@ -7,13 +7,15 @@ import {
CalculatorIcon,
PencilIcon,
TrashIcon,
- PlayIcon
+ PlayIcon,
+ EyeIcon
} from '@heroicons/react/24/outline';
import { propertiesAPI, productsAPI, equipmentAPI, applicationsAPI, nozzlesAPI } from '../../services/api';
import LoadingSpinner from '../../components/UI/LoadingSpinner';
import PropertyMap from '../../components/Maps/PropertyMap';
import ApplicationExecutionModal from '../../components/Applications/ApplicationExecutionModal';
import ApplicationPlanModal from '../../components/Applications/ApplicationPlanModal';
+import ApplicationViewModal from '../../components/Applications/ApplicationViewModal';
import toast from 'react-hot-toast';
const Applications = () => {
@@ -31,6 +33,8 @@ const Applications = () => {
const [loadingRecommendation, setLoadingRecommendation] = useState(false);
const [executingApplication, setExecutingApplication] = useState(null);
const [showExecutionModal, setShowExecutionModal] = useState(false);
+ const [showViewModal, setShowViewModal] = useState(false);
+ const [viewingApplication, setViewingApplication] = useState(null);
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) {
return (
@@ -403,28 +431,39 @@ const Applications = () => {
{application.status === 'planned' && (
+ <>
+
+
+
+ >
+ )}
+ {application.status === 'completed' && (
)}
-
-
@@ -593,6 +632,18 @@ const Applications = () => {
}}
/>
)}
+
+ {/* Application View Modal */}
+ {showViewModal && viewingApplication && (
+ {
+ setShowViewModal(false);
+ setViewingApplication(null);
+ }}
+ />
+ )}
);
};
diff --git a/frontend/src/pages/History/History.js b/frontend/src/pages/History/History.js
index d79137b..b6ea65d 100644
--- a/frontend/src/pages/History/History.js
+++ b/frontend/src/pages/History/History.js
@@ -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 [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 (
+
+
+
+ );
+ }
+
return (
-
History
-
-
Application history coming soon...
+
+
Application History
+
+
+ {/* Summary Statistics */}
+
+
+
+
+
+
Total Applications
+
{totalApplications}
+
+
+
+
+
+
+
+
+
Total Area Treated
+
+ {Math.round(totalAreaTreated / 1000)}k sq ft
+
+
+
+
+
+
+
+
+
+
Total Time
+
+ {Math.round(totalDuration / 3600)}h
+
+
+
+
+
+
+ {/* Filters and Sort */}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {/* Applications List */}
+ {sortedApplications.length === 0 ? (
+
+
+
No completed applications
+
Complete some applications to see them here.
+
+ ) : (
+
+ {sortedApplications.map((application) => {
+ const log = applicationLogs.find(log => log.planId === application.id);
+
+ return (
+
+
+
+
+
+ {application.propertyName} - {application.sectionNames}
+
+
+ Completed
+
+
+
+
+
+
+ {new Date(application.plannedDate).toLocaleDateString()}
+
+
+
+
+ {(application.totalSectionArea || 0).toLocaleString()} sq ft
+
+
+
+
+ {application.equipmentName}
+
+
+ {log && (
+
+
+ {Math.round((log.gpsTrack?.duration || 0) / 60)} min
+
+ )}
+
+
+ {/* GPS Tracking Stats */}
+ {log && (
+
+
+
Avg Speed
+
+ {log.averageSpeed?.toFixed(1) || 0} mph
+
+
+
+
GPS Points
+
+ {log.gpsTrack?.points?.length || 0}
+
+
+
+
Distance
+
+ {Math.round(log.gpsTrack?.totalDistance || 0)} ft
+
+
+
+
+ )}
+
+ {/* Products */}
+ {application.products && application.products.length > 0 && (
+
+
+
+ Products Applied:
+
+
+ {application.products.map((product, index) => (
+
+ {product.productName} ({product.rateAmount} {product.rateUnit})
+
+ ))}
+
+
+ )}
+
+ {application.notes && (
+
+ Notes: {application.notes}
+
+ )}
+
+
+
+
+
+
+
+ );
+ })}
+
+ )}
+
+ {/* Application View Modal */}
+ {showViewModal && viewingApplication && (
+
{
+ setShowViewModal(false);
+ setViewingApplication(null);
+ }}
+ />
+ )}
);
};