archive and stuff
This commit is contained in:
@@ -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}
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user