archive and stuff

This commit is contained in:
Jake Kasper
2025-08-27 13:20:56 -04:00
parent 945de62564
commit e55d876d23
2 changed files with 308 additions and 13 deletions

View File

@@ -429,14 +429,14 @@ const PropertyMap = ({
})}
{/* GPS Tracking Elements */}
{mode === "execution" && (
{(mode === "execution" || mode === "view") && (
<>
{/* GPS Track Polyline */}
{gpsTrack.length > 1 && (
<Polyline
positions={gpsTrack.map(point => [point.lat, point.lng])}
pathOptions={{
color: '#10B981',
color: '#EF4444',
weight: 4,
opacity: 0.8,
}}
@@ -454,8 +454,8 @@ const PropertyMap = ({
)
))}
{/* Current Location */}
{currentLocation && (
{/* Current Location - only for execution mode */}
{currentLocation && mode === "execution" && (
<Marker
position={[currentLocation.lat, currentLocation.lng]}
icon={currentLocationIcon}

View File

@@ -8,7 +8,10 @@ import {
PencilIcon,
TrashIcon,
PlayIcon,
EyeIcon
EyeIcon,
ArchiveBoxIcon,
FunnelIcon,
XMarkIcon
} from '@heroicons/react/24/outline';
import { propertiesAPI, productsAPI, equipmentAPI, applicationsAPI, nozzlesAPI } from '../../services/api';
import LoadingSpinner from '../../components/UI/LoadingSpinner';
@@ -36,6 +39,18 @@ const Applications = () => {
const [showViewModal, setShowViewModal] = useState(false);
const [viewingApplication, setViewingApplication] = useState(null);
// Filtering and sorting state
const [showFilters, setShowFilters] = useState(false);
const [filters, setFilters] = useState({
status: 'all',
dateRange: 'all',
product: 'all',
minArea: '',
maxArea: '',
property: 'all'
});
const [sortBy, setSortBy] = useState('date');
const [sortOrder, setSortOrder] = useState('desc');
useEffect(() => {
fetchApplications();
@@ -195,6 +210,119 @@ const Applications = () => {
}
};
const handleArchiveApplication = async (applicationId) => {
if (!confirm('Are you sure you want to archive this application? It will be moved to the archive and hidden from the main list.')) {
return;
}
try {
await applicationsAPI.archivePlan(applicationId);
toast.success('Application archived successfully');
fetchApplications(); // Refresh the list
} catch (error) {
console.error('Failed to archive application:', error);
toast.error('Failed to archive application');
}
};
// Filter and sort applications
const filteredAndSortedApplications = React.useMemo(() => {
let filtered = applications.filter(app => {
// Status filter
if (filters.status !== 'all' && app.status !== filters.status) {
return false;
}
// Date range filter
if (filters.dateRange !== 'all') {
const appDate = new Date(app.plannedDate);
const now = new Date();
switch (filters.dateRange) {
case 'today':
if (appDate.toDateString() !== now.toDateString()) return false;
break;
case 'week':
const weekAgo = new Date(now.getTime() - 7 * 24 * 60 * 60 * 1000);
if (appDate < weekAgo) return false;
break;
case 'month':
const monthAgo = new Date(now.getTime() - 30 * 24 * 60 * 60 * 1000);
if (appDate < monthAgo) return false;
break;
}
}
// Area filter
const area = app.totalSectionArea || app.sectionArea || 0;
if (filters.minArea && area < parseInt(filters.minArea)) {
return false;
}
if (filters.maxArea && area > parseInt(filters.maxArea)) {
return false;
}
// Property filter
if (filters.property !== 'all' && app.propertyName !== filters.property) {
return false;
}
// Product filter
if (filters.product !== 'all') {
const hasProduct = app.productDetails?.some(p => p.name.toLowerCase().includes(filters.product.toLowerCase())) ||
app.products?.some(p => p.productName?.toLowerCase().includes(filters.product.toLowerCase()));
if (!hasProduct) return false;
}
return true;
});
// Sort
return filtered.sort((a, b) => {
let aVal, bVal;
switch (sortBy) {
case 'date':
aVal = new Date(a.plannedDate);
bVal = new Date(b.plannedDate);
break;
case 'area':
aVal = a.totalSectionArea || a.sectionArea || 0;
bVal = b.totalSectionArea || b.sectionArea || 0;
break;
case 'property':
aVal = a.propertyName || '';
bVal = b.propertyName || '';
break;
case 'status':
aVal = a.status || '';
bVal = b.status || '';
break;
default:
return 0;
}
if (sortOrder === 'desc') {
return bVal > aVal ? 1 : bVal < aVal ? -1 : 0;
} else {
return aVal > bVal ? 1 : aVal < bVal ? -1 : 0;
}
});
}, [applications, filters, sortBy, sortOrder]);
// Get unique values for filter dropdowns
const uniqueProperties = React.useMemo(() => {
const props = [...new Set(applications.map(app => app.propertyName).filter(Boolean))];
return props.sort();
}, [applications]);
const uniqueProducts = React.useMemo(() => {
const products = new Set();
applications.forEach(app => {
app.productDetails?.forEach(p => products.add(p.name));
app.products?.forEach(p => p.productName && products.add(p.productName));
});
return [...products].sort();
}, [applications]);
if (loading) {
return (
<div className="p-6">
@@ -322,6 +450,164 @@ const Applications = () => {
</button>
</div>
{/* Filter Controls */}
{applications.length > 0 && (
<div className="mb-6">
<div className="bg-white p-4 rounded-lg shadow">
<div className="flex items-center justify-between mb-4">
<button
onClick={() => setShowFilters(!showFilters)}
className="flex items-center gap-2 text-gray-700 hover:text-gray-900"
>
<FunnelIcon className="h-5 w-5" />
Filters & Sort
{showFilters && <span className="text-sm text-gray-500">(hide)</span>}
</button>
<div className="text-sm text-gray-600">
{filteredAndSortedApplications.length} of {applications.length} applications
</div>
</div>
{showFilters && (
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
{/* Status Filter */}
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">Status</label>
<select
value={filters.status}
onChange={(e) => setFilters(prev => ({ ...prev, status: e.target.value }))}
className="w-full border border-gray-300 rounded px-3 py-2 text-sm"
>
<option value="all">All Status</option>
<option value="planned">Planned</option>
<option value="in_progress">In Progress</option>
<option value="completed">Completed</option>
</select>
</div>
{/* Date Range Filter */}
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">Date Range</label>
<select
value={filters.dateRange}
onChange={(e) => setFilters(prev => ({ ...prev, dateRange: e.target.value }))}
className="w-full border border-gray-300 rounded px-3 py-2 text-sm"
>
<option value="all">All Dates</option>
<option value="today">Today</option>
<option value="week">Last Week</option>
<option value="month">Last Month</option>
</select>
</div>
{/* Property Filter */}
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">Property</label>
<select
value={filters.property}
onChange={(e) => setFilters(prev => ({ ...prev, property: e.target.value }))}
className="w-full border border-gray-300 rounded px-3 py-2 text-sm"
>
<option value="all">All Properties</option>
{uniqueProperties.map(property => (
<option key={property} value={property}>{property}</option>
))}
</select>
</div>
{/* Sort By */}
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">Sort By</label>
<div className="flex gap-2">
<select
value={sortBy}
onChange={(e) => setSortBy(e.target.value)}
className="flex-1 border border-gray-300 rounded px-3 py-2 text-sm"
>
<option value="date">Date</option>
<option value="property">Property</option>
<option value="area">Area Size</option>
<option value="status">Status</option>
</select>
<button
onClick={() => setSortOrder(sortOrder === 'asc' ? 'desc' : 'asc')}
className="px-3 py-2 border border-gray-300 rounded text-sm hover:bg-gray-50"
>
{sortOrder === 'desc' ? '↓' : '↑'}
</button>
</div>
</div>
{/* Area Range Filter */}
<div className="md:col-span-2">
<label className="block text-sm font-medium text-gray-700 mb-1">Area Range (sq ft)</label>
<div className="flex gap-2 items-center">
<input
type="number"
value={filters.minArea}
onChange={(e) => setFilters(prev => ({ ...prev, minArea: e.target.value }))}
placeholder="Min"
className="flex-1 border border-gray-300 rounded px-3 py-2 text-sm"
/>
<span className="text-gray-500">to</span>
<input
type="number"
value={filters.maxArea}
onChange={(e) => setFilters(prev => ({ ...prev, maxArea: e.target.value }))}
placeholder="Max"
className="flex-1 border border-gray-300 rounded px-3 py-2 text-sm"
/>
</div>
</div>
{/* Product Filter */}
<div className="md:col-span-2">
<label className="block text-sm font-medium text-gray-700 mb-1">Product Contains</label>
<div className="flex gap-2">
<input
type="text"
value={filters.product}
onChange={(e) => setFilters(prev => ({ ...prev, product: e.target.value }))}
placeholder="Search products..."
className="flex-1 border border-gray-300 rounded px-3 py-2 text-sm"
/>
{filters.product !== 'all' && filters.product !== '' && (
<button
onClick={() => setFilters(prev => ({ ...prev, product: 'all' }))}
className="px-3 py-2 text-gray-400 hover:text-gray-600"
>
<XMarkIcon className="h-4 w-4" />
</button>
)}
</div>
</div>
{/* Clear Filters Button */}
<div className="md:col-span-4 pt-2">
<button
onClick={() => {
setFilters({
status: 'all',
dateRange: 'all',
product: 'all',
minArea: '',
maxArea: '',
property: 'all'
});
setSortBy('date');
setSortOrder('desc');
}}
className="text-sm text-blue-600 hover:text-blue-800"
>
Clear all filters
</button>
</div>
</div>
)}
</div>
</div>
)}
{/* Applications List */}
{applications.length === 0 ? (
<div className="card text-center py-12">
@@ -342,7 +628,7 @@ const Applications = () => {
</div>
) : (
<div className="space-y-4">
{applications.map((application) => (
{filteredAndSortedApplications.map((application) => (
<div key={application.id} className="card">
<div className="flex justify-between items-start">
<div className="flex-1">
@@ -456,6 +742,7 @@ const Applications = () => {
</>
)}
{application.status === 'completed' && (
<>
<button
onClick={() => handleViewApplication(application)}
className="p-1 text-indigo-600 hover:text-indigo-800 hover:bg-indigo-50 rounded"
@@ -463,6 +750,14 @@ const Applications = () => {
>
<EyeIcon className="h-4 w-4" />
</button>
<button
onClick={() => handleArchiveApplication(application.id)}
className="p-1 text-gray-600 hover:text-gray-800 hover:bg-gray-50 rounded"
title="Archive application"
>
<ArchiveBoxIcon className="h-4 w-4" />
</button>
</>
)}
</div>
</div>