1493 lines
55 KiB
JavaScript
1493 lines
55 KiB
JavaScript
import React, { useState, useEffect } from 'react';
|
|
import {
|
|
PlusIcon,
|
|
MagnifyingGlassIcon,
|
|
FunnelIcon,
|
|
WrenchScrewdriverIcon,
|
|
TrashIcon,
|
|
PencilIcon,
|
|
EyeIcon
|
|
} from '@heroicons/react/24/outline';
|
|
import { equipmentAPI, nozzlesAPI } from '../../services/api';
|
|
import LoadingSpinner from '../../components/UI/LoadingSpinner';
|
|
import toast from 'react-hot-toast';
|
|
|
|
const Equipment = () => {
|
|
const [equipment, setEquipment] = useState([]);
|
|
const [categories, setCategories] = useState([]);
|
|
const [equipmentTypes, setEquipmentTypes] = useState([]);
|
|
const [loading, setLoading] = useState(true);
|
|
const [showCreateForm, setShowCreateForm] = useState(false);
|
|
const [showEditForm, setShowEditForm] = useState(false);
|
|
const [editingEquipment, setEditingEquipment] = useState(null);
|
|
const [selectedCategory, setSelectedCategory] = useState('');
|
|
const [searchTerm, setSearchTerm] = useState('');
|
|
const [showInactive, setShowInactive] = useState(false);
|
|
const [activeTab, setActiveTab] = useState('all');
|
|
const [showPumpAssignments, setShowPumpAssignments] = useState(false);
|
|
const [showNozzleConfigs, setShowNozzleConfigs] = useState(false);
|
|
const [selectedSprayerForPump, setSelectedSprayerForPump] = useState(null);
|
|
const [selectedSprayerForNozzles, setSelectedSprayerForNozzles] = useState(null);
|
|
|
|
useEffect(() => {
|
|
fetchData();
|
|
}, []);
|
|
|
|
const fetchData = async () => {
|
|
try {
|
|
setLoading(true);
|
|
const [equipmentResponse, categoriesResponse, typesResponse] = await Promise.all([
|
|
equipmentAPI.getAll({
|
|
category_id: selectedCategory,
|
|
is_active: !showInactive
|
|
}),
|
|
equipmentAPI.getCategories(),
|
|
equipmentAPI.getTypes()
|
|
]);
|
|
|
|
setEquipment(equipmentResponse.data.data.equipment || []);
|
|
setCategories(categoriesResponse.data.data.categories || []);
|
|
setEquipmentTypes(typesResponse.data.data.equipmentTypes || []);
|
|
} catch (error) {
|
|
console.error('Failed to fetch equipment:', error);
|
|
toast.error('Failed to load equipment');
|
|
setEquipment([]);
|
|
setCategories([]);
|
|
setEquipmentTypes([]);
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
};
|
|
|
|
const handleSearch = (e) => {
|
|
setSearchTerm(e.target.value);
|
|
};
|
|
|
|
const handleFilterChange = () => {
|
|
fetchData();
|
|
};
|
|
|
|
const handleCreateEquipment = async (equipmentData) => {
|
|
try {
|
|
await equipmentAPI.create(equipmentData);
|
|
toast.success('Equipment created successfully!');
|
|
setShowCreateForm(false);
|
|
fetchData();
|
|
} catch (error) {
|
|
console.error('Failed to create equipment:', error);
|
|
toast.error('Failed to create equipment');
|
|
}
|
|
};
|
|
|
|
const handleEditEquipment = (equipment) => {
|
|
setEditingEquipment(equipment);
|
|
setShowEditForm(true);
|
|
};
|
|
|
|
const handleUpdateEquipment = async (equipmentData) => {
|
|
try {
|
|
await equipmentAPI.update(editingEquipment.id, equipmentData);
|
|
toast.success('Equipment updated successfully!');
|
|
setShowEditForm(false);
|
|
setEditingEquipment(null);
|
|
fetchData();
|
|
} catch (error) {
|
|
console.error('Failed to update equipment:', error);
|
|
toast.error('Failed to update equipment');
|
|
}
|
|
};
|
|
|
|
const handleDeleteEquipment = async (equipmentId) => {
|
|
if (window.confirm('Are you sure you want to delete this equipment?')) {
|
|
try {
|
|
await equipmentAPI.delete(equipmentId);
|
|
toast.success('Equipment deleted successfully');
|
|
fetchData();
|
|
} catch (error) {
|
|
console.error('Failed to delete equipment:', error);
|
|
toast.error('Failed to delete equipment');
|
|
}
|
|
}
|
|
};
|
|
|
|
const handleManagePumpAssignments = (sprayer) => {
|
|
setSelectedSprayerForPump(sprayer);
|
|
setShowPumpAssignments(true);
|
|
};
|
|
|
|
const handleManageNozzleConfigs = (sprayer) => {
|
|
setSelectedSprayerForNozzles(sprayer);
|
|
setShowNozzleConfigs(true);
|
|
};
|
|
|
|
// Filter equipment based on search term and active tab
|
|
const filteredEquipment = equipment.filter(item => {
|
|
const matchesSearch = searchTerm === '' ||
|
|
item.customName?.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
|
item.typeName?.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
|
item.manufacturer?.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
|
item.model?.toLowerCase().includes(searchTerm.toLowerCase());
|
|
|
|
if (activeTab === 'all') return matchesSearch;
|
|
return matchesSearch && item.categoryName?.toLowerCase() === activeTab.toLowerCase();
|
|
});
|
|
|
|
// Get unique category names for tabs
|
|
const categoryTabs = ['all', ...new Set(equipment.map(item => item.categoryName).filter(Boolean))];
|
|
|
|
const EquipmentCard = ({ item }) => (
|
|
<div className="card">
|
|
<div className="flex justify-between items-start mb-3">
|
|
<div className="flex items-start gap-3">
|
|
<div className="p-2 bg-blue-100 rounded-lg">
|
|
<WrenchScrewdriverIcon className="h-5 w-5 text-blue-600" />
|
|
</div>
|
|
<div>
|
|
<h3 className="font-semibold text-gray-900">
|
|
{item.customName || item.typeName}
|
|
</h3>
|
|
{item.manufacturer && item.model && (
|
|
<p className="text-sm text-gray-600">{item.manufacturer} {item.model}</p>
|
|
)}
|
|
<span className={`inline-flex items-center px-2 py-1 rounded-full text-xs font-medium mt-1 ${
|
|
getCategoryColor(item.categoryName)
|
|
}`}>
|
|
{item.categoryName}
|
|
</span>
|
|
{!item.isActive && (
|
|
<span className="inline-flex items-center px-2 py-1 rounded-full text-xs font-medium mt-1 ml-2 bg-red-100 text-red-800">
|
|
Inactive
|
|
</span>
|
|
)}
|
|
</div>
|
|
</div>
|
|
|
|
<div className="flex gap-1">
|
|
{item.categoryName === 'Sprayer' && (
|
|
<>
|
|
<button
|
|
onClick={() => handleManagePumpAssignments(item)}
|
|
className="p-1 text-gray-400 hover:text-purple-600"
|
|
title="Manage pump assignments"
|
|
>
|
|
<WrenchScrewdriverIcon className="h-4 w-4" />
|
|
</button>
|
|
<button
|
|
onClick={() => handleManageNozzleConfigs(item)}
|
|
className="p-1 text-gray-400 hover:text-green-600"
|
|
title="Configure nozzles"
|
|
>
|
|
<EyeIcon className="h-4 w-4" />
|
|
</button>
|
|
</>
|
|
)}
|
|
<button
|
|
onClick={() => handleEditEquipment(item)}
|
|
className="p-1 text-gray-400 hover:text-blue-600"
|
|
title="Edit equipment"
|
|
>
|
|
<PencilIcon className="h-4 w-4" />
|
|
</button>
|
|
<button
|
|
onClick={() => handleDeleteEquipment(item.id)}
|
|
className="p-1 text-gray-400 hover:text-red-600"
|
|
title="Delete equipment"
|
|
>
|
|
<TrashIcon className="h-4 w-4" />
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Equipment-specific details */}
|
|
<div className="space-y-2 text-sm text-gray-600">
|
|
{renderEquipmentDetails(item)}
|
|
</div>
|
|
|
|
{item.notes && (
|
|
<p className="text-sm text-gray-600 mt-3">
|
|
<strong>Notes:</strong> {item.notes}
|
|
</p>
|
|
)}
|
|
|
|
{item.purchaseDate && (
|
|
<p className="text-xs text-gray-500 mt-2">
|
|
Purchased: {new Date(item.purchaseDate).toLocaleDateString()}
|
|
</p>
|
|
)}
|
|
</div>
|
|
);
|
|
|
|
const renderEquipmentDetails = (item) => {
|
|
switch (item.categoryName?.toLowerCase()) {
|
|
case 'mower':
|
|
return (
|
|
<>
|
|
{item.mowerStyle && <p><strong>Style:</strong> {item.mowerStyle.replace('_', ' ')}</p>}
|
|
{item.cuttingWidthInches && <p><strong>Cutting Width:</strong> {item.cuttingWidthInches}"</p>}
|
|
{item.engineHp && <p><strong>Engine:</strong> {item.engineHp} HP</p>}
|
|
{item.fuelType && <p><strong>Fuel:</strong> {item.fuelType}</p>}
|
|
</>
|
|
);
|
|
case 'spreader':
|
|
return (
|
|
<>
|
|
{item.spreaderType && <p><strong>Type:</strong> {item.spreaderType.replace('_', ' ')}</p>}
|
|
{item.capacityLbs && <p><strong>Capacity:</strong> {item.capacityLbs} lbs</p>}
|
|
{item.spreadWidth && <p><strong>Spread Width:</strong> {item.spreadWidth} ft</p>}
|
|
</>
|
|
);
|
|
case 'sprayer':
|
|
return (
|
|
<>
|
|
{item.sprayerType && <p><strong>Type:</strong> {item.sprayerType.replace('_', ' ')}</p>}
|
|
{item.tankSizeGallons && <p><strong>Tank Size:</strong> {item.tankSizeGallons} gal</p>}
|
|
{item.sprayWidthFeet && <p><strong>Spray Width:</strong> {item.sprayWidthFeet} ft</p>}
|
|
{item.pumpGpm && <p><strong>Pump:</strong> {item.pumpGpm} GPM</p>}
|
|
{item.boomSections && <p><strong>Boom Sections:</strong> {item.boomSections}</p>}
|
|
</>
|
|
);
|
|
case 'pump':
|
|
return (
|
|
<>
|
|
{item.pumpType && <p><strong>Type:</strong> {item.pumpType}</p>}
|
|
{item.maxGpm && <p><strong>Max Flow:</strong> {item.maxGpm} GPM</p>}
|
|
{item.maxPsi && <p><strong>Max Pressure:</strong> {item.maxPsi} PSI</p>}
|
|
{item.powerSource && <p><strong>Power:</strong> {item.powerSource}</p>}
|
|
</>
|
|
);
|
|
case 'nozzle':
|
|
return (
|
|
<>
|
|
{item.orificeSize && <p><strong>Orifice:</strong> {item.orificeSize}</p>}
|
|
{item.sprayAngle && <p><strong>Spray Angle:</strong> {item.sprayAngle}°</p>}
|
|
{item.flowRateGpm && <p><strong>Flow Rate:</strong> {item.flowRateGpm} GPM</p>}
|
|
{item.dropletSize && <p><strong>Droplet Size:</strong> {item.dropletSize}</p>}
|
|
{item.sprayPattern && <p><strong>Pattern:</strong> {item.sprayPattern.replace('_', ' ')}</p>}
|
|
{item.quantityOwned && <p><strong>Quantity:</strong> {item.quantityOwned}</p>}
|
|
</>
|
|
);
|
|
default:
|
|
return (
|
|
<>
|
|
{item.toolType && <p><strong>Type:</strong> {item.toolType.replace('_', ' ')}</p>}
|
|
{item.workingWidthInches && <p><strong>Working Width:</strong> {item.workingWidthInches}"</p>}
|
|
</>
|
|
);
|
|
}
|
|
};
|
|
|
|
const getCategoryColor = (categoryName) => {
|
|
const colors = {
|
|
'Mower': 'bg-green-100 text-green-800',
|
|
'Spreader': 'bg-orange-100 text-orange-800',
|
|
'Sprayer': 'bg-blue-100 text-blue-800',
|
|
'Nozzle': 'bg-teal-100 text-teal-800',
|
|
'Pump': 'bg-purple-100 text-purple-800',
|
|
'Aerator': 'bg-yellow-100 text-yellow-800',
|
|
'Dethatcher': 'bg-red-100 text-red-800',
|
|
'Scarifier': 'bg-pink-100 text-pink-800',
|
|
'Trimmer': 'bg-indigo-100 text-indigo-800',
|
|
};
|
|
return colors[categoryName] || 'bg-gray-100 text-gray-800';
|
|
};
|
|
|
|
if (loading) {
|
|
return (
|
|
<div className="p-6">
|
|
<div className="flex justify-center items-center h-64">
|
|
<LoadingSpinner size="lg" />
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<div className="p-6">
|
|
<div className="flex justify-between items-center mb-6">
|
|
<div>
|
|
<h1 className="text-2xl font-bold text-gray-900">Equipment</h1>
|
|
<p className="text-gray-600">Manage your lawn care equipment inventory</p>
|
|
</div>
|
|
<button
|
|
onClick={() => setShowCreateForm(true)}
|
|
className="btn-primary flex items-center gap-2"
|
|
>
|
|
<PlusIcon className="h-5 w-5" />
|
|
Add Equipment
|
|
</button>
|
|
</div>
|
|
|
|
{/* Search and Filters */}
|
|
<div className="card mb-6">
|
|
<div className="flex flex-wrap gap-4">
|
|
{/* Search */}
|
|
<div className="flex-1 min-w-64">
|
|
<div className="relative">
|
|
<MagnifyingGlassIcon className="h-5 w-5 absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400" />
|
|
<input
|
|
type="text"
|
|
className="input pl-10"
|
|
placeholder="Search equipment..."
|
|
value={searchTerm}
|
|
onChange={handleSearch}
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Category Filter */}
|
|
<div className="min-w-48">
|
|
<select
|
|
className="input"
|
|
value={selectedCategory}
|
|
onChange={(e) => {
|
|
setSelectedCategory(e.target.value);
|
|
handleFilterChange();
|
|
}}
|
|
>
|
|
<option value="">All Categories</option>
|
|
{categories.map((category) => (
|
|
<option key={category.id} value={category.id}>
|
|
{category.name}
|
|
</option>
|
|
))}
|
|
</select>
|
|
</div>
|
|
|
|
{/* Show Inactive Toggle */}
|
|
<div className="flex items-center">
|
|
<label className="flex items-center">
|
|
<input
|
|
type="checkbox"
|
|
checked={showInactive}
|
|
onChange={(e) => {
|
|
setShowInactive(e.target.checked);
|
|
handleFilterChange();
|
|
}}
|
|
className="rounded border-gray-300 text-blue-600 focus:ring-blue-500"
|
|
/>
|
|
<span className="ml-2 text-sm text-gray-700">Show inactive</span>
|
|
</label>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Category Tabs */}
|
|
<div className="mb-6">
|
|
<div className="border-b border-gray-200">
|
|
<nav className="-mb-px flex overflow-x-auto">
|
|
{categoryTabs.map((tab) => (
|
|
<button
|
|
key={tab}
|
|
onClick={() => setActiveTab(tab)}
|
|
className={`py-2 px-4 border-b-2 font-medium text-sm whitespace-nowrap ${
|
|
activeTab === tab
|
|
? 'border-blue-500 text-blue-600'
|
|
: 'border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300'
|
|
}`}
|
|
>
|
|
{tab === 'all' ? 'All Equipment' : tab}
|
|
{tab !== 'all' && (
|
|
<span className="ml-2 bg-gray-100 text-gray-600 px-2 py-1 rounded-full text-xs">
|
|
{equipment.filter(item => item.categoryName === tab).length}
|
|
</span>
|
|
)}
|
|
{tab === 'all' && (
|
|
<span className="ml-2 bg-gray-100 text-gray-600 px-2 py-1 rounded-full text-xs">
|
|
{equipment.length}
|
|
</span>
|
|
)}
|
|
</button>
|
|
))}
|
|
</nav>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Equipment Grid */}
|
|
{filteredEquipment.length === 0 ? (
|
|
<div className="card text-center py-12">
|
|
<WrenchScrewdriverIcon className="h-16 w-16 text-gray-300 mx-auto mb-4" />
|
|
<h3 className="text-lg font-medium text-gray-900 mb-2">No Equipment Found</h3>
|
|
<p className="text-gray-600 mb-6">
|
|
{searchTerm || selectedCategory
|
|
? 'Try adjusting your search or filters'
|
|
: 'Start building your equipment inventory'
|
|
}
|
|
</p>
|
|
{!searchTerm && !selectedCategory && (
|
|
<button
|
|
onClick={() => setShowCreateForm(true)}
|
|
className="btn-primary"
|
|
>
|
|
Add Your First Equipment
|
|
</button>
|
|
)}
|
|
</div>
|
|
) : (
|
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
|
{filteredEquipment.map((item) => (
|
|
<EquipmentCard key={item.id} item={item} />
|
|
))}
|
|
</div>
|
|
)}
|
|
|
|
{/* Create Equipment Form Modal */}
|
|
{showCreateForm && (
|
|
<EquipmentFormModal
|
|
isEdit={false}
|
|
equipment={null}
|
|
categories={categories}
|
|
equipmentTypes={equipmentTypes}
|
|
onSubmit={handleCreateEquipment}
|
|
onCancel={() => setShowCreateForm(false)}
|
|
/>
|
|
)}
|
|
|
|
{/* Edit Equipment Form Modal */}
|
|
{showEditForm && editingEquipment && (
|
|
<EquipmentFormModal
|
|
isEdit={true}
|
|
equipment={editingEquipment}
|
|
categories={categories}
|
|
equipmentTypes={equipmentTypes}
|
|
onSubmit={handleUpdateEquipment}
|
|
onCancel={() => {
|
|
setShowEditForm(false);
|
|
setEditingEquipment(null);
|
|
}}
|
|
/>
|
|
)}
|
|
|
|
{/* Pump Assignment Modal */}
|
|
{showPumpAssignments && selectedSprayerForPump && (
|
|
<PumpAssignmentModal
|
|
sprayer={selectedSprayerForPump}
|
|
equipment={equipment.filter(e => e.categoryName === 'Pump')}
|
|
onClose={() => {
|
|
setShowPumpAssignments(false);
|
|
setSelectedSprayerForPump(null);
|
|
fetchData();
|
|
}}
|
|
/>
|
|
)}
|
|
|
|
{/* Nozzle Configuration Modal */}
|
|
{showNozzleConfigs && selectedSprayerForNozzles && (
|
|
<NozzleConfigurationModal
|
|
sprayer={selectedSprayerForNozzles}
|
|
onClose={() => {
|
|
setShowNozzleConfigs(false);
|
|
setSelectedSprayerForNozzles(null);
|
|
fetchData();
|
|
}}
|
|
/>
|
|
)}
|
|
</div>
|
|
);
|
|
};
|
|
|
|
// Equipment Form Modal Component (Create/Edit)
|
|
const EquipmentFormModal = ({ isEdit, equipment, categories, equipmentTypes, onSubmit, onCancel }) => {
|
|
const [formData, setFormData] = useState({
|
|
equipmentTypeId: equipment?.equipmentTypeId || '',
|
|
categoryId: equipment?.categoryId || '',
|
|
customName: equipment?.customName || '',
|
|
manufacturer: equipment?.manufacturer || '',
|
|
model: equipment?.model || '',
|
|
// Spreader fields
|
|
capacityLbs: equipment?.capacityLbs || '',
|
|
spreaderType: equipment?.spreaderType || 'walk_behind',
|
|
spreadWidth: equipment?.spreadWidth || '',
|
|
// Sprayer fields
|
|
tankSizeGallons: equipment?.tankSizeGallons || '',
|
|
sprayerType: equipment?.sprayerType || 'walk_behind',
|
|
sprayWidthFeet: equipment?.sprayWidthFeet || '',
|
|
pumpGpm: equipment?.pumpGpm || '',
|
|
pumpPsi: equipment?.pumpPsi || '',
|
|
boomSections: equipment?.boomSections || '',
|
|
// Mower fields
|
|
mowerStyle: equipment?.mowerStyle || 'push',
|
|
cuttingWidthInches: equipment?.cuttingWidthInches || '',
|
|
engineHp: equipment?.engineHp || '',
|
|
fuelType: equipment?.fuelType || '',
|
|
// Tool fields
|
|
toolType: equipment?.toolType || 'walk_behind',
|
|
workingWidthInches: equipment?.workingWidthInches || '',
|
|
// Pump fields
|
|
pumpType: equipment?.pumpType || '',
|
|
maxGpm: equipment?.maxGpm || '',
|
|
maxPsi: equipment?.maxPsi || '',
|
|
powerSource: equipment?.powerSource || '',
|
|
// Nozzle fields
|
|
orificeSize: equipment?.orificeSize || '',
|
|
sprayAngle: equipment?.sprayAngle || '',
|
|
flowRateGpm: equipment?.flowRateGpm || '',
|
|
dropletSize: equipment?.dropletSize || '',
|
|
sprayPattern: equipment?.sprayPattern || '',
|
|
pressureRangePsi: equipment?.pressureRangePsi || '',
|
|
threadSize: equipment?.threadSize || '',
|
|
material: equipment?.material || '',
|
|
colorCode: equipment?.colorCode || '',
|
|
quantityOwned: equipment?.quantityOwned || 1,
|
|
// General fields
|
|
purchaseDate: equipment?.purchaseDate || '',
|
|
purchasePrice: equipment?.purchasePrice || '',
|
|
notes: equipment?.notes || '',
|
|
isActive: equipment?.isActive !== undefined ? equipment.isActive : true
|
|
});
|
|
|
|
const selectedCategory = categories.find(cat => cat.id === parseInt(formData.categoryId));
|
|
|
|
const handleSubmit = (e) => {
|
|
e.preventDefault();
|
|
|
|
if (!formData.categoryId && !formData.equipmentTypeId) {
|
|
toast.error('Please select a category or equipment type');
|
|
return;
|
|
}
|
|
|
|
if (!formData.customName && !formData.equipmentTypeId) {
|
|
toast.error('Please enter a name or select an equipment type');
|
|
return;
|
|
}
|
|
|
|
const submitData = {
|
|
equipmentTypeId: formData.equipmentTypeId ? parseInt(formData.equipmentTypeId) : null,
|
|
categoryId: formData.categoryId ? parseInt(formData.categoryId) : null,
|
|
customName: formData.customName || null,
|
|
manufacturer: formData.manufacturer || null,
|
|
model: formData.model || null,
|
|
capacityLbs: formData.capacityLbs ? parseFloat(formData.capacityLbs) : null,
|
|
spreaderType: formData.spreaderType || null,
|
|
spreadWidth: formData.spreadWidth ? parseFloat(formData.spreadWidth) : null,
|
|
tankSizeGallons: formData.tankSizeGallons ? parseFloat(formData.tankSizeGallons) : null,
|
|
sprayerType: formData.sprayerType || null,
|
|
sprayWidthFeet: formData.sprayWidthFeet ? parseFloat(formData.sprayWidthFeet) : null,
|
|
pumpGpm: formData.pumpGpm ? parseFloat(formData.pumpGpm) : null,
|
|
pumpPsi: formData.pumpPsi ? parseFloat(formData.pumpPsi) : null,
|
|
boomSections: formData.boomSections ? parseInt(formData.boomSections) : null,
|
|
mowerStyle: formData.mowerStyle || null,
|
|
cuttingWidthInches: formData.cuttingWidthInches ? parseFloat(formData.cuttingWidthInches) : null,
|
|
engineHp: formData.engineHp ? parseFloat(formData.engineHp) : null,
|
|
fuelType: formData.fuelType || null,
|
|
toolType: formData.toolType || null,
|
|
workingWidthInches: formData.workingWidthInches ? parseFloat(formData.workingWidthInches) : null,
|
|
pumpType: formData.pumpType || null,
|
|
maxGpm: formData.maxGpm ? parseFloat(formData.maxGpm) : null,
|
|
maxPsi: formData.maxPsi ? parseFloat(formData.maxPsi) : null,
|
|
powerSource: formData.powerSource || null,
|
|
// Nozzle fields
|
|
orificeSize: formData.orificeSize || null,
|
|
sprayAngle: formData.sprayAngle ? parseInt(formData.sprayAngle) : null,
|
|
flowRateGpm: formData.flowRateGpm ? parseFloat(formData.flowRateGpm) : null,
|
|
dropletSize: formData.dropletSize || null,
|
|
sprayPattern: formData.sprayPattern || null,
|
|
pressureRangePsi: formData.pressureRangePsi || null,
|
|
threadSize: formData.threadSize || null,
|
|
material: formData.material || null,
|
|
colorCode: formData.colorCode || null,
|
|
quantityOwned: formData.quantityOwned ? parseInt(formData.quantityOwned) : null,
|
|
purchaseDate: formData.purchaseDate || null,
|
|
purchasePrice: formData.purchasePrice ? parseFloat(formData.purchasePrice) : null,
|
|
notes: formData.notes || null,
|
|
isActive: formData.isActive
|
|
};
|
|
|
|
onSubmit(submitData);
|
|
};
|
|
|
|
const renderCategorySpecificFields = () => {
|
|
const categoryName = selectedCategory?.name?.toLowerCase();
|
|
|
|
switch (categoryName) {
|
|
case 'mower':
|
|
return (
|
|
<>
|
|
<div className="grid grid-cols-2 gap-4">
|
|
<div>
|
|
<label className="label">Mower Style *</label>
|
|
<select
|
|
className="input"
|
|
value={formData.mowerStyle}
|
|
onChange={(e) => setFormData({ ...formData, mowerStyle: e.target.value })}
|
|
required
|
|
>
|
|
<option value="push">Push</option>
|
|
<option value="self_propelled">Self-Propelled</option>
|
|
<option value="zero_turn">Zero Turn</option>
|
|
<option value="lawn_tractor">Lawn Tractor</option>
|
|
<option value="riding">Riding Mower</option>
|
|
</select>
|
|
</div>
|
|
<div>
|
|
<label className="label">Cutting Width (inches)</label>
|
|
<input
|
|
type="number"
|
|
step="0.1"
|
|
className="input"
|
|
value={formData.cuttingWidthInches}
|
|
onChange={(e) => setFormData({ ...formData, cuttingWidthInches: e.target.value })}
|
|
placeholder="21"
|
|
/>
|
|
</div>
|
|
</div>
|
|
<div className="grid grid-cols-2 gap-4">
|
|
<div>
|
|
<label className="label">Engine HP</label>
|
|
<input
|
|
type="number"
|
|
step="0.1"
|
|
className="input"
|
|
value={formData.engineHp}
|
|
onChange={(e) => setFormData({ ...formData, engineHp: e.target.value })}
|
|
placeholder="6.5"
|
|
/>
|
|
</div>
|
|
<div>
|
|
<label className="label">Fuel Type</label>
|
|
<select
|
|
className="input"
|
|
value={formData.fuelType}
|
|
onChange={(e) => setFormData({ ...formData, fuelType: e.target.value })}
|
|
>
|
|
<option value="">Select fuel type</option>
|
|
<option value="gasoline">Gasoline</option>
|
|
<option value="diesel">Diesel</option>
|
|
<option value="electric">Electric</option>
|
|
<option value="battery">Battery</option>
|
|
<option value="propane">Propane</option>
|
|
</select>
|
|
</div>
|
|
</div>
|
|
</>
|
|
);
|
|
|
|
case 'spreader':
|
|
return (
|
|
<>
|
|
<div className="grid grid-cols-2 gap-4">
|
|
<div>
|
|
<label className="label">Spreader Type *</label>
|
|
<select
|
|
className="input"
|
|
value={formData.spreaderType}
|
|
onChange={(e) => setFormData({ ...formData, spreaderType: e.target.value })}
|
|
required
|
|
>
|
|
<option value="walk_behind">Walk Behind</option>
|
|
<option value="pull_behind">Pull Behind</option>
|
|
<option value="handheld">Handheld</option>
|
|
</select>
|
|
</div>
|
|
<div>
|
|
<label className="label">Capacity (lbs)</label>
|
|
<input
|
|
type="number"
|
|
step="0.1"
|
|
className="input"
|
|
value={formData.capacityLbs}
|
|
onChange={(e) => setFormData({ ...formData, capacityLbs: e.target.value })}
|
|
placeholder="50"
|
|
/>
|
|
</div>
|
|
</div>
|
|
<div>
|
|
<label className="label">Spread Width (feet)</label>
|
|
<input
|
|
type="number"
|
|
step="0.1"
|
|
className="input"
|
|
value={formData.spreadWidth}
|
|
onChange={(e) => setFormData({ ...formData, spreadWidth: e.target.value })}
|
|
placeholder="8"
|
|
/>
|
|
</div>
|
|
</>
|
|
);
|
|
|
|
case 'sprayer':
|
|
return (
|
|
<>
|
|
<div className="grid grid-cols-2 gap-4">
|
|
<div>
|
|
<label className="label">Sprayer Type *</label>
|
|
<select
|
|
className="input"
|
|
value={formData.sprayerType}
|
|
onChange={(e) => setFormData({ ...formData, sprayerType: e.target.value })}
|
|
required
|
|
>
|
|
<option value="walk_behind">Walk Behind</option>
|
|
<option value="tow_behind">Tow Behind</option>
|
|
<option value="mower_mounted">Mower Mounted</option>
|
|
<option value="ride_on">Ride On</option>
|
|
<option value="hand_pump">Hand Pump</option>
|
|
</select>
|
|
</div>
|
|
<div>
|
|
<label className="label">Tank Size (gallons)</label>
|
|
<input
|
|
type="number"
|
|
step="0.1"
|
|
className="input"
|
|
value={formData.tankSizeGallons}
|
|
onChange={(e) => setFormData({ ...formData, tankSizeGallons: e.target.value })}
|
|
placeholder="25"
|
|
/>
|
|
</div>
|
|
</div>
|
|
<div className="grid grid-cols-3 gap-4">
|
|
<div>
|
|
<label className="label">Spray Width (feet)</label>
|
|
<input
|
|
type="number"
|
|
step="0.1"
|
|
className="input"
|
|
value={formData.sprayWidthFeet}
|
|
onChange={(e) => setFormData({ ...formData, sprayWidthFeet: e.target.value })}
|
|
placeholder="10"
|
|
/>
|
|
</div>
|
|
<div>
|
|
<label className="label">Pump GPM</label>
|
|
<input
|
|
type="number"
|
|
step="0.1"
|
|
className="input"
|
|
value={formData.pumpGpm}
|
|
onChange={(e) => setFormData({ ...formData, pumpGpm: e.target.value })}
|
|
placeholder="2.5"
|
|
/>
|
|
</div>
|
|
<div>
|
|
<label className="label">Pump PSI</label>
|
|
<input
|
|
type="number"
|
|
className="input"
|
|
value={formData.pumpPsi}
|
|
onChange={(e) => setFormData({ ...formData, pumpPsi: e.target.value })}
|
|
placeholder="60"
|
|
/>
|
|
</div>
|
|
</div>
|
|
<div>
|
|
<label className="label">Boom Sections</label>
|
|
<input
|
|
type="number"
|
|
className="input"
|
|
value={formData.boomSections}
|
|
onChange={(e) => setFormData({ ...formData, boomSections: e.target.value })}
|
|
placeholder="3"
|
|
/>
|
|
</div>
|
|
</>
|
|
);
|
|
|
|
case 'pump':
|
|
return (
|
|
<>
|
|
<div className="grid grid-cols-2 gap-4">
|
|
<div>
|
|
<label className="label">Pump Type</label>
|
|
<input
|
|
type="text"
|
|
className="input"
|
|
value={formData.pumpType}
|
|
onChange={(e) => setFormData({ ...formData, pumpType: e.target.value })}
|
|
placeholder="Centrifugal, Diaphragm, etc."
|
|
/>
|
|
</div>
|
|
<div>
|
|
<label className="label">Power Source</label>
|
|
<select
|
|
className="input"
|
|
value={formData.powerSource}
|
|
onChange={(e) => setFormData({ ...formData, powerSource: e.target.value })}
|
|
>
|
|
<option value="">Select power source</option>
|
|
<option value="electric">Electric</option>
|
|
<option value="gasoline">Gasoline</option>
|
|
<option value="diesel">Diesel</option>
|
|
<option value="pto">PTO</option>
|
|
<option value="hydraulic">Hydraulic</option>
|
|
</select>
|
|
</div>
|
|
</div>
|
|
<div className="grid grid-cols-2 gap-4">
|
|
<div>
|
|
<label className="label">Max GPM</label>
|
|
<input
|
|
type="number"
|
|
step="0.1"
|
|
className="input"
|
|
value={formData.maxGpm}
|
|
onChange={(e) => setFormData({ ...formData, maxGpm: e.target.value })}
|
|
placeholder="10"
|
|
/>
|
|
</div>
|
|
<div>
|
|
<label className="label">Max PSI</label>
|
|
<input
|
|
type="number"
|
|
className="input"
|
|
value={formData.maxPsi}
|
|
onChange={(e) => setFormData({ ...formData, maxPsi: e.target.value })}
|
|
placeholder="100"
|
|
/>
|
|
</div>
|
|
</div>
|
|
</>
|
|
);
|
|
|
|
case 'nozzle':
|
|
return (
|
|
<>
|
|
<div className="grid grid-cols-3 gap-4">
|
|
<div>
|
|
<label className="label">Orifice Size *</label>
|
|
<input
|
|
type="text"
|
|
className="input"
|
|
value={formData.orificeSize}
|
|
onChange={(e) => setFormData({ ...formData, orificeSize: e.target.value })}
|
|
placeholder="02, 03, 04..."
|
|
required
|
|
/>
|
|
</div>
|
|
<div>
|
|
<label className="label">Spray Angle *</label>
|
|
<input
|
|
type="number"
|
|
className="input"
|
|
value={formData.sprayAngle}
|
|
onChange={(e) => setFormData({ ...formData, sprayAngle: e.target.value })}
|
|
placeholder="80, 110..."
|
|
required
|
|
/>
|
|
</div>
|
|
<div>
|
|
<label className="label">Flow Rate (GPM) *</label>
|
|
<input
|
|
type="number"
|
|
step="0.001"
|
|
className="input"
|
|
value={formData.flowRateGpm}
|
|
onChange={(e) => setFormData({ ...formData, flowRateGpm: e.target.value })}
|
|
placeholder="0.20, 0.30..."
|
|
required
|
|
/>
|
|
</div>
|
|
</div>
|
|
<div className="grid grid-cols-2 gap-4">
|
|
<div>
|
|
<label className="label">Droplet Size *</label>
|
|
<select
|
|
className="input"
|
|
value={formData.dropletSize}
|
|
onChange={(e) => setFormData({ ...formData, dropletSize: e.target.value })}
|
|
required
|
|
>
|
|
<option value="">Select droplet size</option>
|
|
<option value="fine">Fine</option>
|
|
<option value="medium">Medium</option>
|
|
<option value="coarse">Coarse</option>
|
|
<option value="very_coarse">Very Coarse</option>
|
|
<option value="extremely_coarse">Extremely Coarse</option>
|
|
</select>
|
|
</div>
|
|
<div>
|
|
<label className="label">Spray Pattern *</label>
|
|
<select
|
|
className="input"
|
|
value={formData.sprayPattern}
|
|
onChange={(e) => setFormData({ ...formData, sprayPattern: e.target.value })}
|
|
required
|
|
>
|
|
<option value="">Select spray pattern</option>
|
|
<option value="flat_fan">Flat Fan</option>
|
|
<option value="hollow_cone">Hollow Cone</option>
|
|
<option value="full_cone">Full Cone</option>
|
|
<option value="flooding">Flooding</option>
|
|
</select>
|
|
</div>
|
|
</div>
|
|
<div className="grid grid-cols-2 gap-4">
|
|
<div>
|
|
<label className="label">Pressure Range (PSI)</label>
|
|
<input
|
|
type="text"
|
|
className="input"
|
|
value={formData.pressureRangePsi}
|
|
onChange={(e) => setFormData({ ...formData, pressureRangePsi: e.target.value })}
|
|
placeholder="15-60"
|
|
/>
|
|
</div>
|
|
<div>
|
|
<label className="label">Thread Size</label>
|
|
<input
|
|
type="text"
|
|
className="input"
|
|
value={formData.threadSize}
|
|
onChange={(e) => setFormData({ ...formData, threadSize: e.target.value })}
|
|
placeholder="1/4", 3/8"..."
|
|
/>
|
|
</div>
|
|
</div>
|
|
<div className="grid grid-cols-3 gap-4">
|
|
<div>
|
|
<label className="label">Material</label>
|
|
<select
|
|
className="input"
|
|
value={formData.material}
|
|
onChange={(e) => setFormData({ ...formData, material: e.target.value })}
|
|
>
|
|
<option value="">Select material</option>
|
|
<option value="polymer">Polymer</option>
|
|
<option value="stainless_steel">Stainless Steel</option>
|
|
<option value="brass">Brass</option>
|
|
<option value="ceramic">Ceramic</option>
|
|
</select>
|
|
</div>
|
|
<div>
|
|
<label className="label">Color Code</label>
|
|
<input
|
|
type="text"
|
|
className="input"
|
|
value={formData.colorCode}
|
|
onChange={(e) => setFormData({ ...formData, colorCode: e.target.value })}
|
|
placeholder="Yellow, Blue, Red..."
|
|
/>
|
|
</div>
|
|
<div>
|
|
<label className="label">Quantity Owned</label>
|
|
<input
|
|
type="number"
|
|
min="1"
|
|
className="input"
|
|
value={formData.quantityOwned}
|
|
onChange={(e) => setFormData({ ...formData, quantityOwned: e.target.value })}
|
|
placeholder="1"
|
|
/>
|
|
</div>
|
|
</div>
|
|
</>
|
|
);
|
|
|
|
default:
|
|
return (
|
|
<>
|
|
<div className="grid grid-cols-2 gap-4">
|
|
<div>
|
|
<label className="label">Tool Type</label>
|
|
<select
|
|
className="input"
|
|
value={formData.toolType}
|
|
onChange={(e) => setFormData({ ...formData, toolType: e.target.value })}
|
|
>
|
|
<option value="walk_behind">Walk Behind</option>
|
|
<option value="tow_behind">Tow Behind</option>
|
|
<option value="handheld">Handheld</option>
|
|
</select>
|
|
</div>
|
|
<div>
|
|
<label className="label">Working Width (inches)</label>
|
|
<input
|
|
type="number"
|
|
step="0.1"
|
|
className="input"
|
|
value={formData.workingWidthInches}
|
|
onChange={(e) => setFormData({ ...formData, workingWidthInches: e.target.value })}
|
|
placeholder="18"
|
|
/>
|
|
</div>
|
|
</div>
|
|
</>
|
|
);
|
|
}
|
|
};
|
|
|
|
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-2xl max-h-[90vh] overflow-y-auto">
|
|
<h3 className="text-lg font-semibold mb-4">
|
|
{isEdit ? 'Edit Equipment' : 'Add New Equipment'}
|
|
</h3>
|
|
|
|
<form onSubmit={handleSubmit} className="space-y-4">
|
|
{/* Basic Information */}
|
|
<div className="grid grid-cols-2 gap-4">
|
|
<div>
|
|
<label className="label">Equipment Type (Optional)</label>
|
|
<select
|
|
className="input"
|
|
value={formData.equipmentTypeId}
|
|
onChange={(e) => {
|
|
const selectedType = equipmentTypes.find(type => type.id === parseInt(e.target.value));
|
|
setFormData({
|
|
...formData,
|
|
equipmentTypeId: e.target.value,
|
|
categoryId: selectedType?.categoryId || formData.categoryId
|
|
});
|
|
}}
|
|
>
|
|
<option value="">Select from database...</option>
|
|
{equipmentTypes.map((type) => (
|
|
<option key={type.id} value={type.id}>
|
|
{type.name} {type.manufacturer && `- ${type.manufacturer}`}
|
|
</option>
|
|
))}
|
|
</select>
|
|
</div>
|
|
<div>
|
|
<label className="label">Category *</label>
|
|
<select
|
|
className="input"
|
|
value={formData.categoryId}
|
|
onChange={(e) => setFormData({ ...formData, categoryId: e.target.value })}
|
|
required={!formData.equipmentTypeId}
|
|
>
|
|
<option value="">Select category...</option>
|
|
{categories.map((category) => (
|
|
<option key={category.id} value={category.id}>
|
|
{category.name}
|
|
</option>
|
|
))}
|
|
</select>
|
|
</div>
|
|
</div>
|
|
|
|
<div>
|
|
<label className="label">Equipment Name *</label>
|
|
<input
|
|
type="text"
|
|
className="input"
|
|
value={formData.customName}
|
|
onChange={(e) => setFormData({ ...formData, customName: e.target.value })}
|
|
placeholder="My Lawn Mower, Office Spreader, etc."
|
|
required={!formData.equipmentTypeId}
|
|
/>
|
|
</div>
|
|
|
|
<div className="grid grid-cols-2 gap-4">
|
|
<div>
|
|
<label className="label">Manufacturer</label>
|
|
<input
|
|
type="text"
|
|
className="input"
|
|
value={formData.manufacturer}
|
|
onChange={(e) => setFormData({ ...formData, manufacturer: e.target.value })}
|
|
placeholder="Toro, John Deere, etc."
|
|
/>
|
|
</div>
|
|
<div>
|
|
<label className="label">Model</label>
|
|
<input
|
|
type="text"
|
|
className="input"
|
|
value={formData.model}
|
|
onChange={(e) => setFormData({ ...formData, model: e.target.value })}
|
|
placeholder="Model number"
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Category-specific fields */}
|
|
{formData.categoryId && renderCategorySpecificFields()}
|
|
|
|
{/* Purchase Information */}
|
|
<div className="grid grid-cols-2 gap-4">
|
|
<div>
|
|
<label className="label">Purchase Date</label>
|
|
<input
|
|
type="date"
|
|
className="input"
|
|
value={formData.purchaseDate}
|
|
onChange={(e) => setFormData({ ...formData, purchaseDate: e.target.value })}
|
|
/>
|
|
</div>
|
|
<div>
|
|
<label className="label">Purchase Price</label>
|
|
<input
|
|
type="number"
|
|
step="0.01"
|
|
className="input"
|
|
value={formData.purchasePrice}
|
|
onChange={(e) => setFormData({ ...formData, purchasePrice: e.target.value })}
|
|
placeholder="0.00"
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Notes and Status */}
|
|
<div>
|
|
<label className="label">Notes</label>
|
|
<textarea
|
|
className="input"
|
|
rows="3"
|
|
value={formData.notes}
|
|
onChange={(e) => setFormData({ ...formData, notes: e.target.value })}
|
|
placeholder="Maintenance notes, special instructions, etc."
|
|
/>
|
|
</div>
|
|
|
|
{isEdit && (
|
|
<div className="flex items-center">
|
|
<label className="flex items-center">
|
|
<input
|
|
type="checkbox"
|
|
checked={formData.isActive}
|
|
onChange={(e) => setFormData({ ...formData, isActive: e.target.checked })}
|
|
className="rounded border-gray-300 text-blue-600 focus:ring-blue-500"
|
|
/>
|
|
<span className="ml-2 text-sm text-gray-700">Equipment is active</span>
|
|
</label>
|
|
</div>
|
|
)}
|
|
|
|
<div className="flex gap-3 pt-4">
|
|
<button type="submit" className="btn-primary flex-1">
|
|
{isEdit ? 'Update Equipment' : 'Create Equipment'}
|
|
</button>
|
|
<button
|
|
type="button"
|
|
onClick={onCancel}
|
|
className="btn-secondary flex-1"
|
|
>
|
|
Cancel
|
|
</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
// Pump Assignment Modal Component
|
|
const PumpAssignmentModal = ({ sprayer, equipment, onClose }) => {
|
|
const [assignments, setAssignments] = useState([]);
|
|
const [availablePumps, setAvailablePumps] = useState([]);
|
|
const [loading, setLoading] = useState(true);
|
|
const [selectedPump, setSelectedPump] = useState('');
|
|
|
|
useEffect(() => {
|
|
fetchPumpAssignments();
|
|
}, [sprayer.id]);
|
|
|
|
const fetchPumpAssignments = async () => {
|
|
try {
|
|
setLoading(true);
|
|
const [assignmentsResponse, availablePumpsResponse] = await Promise.all([
|
|
nozzlesAPI.getPumpAssignments(sprayer.id),
|
|
Promise.resolve({ data: { data: { pumps: equipment } } })
|
|
]);
|
|
|
|
setAssignments(assignmentsResponse.data.data.assignments || []);
|
|
setAvailablePumps(availablePumpsResponse.data.data.pumps || []);
|
|
} catch (error) {
|
|
console.error('Failed to fetch pump assignments:', error);
|
|
toast.error('Failed to load pump assignments');
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
};
|
|
|
|
const handleAssignPump = async () => {
|
|
if (!selectedPump) return;
|
|
|
|
try {
|
|
await nozzlesAPI.assignPump(sprayer.id, selectedPump);
|
|
toast.success('Pump assigned successfully');
|
|
setSelectedPump('');
|
|
fetchPumpAssignments();
|
|
} catch (error) {
|
|
console.error('Failed to assign pump:', error);
|
|
toast.error('Failed to assign pump');
|
|
}
|
|
};
|
|
|
|
const handleUnassignPump = async (assignmentId) => {
|
|
try {
|
|
await nozzlesAPI.unassignPump(assignmentId);
|
|
toast.success('Pump unassigned successfully');
|
|
fetchPumpAssignments();
|
|
} catch (error) {
|
|
console.error('Failed to unassign pump:', error);
|
|
toast.error('Failed to unassign pump');
|
|
}
|
|
};
|
|
|
|
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-2xl max-h-[90vh] overflow-y-auto">
|
|
<h3 className="text-lg font-semibold mb-4">
|
|
Pump Assignments - {sprayer.customName || sprayer.typeName}
|
|
</h3>
|
|
|
|
{loading ? (
|
|
<LoadingSpinner />
|
|
) : (
|
|
<>
|
|
{/* Add New Pump Assignment */}
|
|
<div className="mb-6 p-4 bg-gray-50 rounded-lg">
|
|
<h4 className="font-medium mb-3">Assign New Pump</h4>
|
|
<div className="flex gap-3">
|
|
<select
|
|
className="input flex-1"
|
|
value={selectedPump}
|
|
onChange={(e) => setSelectedPump(e.target.value)}
|
|
>
|
|
<option value="">Select a pump...</option>
|
|
{availablePumps.map(pump => (
|
|
<option key={pump.id} value={pump.id}>
|
|
{pump.customName || pump.typeName}
|
|
{pump.manufacturer && ` - ${pump.manufacturer}`}
|
|
{pump.maxGpm && ` (${pump.maxGpm} GPM)`}
|
|
</option>
|
|
))}
|
|
</select>
|
|
<button
|
|
onClick={handleAssignPump}
|
|
disabled={!selectedPump}
|
|
className="btn-primary"
|
|
>
|
|
Assign
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Current Assignments */}
|
|
<div className="space-y-3">
|
|
<h4 className="font-medium">Current Pump Assignments</h4>
|
|
{assignments.length === 0 ? (
|
|
<p className="text-gray-500">No pumps assigned to this sprayer</p>
|
|
) : (
|
|
assignments.map(assignment => (
|
|
<div key={assignment.id} className="flex justify-between items-center p-3 bg-gray-50 rounded-lg">
|
|
<div>
|
|
<div className="font-medium">
|
|
{assignment.pump?.customName || assignment.pump?.typeName}
|
|
</div>
|
|
{assignment.pump?.manufacturer && (
|
|
<div className="text-sm text-gray-600">
|
|
{assignment.pump.manufacturer} {assignment.pump.model}
|
|
</div>
|
|
)}
|
|
{assignment.pump?.maxGpm && (
|
|
<div className="text-sm text-gray-600">
|
|
Max Flow: {assignment.pump.maxGpm} GPM
|
|
</div>
|
|
)}
|
|
<div className="text-xs text-gray-500">
|
|
Assigned: {new Date(assignment.assignedDate).toLocaleDateString()}
|
|
</div>
|
|
</div>
|
|
<button
|
|
onClick={() => handleUnassignPump(assignment.id)}
|
|
className="text-red-600 hover:text-red-800"
|
|
>
|
|
<TrashIcon className="h-5 w-5" />
|
|
</button>
|
|
</div>
|
|
))
|
|
)}
|
|
</div>
|
|
</>
|
|
)}
|
|
|
|
<div className="flex gap-3 pt-6 mt-6 border-t">
|
|
<button onClick={onClose} className="btn-secondary flex-1">
|
|
Close
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
// Nozzle Configuration Modal Component
|
|
const NozzleConfigurationModal = ({ sprayer, onClose }) => {
|
|
const [configurations, setConfigurations] = useState([]);
|
|
const [nozzleTypes, setNozzleTypes] = useState([]);
|
|
const [userNozzles, setUserNozzles] = useState([]);
|
|
const [loading, setLoading] = useState(true);
|
|
const [showAddConfig, setShowAddConfig] = useState(false);
|
|
const [configForm, setConfigForm] = useState({
|
|
userNozzleId: '',
|
|
position: '',
|
|
quantityAssigned: 1
|
|
});
|
|
|
|
useEffect(() => {
|
|
fetchNozzleConfigs();
|
|
}, [sprayer.id]);
|
|
|
|
const fetchNozzleConfigs = async () => {
|
|
try {
|
|
setLoading(true);
|
|
const [configsResponse, typesResponse, userNozzlesResponse] = await Promise.all([
|
|
nozzlesAPI.getNozzleConfigurations(sprayer.id),
|
|
nozzlesAPI.getNozzleTypes(),
|
|
nozzlesAPI.getUserNozzles()
|
|
]);
|
|
|
|
setConfigurations(configsResponse.data.data.configurations || []);
|
|
setNozzleTypes(typesResponse.data.data.nozzleTypes || []);
|
|
setUserNozzles(userNozzlesResponse.data.data.userNozzles || []);
|
|
} catch (error) {
|
|
console.error('Failed to fetch nozzle configurations:', error);
|
|
toast.error('Failed to load nozzle configurations');
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
};
|
|
|
|
const handleAddConfiguration = async () => {
|
|
try {
|
|
await nozzlesAPI.addNozzleConfiguration(sprayer.id, configForm);
|
|
toast.success('Nozzle configuration added');
|
|
setShowAddConfig(false);
|
|
setConfigForm({ userNozzleId: '', position: '', quantityAssigned: 1 });
|
|
fetchNozzleConfigs();
|
|
} catch (error) {
|
|
console.error('Failed to add configuration:', error);
|
|
toast.error('Failed to add configuration');
|
|
}
|
|
};
|
|
|
|
const handleRemoveConfiguration = async (configId) => {
|
|
try {
|
|
await nozzlesAPI.removeNozzleConfiguration(configId);
|
|
toast.success('Nozzle configuration removed');
|
|
fetchNozzleConfigs();
|
|
} catch (error) {
|
|
console.error('Failed to remove configuration:', error);
|
|
toast.error('Failed to remove configuration');
|
|
}
|
|
};
|
|
|
|
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">
|
|
Nozzle Configuration - {sprayer.customName || sprayer.typeName}
|
|
</h3>
|
|
|
|
{loading ? (
|
|
<LoadingSpinner />
|
|
) : (
|
|
<>
|
|
{/* Add Configuration Button */}
|
|
<div className="mb-6">
|
|
<button
|
|
onClick={() => setShowAddConfig(true)}
|
|
className="btn-primary flex items-center gap-2"
|
|
>
|
|
<PlusIcon className="h-5 w-5" />
|
|
Add Nozzle Configuration
|
|
</button>
|
|
</div>
|
|
|
|
{/* Add Configuration Form */}
|
|
{showAddConfig && (
|
|
<div className="mb-6 p-4 bg-gray-50 rounded-lg">
|
|
<h4 className="font-medium mb-3">Add Nozzle Configuration</h4>
|
|
<div className="grid grid-cols-3 gap-3">
|
|
<select
|
|
className="input"
|
|
value={configForm.userNozzleId}
|
|
onChange={(e) => setConfigForm({ ...configForm, userNozzleId: e.target.value })}
|
|
>
|
|
<option value="">Select nozzle...</option>
|
|
{userNozzles.map(nozzle => (
|
|
<option key={nozzle.id} value={nozzle.id}>
|
|
{nozzle.nozzleType?.name} ({nozzle.quantity} available)
|
|
</option>
|
|
))}
|
|
</select>
|
|
<input
|
|
type="text"
|
|
className="input"
|
|
placeholder="Position (e.g., left, right, boom_1)"
|
|
value={configForm.position}
|
|
onChange={(e) => setConfigForm({ ...configForm, position: e.target.value })}
|
|
/>
|
|
<input
|
|
type="number"
|
|
className="input"
|
|
placeholder="Quantity"
|
|
min="1"
|
|
value={configForm.quantityAssigned}
|
|
onChange={(e) => setConfigForm({ ...configForm, quantityAssigned: parseInt(e.target.value) })}
|
|
/>
|
|
</div>
|
|
<div className="flex gap-3 mt-3">
|
|
<button onClick={handleAddConfiguration} className="btn-primary">
|
|
Add Configuration
|
|
</button>
|
|
<button
|
|
onClick={() => setShowAddConfig(false)}
|
|
className="btn-secondary"
|
|
>
|
|
Cancel
|
|
</button>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* Current Configurations */}
|
|
<div className="space-y-3">
|
|
<h4 className="font-medium">Current Nozzle Configurations</h4>
|
|
{configurations.length === 0 ? (
|
|
<p className="text-gray-500">No nozzles configured for this sprayer</p>
|
|
) : (
|
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
|
{configurations.map(config => (
|
|
<div key={config.id} className="p-4 bg-gray-50 rounded-lg">
|
|
<div className="flex justify-between items-start mb-2">
|
|
<div className="font-medium">
|
|
{config.userNozzle?.nozzleType?.name}
|
|
</div>
|
|
<button
|
|
onClick={() => handleRemoveConfiguration(config.id)}
|
|
className="text-red-600 hover:text-red-800"
|
|
>
|
|
<TrashIcon className="h-4 w-4" />
|
|
</button>
|
|
</div>
|
|
<div className="text-sm text-gray-600 space-y-1">
|
|
<div><strong>Position:</strong> {config.position}</div>
|
|
<div><strong>Quantity:</strong> {config.quantityAssigned}</div>
|
|
{config.userNozzle?.nozzleType?.manufacturer && (
|
|
<div><strong>Manufacturer:</strong> {config.userNozzle.nozzleType.manufacturer}</div>
|
|
)}
|
|
{config.userNozzle?.nozzleType?.orificeSize && (
|
|
<div><strong>Orifice:</strong> {config.userNozzle.nozzleType.orificeSize}</div>
|
|
)}
|
|
{config.userNozzle?.nozzleType?.sprayAngle && (
|
|
<div><strong>Spray Angle:</strong> {config.userNozzle.nozzleType.sprayAngle}°</div>
|
|
)}
|
|
{config.userNozzle?.nozzleType?.dropletSize && (
|
|
<div><strong>Droplet Size:</strong> {config.userNozzle.nozzleType.dropletSize}</div>
|
|
)}
|
|
<div className="text-xs text-gray-500">
|
|
Assigned: {new Date(config.assignedDate).toLocaleDateString()}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
)}
|
|
</div>
|
|
</>
|
|
)}
|
|
|
|
<div className="flex gap-3 pt-6 mt-6 border-t">
|
|
<button onClick={onClose} className="btn-secondary flex-1">
|
|
Close
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
export default Equipment; |