admin
This commit is contained in:
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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'),
|
||||
|
||||
Reference in New Issue
Block a user