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

@@ -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">
<div className="flex justify-center items-center h-64">
<LoadingSpinner size="lg" />
</div>
</div>
);
}
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-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>
);