diff --git a/frontend/src/pages/Nozzles/Nozzles.js b/frontend/src/pages/Nozzles/Nozzles.js new file mode 100644 index 0000000..507f749 --- /dev/null +++ b/frontend/src/pages/Nozzles/Nozzles.js @@ -0,0 +1,515 @@ +import React, { useState, useEffect } from 'react'; +import { + PlusIcon, + MagnifyingGlassIcon, + TrashIcon, + PencilIcon, + BeakerIcon +} from '@heroicons/react/24/outline'; +import { nozzlesAPI } from '../../services/api'; +import LoadingSpinner from '../../components/UI/LoadingSpinner'; +import toast from 'react-hot-toast'; + +const Nozzles = () => { + const [userNozzles, setUserNozzles] = useState([]); + const [nozzleTypes, setNozzleTypes] = useState([]); + const [loading, setLoading] = useState(true); + const [showCreateForm, setShowCreateForm] = useState(false); + const [showEditForm, setShowEditForm] = useState(false); + const [editingNozzle, setEditingNozzle] = useState(null); + const [searchTerm, setSearchTerm] = useState(''); + const [selectedManufacturer, setSelectedManufacturer] = useState(''); + const [selectedDropletSize, setSelectedDropletSize] = useState(''); + + useEffect(() => { + fetchData(); + }, []); + + const fetchData = async () => { + try { + setLoading(true); + const [userNozzlesResponse, nozzleTypesResponse] = await Promise.all([ + nozzlesAPI.getUserNozzles(), + nozzlesAPI.getNozzleTypes() + ]); + + setUserNozzles(userNozzlesResponse.data.data.userNozzles || []); + setNozzleTypes(nozzleTypesResponse.data.data.nozzleTypes || []); + } catch (error) { + console.error('Failed to fetch nozzles:', error); + toast.error('Failed to load nozzles'); + setUserNozzles([]); + setNozzleTypes([]); + } finally { + setLoading(false); + } + }; + + const handleCreateNozzle = async (nozzleData) => { + try { + await nozzlesAPI.create(nozzleData); + toast.success('Nozzle added successfully!'); + setShowCreateForm(false); + fetchData(); + } catch (error) { + console.error('Failed to create nozzle:', error); + toast.error('Failed to add nozzle'); + } + }; + + const handleEditNozzle = (nozzle) => { + setEditingNozzle(nozzle); + setShowEditForm(true); + }; + + const handleUpdateNozzle = async (nozzleData) => { + try { + await nozzlesAPI.update(editingNozzle.id, nozzleData); + toast.success('Nozzle updated successfully!'); + setShowEditForm(false); + setEditingNozzle(null); + fetchData(); + } catch (error) { + console.error('Failed to update nozzle:', error); + toast.error('Failed to update nozzle'); + } + }; + + const handleDeleteNozzle = async (nozzleId) => { + if (window.confirm('Are you sure you want to delete this nozzle?')) { + try { + await nozzlesAPI.delete(nozzleId); + toast.success('Nozzle deleted successfully'); + fetchData(); + } catch (error) { + console.error('Failed to delete nozzle:', error); + toast.error('Failed to delete nozzle'); + } + } + }; + + // Filter nozzles based on search and filters + const filteredNozzles = userNozzles.filter(nozzle => { + const matchesSearch = searchTerm === '' || + nozzle.nozzleType?.name?.toLowerCase().includes(searchTerm.toLowerCase()) || + nozzle.customName?.toLowerCase().includes(searchTerm.toLowerCase()) || + nozzle.nozzleType?.manufacturer?.toLowerCase().includes(searchTerm.toLowerCase()); + + const matchesManufacturer = selectedManufacturer === '' || + nozzle.nozzleType?.manufacturer === selectedManufacturer; + + const matchesDropletSize = selectedDropletSize === '' || + nozzle.nozzleType?.dropletSize === selectedDropletSize; + + return matchesSearch && matchesManufacturer && matchesDropletSize; + }); + + // Get unique manufacturers and droplet sizes for filters + const manufacturers = [...new Set(nozzleTypes.map(type => type.manufacturer).filter(Boolean))]; + const dropletSizes = [...new Set(nozzleTypes.map(type => type.dropletSize).filter(Boolean))]; + + const NozzleCard = ({ nozzle }) => ( +
+
+
+
+ +
+
+

+ {nozzle.customName || nozzle.nozzleType?.name} +

+ {nozzle.nozzleType?.manufacturer && ( +

{nozzle.nozzleType.manufacturer} {nozzle.nozzleType.model}

+ )} +
+ + Qty: {nozzle.quantity} + + + {nozzle.condition} + +
+
+
+ +
+ + +
+
+ + {/* Nozzle Specifications */} +
+ {nozzle.nozzleType?.orificeSize && ( +
Orifice: {nozzle.nozzleType.orificeSize}
+ )} + {nozzle.nozzleType?.sprayAngle && ( +
Spray Angle: {nozzle.nozzleType.sprayAngle}°
+ )} + {nozzle.nozzleType?.dropletSize && ( +
Droplet Size: {nozzle.nozzleType.dropletSize}
+ )} + {nozzle.nozzleType?.sprayPattern && ( +
Pattern: {nozzle.nozzleType.sprayPattern.replace('_', ' ')}
+ )} + {nozzle.nozzleType?.flowRateGpm && ( +
Flow Rate: {nozzle.nozzleType.flowRateGpm} GPM @ rated PSI
+ )} + {nozzle.nozzleType?.pressureRangePsi && ( +
Pressure Range: {nozzle.nozzleType.pressureRangePsi} PSI
+ )} +
+ + {nozzle.notes && ( +

+ Notes: {nozzle.notes} +

+ )} + + {nozzle.purchaseDate && ( +

+ Purchased: {new Date(nozzle.purchaseDate).toLocaleDateString()} +

+ )} +
+ ); + + const getConditionColor = (condition) => { + const colors = { + 'excellent': 'bg-green-100 text-green-800', + 'good': 'bg-blue-100 text-blue-800', + 'fair': 'bg-yellow-100 text-yellow-800', + 'poor': 'bg-orange-100 text-orange-800', + 'needs_replacement': 'bg-red-100 text-red-800', + }; + return colors[condition] || 'bg-gray-100 text-gray-800'; + }; + + if (loading) { + return ( +
+
+ +
+
+ ); + } + + return ( +
+
+
+

Nozzles

+

Manage your spray nozzle inventory with specifications

+
+ +
+ + {/* Search and Filters */} +
+
+ {/* Search */} +
+
+ + setSearchTerm(e.target.value)} + /> +
+
+ + {/* Manufacturer Filter */} +
+ +
+ + {/* Droplet Size Filter */} +
+ +
+
+
+ + {/* Nozzles Grid */} + {filteredNozzles.length === 0 ? ( +
+ +

No Nozzles Found

+

+ {searchTerm || selectedManufacturer || selectedDropletSize + ? 'Try adjusting your search or filters' + : 'Start building your nozzle inventory' + } +

+ {!searchTerm && !selectedManufacturer && !selectedDropletSize && ( + + )} +
+ ) : ( +
+ {filteredNozzles.map((nozzle) => ( + + ))} +
+ )} + + {/* Create Nozzle Form Modal */} + {showCreateForm && ( + setShowCreateForm(false)} + /> + )} + + {/* Edit Nozzle Form Modal */} + {showEditForm && editingNozzle && ( + { + setShowEditForm(false); + setEditingNozzle(null); + }} + /> + )} +
+ ); +}; + +// Nozzle Form Modal Component +const NozzleFormModal = ({ isEdit, nozzle, nozzleTypes, onSubmit, onCancel }) => { + const [formData, setFormData] = useState({ + nozzleTypeId: nozzle?.nozzleTypeId || '', + customName: nozzle?.customName || '', + quantity: nozzle?.quantity || 1, + condition: nozzle?.condition || 'good', + purchaseDate: nozzle?.purchaseDate || '', + notes: nozzle?.notes || '' + }); + + const [selectedNozzleType, setSelectedNozzleType] = useState( + nozzle?.nozzleType || null + ); + + useEffect(() => { + if (formData.nozzleTypeId) { + const nozzleType = nozzleTypes.find(type => type.id === parseInt(formData.nozzleTypeId)); + setSelectedNozzleType(nozzleType); + } + }, [formData.nozzleTypeId, nozzleTypes]); + + const handleSubmit = (e) => { + e.preventDefault(); + + if (!formData.nozzleTypeId) { + toast.error('Please select a nozzle type'); + return; + } + + const submitData = { + nozzleTypeId: parseInt(formData.nozzleTypeId), + customName: formData.customName || null, + quantity: parseInt(formData.quantity), + condition: formData.condition, + purchaseDate: formData.purchaseDate || null, + notes: formData.notes || null + }; + + onSubmit(submitData); + }; + + // Group nozzle types by manufacturer and droplet size for better organization + const groupedNozzleTypes = nozzleTypes.reduce((groups, type) => { + const key = `${type.manufacturer || 'Other'} - ${type.dropletSize || 'Unknown'}`; + if (!groups[key]) { + groups[key] = []; + } + groups[key].push(type); + return groups; + }, {}); + + return ( +
+
+

+ {isEdit ? 'Edit Nozzle' : 'Add New Nozzle'} +

+ +
+ {/* Nozzle Type Selection */} +
+ + +
+ + {/* Nozzle Specifications Preview */} + {selectedNozzleType && ( +
+

Nozzle Specifications

+
+
Manufacturer: {selectedNozzleType.manufacturer}
+
Model: {selectedNozzleType.model}
+
Orifice Size: {selectedNozzleType.orificeSize}
+
Spray Angle: {selectedNozzleType.sprayAngle}°
+
Flow Rate: {selectedNozzleType.flowRateGpm} GPM
+
Droplet Size: {selectedNozzleType.dropletSize}
+
Spray Pattern: {selectedNozzleType.sprayPattern?.replace('_', ' ')}
+
Pressure Range: {selectedNozzleType.pressureRangePsi} PSI
+
+
+ )} + + {/* Custom Name */} +
+ + setFormData({ ...formData, customName: e.target.value })} + placeholder="e.g., Backyard Sprayer Nozzles" + /> +
+ + {/* Quantity and Condition */} +
+
+ + setFormData({ ...formData, quantity: e.target.value })} + required + /> +
+
+ + +
+
+ + {/* Purchase Date */} +
+ + setFormData({ ...formData, purchaseDate: e.target.value })} + /> +
+ + {/* Notes */} +
+ +