asdfas
This commit is contained in:
@@ -216,12 +216,14 @@ router.get('/plans/:id', validateParams(idParamSchema), async (req, res, next) =
|
||||
`SELECT ap.*, ls.name as section_name, ls.area as section_area, ls.polygon_data,
|
||||
p.id as property_id, p.name as property_name, p.address as property_address,
|
||||
ue.id as equipment_id, ue.custom_name as equipment_name,
|
||||
et.name as equipment_type, et.category as equipment_category
|
||||
et.name as equipment_type, et.category as equipment_category,
|
||||
nz.id as nozzle_id, nz.custom_name as nozzle_name, nz.flow_rate_gpm, nz.spray_angle
|
||||
FROM application_plans ap
|
||||
JOIN lawn_sections ls ON ap.lawn_section_id = ls.id
|
||||
JOIN properties p ON ls.property_id = p.id
|
||||
LEFT JOIN user_equipment ue ON ap.equipment_id = ue.id
|
||||
LEFT JOIN equipment_types et ON ue.equipment_type_id = et.id
|
||||
LEFT JOIN user_equipment nz ON ap.nozzle_id = nz.id
|
||||
WHERE ap.id = $1 AND ap.user_id = $2`,
|
||||
[planId, req.user.id]
|
||||
);
|
||||
@@ -271,6 +273,12 @@ router.get('/plans/:id', validateParams(idParamSchema), async (req, res, next) =
|
||||
type: plan.equipment_type,
|
||||
category: plan.equipment_category
|
||||
},
|
||||
nozzle: plan.nozzle_id ? {
|
||||
id: plan.nozzle_id,
|
||||
name: plan.nozzle_name,
|
||||
flowRateGpm: plan.flow_rate_gpm,
|
||||
sprayAngle: plan.spray_angle
|
||||
} : null,
|
||||
products: productsResult.rows.map(product => ({
|
||||
id: product.id,
|
||||
productId: product.product_id,
|
||||
|
||||
@@ -536,6 +536,19 @@ const ApplicationPlanModal = ({
|
||||
editingPlan
|
||||
}) => {
|
||||
const [loadingProperty, setLoadingProperty] = useState(false);
|
||||
const [showSpreaderSettingPrompt, setShowSpreaderSettingPrompt] = useState(false);
|
||||
const [missingSpreaderSetting, setMissingSpreaderSetting] = useState({
|
||||
productId: null,
|
||||
userProductId: null,
|
||||
equipmentId: null,
|
||||
productName: '',
|
||||
equipmentName: ''
|
||||
});
|
||||
const [newSpreaderSetting, setNewSpreaderSetting] = useState({
|
||||
settingValue: '',
|
||||
rateDescription: '',
|
||||
notes: ''
|
||||
});
|
||||
|
||||
const [planData, setPlanData] = useState({
|
||||
propertyId: '',
|
||||
@@ -550,33 +563,87 @@ const ApplicationPlanModal = ({
|
||||
notes: ''
|
||||
});
|
||||
|
||||
// Reset form when modal opens fresh (not editing)
|
||||
useEffect(() => {
|
||||
if (!editingPlan) {
|
||||
setPlanData({
|
||||
propertyId: '',
|
||||
selectedAreas: [],
|
||||
productId: '',
|
||||
selectedProduct: null,
|
||||
selectedProducts: [],
|
||||
applicationType: '',
|
||||
equipmentId: '',
|
||||
nozzleId: '',
|
||||
plannedDate: '',
|
||||
notes: ''
|
||||
});
|
||||
}
|
||||
}, [editingPlan]);
|
||||
|
||||
// Initialize form with editing data
|
||||
useEffect(() => {
|
||||
if (editingPlan && products.length > 0) {
|
||||
const propertyId = editingPlan.section?.propertyId || editingPlan.property?.id;
|
||||
|
||||
// Find the product from the plans products array
|
||||
const planProduct = editingPlan.products?.[0];
|
||||
let selectedProduct = null;
|
||||
if (planProduct) {
|
||||
if (planProduct.productId) {
|
||||
selectedProduct = products.find(p => p.uniqueId === `shared_${planProduct.productId}`);
|
||||
} else if (planProduct.userProductId) {
|
||||
selectedProduct = products.find(p => p.uniqueId === `user_${planProduct.userProductId}`);
|
||||
// Determine application type from the first product
|
||||
const firstProduct = editingPlan.products?.[0];
|
||||
const applicationType = firstProduct?.productType === 'granular' ? 'granular' : 'liquid';
|
||||
|
||||
// Handle different application types
|
||||
if (applicationType === 'granular') {
|
||||
// Granular - single product
|
||||
let selectedProduct = null;
|
||||
if (firstProduct) {
|
||||
if (firstProduct.productId) {
|
||||
selectedProduct = products.find(p => p.uniqueId === `shared_${firstProduct.productId}`);
|
||||
} else if (firstProduct.userProductId) {
|
||||
selectedProduct = products.find(p => p.uniqueId === `user_${firstProduct.userProductId}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
setPlanData({
|
||||
propertyId: propertyId?.toString() || '',
|
||||
selectedAreas: [editingPlan.section?.id],
|
||||
productId: selectedProduct?.uniqueId || '',
|
||||
selectedProduct: selectedProduct,
|
||||
applicationType: planProduct?.applicationType || '',
|
||||
equipmentId: editingPlan.equipment?.id?.toString() || '',
|
||||
nozzleId: editingPlan.nozzle?.id?.toString() || '',
|
||||
plannedDate: editingPlan.plannedDate ? new Date(editingPlan.plannedDate).toISOString().split('T')[0] : '',
|
||||
notes: editingPlan.notes || ''
|
||||
});
|
||||
setPlanData({
|
||||
propertyId: propertyId?.toString() || '',
|
||||
selectedAreas: [editingPlan.section?.id],
|
||||
productId: selectedProduct?.uniqueId || '',
|
||||
selectedProduct: selectedProduct,
|
||||
selectedProducts: [],
|
||||
applicationType: 'granular',
|
||||
equipmentId: editingPlan.equipment?.id?.toString() || '',
|
||||
nozzleId: editingPlan.nozzle?.id?.toString() || '',
|
||||
plannedDate: editingPlan.plannedDate ? new Date(editingPlan.plannedDate).toISOString().split('T')[0] : '',
|
||||
notes: editingPlan.notes || ''
|
||||
});
|
||||
} else {
|
||||
// Liquid - multiple products (tank mix)
|
||||
const selectedProducts = editingPlan.products?.map(product => {
|
||||
let foundProduct = null;
|
||||
if (product.productId) {
|
||||
foundProduct = products.find(p => p.uniqueId === `shared_${product.productId}`);
|
||||
} else if (product.userProductId) {
|
||||
foundProduct = products.find(p => p.uniqueId === `user_${product.userProductId}`);
|
||||
}
|
||||
|
||||
return {
|
||||
product: foundProduct,
|
||||
rateAmount: product.rateAmount || 1,
|
||||
rateUnit: product.rateUnit || 'oz/1000 sq ft'
|
||||
};
|
||||
}).filter(item => item.product) || [];
|
||||
|
||||
setPlanData({
|
||||
propertyId: propertyId?.toString() || '',
|
||||
selectedAreas: [editingPlan.section?.id],
|
||||
productId: '',
|
||||
selectedProduct: null,
|
||||
selectedProducts: selectedProducts,
|
||||
applicationType: 'liquid',
|
||||
equipmentId: editingPlan.equipment?.id?.toString() || '',
|
||||
nozzleId: editingPlan.nozzle?.id?.toString() || '',
|
||||
plannedDate: editingPlan.plannedDate ? new Date(editingPlan.plannedDate).toISOString().split('T')[0] : '',
|
||||
notes: editingPlan.notes || ''
|
||||
});
|
||||
}
|
||||
|
||||
// Only fetch property details if we don't already have them
|
||||
if (propertyId && (!selectedPropertyDetails || selectedPropertyDetails.id !== propertyId)) {
|
||||
@@ -595,6 +662,91 @@ const ApplicationPlanModal = ({
|
||||
}
|
||||
};
|
||||
|
||||
// Check for spreader settings when granular product + equipment selected
|
||||
const checkSpreaderSetting = async (product, equipmentId) => {
|
||||
if (!product || !equipmentId ||
|
||||
(product.productType !== 'granular' && product.customProductType !== 'granular')) {
|
||||
return;
|
||||
}
|
||||
|
||||
const selectedEquipment = equipment.find(eq => eq.id === parseInt(equipmentId));
|
||||
if (!selectedEquipment || selectedEquipment.categoryName !== 'Spreader') {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// Check if spreader setting exists
|
||||
const productApiId = product.isShared ? product.id : product.id;
|
||||
const endpoint = product.isShared
|
||||
? `/api/product-spreader-settings/product/${productApiId}`
|
||||
: `/api/product-spreader-settings/user-product/${productApiId}`;
|
||||
|
||||
const response = await fetch(endpoint, {
|
||||
headers: {
|
||||
'Authorization': `Bearer ${localStorage.getItem('authToken')}`
|
||||
}
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
const data = await response.json();
|
||||
const settings = data.data?.settings || [];
|
||||
|
||||
// Check if there's a setting for this specific equipment
|
||||
const existingSetting = settings.find(setting =>
|
||||
setting.equipmentId === selectedEquipment.id
|
||||
);
|
||||
|
||||
if (!existingSetting) {
|
||||
// No setting found - prompt user to create one
|
||||
setMissingSpreaderSetting({
|
||||
productId: product.isShared ? product.id : null,
|
||||
userProductId: product.isShared ? null : product.id,
|
||||
equipmentId: selectedEquipment.id,
|
||||
productName: product.customName || product.name,
|
||||
equipmentName: selectedEquipment.customName
|
||||
});
|
||||
setShowSpreaderSettingPrompt(true);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to check spreader settings:', error);
|
||||
}
|
||||
};
|
||||
|
||||
// Save new spreader setting
|
||||
const saveSpreaderSetting = async () => {
|
||||
try {
|
||||
const settingData = {
|
||||
...(missingSpreaderSetting.productId && { productId: missingSpreaderSetting.productId }),
|
||||
...(missingSpreaderSetting.userProductId && { userProductId: missingSpreaderSetting.userProductId }),
|
||||
equipmentId: missingSpreaderSetting.equipmentId,
|
||||
settingValue: newSpreaderSetting.settingValue,
|
||||
rateDescription: newSpreaderSetting.rateDescription || null,
|
||||
notes: newSpreaderSetting.notes || null
|
||||
};
|
||||
|
||||
const response = await fetch('/api/product-spreader-settings', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': `Bearer ${localStorage.getItem('authToken')}`
|
||||
},
|
||||
body: JSON.stringify(settingData)
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
toast.success('Spreader setting saved successfully');
|
||||
setShowSpreaderSettingPrompt(false);
|
||||
setNewSpreaderSetting({ settingValue: '', rateDescription: '', notes: '' });
|
||||
} else {
|
||||
toast.error('Failed to save spreader setting');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to save spreader setting:', error);
|
||||
toast.error('Failed to save spreader setting');
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
// Filter equipment based on application type
|
||||
const availableEquipment = equipment.filter(eq => {
|
||||
@@ -798,6 +950,10 @@ const ApplicationPlanModal = ({
|
||||
productId: e.target.value,
|
||||
selectedProduct: selectedProduct
|
||||
});
|
||||
// Check for spreader settings when granular product and spreader are both selected
|
||||
if (selectedProduct && planData.equipmentId) {
|
||||
checkSpreaderSetting(selectedProduct, planData.equipmentId);
|
||||
}
|
||||
}}
|
||||
required
|
||||
>
|
||||
@@ -951,7 +1107,14 @@ const ApplicationPlanModal = ({
|
||||
<select
|
||||
className="input"
|
||||
value={planData.equipmentId}
|
||||
onChange={(e) => setPlanData({ ...planData, equipmentId: e.target.value })}
|
||||
onChange={(e) => {
|
||||
const newEquipmentId = e.target.value;
|
||||
setPlanData({ ...planData, equipmentId: newEquipmentId });
|
||||
// Check for spreader settings when granular product and spreader are both selected
|
||||
if (planData.applicationType === 'granular' && planData.selectedProduct && newEquipmentId) {
|
||||
checkSpreaderSetting(planData.selectedProduct, newEquipmentId);
|
||||
}
|
||||
}}
|
||||
required
|
||||
>
|
||||
<option value="">Select equipment...</option>
|
||||
@@ -1041,6 +1204,83 @@ const ApplicationPlanModal = ({
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
{/* Spreader Setting Prompt Modal */}
|
||||
{showSpreaderSettingPrompt && (
|
||||
<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-md">
|
||||
<h3 className="text-lg font-semibold mb-4">Spreader Setting Required</h3>
|
||||
<p className="text-sm text-gray-600 mb-4">
|
||||
No spreader setting found for <strong>{missingSpreaderSetting.productName}</strong> with <strong>{missingSpreaderSetting.equipmentName}</strong>.
|
||||
<br />Please enter the spreader setting to continue.
|
||||
</p>
|
||||
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<label className="label">Spreader Setting *</label>
|
||||
<input
|
||||
type="text"
|
||||
className="input"
|
||||
value={newSpreaderSetting.settingValue}
|
||||
onChange={(e) => setNewSpreaderSetting({
|
||||
...newSpreaderSetting,
|
||||
settingValue: e.target.value
|
||||
})}
|
||||
placeholder="e.g., 3.5, B, 4"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="label">Rate Description (Optional)</label>
|
||||
<input
|
||||
type="text"
|
||||
className="input"
|
||||
value={newSpreaderSetting.rateDescription}
|
||||
onChange={(e) => setNewSpreaderSetting({
|
||||
...newSpreaderSetting,
|
||||
rateDescription: e.target.value
|
||||
})}
|
||||
placeholder="e.g., 2 lbs per 1000 sq ft"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="label">Notes (Optional)</label>
|
||||
<textarea
|
||||
className="input"
|
||||
rows="2"
|
||||
value={newSpreaderSetting.notes}
|
||||
onChange={(e) => setNewSpreaderSetting({
|
||||
...newSpreaderSetting,
|
||||
notes: e.target.value
|
||||
})}
|
||||
placeholder="Additional notes or instructions"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex gap-3 mt-6">
|
||||
<button
|
||||
onClick={saveSpreaderSetting}
|
||||
className="btn-primary flex-1"
|
||||
disabled={!newSpreaderSetting.settingValue}
|
||||
>
|
||||
Save Setting
|
||||
</button>
|
||||
<button
|
||||
onClick={() => {
|
||||
setShowSpreaderSettingPrompt(false);
|
||||
setNewSpreaderSetting({ settingValue: '', rateDescription: '', notes: '' });
|
||||
}}
|
||||
className="btn-secondary flex-1"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user