From e1ac44e4ef0945c348427ed3988606cf29efa165 Mon Sep 17 00:00:00 2001 From: Jake Kasper Date: Thu, 21 Aug 2025 19:48:22 -0400 Subject: [PATCH] products page and products --- backend/scripts/seed-products.sql | 176 +++++++++ frontend/src/pages/Products/Products.js | 468 +++++++++++++++++++++++- 2 files changed, 640 insertions(+), 4 deletions(-) create mode 100644 backend/scripts/seed-products.sql diff --git a/backend/scripts/seed-products.sql b/backend/scripts/seed-products.sql new file mode 100644 index 0000000..3b153b8 --- /dev/null +++ b/backend/scripts/seed-products.sql @@ -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; \ No newline at end of file diff --git a/frontend/src/pages/Products/Products.js b/frontend/src/pages/Products/Products.js index 8d6d0fc..fd5de9e 100644 --- a/frontend/src/pages/Products/Products.js +++ b/frontend/src/pages/Products/Products.js @@ -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 }) => ( +
+
+
+
+ +
+
+

+ {isUserProduct ? product.customName || product.baseProductName : product.name} +

+ {product.brand && ( +

{product.brand}

+ )} + + {product.productType || 'Unknown'} + +
+
+ + {isUserProduct && ( +
+ + +
+ )} +
+ + {product.categoryName && ( +

+ Category: {product.categoryName} +

+ )} + + {product.activeIngredients && ( +

+ Active Ingredients: {product.activeIngredients} +

+ )} + + {/* Application Rates */} + {isUserProduct && product.customRateAmount && ( +
+

Your Rate:

+

+ {product.customRateAmount} {product.customRateUnit} +

+
+ )} + + {!isUserProduct && product.rates && product.rates.length > 0 && ( +
+

Application Rates:

+ {product.rates.slice(0, 2).map((rate, index) => ( +
+ {rate.applicationType}: {rate.rateAmount} {rate.rateUnit} +
+ ))} + {product.rates.length > 2 && ( +

+ +{product.rates.length - 2} more rates +

+ )} +
+ )} + + {product.notes && ( +

+ Notes: {product.notes} +

+ )} + + {!isUserProduct && ( +
+ +
+ )} +
+ ); + + if (loading) { + return ( +
+
+ +
+
+ ); + } + return (
-

Products

-
-

Product management coming soon...

+
+
+

Products

+

Manage your lawn care products and application rates

+
+ +
+ + {/* Search and Filters */} +
+
+ {/* Search */} +
+
+ + +
+
+ + {/* Category Filter */} +
+ +
+ + {/* Type Filter */} +
+ +
+
+
+ + {/* Tabs */} +
+
+ +
+
+ + {/* Products Grid */} + {activeTab === 'shared' ? ( + sharedProducts.length === 0 ? ( +
+ +

No Products Found

+

Try adjusting your search or filters

+
+ ) : ( +
+ {sharedProducts.map((product) => ( + + ))} +
+ ) + ) : ( + userProducts.length === 0 ? ( +
+ +

No Custom Products Yet

+

Create your own products with custom rates and notes

+ +
+ ) : ( +
+ {userProducts.map((product) => ( + + ))} +
+ ) + )} + + {/* Create Product Form Modal */} + {showCreateForm && ( + setShowCreateForm(false)} + sharedProducts={sharedProducts} + categories={categories} + /> + )} +
+ ); +}; + +// 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 ( +
+
+

Add Custom Product

+ +
+
+ + +

+ Choose from our database or create a completely custom product below +

+
+ +
+ + setFormData({ ...formData, customName: e.target.value })} + placeholder="e.g., My 24-0-11 Blend, Custom Herbicide Mix" + /> +

+ Required if no base product selected. Used to identify this product. +

+
+ +
+
+ + setFormData({ ...formData, customRateAmount: e.target.value })} + placeholder="2.5" + /> +
+
+ + +
+
+ +
+ +