products page and products

This commit is contained in:
Jake Kasper
2025-08-21 19:48:22 -04:00
parent e75db9978c
commit e1ac44e4ef
2 changed files with 640 additions and 4 deletions

View File

@@ -0,0 +1,176 @@
-- Seed products and categories for TurfTracker
-- Based on common lawn care chemicals and fertilizers
-- Insert product categories
INSERT INTO product_categories (name, description) VALUES
('Herbicides', 'Products for weed control and prevention'),
('Fertilizers', 'Nutrients for lawn growth and health'),
('Fungicides', 'Products for disease prevention and treatment'),
('Insecticides', 'Products for insect control'),
('Pre-emergent', 'Products that prevent weeds from germinating'),
('Post-emergent', 'Products that kill existing weeds'),
('Growth Regulators', 'Products that modify plant growth')
ON CONFLICT (name) DO NOTHING;
-- Get category IDs for reference
DO $$
DECLARE
herbicide_cat_id INTEGER;
fertilizer_cat_id INTEGER;
fungicide_cat_id INTEGER;
insecticide_cat_id INTEGER;
pre_emergent_cat_id INTEGER;
post_emergent_cat_id INTEGER;
growth_reg_cat_id INTEGER;
BEGIN
SELECT id INTO herbicide_cat_id FROM product_categories WHERE name = 'Herbicides';
SELECT id INTO fertilizer_cat_id FROM product_categories WHERE name = 'Fertilizers';
SELECT id INTO fungicide_cat_id FROM product_categories WHERE name = 'Fungicides';
SELECT id INTO insecticide_cat_id FROM product_categories WHERE name = 'Insecticides';
SELECT id INTO pre_emergent_cat_id FROM product_categories WHERE name = 'Pre-emergent';
SELECT id INTO post_emergent_cat_id FROM product_categories WHERE name = 'Post-emergent';
SELECT id INTO growth_reg_cat_id FROM product_categories WHERE name = 'Growth Regulators';
-- Insert herbicides
INSERT INTO products (name, brand, category_id, product_type, active_ingredients, description) VALUES
('Tenacity', 'Syngenta', herbicide_cat_id, 'liquid', 'Mesotrione 40%', 'Selective herbicide for crabgrass and broadleaf weeds. Safe for new seeding.'),
('Prodiamine 65WDG', 'Barricade', pre_emergent_cat_id, 'granular', 'Prodiamine 65%', 'Pre-emergent herbicide for crabgrass and annual grass control'),
('2,4-D Amine', 'Various', post_emergent_cat_id, 'liquid', '2,4-D Dimethylamine Salt 38.3%', 'Selective broadleaf herbicide for dandelions, clover, and other weeds'),
('Triclopyr 4E', 'Garlon', herbicide_cat_id, 'liquid', 'Triclopyr 61.6%', 'Selective herbicide for woody plants and tough broadleaf weeds'),
('Glyphosate 41%', 'Roundup Pro', herbicide_cat_id, 'liquid', 'Glyphosate 41%', 'Non-selective systemic herbicide for total vegetation control'),
('3-Way Herbicide', 'Various', post_emergent_cat_id, 'liquid', '2,4-D + Dicamba + MCPP', 'Combination herbicide for broadleaf weed control in lawns'),
('Dismiss', 'FMC', herbicide_cat_id, 'granular', 'Sulfentrazone 75%', 'Selective herbicide for sedge and certain broadleaf weeds'),
('Sedgehammer', 'Gowan', herbicide_cat_id, 'granular', 'Halosulfuron-methyl 75%', 'Selective post-emergent herbicide for sedges'),
('Pendimethalin 3.3EC', 'Pendulum', pre_emergent_cat_id, 'liquid', 'Pendimethalin 38.7%', 'Pre-emergent herbicide for annual grasses and certain broadleaf weeds'),
('Quinclorac 75WP', 'Drive', herbicide_cat_id, 'granular', 'Quinclorac 75%', 'Selective post-emergent herbicide for crabgrass and other annual grasses')
ON CONFLICT (name, brand) DO NOTHING;
-- Insert fertilizers
INSERT INTO products (name, brand, category_id, product_type, active_ingredients, description) VALUES
('Milorganite 6-4-0', 'Milorganite', fertilizer_cat_id, 'granular', 'Nitrogen 6%, Phosphorus 4%, Iron 2.5%', 'Organic slow-release nitrogen fertilizer with iron'),
('32-0-4 Slow Release', 'Various', fertilizer_cat_id, 'granular', 'Nitrogen 32%, Potassium 4%', 'High nitrogen slow-release fertilizer for rapid greening'),
('24-0-11', 'Various', fertilizer_cat_id, 'granular', 'Nitrogen 24%, Potassium 11%', 'Balanced fertilizer with potassium for root development'),
('Liquid Nitrogen 28-0-0', 'Various', fertilizer_cat_id, 'liquid', 'Urea Ammonium Nitrate 28%', 'Fast-acting liquid nitrogen for quick green-up'),
('Iron Sulfate 20%', 'Various', fertilizer_cat_id, 'granular', 'Iron 20%', 'Iron supplement for chlorosis correction and green color'),
('Starter Fertilizer 18-24-12', 'Various', fertilizer_cat_id, 'granular', 'Nitrogen 18%, Phosphorus 24%, Potassium 12%', 'High phosphorus fertilizer for new seeding and sodding'),
('Organic Lawn Food 10-0-2', 'Espoma', fertilizer_cat_id, 'granular', 'Nitrogen 10%, Potassium 2%', 'Organic slow-release fertilizer with beneficial microbes'),
('Liquid Iron 6%', 'Various', fertilizer_cat_id, 'liquid', 'Iron 6%', 'Chelated iron for quick correction of iron deficiency'),
('Winterizer 13-0-44', 'Various', fertilizer_cat_id, 'granular', 'Nitrogen 13%, Potassium 44%', 'High potassium fertilizer for winter preparation'),
('Urea 46-0-0', 'Various', fertilizer_cat_id, 'granular', 'Nitrogen 46%', 'Fast-release nitrogen fertilizer for quick feeding')
ON CONFLICT (name, brand) DO NOTHING;
-- Insert fungicides
INSERT INTO products (name, brand, category_id, product_type, active_ingredients, description) VALUES
('Propiconazole 14.3', 'Banner Maxx', fungicide_cat_id, 'liquid', 'Propiconazole 14.3%', 'Systemic fungicide for brown patch, dollar spot, and other diseases'),
('Azoxystrobin 22.9%', 'Heritage', fungicide_cat_id, 'liquid', 'Azoxystrobin 22.9%', 'Strobilurin fungicide for broad spectrum disease control'),
('Thiophanate-methyl 70WP', 'Cleary''s 3336', fungicide_cat_id, 'granular', 'Thiophanate-methyl 70%', 'Systemic fungicide for summer patch and other diseases'),
('Myclobutanil 20EW', 'Eagle', fungicide_cat_id, 'liquid', 'Myclobutanil 20%', 'Systemic fungicide for powdery mildew and rust'),
('Chlorothalonil 82.5%', 'Daconil Ultrex', fungicide_cat_id, 'granular', 'Chlorothalonil 82.5%', 'Contact fungicide for preventive disease control'),
('Iprodione 2SE', 'Chipco 26019', fungicide_cat_id, 'liquid', 'Iprodione 23.3%', 'Contact fungicide for dollar spot and brown patch'),
('Fludioxonil 1.12G', 'Medallion', fungicide_cat_id, 'granular', 'Fludioxonil 1.12%', 'Granular fungicide for pythium and other soil-borne diseases')
ON CONFLICT (name, brand) DO NOTHING;
-- Insert insecticides
INSERT INTO products (name, brand, category_id, product_type, active_ingredients, description) VALUES
('Imidacloprid 75WP', 'Merit', insecticide_cat_id, 'granular', 'Imidacloprid 75%', 'Systemic insecticide for grub control and surface insects'),
('Bifenthrin 7.9%', 'Talstar P', insecticide_cat_id, 'liquid', 'Bifenthrin 7.9%', 'Broad spectrum insecticide for surface insects and grubs'),
('Carbaryl 80WP', 'Sevin', insecticide_cat_id, 'granular', 'Carbaryl 80%', 'Contact insecticide for surface insects and caterpillars'),
('Chlorantraniliprole 0.2G', 'Acelepryn', insecticide_cat_id, 'granular', 'Chlorantraniliprole 0.2%', 'Long-lasting grub control and surface insect control'),
('Lambda-cyhalothrin 9.7%', 'Demand CS', insecticide_cat_id, 'liquid', 'Lambda-cyhalothrin 9.7%', 'Fast-acting insecticide for immediate insect control'),
('Thiamethoxam 25WG', 'Meridian', insecticide_cat_id, 'granular', 'Thiamethoxam 25%', 'Systemic insecticide for grub prevention and surface insects')
ON CONFLICT (name, brand) DO NOTHING;
END $$;
-- Insert application rates for key products
DO $$
DECLARE
product_record RECORD;
BEGIN
-- Tenacity rates
SELECT id INTO product_record FROM products WHERE name = 'Tenacity' LIMIT 1;
IF FOUND THEN
INSERT INTO product_rates (product_id, application_type, rate_amount, rate_unit, notes) VALUES
(product_record.id, 'Pre-emergent', 5, 'fl oz/acre', 'Apply before weed germination'),
(product_record.id, 'Post-emergent', 8, 'fl oz/acre', 'Apply to actively growing weeds'),
(product_record.id, 'At seeding', 4, 'fl oz/acre', 'Safe to apply at time of seeding')
ON CONFLICT DO NOTHING;
END IF;
-- Prodiamine rates
SELECT id INTO product_record FROM products WHERE name = 'Prodiamine 65WDG' LIMIT 1;
IF FOUND THEN
INSERT INTO product_rates (product_id, application_type, rate_amount, rate_unit, notes) VALUES
(product_record.id, 'Crabgrass prevention', 0.83, 'lbs/acre', 'Apply before soil temperature reaches 55°F'),
(product_record.id, 'Annual grass control', 1.1, 'lbs/acre', 'Higher rate for better control')
ON CONFLICT DO NOTHING;
END IF;
-- Milorganite rates
SELECT id INTO product_record FROM products WHERE name = 'Milorganite 6-4-0' LIMIT 1;
IF FOUND THEN
INSERT INTO product_rates (product_id, application_type, rate_amount, rate_unit, notes) VALUES
(product_record.id, 'Spring feeding', 32, 'lbs/1000 sq ft', 'Apply when grass begins active growth'),
(product_record.id, 'Summer feeding', 32, 'lbs/1000 sq ft', 'Safe for summer application'),
(product_record.id, 'Fall feeding', 32, 'lbs/1000 sq ft', 'Builds root system for winter')
ON CONFLICT DO NOTHING;
END IF;
-- 32-0-4 rates
SELECT id INTO product_record FROM products WHERE name = '32-0-4 Slow Release' LIMIT 1;
IF FOUND THEN
INSERT INTO product_rates (product_id, application_type, rate_amount, rate_unit, notes) VALUES
(product_record.id, 'Spring application', 3.125, 'lbs/1000 sq ft', '1 lb nitrogen per 1000 sq ft'),
(product_record.id, 'Summer application', 2.5, 'lbs/1000 sq ft', '0.8 lbs nitrogen per 1000 sq ft'),
(product_record.id, 'Fall application', 3.75, 'lbs/1000 sq ft', '1.2 lbs nitrogen per 1000 sq ft')
ON CONFLICT DO NOTHING;
END IF;
-- Liquid Nitrogen rates
SELECT id INTO product_record FROM products WHERE name = 'Liquid Nitrogen 28-0-0' LIMIT 1;
IF FOUND THEN
INSERT INTO product_rates (product_id, application_type, rate_amount, rate_unit, notes) VALUES
(product_record.id, 'Quick green-up', 2, 'fl oz/1000 sq ft', 'Light application for color'),
(product_record.id, 'Regular feeding', 4, 'fl oz/1000 sq ft', '0.25 lbs nitrogen per 1000 sq ft'),
(product_record.id, 'Heavy feeding', 6, 'fl oz/1000 sq ft', '0.5 lbs nitrogen per 1000 sq ft')
ON CONFLICT DO NOTHING;
END IF;
-- 2,4-D rates
SELECT id INTO product_record FROM products WHERE name = '2,4-D Amine' LIMIT 1;
IF FOUND THEN
INSERT INTO product_rates (product_id, application_type, rate_amount, rate_unit, notes) VALUES
(product_record.id, 'Broadleaf weeds', 1, 'fl oz/1000 sq ft', 'Apply to actively growing weeds'),
(product_record.id, 'Spot treatment', 2, 'fl oz/gallon', 'For hand sprayer applications')
ON CONFLICT DO NOTHING;
END IF;
-- Merit rates
SELECT id INTO product_record FROM products WHERE name = 'Imidacloprid 75WP' LIMIT 1;
IF FOUND THEN
INSERT INTO product_rates (product_id, application_type, rate_amount, rate_unit, notes) VALUES
(product_record.id, 'Grub prevention', 0.15, 'lbs/1000 sq ft', 'Apply in late spring/early summer'),
(product_record.id, 'Surface insects', 0.1, 'lbs/1000 sq ft', 'Lower rate for surface feeding insects')
ON CONFLICT DO NOTHING;
END IF;
-- Propiconazole rates
SELECT id INTO product_record FROM products WHERE name = 'Propiconazole 14.3' LIMIT 1;
IF FOUND THEN
INSERT INTO product_rates (product_id, application_type, rate_amount, rate_unit, notes) VALUES
(product_record.id, 'Brown patch', 1, 'fl oz/1000 sq ft', 'Apply at first sign of disease'),
(product_record.id, 'Dollar spot', 0.5, 'fl oz/1000 sq ft', 'Lower rate for dollar spot control'),
(product_record.id, 'Preventive', 0.75, 'fl oz/1000 sq ft', 'Apply before disease pressure')
ON CONFLICT DO NOTHING;
END IF;
END $$;
-- Create some common product combinations that users often mix
INSERT INTO products (name, brand, category_id, product_type, active_ingredients, description) VALUES
('Tank Mix: 2,4-D + Triclopyr', 'Custom', post_emergent_cat_id, 'liquid', '2,4-D + Triclopyr', 'Common tank mix for tough broadleaf and woody weeds'),
('Tank Mix: Prodiamine + Fertilizer', 'Custom', pre_emergent_cat_id, 'granular', 'Prodiamine + 19-0-6', 'Pre-emergent with slow-release fertilizer'),
('Tank Mix: Fungicide + Insecticide', 'Custom', fungicide_cat_id, 'liquid', 'Propiconazole + Bifenthrin', 'Disease and insect control combination')
ON CONFLICT (name, brand) DO NOTHING;
COMMIT;

View File

@@ -1,11 +1,471 @@
import React from 'react'; import React, { useState, useEffect } from 'react';
import {
PlusIcon,
MagnifyingGlassIcon,
FunnelIcon,
BeakerIcon,
TrashIcon,
PencilIcon
} from '@heroicons/react/24/outline';
import { productsAPI } from '../../services/api';
import LoadingSpinner from '../../components/UI/LoadingSpinner';
import toast from 'react-hot-toast';
const Products = () => { const Products = () => {
const [sharedProducts, setSharedProducts] = useState([]);
const [userProducts, setUserProducts] = useState([]);
const [categories, setCategories] = useState([]);
const [loading, setLoading] = useState(true);
const [showCreateForm, setShowCreateForm] = useState(false);
const [searchTerm, setSearchTerm] = useState('');
const [selectedCategory, setSelectedCategory] = useState('');
const [selectedType, setSelectedType] = useState('');
const [activeTab, setActiveTab] = useState('shared'); // 'shared' or 'custom'
useEffect(() => {
fetchData();
}, []);
const fetchData = async () => {
try {
setLoading(true);
const [productsResponse, categoriesResponse] = await Promise.all([
productsAPI.getAll({
category: selectedCategory,
type: selectedType,
search: searchTerm
}),
productsAPI.getCategories()
]);
setSharedProducts(productsResponse.data.data.sharedProducts || []);
setUserProducts(productsResponse.data.data.userProducts || []);
setCategories(categoriesResponse.data.data.categories || []);
} catch (error) {
console.error('Failed to fetch products:', error);
toast.error('Failed to load products');
setSharedProducts([]);
setUserProducts([]);
} finally {
setLoading(false);
}
};
const handleSearch = (e) => {
setSearchTerm(e.target.value);
// Debounce search
setTimeout(() => {
fetchData();
}, 300);
};
const handleFilterChange = () => {
fetchData();
};
const handleCreateProduct = async (productData) => {
try {
await productsAPI.createUserProduct(productData);
toast.success('Custom product created successfully!');
setShowCreateForm(false);
fetchData();
} catch (error) {
console.error('Failed to create product:', error);
toast.error('Failed to create product');
}
};
const handleDeleteUserProduct = async (productId) => {
if (window.confirm('Are you sure you want to delete this custom product?')) {
try {
await productsAPI.deleteUserProduct(productId);
toast.success('Product deleted successfully');
fetchData();
} catch (error) {
console.error('Failed to delete product:', error);
toast.error('Failed to delete product');
}
}
};
const ProductCard = ({ product, isUserProduct = false }) => (
<div className="card">
<div className="flex justify-between items-start mb-3">
<div className="flex items-start gap-3">
<div className="p-2 bg-green-100 rounded-lg">
<BeakerIcon className="h-5 w-5 text-green-600" />
</div>
<div>
<h3 className="font-semibold text-gray-900">
{isUserProduct ? product.customName || product.baseProductName : product.name}
</h3>
{product.brand && (
<p className="text-sm text-gray-600">{product.brand}</p>
)}
<span className={`inline-flex items-center px-2 py-1 rounded-full text-xs font-medium mt-1 ${
product.productType === 'liquid'
? 'bg-blue-100 text-blue-800'
: 'bg-green-100 text-green-800'
}`}>
{product.productType || 'Unknown'}
</span>
</div>
</div>
{isUserProduct && (
<div className="flex gap-1">
<button
onClick={() => {/* Handle edit */}}
className="p-1 text-gray-400 hover:text-blue-600"
>
<PencilIcon className="h-4 w-4" />
</button>
<button
onClick={() => handleDeleteUserProduct(product.id)}
className="p-1 text-gray-400 hover:text-red-600"
>
<TrashIcon className="h-4 w-4" />
</button>
</div>
)}
</div>
{product.categoryName && (
<p className="text-sm text-gray-500 mb-2">
Category: {product.categoryName}
</p>
)}
{product.activeIngredients && (
<p className="text-sm text-gray-700 mb-3">
<strong>Active Ingredients:</strong> {product.activeIngredients}
</p>
)}
{/* Application Rates */}
{isUserProduct && product.customRateAmount && (
<div className="bg-gray-50 p-3 rounded-lg mb-3">
<p className="text-sm font-medium text-gray-900">Your Rate:</p>
<p className="text-sm text-gray-700">
{product.customRateAmount} {product.customRateUnit}
</p>
</div>
)}
{!isUserProduct && product.rates && product.rates.length > 0 && (
<div className="bg-gray-50 p-3 rounded-lg mb-3">
<p className="text-sm font-medium text-gray-900 mb-2">Application Rates:</p>
{product.rates.slice(0, 2).map((rate, index) => (
<div key={index} className="text-sm text-gray-700">
<strong>{rate.applicationType}:</strong> {rate.rateAmount} {rate.rateUnit}
</div>
))}
{product.rates.length > 2 && (
<p className="text-xs text-gray-500 mt-1">
+{product.rates.length - 2} more rates
</p>
)}
</div>
)}
{product.notes && (
<p className="text-sm text-gray-600 mt-2">
<strong>Notes:</strong> {product.notes}
</p>
)}
{!isUserProduct && (
<div className="mt-3 pt-3 border-t border-gray-200">
<button
onClick={() => {/* Handle add to custom products */}}
className="text-blue-600 hover:text-blue-700 text-sm font-medium"
>
Add to My Products
</button>
</div>
)}
</div>
);
if (loading) {
return (
<div className="p-6">
<div className="flex justify-center items-center h-64">
<LoadingSpinner size="lg" />
</div>
</div>
);
}
return ( return (
<div className="p-6"> <div className="p-6">
<h1 className="text-2xl font-bold text-gray-900 mb-6">Products</h1> <div className="flex justify-between items-center mb-6">
<div className="card"> <div>
<p className="text-gray-600">Product management coming soon...</p> <h1 className="text-2xl font-bold text-gray-900">Products</h1>
<p className="text-gray-600">Manage your lawn care products and application rates</p>
</div>
<button
onClick={() => setShowCreateForm(true)}
className="btn-primary flex items-center gap-2"
>
<PlusIcon className="h-5 w-5" />
Add Custom Product
</button>
</div>
{/* Search and Filters */}
<div className="card mb-6">
<div className="flex flex-wrap gap-4">
{/* Search */}
<div className="flex-1 min-w-64">
<div className="relative">
<MagnifyingGlassIcon className="h-5 w-5 absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400" />
<input
type="text"
className="input pl-10"
placeholder="Search products..."
value={searchTerm}
onChange={handleSearch}
/>
</div>
</div>
{/* Category Filter */}
<div className="min-w-48">
<select
className="input"
value={selectedCategory}
onChange={(e) => {
setSelectedCategory(e.target.value);
handleFilterChange();
}}
>
<option value="">All Categories</option>
{categories.map((category) => (
<option key={category.id} value={category.id}>
{category.name}
</option>
))}
</select>
</div>
{/* Type Filter */}
<div className="min-w-32">
<select
className="input"
value={selectedType}
onChange={(e) => {
setSelectedType(e.target.value);
handleFilterChange();
}}
>
<option value="">All Types</option>
<option value="liquid">Liquid</option>
<option value="granular">Granular</option>
</select>
</div>
</div>
</div>
{/* Tabs */}
<div className="mb-6">
<div className="border-b border-gray-200">
<nav className="-mb-px flex">
<button
onClick={() => setActiveTab('shared')}
className={`py-2 px-4 border-b-2 font-medium text-sm ${
activeTab === 'shared'
? 'border-blue-500 text-blue-600'
: 'border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300'
}`}
>
Shared Products ({sharedProducts.length})
</button>
<button
onClick={() => setActiveTab('custom')}
className={`py-2 px-4 border-b-2 font-medium text-sm ${
activeTab === 'custom'
? 'border-blue-500 text-blue-600'
: 'border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300'
}`}
>
My Custom Products ({userProducts.length})
</button>
</nav>
</div>
</div>
{/* Products Grid */}
{activeTab === 'shared' ? (
sharedProducts.length === 0 ? (
<div className="card text-center py-12">
<BeakerIcon className="h-16 w-16 text-gray-300 mx-auto mb-4" />
<h3 className="text-lg font-medium text-gray-900 mb-2">No Products Found</h3>
<p className="text-gray-600">Try adjusting your search or filters</p>
</div>
) : (
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
{sharedProducts.map((product) => (
<ProductCard key={product.id} product={product} />
))}
</div>
)
) : (
userProducts.length === 0 ? (
<div className="card text-center py-12">
<BeakerIcon className="h-16 w-16 text-gray-300 mx-auto mb-4" />
<h3 className="text-lg font-medium text-gray-900 mb-2">No Custom Products Yet</h3>
<p className="text-gray-600 mb-6">Create your own products with custom rates and notes</p>
<button
onClick={() => setShowCreateForm(true)}
className="btn-primary"
>
Add Your First Product
</button>
</div>
) : (
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
{userProducts.map((product) => (
<ProductCard key={product.id} product={product} isUserProduct={true} />
))}
</div>
)
)}
{/* Create Product Form Modal */}
{showCreateForm && (
<CreateProductModal
onSubmit={handleCreateProduct}
onCancel={() => setShowCreateForm(false)}
sharedProducts={sharedProducts}
categories={categories}
/>
)}
</div>
);
};
// Create Product Modal Component
const CreateProductModal = ({ onSubmit, onCancel, sharedProducts, categories }) => {
const [formData, setFormData] = useState({
productId: '',
customName: '',
customRateAmount: '',
customRateUnit: 'lbs/1000 sq ft',
notes: ''
});
const handleSubmit = (e) => {
e.preventDefault();
if (!formData.productId && !formData.customName) {
toast.error('Please select a base product or enter a custom name');
return;
}
const submitData = {
productId: 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 (
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
<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">Add Custom Product</h3>
<form onSubmit={handleSubmit} className="space-y-4">
<div>
<label className="label">Base Product (Optional)</label>
<select
className="input"
value={formData.productId}
onChange={(e) => setFormData({ ...formData, productId: e.target.value })}
>
<option value="">Select a base product...</option>
{sharedProducts.map((product) => (
<option key={product.id} value={product.id}>
{product.name} {product.brand && `- ${product.brand}`}
</option>
))}
</select>
<p className="text-xs text-gray-500 mt-1">
Choose from our database or create a completely custom product below
</p>
</div>
<div>
<label className="label">Custom Name</label>
<input
type="text"
className="input"
value={formData.customName}
onChange={(e) => setFormData({ ...formData, customName: e.target.value })}
placeholder="e.g., My 24-0-11 Blend, Custom Herbicide Mix"
/>
<p className="text-xs text-gray-500 mt-1">
Required if no base product selected. Used to identify this product.
</p>
</div>
<div className="grid grid-cols-2 gap-4">
<div>
<label className="label">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">
<button type="submit" className="btn-primary flex-1">
Create Product
</button>
<button
type="button"
onClick={onCancel}
className="btn-secondary flex-1"
>
Cancel
</button>
</div>
</form>
</div> </div>
</div> </div>
); );