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,
|
`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,
|
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,
|
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
|
FROM application_plans ap
|
||||||
JOIN lawn_sections ls ON ap.lawn_section_id = ls.id
|
JOIN lawn_sections ls ON ap.lawn_section_id = ls.id
|
||||||
JOIN properties p ON ls.property_id = p.id
|
JOIN properties p ON ls.property_id = p.id
|
||||||
LEFT JOIN user_equipment ue ON ap.equipment_id = ue.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 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`,
|
WHERE ap.id = $1 AND ap.user_id = $2`,
|
||||||
[planId, req.user.id]
|
[planId, req.user.id]
|
||||||
);
|
);
|
||||||
@@ -271,6 +273,12 @@ router.get('/plans/:id', validateParams(idParamSchema), async (req, res, next) =
|
|||||||
type: plan.equipment_type,
|
type: plan.equipment_type,
|
||||||
category: plan.equipment_category
|
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 => ({
|
products: productsResult.rows.map(product => ({
|
||||||
id: product.id,
|
id: product.id,
|
||||||
productId: product.product_id,
|
productId: product.product_id,
|
||||||
|
|||||||
@@ -536,6 +536,19 @@ const ApplicationPlanModal = ({
|
|||||||
editingPlan
|
editingPlan
|
||||||
}) => {
|
}) => {
|
||||||
const [loadingProperty, setLoadingProperty] = useState(false);
|
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({
|
const [planData, setPlanData] = useState({
|
||||||
propertyId: '',
|
propertyId: '',
|
||||||
@@ -550,33 +563,87 @@ const ApplicationPlanModal = ({
|
|||||||
notes: ''
|
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
|
// Initialize form with editing data
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (editingPlan && products.length > 0) {
|
if (editingPlan && products.length > 0) {
|
||||||
const propertyId = editingPlan.section?.propertyId || editingPlan.property?.id;
|
const propertyId = editingPlan.section?.propertyId || editingPlan.property?.id;
|
||||||
|
|
||||||
// Find the product from the plans products array
|
// Determine application type from the first product
|
||||||
const planProduct = editingPlan.products?.[0];
|
const firstProduct = editingPlan.products?.[0];
|
||||||
let selectedProduct = null;
|
const applicationType = firstProduct?.productType === 'granular' ? 'granular' : 'liquid';
|
||||||
if (planProduct) {
|
|
||||||
if (planProduct.productId) {
|
// Handle different application types
|
||||||
selectedProduct = products.find(p => p.uniqueId === `shared_${planProduct.productId}`);
|
if (applicationType === 'granular') {
|
||||||
} else if (planProduct.userProductId) {
|
// Granular - single product
|
||||||
selectedProduct = products.find(p => p.uniqueId === `user_${planProduct.userProductId}`);
|
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({
|
setPlanData({
|
||||||
propertyId: propertyId?.toString() || '',
|
propertyId: propertyId?.toString() || '',
|
||||||
selectedAreas: [editingPlan.section?.id],
|
selectedAreas: [editingPlan.section?.id],
|
||||||
productId: selectedProduct?.uniqueId || '',
|
productId: selectedProduct?.uniqueId || '',
|
||||||
selectedProduct: selectedProduct,
|
selectedProduct: selectedProduct,
|
||||||
applicationType: planProduct?.applicationType || '',
|
selectedProducts: [],
|
||||||
equipmentId: editingPlan.equipment?.id?.toString() || '',
|
applicationType: 'granular',
|
||||||
nozzleId: editingPlan.nozzle?.id?.toString() || '',
|
equipmentId: editingPlan.equipment?.id?.toString() || '',
|
||||||
plannedDate: editingPlan.plannedDate ? new Date(editingPlan.plannedDate).toISOString().split('T')[0] : '',
|
nozzleId: editingPlan.nozzle?.id?.toString() || '',
|
||||||
notes: editingPlan.notes || ''
|
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
|
// Only fetch property details if we don't already have them
|
||||||
if (propertyId && (!selectedPropertyDetails || selectedPropertyDetails.id !== propertyId)) {
|
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
|
// Filter equipment based on application type
|
||||||
const availableEquipment = equipment.filter(eq => {
|
const availableEquipment = equipment.filter(eq => {
|
||||||
@@ -798,6 +950,10 @@ const ApplicationPlanModal = ({
|
|||||||
productId: e.target.value,
|
productId: e.target.value,
|
||||||
selectedProduct: selectedProduct
|
selectedProduct: selectedProduct
|
||||||
});
|
});
|
||||||
|
// Check for spreader settings when granular product and spreader are both selected
|
||||||
|
if (selectedProduct && planData.equipmentId) {
|
||||||
|
checkSpreaderSetting(selectedProduct, planData.equipmentId);
|
||||||
|
}
|
||||||
}}
|
}}
|
||||||
required
|
required
|
||||||
>
|
>
|
||||||
@@ -951,7 +1107,14 @@ const ApplicationPlanModal = ({
|
|||||||
<select
|
<select
|
||||||
className="input"
|
className="input"
|
||||||
value={planData.equipmentId}
|
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
|
required
|
||||||
>
|
>
|
||||||
<option value="">Select equipment...</option>
|
<option value="">Select equipment...</option>
|
||||||
@@ -1041,6 +1204,83 @@ const ApplicationPlanModal = ({
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</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>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
Reference in New Issue
Block a user