tank mix 1

This commit is contained in:
Jake Kasper
2025-08-26 06:58:21 -05:00
parent 2e41a42092
commit 054f743e2d
2 changed files with 306 additions and 96 deletions

View File

@@ -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
};

View File

@@ -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 && (