edit fixes

This commit is contained in:
Jake Kasper
2025-08-21 21:05:15 -04:00
parent 33894dec6c
commit a0a9ec5411
3 changed files with 341 additions and 77 deletions

View File

@@ -0,0 +1,60 @@
-- Safe category cleanup that handles existing singular forms
-- This will consolidate similar categories and fix naming
-- First, update any products that reference plural categories to point to singular ones
-- Update any references to 'Fertilizers' to point to 'Fertilizer' (if it exists)
UPDATE products SET category_id = (
SELECT id FROM product_categories WHERE name = 'Fertilizer' LIMIT 1
) WHERE category_id = (
SELECT id FROM product_categories WHERE name = 'Fertilizers' LIMIT 1
) AND EXISTS (SELECT 1 FROM product_categories WHERE name = 'Fertilizer');
-- Update any references to 'Herbicides' to point to 'Herbicide' (if it exists)
UPDATE products SET category_id = (
SELECT id FROM product_categories WHERE name = 'Herbicide' LIMIT 1
) WHERE category_id = (
SELECT id FROM product_categories WHERE name = 'Herbicides' LIMIT 1
) AND EXISTS (SELECT 1 FROM product_categories WHERE name = 'Herbicide');
-- Update any references to 'Fungicides' to point to 'Fungicide' (if it exists)
UPDATE products SET category_id = (
SELECT id FROM product_categories WHERE name = 'Fungicide' LIMIT 1
) WHERE category_id = (
SELECT id FROM product_categories WHERE name = 'Fungicides' LIMIT 1
) AND EXISTS (SELECT 1 FROM product_categories WHERE name = 'Fungicide');
-- Update any references to 'Insecticides' to point to 'Insecticide' (if it exists)
UPDATE products SET category_id = (
SELECT id FROM product_categories WHERE name = 'Insecticide' LIMIT 1
) WHERE category_id = (
SELECT id FROM product_categories WHERE name = 'Insecticides' LIMIT 1
) AND EXISTS (SELECT 1 FROM product_categories WHERE name = 'Insecticide');
-- Delete plural categories that now have singular equivalents
DELETE FROM product_categories WHERE name IN ('Fertilizers', 'Herbicides', 'Fungicides', 'Insecticides')
AND EXISTS (SELECT 1 FROM product_categories WHERE name IN ('Fertilizer', 'Herbicide', 'Fungicide', 'Insecticide'));
-- Update remaining plural categories to singular (only if singular doesn't exist)
UPDATE product_categories SET name = 'Surfactant' WHERE name = 'Surfactants'
AND NOT EXISTS (SELECT 1 FROM product_categories WHERE name = 'Surfactant');
UPDATE product_categories SET name = 'Adjuvant' WHERE name = 'Adjuvants'
AND NOT EXISTS (SELECT 1 FROM product_categories WHERE name = 'Adjuvant');
UPDATE product_categories SET name = 'Growth Regulator' WHERE name = 'Growth Regulators'
AND NOT EXISTS (SELECT 1 FROM product_categories WHERE name = 'Growth Regulator');
-- Add any missing core categories (only if they don't exist)
INSERT INTO product_categories (name, description) VALUES
('Herbicide', 'Products for weed control and prevention'),
('Fertilizer', 'Nutrients for lawn growth and health'),
('Fungicide', 'Products for disease prevention and treatment'),
('Insecticide', 'Products for insect control'),
('Pre-emergent', 'Products that prevent weeds from germinating'),
('Post-emergent', 'Products that kill existing weeds'),
('Growth Regulator', 'Products that modify plant growth'),
('Surfactant', 'Products that improve spray coverage and penetration'),
('Adjuvant', 'Products that enhance pesticide performance'),
('Seed', 'Grass seeds and seed treatments'),
('Soil Amendment', 'Products that improve soil conditions')
ON CONFLICT (name) DO NOTHING;

View File

@@ -80,7 +80,14 @@ const userProductSchema = Joi.object({
customName: Joi.string().max(255).allow(null).optional(), customName: Joi.string().max(255).allow(null).optional(),
customRateAmount: Joi.number().positive().allow(null).optional(), customRateAmount: Joi.number().positive().allow(null).optional(),
customRateUnit: Joi.string().max(50).allow(null).optional(), customRateUnit: Joi.string().max(50).allow(null).optional(),
notes: Joi.string().allow(null, '').optional() notes: Joi.string().allow(null, '').optional(),
// Additional fields for advanced editing
brand: Joi.string().max(100).allow(null).optional(),
categoryId: Joi.number().integer().positive().allow(null).optional(),
productType: Joi.string().valid('granular', 'liquid', 'seed', 'powder').allow(null).optional(),
activeIngredients: Joi.string().allow(null).optional(),
description: Joi.string().allow(null).optional(),
isAdvancedEdit: Joi.boolean().optional()
}); });
// Application validation schemas // Application validation schemas

View File

@@ -583,26 +583,68 @@ const EditProductModal = ({ product, onSubmit, onCancel, sharedProducts, categor
customName: product.customName || '', customName: product.customName || '',
customRateAmount: product.customRateAmount || '', customRateAmount: product.customRateAmount || '',
customRateUnit: product.customRateUnit || 'lbs/1000 sq ft', customRateUnit: product.customRateUnit || 'lbs/1000 sq ft',
notes: product.notes || '' notes: product.notes || '',
// Add fields for full editing capability
brand: product.brand || '',
categoryId: product.categoryId || '',
productType: product.productType || '',
activeIngredients: product.activeIngredients || '',
description: product.description || ''
}); });
const [editMode, setEditMode] = useState('basic'); // 'basic' or 'advanced'
const handleSubmit = (e) => { const handleSubmit = (e) => {
e.preventDefault(); e.preventDefault();
if (!formData.productId && !formData.customName) { if (editMode === 'basic') {
toast.error('Please select a base product or enter a custom name'); // Basic mode validation
return; if (!formData.productId && !formData.customName) {
toast.error('Please select a base product or enter a custom name');
return;
}
const submitData = {
productId: formData.productId ? parseInt(formData.productId) : null,
customName: formData.customName || null,
customRateAmount: formData.customRateAmount ? parseFloat(formData.customRateAmount) : null,
customRateUnit: formData.customRateUnit || null,
notes: formData.notes || null
};
onSubmit(submitData);
} else {
// Advanced mode validation and submission
if (!formData.customName) {
toast.error('Product name is required');
return;
}
if (!formData.categoryId) {
toast.error('Category is required');
return;
}
if (!formData.productType) {
toast.error('Product type is required');
return;
}
const submitData = {
productId: formData.productId ? parseInt(formData.productId) : null,
customName: formData.customName,
customRateAmount: formData.customRateAmount ? parseFloat(formData.customRateAmount) : null,
customRateUnit: formData.customRateUnit || null,
notes: formData.notes || null,
// Additional fields for advanced mode
brand: formData.brand || null,
categoryId: formData.categoryId ? parseInt(formData.categoryId) : null,
productType: formData.productType || null,
activeIngredients: formData.activeIngredients || null,
description: formData.description || null,
isAdvancedEdit: true // Flag to indicate this is an advanced edit
};
onSubmit(submitData);
} }
const submitData = {
productId: formData.productId ? parseInt(formData.productId) : null,
customName: formData.customName || null,
customRateAmount: formData.customRateAmount ? parseFloat(formData.customRateAmount) : null,
customRateUnit: formData.customRateUnit || null,
notes: formData.notes || null
};
onSubmit(submitData);
}; };
return ( return (
@@ -610,73 +652,228 @@ const EditProductModal = ({ product, onSubmit, onCancel, sharedProducts, categor
<div className="bg-white rounded-lg p-6 w-full max-w-lg max-h-[90vh] overflow-y-auto"> <div className="bg-white rounded-lg p-6 w-full max-w-lg max-h-[90vh] overflow-y-auto">
<h3 className="text-lg font-semibold mb-4">Edit Product</h3> <h3 className="text-lg font-semibold mb-4">Edit Product</h3>
<form onSubmit={handleSubmit} className="space-y-4"> {/* Edit Mode Toggle */}
<div> <div className="mb-4">
<label className="label">Base Product (Optional)</label> <div className="flex bg-gray-100 rounded-lg p-1">
<select <button
className="input" type="button"
value={formData.productId} onClick={() => setEditMode('basic')}
onChange={(e) => setFormData({ ...formData, productId: e.target.value })} className={`flex-1 py-2 px-4 rounded-md text-sm font-medium transition-colors ${
editMode === 'basic'
? 'bg-white text-gray-900 shadow-sm'
: 'text-gray-600 hover:text-gray-900'
}`}
> >
<option value="">Select a base product...</option> Basic Edit
{sharedProducts.map((sharedProduct) => ( </button>
<option key={sharedProduct.id} value={sharedProduct.id}> <button
{sharedProduct.name} {sharedProduct.brand && `- ${sharedProduct.brand}`} type="button"
</option> onClick={() => setEditMode('advanced')}
))} className={`flex-1 py-2 px-4 rounded-md text-sm font-medium transition-colors ${
</select> editMode === 'advanced'
? 'bg-white text-gray-900 shadow-sm'
: 'text-gray-600 hover:text-gray-900'
}`}
>
Advanced Edit
</button>
</div> </div>
</div>
<div> <form onSubmit={handleSubmit} className="space-y-4">
<label className="label">Custom Name</label> {editMode === 'basic' ? (
<input // Basic editing - existing functionality
type="text" <>
className="input" <div>
value={formData.customName} <label className="label">Base Product (Optional)</label>
onChange={(e) => setFormData({ ...formData, customName: e.target.value })} <select
placeholder="e.g., My 24-0-11 Blend, Custom Herbicide Mix" className="input"
/> value={formData.productId}
</div> onChange={(e) => setFormData({ ...formData, productId: e.target.value })}
>
<option value="">Select a base product...</option>
{sharedProducts.map((sharedProduct) => (
<option key={sharedProduct.id} value={sharedProduct.id}>
{sharedProduct.name} {sharedProduct.brand && `- ${sharedProduct.brand}`}
</option>
))}
</select>
</div>
<div className="grid grid-cols-2 gap-4"> <div>
<div> <label className="label">Custom Name</label>
<label className="label">Application Rate</label> <input
<input type="text"
type="number" className="input"
step="0.01" value={formData.customName}
className="input" onChange={(e) => setFormData({ ...formData, customName: e.target.value })}
value={formData.customRateAmount} placeholder="e.g., My 24-0-11 Blend, Custom Herbicide Mix"
onChange={(e) => setFormData({ ...formData, customRateAmount: e.target.value })} />
placeholder="2.5" </div>
/>
</div>
<div>
<label className="label">Rate Unit</label>
<select
className="input"
value={formData.customRateUnit}
onChange={(e) => setFormData({ ...formData, customRateUnit: e.target.value })}
>
<option value="lbs/1000 sq ft">lbs/1000 sq ft</option>
<option value="oz/1000 sq ft">oz/1000 sq ft</option>
<option value="gal/acre">gal/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>
<option value="tbsp/1000 sq ft">tbsp/1000 sq ft</option>
</select>
</div>
</div>
<div> <div className="grid grid-cols-2 gap-4">
<label className="label">Notes</label> <div>
<textarea <label className="label">Application Rate</label>
className="input" <input
rows="3" type="number"
value={formData.notes} step="0.01"
onChange={(e) => setFormData({ ...formData, notes: e.target.value })} className="input"
placeholder="Special mixing instructions, storage notes, etc." value={formData.customRateAmount}
/> onChange={(e) => setFormData({ ...formData, customRateAmount: e.target.value })}
</div> placeholder="2.5"
/>
</div>
<div>
<label className="label">Rate Unit</label>
<select
className="input"
value={formData.customRateUnit}
onChange={(e) => setFormData({ ...formData, customRateUnit: e.target.value })}
>
<option value="lbs/1000 sq ft">lbs/1000 sq ft</option>
<option value="oz/1000 sq ft">oz/1000 sq ft</option>
<option value="gal/acre">gal/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>
<option value="tbsp/1000 sq ft">tbsp/1000 sq ft</option>
</select>
</div>
</div>
<div>
<label className="label">Notes</label>
<textarea
className="input"
rows="3"
value={formData.notes}
onChange={(e) => setFormData({ ...formData, notes: e.target.value })}
placeholder="Special mixing instructions, storage notes, etc."
/>
</div>
</>
) : (
// Advanced editing - all fields
<>
<div>
<label className="label">Product Name *</label>
<input
type="text"
className="input"
value={formData.customName}
onChange={(e) => setFormData({ ...formData, customName: e.target.value })}
placeholder="Product name"
required
/>
</div>
<div>
<label className="label">Brand</label>
<input
type="text"
className="input"
value={formData.brand}
onChange={(e) => setFormData({ ...formData, brand: e.target.value })}
placeholder="Manufacturer or brand name"
/>
</div>
<div className="grid grid-cols-2 gap-4">
<div>
<label className="label">Category *</label>
<select
className="input"
value={formData.categoryId}
onChange={(e) => setFormData({ ...formData, categoryId: e.target.value })}
required
>
<option value="">Select category...</option>
{categories.map((category) => (
<option key={category.id} value={category.id}>
{category.name}
</option>
))}
</select>
</div>
<div>
<label className="label">Product Type *</label>
<select
className="input"
value={formData.productType}
onChange={(e) => setFormData({ ...formData, productType: e.target.value })}
required
>
<option value="">Select type...</option>
<option value="liquid">Liquid</option>
<option value="granular">Granular</option>
<option value="seed">Seed</option>
<option value="powder">Powder</option>
</select>
</div>
</div>
<div>
<label className="label">Active Ingredients</label>
<input
type="text"
className="input"
value={formData.activeIngredients}
onChange={(e) => setFormData({ ...formData, activeIngredients: e.target.value })}
placeholder="e.g., 2,4-D 38.3%, Dicamba 2.77%"
/>
</div>
<div>
<label className="label">Description</label>
<textarea
className="input"
rows="3"
value={formData.description}
onChange={(e) => setFormData({ ...formData, description: e.target.value })}
placeholder="Product description and usage information"
/>
</div>
<div className="grid grid-cols-2 gap-4">
<div>
<label className="label">Default Application Rate</label>
<input
type="number"
step="0.01"
className="input"
value={formData.customRateAmount}
onChange={(e) => setFormData({ ...formData, customRateAmount: e.target.value })}
placeholder="2.5"
/>
</div>
<div>
<label className="label">Rate Unit</label>
<select
className="input"
value={formData.customRateUnit}
onChange={(e) => setFormData({ ...formData, customRateUnit: e.target.value })}
>
<option value="lbs/1000 sq ft">lbs/1000 sq ft</option>
<option value="oz/1000 sq ft">oz/1000 sq ft</option>
<option value="gal/acre">gal/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>
<option value="tbsp/1000 sq ft">tbsp/1000 sq ft</option>
</select>
</div>
</div>
<div>
<label className="label">Notes</label>
<textarea
className="input"
rows="3"
value={formData.notes}
onChange={(e) => setFormData({ ...formData, notes: e.target.value })}
placeholder="Special mixing instructions, storage notes, etc."
/>
</div>
</>
)}
<div className="flex gap-3 pt-4"> <div className="flex gap-3 pt-4">
<button type="submit" className="btn-primary flex-1"> <button type="submit" className="btn-primary flex-1">