archive and stuff
This commit is contained in:
@@ -429,14 +429,14 @@ const PropertyMap = ({
|
|||||||
})}
|
})}
|
||||||
|
|
||||||
{/* GPS Tracking Elements */}
|
{/* GPS Tracking Elements */}
|
||||||
{mode === "execution" && (
|
{(mode === "execution" || mode === "view") && (
|
||||||
<>
|
<>
|
||||||
{/* GPS Track Polyline */}
|
{/* GPS Track Polyline */}
|
||||||
{gpsTrack.length > 1 && (
|
{gpsTrack.length > 1 && (
|
||||||
<Polyline
|
<Polyline
|
||||||
positions={gpsTrack.map(point => [point.lat, point.lng])}
|
positions={gpsTrack.map(point => [point.lat, point.lng])}
|
||||||
pathOptions={{
|
pathOptions={{
|
||||||
color: '#10B981',
|
color: '#EF4444',
|
||||||
weight: 4,
|
weight: 4,
|
||||||
opacity: 0.8,
|
opacity: 0.8,
|
||||||
}}
|
}}
|
||||||
@@ -454,8 +454,8 @@ const PropertyMap = ({
|
|||||||
)
|
)
|
||||||
))}
|
))}
|
||||||
|
|
||||||
{/* Current Location */}
|
{/* Current Location - only for execution mode */}
|
||||||
{currentLocation && (
|
{currentLocation && mode === "execution" && (
|
||||||
<Marker
|
<Marker
|
||||||
position={[currentLocation.lat, currentLocation.lng]}
|
position={[currentLocation.lat, currentLocation.lng]}
|
||||||
icon={currentLocationIcon}
|
icon={currentLocationIcon}
|
||||||
|
|||||||
@@ -8,7 +8,10 @@ import {
|
|||||||
PencilIcon,
|
PencilIcon,
|
||||||
TrashIcon,
|
TrashIcon,
|
||||||
PlayIcon,
|
PlayIcon,
|
||||||
EyeIcon
|
EyeIcon,
|
||||||
|
ArchiveBoxIcon,
|
||||||
|
FunnelIcon,
|
||||||
|
XMarkIcon
|
||||||
} from '@heroicons/react/24/outline';
|
} from '@heroicons/react/24/outline';
|
||||||
import { propertiesAPI, productsAPI, equipmentAPI, applicationsAPI, nozzlesAPI } from '../../services/api';
|
import { propertiesAPI, productsAPI, equipmentAPI, applicationsAPI, nozzlesAPI } from '../../services/api';
|
||||||
import LoadingSpinner from '../../components/UI/LoadingSpinner';
|
import LoadingSpinner from '../../components/UI/LoadingSpinner';
|
||||||
@@ -36,6 +39,18 @@ const Applications = () => {
|
|||||||
const [showViewModal, setShowViewModal] = useState(false);
|
const [showViewModal, setShowViewModal] = useState(false);
|
||||||
const [viewingApplication, setViewingApplication] = useState(null);
|
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(() => {
|
useEffect(() => {
|
||||||
fetchApplications();
|
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) {
|
if (loading) {
|
||||||
return (
|
return (
|
||||||
<div className="p-6">
|
<div className="p-6">
|
||||||
@@ -322,6 +450,164 @@ const Applications = () => {
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</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 List */}
|
||||||
{applications.length === 0 ? (
|
{applications.length === 0 ? (
|
||||||
<div className="card text-center py-12">
|
<div className="card text-center py-12">
|
||||||
@@ -342,7 +628,7 @@ const Applications = () => {
|
|||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
{applications.map((application) => (
|
{filteredAndSortedApplications.map((application) => (
|
||||||
<div key={application.id} className="card">
|
<div key={application.id} className="card">
|
||||||
<div className="flex justify-between items-start">
|
<div className="flex justify-between items-start">
|
||||||
<div className="flex-1">
|
<div className="flex-1">
|
||||||
@@ -456,13 +742,22 @@ const Applications = () => {
|
|||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
{application.status === 'completed' && (
|
{application.status === 'completed' && (
|
||||||
<button
|
<>
|
||||||
onClick={() => handleViewApplication(application)}
|
<button
|
||||||
className="p-1 text-indigo-600 hover:text-indigo-800 hover:bg-indigo-50 rounded"
|
onClick={() => handleViewApplication(application)}
|
||||||
title="View completed application"
|
className="p-1 text-indigo-600 hover:text-indigo-800 hover:bg-indigo-50 rounded"
|
||||||
>
|
title="View completed application"
|
||||||
<EyeIcon className="h-4 w-4" />
|
>
|
||||||
</button>
|
<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>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user