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

@@ -5,7 +5,8 @@ import {
PlusIcon,
PencilIcon,
TrashIcon,
ExclamationTriangleIcon
ExclamationTriangleIcon,
ArrowUpIcon
} from '@heroicons/react/24/outline';
import { adminAPI, equipmentAPI } from '../../services/api';
import LoadingSpinner from '../../components/UI/LoadingSpinner';
@@ -13,11 +14,13 @@ import toast from 'react-hot-toast';
const AdminEquipment = () => {
const [equipment, setEquipment] = useState([]);
const [userEquipment, setUserEquipment] = useState([]);
const [equipmentTypes, setEquipmentTypes] = useState([]);
const [loading, setLoading] = useState(true);
const [searchTerm, setSearchTerm] = useState('');
const [typeFilter, setTypeFilter] = useState('all');
const [categoryFilter, setCategoryFilter] = useState('all');
const [equipmentTypeFilter, setEquipmentTypeFilter] = useState('all');
const [selectedEquipment, setSelectedEquipment] = useState(null);
const [showCreateModal, setShowCreateModal] = useState(false);
const [showEditModal, setShowEditModal] = useState(false);
@@ -39,12 +42,14 @@ const AdminEquipment = () => {
const fetchData = async () => {
try {
setLoading(true);
const [equipmentResponse, typesResponse] = await Promise.all([
const [equipmentResponse, userEquipmentResponse, typesResponse] = await Promise.all([
equipmentAPI.getAll(),
adminAPI.getAllUserEquipment(),
equipmentAPI.getTypes()
]);
setEquipment(equipmentResponse.data.data.equipment || []);
setUserEquipment(userEquipmentResponse.data.data.userEquipment || []);
setEquipmentTypes(typesResponse.data.data.equipmentTypes || []);
} catch (error) {
console.error('Failed to fetch equipment:', error);
@@ -95,6 +100,17 @@ const AdminEquipment = () => {
}
};
const handlePromoteToShared = async (userEquipment) => {
try {
await adminAPI.promoteUserEquipment(userEquipment.id);
toast.success(`"${userEquipment.customName}" promoted to shared equipment type`);
fetchData(); // Refresh the data
} catch (error) {
console.error('Failed to promote equipment:', error);
toast.error('Failed to promote equipment to shared');
}
};
const resetForm = () => {
setFormData({
customName: '',
@@ -122,8 +138,14 @@ const AdminEquipment = () => {
setShowEditModal(true);
};
// Combine shared equipment (converted to user equipment format) and user equipment
const allEquipment = [
...(equipmentTypeFilter === 'custom' ? [] : equipment.map(e => ({ ...e, isShared: true, userName: 'System', userEmail: 'system@turftracker.com' }))),
...(equipmentTypeFilter === 'shared' ? [] : userEquipment.map(e => ({ ...e, isShared: false })))
];
// Filter equipment based on filters
const filteredEquipment = equipment.filter(equip => {
const filteredEquipment = allEquipment.filter(equip => {
const matchesSearch = !searchTerm ||
(equip.customName && equip.customName.toLowerCase().includes(searchTerm.toLowerCase())) ||
(equip.manufacturer && equip.manufacturer.toLowerCase().includes(searchTerm.toLowerCase())) ||
@@ -280,7 +302,7 @@ const AdminEquipment = () => {
</div>
{/* Search and Filters */}
<div className="grid grid-cols-1 md:grid-cols-3 gap-4 mb-6">
<div className="grid grid-cols-1 md:grid-cols-4 gap-4 mb-6">
<div className="relative">
<MagnifyingGlassIcon className="h-5 w-5 absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400" />
<input
@@ -313,6 +335,15 @@ const AdminEquipment = () => {
<option value="aerator">Aerators</option>
<option value="seeder">Seeders</option>
</select>
<select
value={equipmentTypeFilter}
onChange={(e) => setEquipmentTypeFilter(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 Equipment</option>
<option value="shared">Shared Only</option>
<option value="custom">Custom Only</option>
</select>
</div>
</div>
@@ -372,11 +403,26 @@ const AdminEquipment = () => {
</div>
</td>
<td className="px-6 py-4 whitespace-nowrap">
<div className="text-sm text-gray-900">{equip.userFirstName} {equip.userLastName}</div>
<div className="text-sm text-gray-500">{equip.userEmail}</div>
{equip.isShared ? (
<span className="text-gray-500">System</span>
) : (
<div>
<div className="text-sm font-medium text-gray-900">{equip.userName}</div>
<div className="text-sm text-gray-500">{equip.userEmail}</div>
</div>
)}
</td>
<td className="px-6 py-4 whitespace-nowrap text-sm font-medium">
<div className="flex items-center space-x-2">
{!equip.isShared && (
<button
onClick={() => handlePromoteToShared(equip)}
className="text-green-600 hover:text-green-900"
title="Promote to Shared Equipment Type"
>
<ArrowUpIcon className="h-4 w-4" />
</button>
)}
<button
onClick={() => openEditModal(equip)}
className="text-indigo-600 hover:text-indigo-900"

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"

View File

@@ -209,9 +209,15 @@ export const adminAPI = {
// Products management
getProducts: (params) => apiClient.get('/admin/products', { params }),
getAllUserProducts: (params) => apiClient.get('/admin/products/user', { params }),
createProduct: (productData) => apiClient.post('/admin/products', productData),
updateProduct: (id, productData) => apiClient.put(`/admin/products/${id}`, productData),
deleteProduct: (id) => apiClient.delete(`/admin/products/${id}`),
promoteUserProduct: (id) => apiClient.post(`/admin/products/user/${id}/promote`),
// Equipment management
getAllUserEquipment: (params) => apiClient.get('/admin/equipment/user', { params }),
promoteUserEquipment: (id) => apiClient.post(`/admin/equipment/user/${id}/promote`),
// System health
getSystemHealth: () => apiClient.get('/admin/system/health'),