edit fixes
This commit is contained in:
60
backend/scripts/fix-categories-safe.sql
Normal file
60
backend/scripts/fix-categories-safe.sql
Normal 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;
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -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">
|
||||||
|
|||||||
Reference in New Issue
Block a user