tank mix 1
This commit is contained in:
@@ -89,27 +89,43 @@ router.get('/plans', async (req, res, next) => {
|
||||
queryParams
|
||||
);
|
||||
|
||||
// Get spreader settings for each plan
|
||||
// Get spreader settings and product details for each plan
|
||||
const plansWithSettings = await Promise.all(
|
||||
result.rows.map(async (plan) => {
|
||||
let spreaderSetting = null;
|
||||
let productDetails = [];
|
||||
|
||||
// Only get spreader settings for granular applications with equipment
|
||||
if (plan.equipment_name) {
|
||||
// Get the first product for this plan to determine if it's granular
|
||||
const productResult = await pool.query(
|
||||
`SELECT app.product_id, app.user_product_id, p.product_type as shared_type, up.custom_product_type as user_type
|
||||
// Get all products for this plan
|
||||
const productsResult = await pool.query(
|
||||
`SELECT app.*,
|
||||
COALESCE(p.name, up.custom_name) as product_name,
|
||||
COALESCE(p.brand, up.custom_brand) as product_brand,
|
||||
COALESCE(p.product_type, up.custom_product_type) as product_type,
|
||||
p.name as shared_name,
|
||||
up.custom_name as user_name
|
||||
FROM application_plan_products app
|
||||
LEFT JOIN products p ON app.product_id = p.id
|
||||
LEFT JOIN user_products up ON app.user_product_id = up.id
|
||||
WHERE app.plan_id = $1
|
||||
LIMIT 1`,
|
||||
WHERE app.plan_id = $1`,
|
||||
[plan.id]
|
||||
);
|
||||
|
||||
if (productResult.rows.length > 0) {
|
||||
const product = productResult.rows[0];
|
||||
const productType = product.shared_type || product.user_type;
|
||||
productDetails = productsResult.rows.map(product => ({
|
||||
name: product.product_name,
|
||||
brand: product.product_brand,
|
||||
type: product.product_type,
|
||||
rateAmount: parseFloat(product.rate_amount || 0),
|
||||
rateUnit: product.rate_unit,
|
||||
calculatedAmount: parseFloat(product.calculated_product_amount || 0),
|
||||
isShared: !!product.shared_name
|
||||
}));
|
||||
|
||||
// Only get spreader settings for granular applications with equipment
|
||||
if (plan.equipment_name && productsResult.rows.length > 0) {
|
||||
const firstProduct = productsResult.rows[0];
|
||||
const productType = firstProduct.shared_name ?
|
||||
(await pool.query('SELECT product_type FROM products WHERE id = $1', [firstProduct.product_id])).rows[0]?.product_type :
|
||||
firstProduct.custom_product_type;
|
||||
|
||||
if (productType === 'granular') {
|
||||
// Get equipment ID
|
||||
@@ -122,14 +138,13 @@ router.get('/plans', async (req, res, next) => {
|
||||
const equipmentId = equipmentResult.rows[0].id;
|
||||
spreaderSetting = await getSpreaderSettingsForEquipment(
|
||||
equipmentId,
|
||||
product.product_id,
|
||||
product.user_product_id,
|
||||
firstProduct.product_id,
|
||||
firstProduct.user_product_id,
|
||||
req.user.id
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
id: plan.id,
|
||||
@@ -146,6 +161,7 @@ router.get('/plans', async (req, res, next) => {
|
||||
totalWaterAmount: parseFloat(plan.total_water_amount || 0),
|
||||
avgSpeedMph: parseFloat(plan.avg_speed_mph || 0),
|
||||
spreaderSetting: spreaderSetting?.setting_value || null,
|
||||
productDetails: productDetails,
|
||||
createdAt: plan.created_at,
|
||||
updatedAt: plan.updated_at
|
||||
};
|
||||
|
||||
@@ -301,9 +301,27 @@ const Applications = () => {
|
||||
Products: {application.productCount}
|
||||
</p>
|
||||
{/* Display calculated amounts */}
|
||||
{application.totalProductAmount > 0 && (
|
||||
{(application.totalProductAmount > 0 || (application.productDetails && application.productDetails.length > 0)) && (
|
||||
<div className="text-sm text-green-600 mt-2 space-y-1">
|
||||
<p className="font-medium">Calculated Requirements:</p>
|
||||
|
||||
{/* Show individual products for liquid tank mix */}
|
||||
{application.productDetails && application.productDetails.length > 1 ? (
|
||||
<>
|
||||
{application.productDetails.map((product, index) => (
|
||||
<p key={index}>
|
||||
• {product.name}{product.brand ? ` (${product.brand})` : ''}: {product.calculatedAmount.toFixed(2)} oz
|
||||
</p>
|
||||
))}
|
||||
{application.totalWaterAmount > 0 && (
|
||||
<p>• Water: {application.totalWaterAmount.toFixed(2)} gallons</p>
|
||||
)}
|
||||
{application.avgSpeedMph > 0 && (
|
||||
<p>• Target Speed: {application.avgSpeedMph.toFixed(1)} mph</p>
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<p>• Product: {application.totalProductAmount.toFixed(2)} {application.totalWaterAmount > 0 ? 'oz' : 'lbs'}</p>
|
||||
{application.totalWaterAmount > 0 && (
|
||||
<p>• Water: {application.totalWaterAmount.toFixed(2)} gallons</p>
|
||||
@@ -314,6 +332,8 @@ const Applications = () => {
|
||||
{application.spreaderSetting && (
|
||||
<p>• Spreader Setting: {application.spreaderSetting}</p>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
{application.notes && (
|
||||
@@ -398,7 +418,17 @@ const Applications = () => {
|
||||
sprayAngle: selectedNozzle.sprayAngle
|
||||
}
|
||||
}),
|
||||
products: [{
|
||||
products: planData.applicationType === 'liquid'
|
||||
? planData.selectedProducts.map(item => ({
|
||||
...(item.product?.isShared
|
||||
? { productId: parseInt(item.product.id) }
|
||||
: { userProductId: parseInt(item.product.id) }
|
||||
),
|
||||
rateAmount: parseFloat(item.rateAmount || 1),
|
||||
rateUnit: item.rateUnit || 'oz/1000 sq ft',
|
||||
applicationType: planData.applicationType
|
||||
}))
|
||||
: [{
|
||||
...(planData.selectedProduct?.isShared
|
||||
? { productId: parseInt(planData.selectedProduct.id) }
|
||||
: { userProductId: parseInt(planData.selectedProduct.id) }
|
||||
@@ -442,7 +472,17 @@ const Applications = () => {
|
||||
sprayAngle: selectedNozzle.sprayAngle
|
||||
}
|
||||
}),
|
||||
products: [{
|
||||
products: planData.applicationType === 'liquid'
|
||||
? planData.selectedProducts.map(item => ({
|
||||
...(item.product?.isShared
|
||||
? { productId: parseInt(item.product.id) }
|
||||
: { userProductId: parseInt(item.product.id) }
|
||||
),
|
||||
rateAmount: parseFloat(item.rateAmount || 1),
|
||||
rateUnit: item.rateUnit || 'oz/1000 sq ft',
|
||||
applicationType: planData.applicationType
|
||||
}))
|
||||
: [{
|
||||
...(planData.selectedProduct?.isShared
|
||||
? { productId: parseInt(planData.selectedProduct.id) }
|
||||
: { userProductId: parseInt(planData.selectedProduct.id) }
|
||||
@@ -493,6 +533,7 @@ const ApplicationPlanModal = ({
|
||||
selectedAreas: [],
|
||||
productId: '',
|
||||
selectedProduct: null,
|
||||
selectedProducts: [], // For liquid tank mixing - array of {product, rate}
|
||||
applicationType: '', // 'liquid' or 'granular'
|
||||
equipmentId: '',
|
||||
nozzleId: '',
|
||||
@@ -564,10 +605,29 @@ const ApplicationPlanModal = ({
|
||||
return;
|
||||
}
|
||||
|
||||
if (!planData.applicationType) {
|
||||
toast.error('Please select an application type');
|
||||
return;
|
||||
}
|
||||
|
||||
// Validate product selection based on application type
|
||||
if (planData.applicationType === 'granular') {
|
||||
if (!planData.productId) {
|
||||
toast.error('Please select a product');
|
||||
return;
|
||||
}
|
||||
} else if (planData.applicationType === 'liquid') {
|
||||
if (planData.selectedProducts.length === 0) {
|
||||
toast.error('Please select at least one product for tank mixing');
|
||||
return;
|
||||
}
|
||||
// Validate that all selected products have rates
|
||||
const missingRates = planData.selectedProducts.filter(p => !p.rateAmount || p.rateAmount <= 0);
|
||||
if (missingRates.length > 0) {
|
||||
toast.error('Please enter application rates for all selected products');
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (!planData.equipmentId) {
|
||||
toast.error('Please select equipment');
|
||||
@@ -686,7 +746,34 @@ const ApplicationPlanModal = ({
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Product Selection */}
|
||||
{/* Application Type Selection */}
|
||||
<div>
|
||||
<label className="label flex items-center gap-2">
|
||||
<BeakerIcon className="h-5 w-5" />
|
||||
Application Type *
|
||||
</label>
|
||||
<select
|
||||
className="input"
|
||||
value={planData.applicationType}
|
||||
onChange={(e) => {
|
||||
setPlanData({
|
||||
...planData,
|
||||
applicationType: e.target.value,
|
||||
productId: '',
|
||||
selectedProduct: null,
|
||||
selectedProducts: []
|
||||
});
|
||||
}}
|
||||
required
|
||||
>
|
||||
<option value="">Select application type...</option>
|
||||
<option value="liquid">Liquid (Tank Mix)</option>
|
||||
<option value="granular">Granular</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
{/* Product Selection - Single for Granular */}
|
||||
{planData.applicationType === 'granular' && (
|
||||
<div>
|
||||
<label className="label flex items-center gap-2">
|
||||
<BeakerIcon className="h-5 w-5" />
|
||||
@@ -697,33 +784,20 @@ const ApplicationPlanModal = ({
|
||||
value={planData.productId}
|
||||
onChange={(e) => {
|
||||
const selectedProduct = products.find(p => p.uniqueId === e.target.value);
|
||||
console.log('Selected product:', selectedProduct);
|
||||
|
||||
// Determine application type from product type
|
||||
let applicationType = '';
|
||||
if (selectedProduct) {
|
||||
const productType = selectedProduct.productType || selectedProduct.customProductType;
|
||||
// Map product types to application types
|
||||
if (productType && (productType.toLowerCase().includes('liquid') || productType.toLowerCase().includes('concentrate'))) {
|
||||
applicationType = 'liquid';
|
||||
} else if (productType && (productType.toLowerCase().includes('granular') || productType.toLowerCase().includes('granule'))) {
|
||||
applicationType = 'granular';
|
||||
}
|
||||
}
|
||||
|
||||
setPlanData({
|
||||
...planData,
|
||||
productId: e.target.value,
|
||||
selectedProduct: selectedProduct,
|
||||
applicationType: applicationType
|
||||
selectedProduct: selectedProduct
|
||||
});
|
||||
}}
|
||||
required
|
||||
>
|
||||
<option value="">Select a product...</option>
|
||||
{products.map((product) => {
|
||||
const displayName = product.customName || product.name;
|
||||
{products.filter(product => {
|
||||
const productType = product.productType || product.customProductType;
|
||||
return productType && (productType.toLowerCase().includes('granular') || productType.toLowerCase().includes('granule'));
|
||||
}).map((product) => {
|
||||
const displayName = product.customName || product.name;
|
||||
const brand = product.brand || product.customBrand;
|
||||
const rateInfo = product.customRateAmount && product.customRateUnit
|
||||
? ` (${product.customRateAmount} ${product.customRateUnit})`
|
||||
@@ -731,12 +805,132 @@ const ApplicationPlanModal = ({
|
||||
|
||||
return (
|
||||
<option key={product.uniqueId} value={product.uniqueId}>
|
||||
{displayName}{brand ? ` - ${brand}` : ''}{productType ? ` (${productType})` : ''}{rateInfo}
|
||||
{displayName}{brand ? ` - ${brand}` : ''}{rateInfo}
|
||||
</option>
|
||||
);
|
||||
})}
|
||||
</select>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Product Selection - Multiple for Liquid Tank Mix */}
|
||||
{planData.applicationType === 'liquid' && (
|
||||
<div>
|
||||
<label className="label flex items-center gap-2">
|
||||
<BeakerIcon className="h-5 w-5" />
|
||||
Tank Mix Products *
|
||||
</label>
|
||||
|
||||
{/* Selected Products List */}
|
||||
{planData.selectedProducts.length > 0 && (
|
||||
<div className="space-y-2 mb-3">
|
||||
{planData.selectedProducts.map((item, index) => (
|
||||
<div key={index} className="flex items-center gap-2 p-2 bg-blue-50 border border-blue-200 rounded">
|
||||
<div className="flex-1">
|
||||
<span className="font-medium">{item.product.customName || item.product.name}</span>
|
||||
{item.product.brand || item.product.customBrand ? (
|
||||
<span className="text-gray-600"> - {item.product.brand || item.product.customBrand}</span>
|
||||
) : null}
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<input
|
||||
type="number"
|
||||
step="0.1"
|
||||
min="0"
|
||||
value={item.rateAmount || ''}
|
||||
onChange={(e) => {
|
||||
const newProducts = [...planData.selectedProducts];
|
||||
newProducts[index] = {
|
||||
...item,
|
||||
rateAmount: parseFloat(e.target.value) || 0
|
||||
};
|
||||
setPlanData({ ...planData, selectedProducts: newProducts });
|
||||
}}
|
||||
className="w-20 px-2 py-1 text-sm border rounded"
|
||||
placeholder="Rate"
|
||||
/>
|
||||
<select
|
||||
value={item.rateUnit || 'oz/1000 sq ft'}
|
||||
onChange={(e) => {
|
||||
const newProducts = [...planData.selectedProducts];
|
||||
newProducts[index] = {
|
||||
...item,
|
||||
rateUnit: e.target.value
|
||||
};
|
||||
setPlanData({ ...planData, selectedProducts: newProducts });
|
||||
}}
|
||||
className="text-sm border rounded px-1 py-1"
|
||||
>
|
||||
<option value="oz/1000 sq ft">oz/1000 sq ft</option>
|
||||
<option value="oz/acre">oz/acre</option>
|
||||
<option value="fl oz/1000 sq ft">fl oz/1000 sq ft</option>
|
||||
<option value="ml/1000 sq ft">ml/1000 sq ft</option>
|
||||
</select>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
const newProducts = planData.selectedProducts.filter((_, i) => i !== index);
|
||||
setPlanData({ ...planData, selectedProducts: newProducts });
|
||||
}}
|
||||
className="text-red-600 hover:text-red-800 p-1"
|
||||
>
|
||||
✕
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Add Product Dropdown */}
|
||||
<select
|
||||
className="input"
|
||||
value=""
|
||||
onChange={(e) => {
|
||||
if (e.target.value) {
|
||||
const selectedProduct = products.find(p => p.uniqueId === e.target.value);
|
||||
if (selectedProduct && !planData.selectedProducts.some(p => p.product.uniqueId === selectedProduct.uniqueId)) {
|
||||
const newProduct = {
|
||||
product: selectedProduct,
|
||||
rateAmount: selectedProduct.customRateAmount || 1,
|
||||
rateUnit: selectedProduct.customRateUnit || 'oz/1000 sq ft'
|
||||
};
|
||||
setPlanData({
|
||||
...planData,
|
||||
selectedProducts: [...planData.selectedProducts, newProduct]
|
||||
});
|
||||
}
|
||||
}
|
||||
}}
|
||||
>
|
||||
<option value="">Add a product to tank mix...</option>
|
||||
{products.filter(product => {
|
||||
const productType = product.productType || product.customProductType;
|
||||
const isLiquid = productType && (productType.toLowerCase().includes('liquid') || productType.toLowerCase().includes('concentrate'));
|
||||
const notAlreadySelected = !planData.selectedProducts.some(p => p.product.uniqueId === product.uniqueId);
|
||||
return isLiquid && notAlreadySelected;
|
||||
}).map((product) => {
|
||||
const displayName = product.customName || product.name;
|
||||
const brand = product.brand || product.customBrand;
|
||||
const rateInfo = product.customRateAmount && product.customRateUnit
|
||||
? ` (${product.customRateAmount} ${product.customRateUnit})`
|
||||
: '';
|
||||
|
||||
return (
|
||||
<option key={product.uniqueId} value={product.uniqueId}>
|
||||
{displayName}{brand ? ` - ${brand}` : ''}{rateInfo}
|
||||
</option>
|
||||
);
|
||||
})}
|
||||
</select>
|
||||
|
||||
{planData.selectedProducts.length === 0 && (
|
||||
<p className="text-sm text-gray-600 mt-1">
|
||||
Select liquid products to mix in the tank. You can add herbicides, surfactants, and other liquid products.
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Equipment Selection */}
|
||||
{planData.applicationType && (
|
||||
|
||||
Reference in New Issue
Block a user