spreader stuff

This commit is contained in:
Jake Kasper
2025-08-24 13:41:12 -04:00
parent 3ad4782021
commit 229454c466
10 changed files with 616 additions and 17 deletions

View File

@@ -275,7 +275,7 @@ const Applications = () => {
const planPayload = {
lawnSectionId: parseInt(planData.selectedAreas[0]),
equipmentId: parseInt(planData.equipmentId),
nozzleId: planData.nozzleId ? parseInt(planData.nozzleId) : null,
...(planData.applicationType === 'liquid' && planData.nozzleId && { nozzleId: parseInt(planData.nozzleId) }),
plannedDate: planData.plannedDate || new Date().toISOString().split('T')[0],
notes: planData.notes || '',
areaSquareFeet: areaSquareFeet,
@@ -288,11 +288,13 @@ const Applications = () => {
capacityLbs: selectedEquipment?.capacityLbs,
spreadWidth: selectedEquipment?.spreadWidth
},
nozzle: selectedNozzle ? {
id: selectedNozzle.id,
flowRateGpm: selectedNozzle.flowRateGpm,
sprayAngle: selectedNozzle.sprayAngle
} : null,
...(planData.applicationType === 'liquid' && selectedNozzle && {
nozzle: {
id: selectedNozzle.id,
flowRateGpm: selectedNozzle.flowRateGpm,
sprayAngle: selectedNozzle.sprayAngle
}
}),
products: [{
...(planData.selectedProduct?.isShared
? { productId: parseInt(planData.selectedProduct.id) }
@@ -317,7 +319,7 @@ const Applications = () => {
const planPayload = {
lawnSectionId: parseInt(areaId),
equipmentId: parseInt(planData.equipmentId),
nozzleId: planData.nozzleId ? parseInt(planData.nozzleId) : null,
...(planData.applicationType === 'liquid' && planData.nozzleId && { nozzleId: parseInt(planData.nozzleId) }),
plannedDate: new Date().toISOString().split('T')[0],
notes: planData.notes || '',
areaSquareFeet: areaSquareFeet,
@@ -330,11 +332,13 @@ const Applications = () => {
capacityLbs: selectedEquipment?.capacityLbs,
spreadWidth: selectedEquipment?.spreadWidth
},
nozzle: selectedNozzle ? {
id: selectedNozzle.id,
flowRateGpm: selectedNozzle.flowRateGpm,
sprayAngle: selectedNozzle.sprayAngle
} : null,
...(planData.applicationType === 'liquid' && selectedNozzle && {
nozzle: {
id: selectedNozzle.id,
flowRateGpm: selectedNozzle.flowRateGpm,
sprayAngle: selectedNozzle.sprayAngle
}
}),
products: [{
...(planData.selectedProduct?.isShared
? { productId: parseInt(planData.selectedProduct.id) }

View File

@@ -493,6 +493,7 @@ const EquipmentFormModal = ({ isEdit, equipment, categories, equipmentTypes, onS
customName: equipment?.customName || '',
manufacturer: equipment?.manufacturer || '',
model: equipment?.model || '',
notes: equipment?.notes || '',
// Spreader fields
capacityLbs: equipment?.capacityLbs || '',
spreaderType: equipment?.spreaderType || 'walk_behind',

View File

@@ -7,7 +7,7 @@ import {
TrashIcon,
PencilIcon
} from '@heroicons/react/24/outline';
import { productsAPI } from '../../services/api';
import { productsAPI, productSpreaderSettingsAPI } from '../../services/api';
import LoadingSpinner from '../../components/UI/LoadingSpinner';
import toast from 'react-hot-toast';
@@ -67,8 +67,27 @@ const Products = () => {
const handleCreateProduct = async (productData) => {
try {
await productsAPI.createUserProduct(productData);
toast.success('Custom product created successfully!');
// Create the product first
const response = await productsAPI.createUserProduct(productData);
const createdProduct = response.data.data.product;
// Save spreader settings if any
if (productData.spreaderSettings && productData.spreaderSettings.length > 0) {
const settingPromises = productData.spreaderSettings.map(setting =>
productSpreaderSettingsAPI.create({
userProductId: createdProduct.id,
spreaderBrand: setting.spreaderBrand,
spreaderModel: setting.spreaderModel || null,
settingValue: setting.settingValue,
rateDescription: setting.rateDescription || null,
notes: setting.notes || null
})
);
await Promise.all(settingPromises);
}
toast.success(`Custom product created successfully${productData.spreaderSettings?.length ? ` with ${productData.spreaderSettings.length} spreader setting(s)` : ''}!`);
setShowCreateForm(false);
fetchData();
} catch (error) {
@@ -397,6 +416,35 @@ const CreateProductModal = ({ onSubmit, onCancel, sharedProducts, categories })
notes: ''
});
const [spreaderSettings, setSpreaderSettings] = useState([]);
const [newSpreaderSetting, setNewSpreaderSetting] = useState({
spreaderBrand: '',
spreaderModel: '',
settingValue: '',
rateDescription: '',
notes: ''
});
const addSpreaderSetting = () => {
if (!newSpreaderSetting.spreaderBrand || !newSpreaderSetting.settingValue) {
toast.error('Please enter spreader brand and setting value');
return;
}
setSpreaderSettings([...spreaderSettings, { ...newSpreaderSetting, id: Date.now() }]);
setNewSpreaderSetting({
spreaderBrand: '',
spreaderModel: '',
settingValue: '',
rateDescription: '',
notes: ''
});
};
const removeSpreaderSetting = (id) => {
setSpreaderSettings(spreaderSettings.filter(setting => setting.id !== id));
};
const handleSubmit = (e) => {
e.preventDefault();
@@ -416,7 +464,8 @@ const CreateProductModal = ({ onSubmit, onCancel, sharedProducts, categories })
customName: formData.customName || null,
customRateAmount: formData.customRateAmount ? parseFloat(formData.customRateAmount) : null,
customRateUnit: formData.customRateUnit || null,
notes: formData.notes || null
notes: formData.notes || null,
spreaderSettings: formData.productType === 'granular' ? spreaderSettings : []
};
onSubmit(submitData);
@@ -547,6 +596,98 @@ const CreateProductModal = ({ onSubmit, onCancel, sharedProducts, categories })
</div>
</div>
{/* Spreader Settings for Granular Products */}
{formData.productType === 'granular' && (
<div className="border-t pt-4">
<h4 className="text-md font-semibold text-gray-900 mb-3">Spreader Settings</h4>
<p className="text-sm text-gray-600 mb-4">
Add spreader settings for different brands/models. This helps determine the correct spreader dial setting when applying this product.
</p>
{/* List existing spreader settings */}
{spreaderSettings.length > 0 && (
<div className="space-y-2 mb-4">
<label className="label">Added Settings:</label>
{spreaderSettings.map((setting) => (
<div key={setting.id} className="flex items-center justify-between bg-gray-50 p-3 rounded-lg">
<div className="flex-1">
<div className="font-medium">
{setting.spreaderBrand} {setting.spreaderModel && `${setting.spreaderModel}`} - Setting: {setting.settingValue}
</div>
{setting.rateDescription && (
<div className="text-sm text-gray-600">{setting.rateDescription}</div>
)}
</div>
<button
type="button"
onClick={() => removeSpreaderSetting(setting.id)}
className="text-red-600 hover:text-red-800"
>
<TrashIcon className="h-4 w-4" />
</button>
</div>
))}
</div>
)}
{/* Add new spreader setting form */}
<div className="bg-blue-50 p-4 rounded-lg space-y-3">
<h5 className="text-sm font-semibold text-blue-900">Add Spreader Setting</h5>
<div className="grid grid-cols-2 gap-3">
<div>
<label className="label text-xs">Spreader Brand *</label>
<input
type="text"
className="input text-sm"
value={newSpreaderSetting.spreaderBrand}
onChange={(e) => setNewSpreaderSetting({ ...newSpreaderSetting, spreaderBrand: e.target.value })}
placeholder="LESCO, PermaGreen, Cyclone, etc."
/>
</div>
<div>
<label className="label text-xs">Model (Optional)</label>
<input
type="text"
className="input text-sm"
value={newSpreaderSetting.spreaderModel}
onChange={(e) => setNewSpreaderSetting({ ...newSpreaderSetting, spreaderModel: e.target.value })}
placeholder="Model name/number"
/>
</div>
</div>
<div className="grid grid-cols-2 gap-3">
<div>
<label className="label text-xs">Setting Value *</label>
<input
type="text"
className="input text-sm"
value={newSpreaderSetting.settingValue}
onChange={(e) => setNewSpreaderSetting({ ...newSpreaderSetting, settingValue: e.target.value })}
placeholder="#14, 4, 20, etc."
/>
</div>
<div>
<label className="label text-xs">Rate Description</label>
<input
type="text"
className="input text-sm"
value={newSpreaderSetting.rateDescription}
onChange={(e) => setNewSpreaderSetting({ ...newSpreaderSetting, rateDescription: e.target.value })}
placeholder="e.g., 1 lb N per 1000 sq ft"
/>
</div>
</div>
<button
type="button"
onClick={addSpreaderSetting}
className="btn-primary text-sm px-3 py-1"
>
Add Setting
</button>
</div>
</div>
)}
<div>
<label className="label">Notes</label>
<textarea

View File

@@ -181,6 +181,22 @@ export const applicationsAPI = {
getStats: (params) => apiClient.get('/applications/stats', { params }),
};
// Spreader Settings API endpoints
export const spreaderSettingsAPI = {
getAll: () => apiClient.get('/spreader-settings'),
getBrands: () => apiClient.get('/spreader-settings/brands'),
getByBrand: (brand) => apiClient.get(`/spreader-settings/${brand}`),
};
// Product Spreader Settings API endpoints
export const productSpreaderSettingsAPI = {
getByProduct: (productId) => apiClient.get(`/product-spreader-settings/product/${productId}`),
getByUserProduct: (userProductId) => apiClient.get(`/product-spreader-settings/user-product/${userProductId}`),
create: (settingData) => apiClient.post('/product-spreader-settings', settingData),
update: (id, settingData) => apiClient.put(`/product-spreader-settings/${id}`, settingData),
delete: (id) => apiClient.delete(`/product-spreader-settings/${id}`),
};
// Weather API endpoints
export const weatherAPI = {
getCurrent: (propertyId) => apiClient.get(`/weather/${propertyId}`),