asdfsadfdsa
This commit is contained in:
@@ -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 (
|
||||
<div className="p-6">
|
||||
@@ -403,28 +431,39 @@ const Applications = () => {
|
||||
</p>
|
||||
<div className="flex gap-2 mt-2">
|
||||
{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
|
||||
onClick={() => handleExecuteApplication(application)}
|
||||
className="p-1 text-green-600 hover:text-green-800 hover:bg-green-50 rounded"
|
||||
title="Execute application"
|
||||
onClick={() => handleViewApplication(application)}
|
||||
className="p-1 text-indigo-600 hover:text-indigo-800 hover:bg-indigo-50 rounded"
|
||||
title="View completed application"
|
||||
>
|
||||
<PlayIcon className="h-4 w-4" />
|
||||
<EyeIcon 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>
|
||||
</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>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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 (
|
||||
<div className="p-6">
|
||||
<LoadingSpinner />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="p-6">
|
||||
<h1 className="text-2xl font-bold text-gray-900 mb-6">History</h1>
|
||||
<div className="card">
|
||||
<p className="text-gray-600">Application history coming soon...</p>
|
||||
<div className="flex justify-between items-center mb-6">
|
||||
<h1 className="text-2xl font-bold text-gray-900">Application History</h1>
|
||||
<button
|
||||
onClick={fetchHistoryData}
|
||||
className="px-4 py-2 bg-blue-600 text-white rounded hover:bg-blue-700"
|
||||
>
|
||||
Refresh
|
||||
</button>
|
||||
</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>
|
||||
);
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user