This commit is contained in:
Jake Kasper
2025-08-29 08:28:30 -04:00
parent 72670e0386
commit 822c200d86
4 changed files with 429 additions and 14 deletions

View File

@@ -6,7 +6,8 @@ import {
PencilIcon,
TrashIcon,
ExclamationTriangleIcon,
TagIcon
TagIcon,
ArrowUpIcon
} from '@heroicons/react/24/outline';
import { adminAPI, productsAPI } from '../../services/api';
import LoadingSpinner from '../../components/UI/LoadingSpinner';
@@ -20,6 +21,7 @@ const AdminProducts = () => {
const [searchTerm, setSearchTerm] = useState('');
const [categoryFilter, setCategoryFilter] = useState('all');
const [typeFilter, setTypeFilter] = useState('all');
const [productTypeFilter, setProductTypeFilter] = useState('all');
const [selectedProduct, setSelectedProduct] = useState(null);
const [showCreateModal, setShowCreateModal] = useState(false);
const [showEditModal, setShowEditModal] = useState(false);
@@ -41,17 +43,21 @@ const AdminProducts = () => {
const fetchData = async () => {
try {
setLoading(true);
const [productsResponse, categoriesResponse] = await Promise.all([
const [productsResponse, userProductsResponse, categoriesResponse] = await Promise.all([
adminAPI.getProducts({
search: searchTerm,
category: categoryFilter !== 'all' ? categoryFilter : '',
type: typeFilter !== 'all' ? typeFilter : ''
}),
adminAPI.getAllUserProducts({
search: searchTerm,
category: categoryFilter !== 'all' ? categoryFilter : ''
}),
productsAPI.getCategories()
]);
setProducts(productsResponse.data.data.products || []);
setUserProducts([]); // Admin can only manage shared products for now
setUserProducts(userProductsResponse.data.data.userProducts || []);
setCategories(categoriesResponse.data.data.categories || []);
} catch (error) {
console.error('Failed to fetch products:', error);
@@ -106,6 +112,17 @@ const AdminProducts = () => {
}
};
const handlePromoteToShared = async (userProduct) => {
try {
await adminAPI.promoteUserProduct(userProduct.id);
toast.success(`"${userProduct.customName}" promoted to shared product`);
fetchData(); // Refresh the data
} catch (error) {
console.error('Failed to promote product:', error);
toast.error('Failed to promote product to shared');
}
};
const resetForm = () => {
setFormData({
name: '',
@@ -161,8 +178,11 @@ const AdminProducts = () => {
}));
};
// Only show shared products for admin management
const allProducts = products.map(p => ({ ...p, isShared: true }));
// Combine shared and user products based on filter
const allProducts = [
...(productTypeFilter === 'custom' ? [] : products.map(p => ({ ...p, isShared: true }))),
...(productTypeFilter === 'shared' ? [] : userProducts.map(p => ({ ...p, isShared: false })))
];
const ProductForm = ({ onSubmit, submitText }) => (
<form onSubmit={onSubmit} className="space-y-4">
@@ -377,6 +397,15 @@ const AdminProducts = () => {
<option value="seed">Seed</option>
<option value="powder">Powder</option>
</select>
<select
value={productTypeFilter}
onChange={(e) => setProductTypeFilter(e.target.value)}
className="px-4 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500"
>
<option value="all">All Products</option>
<option value="shared">Shared Only</option>
<option value="custom">Custom Only</option>
</select>
</div>
</div>
@@ -402,6 +431,12 @@ const AdminProducts = () => {
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Source
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Owner
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Usage
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Actions
</th>
@@ -427,13 +462,13 @@ const AdminProducts = () => {
</td>
<td className="px-6 py-4 whitespace-nowrap">
<span className={`px-2 py-1 text-xs font-medium rounded-full ${
(product.productType || product.customProductType) === 'granular'
(product.productType || product.productType) === 'granular'
? 'bg-blue-100 text-blue-800'
: (product.productType || product.customProductType) === 'liquid'
: (product.productType || product.productType) === 'liquid'
? 'bg-green-100 text-green-800'
: 'bg-gray-100 text-gray-800'
}`}>
{product.productType || product.customProductType}
{product.productType}
</span>
</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-900">
@@ -448,8 +483,40 @@ const AdminProducts = () => {
{product.isShared ? 'Shared' : 'Custom'}
</span>
</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-900">
{product.isShared ? (
<span className="text-gray-500">System</span>
) : (
<div>
<div className="font-medium">{product.userName}</div>
<div className="text-xs text-gray-500">{product.userEmail}</div>
</div>
)}
</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-900">
{product.isShared ? (
<div>
<div>{product.rateCount || 0} rates</div>
<div className="text-xs text-gray-500">{product.usageCount || 0} users</div>
</div>
) : (
<div>
<div>{product.spreaderSettingsCount || 0} spreader settings</div>
<div className="text-xs text-gray-500">{product.usageCount || 0} applications</div>
</div>
)}
</td>
<td className="px-6 py-4 whitespace-nowrap text-sm font-medium">
<div className="flex items-center space-x-2">
{!product.isShared && (
<button
onClick={() => handlePromoteToShared(product)}
className="text-green-600 hover:text-green-900"
title="Promote to Shared Product"
>
<ArrowUpIcon className="h-4 w-4" />
</button>
)}
<button
onClick={() => openEditModal(product)}
className="text-indigo-600 hover:text-indigo-900"