products page and products
This commit is contained in:
176
backend/scripts/seed-products.sql
Normal file
176
backend/scripts/seed-products.sql
Normal 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;
|
||||
@@ -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 [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">
|
||||
<h1 className="text-2xl font-bold text-gray-900 mb-6">Products</h1>
|
||||
<div className="card">
|
||||
<p className="text-gray-600">Product management coming soon...</p>
|
||||
<div className="flex justify-center items-center h-64">
|
||||
<LoadingSpinner size="lg" />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="p-6">
|
||||
<div className="flex justify-between items-center mb-6">
|
||||
<div>
|
||||
<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>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user