applications page
This commit is contained in:
@@ -1,11 +1,332 @@
|
|||||||
import React from 'react';
|
import React, { useState, useEffect } from 'react';
|
||||||
|
import {
|
||||||
|
PlusIcon,
|
||||||
|
MapPinIcon,
|
||||||
|
BeakerIcon,
|
||||||
|
WrenchScrewdriverIcon,
|
||||||
|
CalculatorIcon
|
||||||
|
} from '@heroicons/react/24/outline';
|
||||||
|
import { propertiesAPI, productsAPI, equipmentAPI } from '../../services/api';
|
||||||
|
import LoadingSpinner from '../../components/UI/LoadingSpinner';
|
||||||
|
import toast from 'react-hot-toast';
|
||||||
|
|
||||||
const Applications = () => {
|
const Applications = () => {
|
||||||
|
const [showPlanForm, setShowPlanForm] = useState(false);
|
||||||
|
const [applications, setApplications] = useState([]);
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
fetchApplications();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const fetchApplications = async () => {
|
||||||
|
try {
|
||||||
|
setLoading(true);
|
||||||
|
// TODO: Implement applications API endpoint
|
||||||
|
setApplications([]);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to fetch applications:', error);
|
||||||
|
toast.error('Failed to load applications');
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (loading) {
|
||||||
|
return (
|
||||||
|
<div className="p-6">
|
||||||
|
<div className="flex justify-center items-center h-64">
|
||||||
|
<LoadingSpinner size="lg" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="p-6">
|
<div className="p-6">
|
||||||
<h1 className="text-2xl font-bold text-gray-900 mb-6">Applications</h1>
|
<div className="flex justify-between items-center mb-6">
|
||||||
<div className="card">
|
<div>
|
||||||
<p className="text-gray-600">Application management coming soon...</p>
|
<h1 className="text-2xl font-bold text-gray-900">Applications</h1>
|
||||||
|
<p className="text-gray-600">Plan, track, and log your lawn applications</p>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
onClick={() => setShowPlanForm(true)}
|
||||||
|
className="btn-primary flex items-center gap-2"
|
||||||
|
>
|
||||||
|
<PlusIcon className="h-5 w-5" />
|
||||||
|
Plan Application
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Applications List */}
|
||||||
|
{applications.length === 0 ? (
|
||||||
|
<div className="card text-center py-12">
|
||||||
|
<CalculatorIcon className="h-16 w-16 text-gray-300 mx-auto mb-4" />
|
||||||
|
<h3 className="text-lg font-medium text-gray-900 mb-2">No Applications Yet</h3>
|
||||||
|
<p className="text-gray-600 mb-6">
|
||||||
|
Start by planning your first lawn application
|
||||||
|
</p>
|
||||||
|
<button
|
||||||
|
onClick={() => setShowPlanForm(true)}
|
||||||
|
className="btn-primary"
|
||||||
|
>
|
||||||
|
Plan Your First Application
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="space-y-4">
|
||||||
|
{applications.map((application) => (
|
||||||
|
<div key={application.id} className="card">
|
||||||
|
{/* Application card content will go here */}
|
||||||
|
<p>Application: {application.id}</p>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Plan Application Modal */}
|
||||||
|
{showPlanForm && (
|
||||||
|
<ApplicationPlanModal
|
||||||
|
onClose={() => setShowPlanForm(false)}
|
||||||
|
onSubmit={(planData) => {
|
||||||
|
console.log('Plan submitted:', planData);
|
||||||
|
setShowPlanForm(false);
|
||||||
|
fetchApplications();
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Application Planning Modal Component
|
||||||
|
const ApplicationPlanModal = ({ onClose, onSubmit }) => {
|
||||||
|
const [properties, setProperties] = useState([]);
|
||||||
|
const [products, setProducts] = useState([]);
|
||||||
|
const [equipment, setEquipment] = useState([]);
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
|
|
||||||
|
const [planData, setPlanData] = useState({
|
||||||
|
propertyId: '',
|
||||||
|
selectedAreas: [],
|
||||||
|
productId: '',
|
||||||
|
applicationType: '', // 'liquid' or 'granular'
|
||||||
|
equipmentId: '',
|
||||||
|
notes: ''
|
||||||
|
});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
fetchPlanningData();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const fetchPlanningData = async () => {
|
||||||
|
try {
|
||||||
|
setLoading(true);
|
||||||
|
const [propertiesResponse, productsResponse, equipmentResponse] = await Promise.all([
|
||||||
|
propertiesAPI.getAll(),
|
||||||
|
productsAPI.getUserProducts(),
|
||||||
|
equipmentAPI.getAll()
|
||||||
|
]);
|
||||||
|
|
||||||
|
setProperties(propertiesResponse.data.data.properties || []);
|
||||||
|
setProducts(productsResponse.data.data.products || []);
|
||||||
|
setEquipment(equipmentResponse.data.data.equipment || []);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to fetch planning data:', error);
|
||||||
|
toast.error('Failed to load planning data');
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const selectedProperty = properties.find(p => p.id === parseInt(planData.propertyId));
|
||||||
|
|
||||||
|
// Filter equipment based on application type
|
||||||
|
const availableEquipment = equipment.filter(eq => {
|
||||||
|
if (planData.applicationType === 'liquid') {
|
||||||
|
return eq.categoryName === 'Sprayer';
|
||||||
|
} else if (planData.applicationType === 'granular') {
|
||||||
|
return eq.categoryName === 'Spreader';
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleSubmit = (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
|
||||||
|
if (!planData.propertyId || planData.selectedAreas.length === 0) {
|
||||||
|
toast.error('Please select a property and at least one area');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!planData.productId) {
|
||||||
|
toast.error('Please select a product');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!planData.equipmentId) {
|
||||||
|
toast.error('Please select equipment');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
onSubmit(planData);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleAreaToggle = (areaId) => {
|
||||||
|
setPlanData(prev => ({
|
||||||
|
...prev,
|
||||||
|
selectedAreas: prev.selectedAreas.includes(areaId)
|
||||||
|
? prev.selectedAreas.filter(id => id !== areaId)
|
||||||
|
: [...prev.selectedAreas, areaId]
|
||||||
|
}));
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
|
||||||
|
<div className="bg-white rounded-lg p-6 w-full max-w-4xl max-h-[90vh] overflow-y-auto">
|
||||||
|
<h3 className="text-lg font-semibold mb-4">Plan Application</h3>
|
||||||
|
|
||||||
|
{loading ? (
|
||||||
|
<LoadingSpinner />
|
||||||
|
) : (
|
||||||
|
<form onSubmit={handleSubmit} className="space-y-6">
|
||||||
|
{/* Property Selection */}
|
||||||
|
<div>
|
||||||
|
<label className="label flex items-center gap-2">
|
||||||
|
<MapPinIcon className="h-5 w-5" />
|
||||||
|
Property *
|
||||||
|
</label>
|
||||||
|
<select
|
||||||
|
className="input"
|
||||||
|
value={planData.propertyId}
|
||||||
|
onChange={(e) => setPlanData({ ...planData, propertyId: e.target.value, selectedAreas: [] })}
|
||||||
|
required
|
||||||
|
>
|
||||||
|
<option value="">Select a property...</option>
|
||||||
|
{properties.map((property) => (
|
||||||
|
<option key={property.id} value={property.id}>
|
||||||
|
{property.name} - {property.address}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Area Selection */}
|
||||||
|
{selectedProperty && selectedProperty.sections && selectedProperty.sections.length > 0 && (
|
||||||
|
<div>
|
||||||
|
<label className="label">Application Areas *</label>
|
||||||
|
<div className="grid grid-cols-2 md:grid-cols-3 gap-3">
|
||||||
|
{selectedProperty.sections.map((section) => (
|
||||||
|
<label key={section.id} className="flex items-center">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
checked={planData.selectedAreas.includes(section.id)}
|
||||||
|
onChange={() => handleAreaToggle(section.id)}
|
||||||
|
className="rounded border-gray-300 text-blue-600 focus:ring-blue-500"
|
||||||
|
/>
|
||||||
|
<span className="ml-2 text-sm">
|
||||||
|
{section.name} ({section.sqFt ? `${section.sqFt} sq ft` : 'No size'})
|
||||||
|
</span>
|
||||||
|
</label>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
{planData.selectedAreas.length > 0 && (
|
||||||
|
<p className="text-sm text-gray-600 mt-2">
|
||||||
|
Total area: {selectedProperty.sections
|
||||||
|
.filter(s => planData.selectedAreas.includes(s.id))
|
||||||
|
.reduce((total, s) => total + (s.sqFt || 0), 0)} sq ft
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Product Selection */}
|
||||||
|
<div>
|
||||||
|
<label className="label flex items-center gap-2">
|
||||||
|
<BeakerIcon className="h-5 w-5" />
|
||||||
|
Product *
|
||||||
|
</label>
|
||||||
|
<select
|
||||||
|
className="input"
|
||||||
|
value={planData.productId}
|
||||||
|
onChange={(e) => {
|
||||||
|
const selectedProduct = products.find(p => p.id === parseInt(e.target.value));
|
||||||
|
setPlanData({
|
||||||
|
...planData,
|
||||||
|
productId: e.target.value,
|
||||||
|
applicationType: selectedProduct?.applicationType || ''
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
required
|
||||||
|
>
|
||||||
|
<option value="">Select a product...</option>
|
||||||
|
{products.map((product) => (
|
||||||
|
<option key={product.id} value={product.id}>
|
||||||
|
{product.name} - {product.applicationType}
|
||||||
|
{product.applicationRate && ` (${product.applicationRate} ${product.applicationRateUnit})`}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Equipment Selection */}
|
||||||
|
{planData.applicationType && (
|
||||||
|
<div>
|
||||||
|
<label className="label flex items-center gap-2">
|
||||||
|
<WrenchScrewdriverIcon className="h-5 w-5" />
|
||||||
|
Equipment * ({planData.applicationType})
|
||||||
|
</label>
|
||||||
|
<select
|
||||||
|
className="input"
|
||||||
|
value={planData.equipmentId}
|
||||||
|
onChange={(e) => setPlanData({ ...planData, equipmentId: e.target.value })}
|
||||||
|
required
|
||||||
|
>
|
||||||
|
<option value="">Select equipment...</option>
|
||||||
|
{availableEquipment.map((eq) => (
|
||||||
|
<option key={eq.id} value={eq.id}>
|
||||||
|
{eq.customName || eq.typeName}
|
||||||
|
{eq.manufacturer && ` - ${eq.manufacturer}`}
|
||||||
|
{eq.tankSizeGallons && ` (${eq.tankSizeGallons} gal)`}
|
||||||
|
{eq.capacityLbs && ` (${eq.capacityLbs} lbs)`}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
{availableEquipment.length === 0 && (
|
||||||
|
<p className="text-sm text-orange-600 mt-1">
|
||||||
|
No {planData.applicationType === 'liquid' ? 'sprayers' : 'spreaders'} found.
|
||||||
|
Please add equipment first.
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Notes */}
|
||||||
|
<div>
|
||||||
|
<label className="label">Notes</label>
|
||||||
|
<textarea
|
||||||
|
className="input"
|
||||||
|
rows="3"
|
||||||
|
value={planData.notes}
|
||||||
|
onChange={(e) => setPlanData({ ...planData, notes: e.target.value })}
|
||||||
|
placeholder="Application notes, weather conditions, etc."
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex gap-3 pt-4">
|
||||||
|
<button type="submit" className="btn-primary flex-1">
|
||||||
|
Create Application Plan
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={onClose}
|
||||||
|
className="btn-secondary flex-1"
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
Reference in New Issue
Block a user