From 5664a14520ea3b400fe824c904e6b537cf2aa2a2 Mon Sep 17 00:00:00 2001 From: Jake Kasper Date: Wed, 27 Aug 2025 12:57:14 -0400 Subject: [PATCH] asdfsadfdsa --- .../Applications/ApplicationViewModal.js | 246 +++++++++++++ .../src/pages/Applications/Applications.js | 89 ++++- frontend/src/pages/History/History.js | 327 +++++++++++++++++- 3 files changed, 639 insertions(+), 23 deletions(-) create mode 100644 frontend/src/components/Applications/ApplicationViewModal.js 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 +
+
+
+
Coverage
+
100%
+
+
+ )} + + {/* 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); + }} + /> + )}
); };