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

@@ -301,18 +301,38 @@ 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>
<p> Product: {application.totalProductAmount.toFixed(2)} {application.totalWaterAmount > 0 ? 'oz' : 'lbs'}</p>
{application.totalWaterAmount > 0 && (
<p> Water: {application.totalWaterAmount.toFixed(2)} gallons</p>
)}
{application.avgSpeedMph > 0 && (
<p> Target Speed: {application.avgSpeedMph.toFixed(1)} mph</p>
)}
{application.spreaderSetting && (
<p> Spreader Setting: {application.spreaderSetting}</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>
)}
{application.avgSpeedMph > 0 && (
<p> Target Speed: {application.avgSpeedMph.toFixed(1)} mph</p>
)}
{application.spreaderSetting && (
<p> Spreader Setting: {application.spreaderSetting}</p>
)}
</>
)}
</div>
)}
@@ -398,15 +418,25 @@ const Applications = () => {
sprayAngle: selectedNozzle.sprayAngle
}
}),
products: [{
...(planData.selectedProduct?.isShared
? { productId: parseInt(planData.selectedProduct.id) }
: { userProductId: parseInt(planData.selectedProduct.id) }
),
rateAmount: parseFloat(planData.selectedProduct?.customRateAmount || planData.selectedProduct?.rateAmount || 1),
rateUnit: planData.selectedProduct?.customRateUnit || planData.selectedProduct?.rateUnit || 'per 1000sqft',
applicationType: planData.applicationType
}]
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) }
),
rateAmount: parseFloat(planData.selectedProduct?.customRateAmount || planData.selectedProduct?.rateAmount || 1),
rateUnit: planData.selectedProduct?.customRateUnit || planData.selectedProduct?.rateUnit || 'per 1000sqft',
applicationType: planData.applicationType
}]
};
await applicationsAPI.updatePlan(editingPlan.id, planPayload);
@@ -442,15 +472,25 @@ const Applications = () => {
sprayAngle: selectedNozzle.sprayAngle
}
}),
products: [{
...(planData.selectedProduct?.isShared
? { productId: parseInt(planData.selectedProduct.id) }
: { userProductId: parseInt(planData.selectedProduct.id) }
),
rateAmount: parseFloat(planData.selectedProduct?.customRateAmount || planData.selectedProduct?.rateAmount || 1),
rateUnit: planData.selectedProduct?.customRateUnit || planData.selectedProduct?.rateUnit || 'per 1000sqft',
applicationType: planData.applicationType
}]
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) }
),
rateAmount: parseFloat(planData.selectedProduct?.customRateAmount || planData.selectedProduct?.rateAmount || 1),
rateUnit: planData.selectedProduct?.customRateUnit || planData.selectedProduct?.rateUnit || 'per 1000sqft',
applicationType: planData.applicationType
}]
};
return applicationsAPI.createPlan(planPayload);
@@ -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,11 +605,30 @@ const ApplicationPlanModal = ({
return;
}
if (!planData.productId) {
toast.error('Please select a product');
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');
return;
@@ -686,58 +746,192 @@ const ApplicationPlanModal = ({
</div>
)}
{/* Product Selection */}
{/* Application Type Selection */}
<div>
<label className="label flex items-center gap-2">
<BeakerIcon className="h-5 w-5" />
Product *
Application Type *
</label>
<select
className="input"
value={planData.productId}
value={planData.applicationType}
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
applicationType: e.target.value,
productId: '',
selectedProduct: null,
selectedProducts: []
});
}}
required
>
<option value="">Select a product...</option>
{products.map((product) => {
const displayName = product.customName || product.name;
const productType = product.productType || product.customProductType;
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}` : ''}{productType ? ` (${productType})` : ''}{rateInfo}
</option>
);
})}
<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" />
Product *
</label>
<select
className="input"
value={planData.productId}
onChange={(e) => {
const selectedProduct = products.find(p => p.uniqueId === e.target.value);
setPlanData({
...planData,
productId: e.target.value,
selectedProduct: selectedProduct
});
}}
required
>
<option value="">Select a product...</option>
{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})`
: '';
return (
<option key={product.uniqueId} value={product.uniqueId}>
{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 && (
<div>