admin
This commit is contained in:
@@ -711,17 +711,12 @@ router.get('/equipment/user', async (req, res, next) => {
|
|||||||
|
|
||||||
const result = await pool.query(`
|
const result = await pool.query(`
|
||||||
SELECT ue.*, u.email as user_email, u.first_name, u.last_name,
|
SELECT ue.*, u.email as user_email, u.first_name, u.last_name,
|
||||||
ec.name as category_name, et.name as type_name, et.manufacturer as type_manufacturer, et.model as type_model,
|
ec.name as category_name, et.name as type_name, et.manufacturer as type_manufacturer, et.model as type_model
|
||||||
COUNT(DISTINCT nc.id) as nozzle_configurations,
|
|
||||||
COUNT(DISTINCT app.id) as usage_count
|
|
||||||
FROM user_equipment ue
|
FROM user_equipment ue
|
||||||
JOIN users u ON ue.user_id = u.id
|
JOIN users u ON ue.user_id = u.id
|
||||||
LEFT JOIN equipment_categories ec ON ue.category_id = ec.id
|
LEFT JOIN equipment_categories ec ON ue.category_id = ec.id
|
||||||
LEFT JOIN equipment_types et ON ue.equipment_type_id = et.id
|
LEFT JOIN equipment_types et ON ue.equipment_type_id = et.id
|
||||||
LEFT JOIN nozzle_configurations nc ON ue.id = nc.sprayer_id
|
|
||||||
LEFT JOIN application_plans app ON ue.id = app.equipment_id
|
|
||||||
${whereClause}
|
${whereClause}
|
||||||
GROUP BY ue.id, u.email, u.first_name, u.last_name, ec.name, et.name, et.manufacturer, et.model
|
|
||||||
ORDER BY u.email, ue.custom_name
|
ORDER BY u.email, ue.custom_name
|
||||||
`, queryParams);
|
`, queryParams);
|
||||||
|
|
||||||
@@ -756,8 +751,6 @@ router.get('/equipment/user', async (req, res, next) => {
|
|||||||
serialNumber: equipment.serial_number,
|
serialNumber: equipment.serial_number,
|
||||||
notes: equipment.notes,
|
notes: equipment.notes,
|
||||||
isActive: equipment.is_active,
|
isActive: equipment.is_active,
|
||||||
nozzleConfigurations: parseInt(equipment.nozzle_configurations),
|
|
||||||
usageCount: parseInt(equipment.usage_count),
|
|
||||||
createdAt: equipment.created_at,
|
createdAt: equipment.created_at,
|
||||||
updatedAt: equipment.updated_at
|
updatedAt: equipment.updated_at
|
||||||
}))
|
}))
|
||||||
@@ -822,4 +815,80 @@ router.post('/equipment/user/:id/promote', validateParams(idParamSchema), async
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// @route GET /api/admin/products/:id/rates
|
||||||
|
// @desc Get application rates for a specific shared product
|
||||||
|
// @access Private (Admin)
|
||||||
|
router.get('/products/:id/rates', validateParams(idParamSchema), async (req, res, next) => {
|
||||||
|
try {
|
||||||
|
const productId = req.params.id;
|
||||||
|
|
||||||
|
const result = await pool.query(`
|
||||||
|
SELECT pr.*, p.name as product_name, p.brand as product_brand
|
||||||
|
FROM product_rates pr
|
||||||
|
JOIN products p ON pr.product_id = p.id
|
||||||
|
WHERE pr.product_id = $1
|
||||||
|
ORDER BY pr.application_type, pr.rate_amount
|
||||||
|
`, [productId]);
|
||||||
|
|
||||||
|
res.json({
|
||||||
|
success: true,
|
||||||
|
data: {
|
||||||
|
rates: result.rows.map(rate => ({
|
||||||
|
id: rate.id,
|
||||||
|
productId: rate.product_id,
|
||||||
|
productName: rate.product_name,
|
||||||
|
productBrand: rate.product_brand,
|
||||||
|
applicationType: rate.application_type,
|
||||||
|
rateAmount: parseFloat(rate.rate_amount),
|
||||||
|
rateUnit: rate.rate_unit,
|
||||||
|
notes: rate.notes,
|
||||||
|
createdAt: rate.created_at
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
next(error);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// @route GET /api/admin/products/user/:id/spreader-settings
|
||||||
|
// @desc Get spreader settings for a specific user product
|
||||||
|
// @access Private (Admin)
|
||||||
|
router.get('/products/user/:id/spreader-settings', validateParams(idParamSchema), async (req, res, next) => {
|
||||||
|
try {
|
||||||
|
const userProductId = req.params.id;
|
||||||
|
|
||||||
|
const result = await pool.query(`
|
||||||
|
SELECT pss.*, ue.custom_name as equipment_name, ss.brand_name, ss.model, ss.setting_number
|
||||||
|
FROM product_spreader_settings pss
|
||||||
|
JOIN user_equipment ue ON pss.user_equipment_id = ue.id
|
||||||
|
LEFT JOIN spreader_settings ss ON pss.spreader_setting_id = ss.id
|
||||||
|
WHERE pss.user_product_id = $1
|
||||||
|
ORDER BY ue.custom_name, ss.brand_name
|
||||||
|
`, [userProductId]);
|
||||||
|
|
||||||
|
res.json({
|
||||||
|
success: true,
|
||||||
|
data: {
|
||||||
|
spreaderSettings: result.rows.map(setting => ({
|
||||||
|
id: setting.id,
|
||||||
|
userProductId: setting.user_product_id,
|
||||||
|
userEquipmentId: setting.user_equipment_id,
|
||||||
|
equipmentName: setting.equipment_name,
|
||||||
|
spreaderSettingId: setting.spreader_setting_id,
|
||||||
|
brandName: setting.brand_name,
|
||||||
|
model: setting.model,
|
||||||
|
settingNumber: setting.setting_number,
|
||||||
|
customRate: setting.custom_rate ? parseFloat(setting.custom_rate) : null,
|
||||||
|
customRateUnit: setting.custom_rate_unit,
|
||||||
|
notes: setting.notes,
|
||||||
|
createdAt: setting.created_at
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
next(error);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
module.exports = router;
|
module.exports = router;
|
||||||
@@ -7,7 +7,8 @@ import {
|
|||||||
TrashIcon,
|
TrashIcon,
|
||||||
ExclamationTriangleIcon,
|
ExclamationTriangleIcon,
|
||||||
TagIcon,
|
TagIcon,
|
||||||
ArrowUpIcon
|
ArrowUpIcon,
|
||||||
|
InformationCircleIcon
|
||||||
} from '@heroicons/react/24/outline';
|
} from '@heroicons/react/24/outline';
|
||||||
import { adminAPI, productsAPI } from '../../services/api';
|
import { adminAPI, productsAPI } from '../../services/api';
|
||||||
import LoadingSpinner from '../../components/UI/LoadingSpinner';
|
import LoadingSpinner from '../../components/UI/LoadingSpinner';
|
||||||
@@ -26,6 +27,8 @@ const AdminProducts = () => {
|
|||||||
const [showCreateModal, setShowCreateModal] = useState(false);
|
const [showCreateModal, setShowCreateModal] = useState(false);
|
||||||
const [showEditModal, setShowEditModal] = useState(false);
|
const [showEditModal, setShowEditModal] = useState(false);
|
||||||
const [showDeleteModal, setShowDeleteModal] = useState(false);
|
const [showDeleteModal, setShowDeleteModal] = useState(false);
|
||||||
|
const [showDetailsModal, setShowDetailsModal] = useState(false);
|
||||||
|
const [productDetails, setProductDetails] = useState({ rates: [], spreaderSettings: [] });
|
||||||
const [formData, setFormData] = useState({
|
const [formData, setFormData] = useState({
|
||||||
name: '',
|
name: '',
|
||||||
brand: '',
|
brand: '',
|
||||||
@@ -123,6 +126,30 @@ const AdminProducts = () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const showProductDetails = async (product) => {
|
||||||
|
try {
|
||||||
|
setSelectedProduct(product);
|
||||||
|
let rates = [];
|
||||||
|
let spreaderSettings = [];
|
||||||
|
|
||||||
|
if (product.isShared) {
|
||||||
|
// Fetch application rates for shared products
|
||||||
|
const ratesResponse = await adminAPI.getProductRates(product.id);
|
||||||
|
rates = ratesResponse.data.data.rates || [];
|
||||||
|
} else {
|
||||||
|
// Fetch spreader settings for user products
|
||||||
|
const settingsResponse = await adminAPI.getUserProductSpreaderSettings(product.id);
|
||||||
|
spreaderSettings = settingsResponse.data.data.spreaderSettings || [];
|
||||||
|
}
|
||||||
|
|
||||||
|
setProductDetails({ rates, spreaderSettings });
|
||||||
|
setShowDetailsModal(true);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to fetch product details:', error);
|
||||||
|
toast.error('Failed to load product details');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const resetForm = () => {
|
const resetForm = () => {
|
||||||
setFormData({
|
setFormData({
|
||||||
name: '',
|
name: '',
|
||||||
@@ -508,6 +535,13 @@ const AdminProducts = () => {
|
|||||||
</td>
|
</td>
|
||||||
<td className="px-6 py-4 whitespace-nowrap text-sm font-medium">
|
<td className="px-6 py-4 whitespace-nowrap text-sm font-medium">
|
||||||
<div className="flex items-center space-x-2">
|
<div className="flex items-center space-x-2">
|
||||||
|
<button
|
||||||
|
onClick={() => showProductDetails(product)}
|
||||||
|
className="text-blue-600 hover:text-blue-900"
|
||||||
|
title="View Rates & Settings"
|
||||||
|
>
|
||||||
|
<InformationCircleIcon className="h-4 w-4" />
|
||||||
|
</button>
|
||||||
{!product.isShared && (
|
{!product.isShared && (
|
||||||
<button
|
<button
|
||||||
onClick={() => handlePromoteToShared(product)}
|
onClick={() => handlePromoteToShared(product)}
|
||||||
@@ -605,6 +639,114 @@ const AdminProducts = () => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{/* Product Details Modal */}
|
||||||
|
{showDetailsModal && selectedProduct && (
|
||||||
|
<div className="fixed inset-0 bg-gray-600 bg-opacity-50 overflow-y-auto h-full w-full z-50">
|
||||||
|
<div className="relative top-10 mx-auto p-5 border w-full max-w-4xl shadow-lg rounded-md bg-white">
|
||||||
|
<div className="mt-3">
|
||||||
|
<h3 className="text-lg font-medium text-gray-900 mb-4">
|
||||||
|
{selectedProduct.isShared ? 'Application Rates' : 'Spreader Settings'} - {selectedProduct.name || selectedProduct.customName}
|
||||||
|
</h3>
|
||||||
|
|
||||||
|
{selectedProduct.isShared ? (
|
||||||
|
<div>
|
||||||
|
<h4 className="text-md font-semibold text-gray-800 mb-3">Application Rates ({productDetails.rates.length})</h4>
|
||||||
|
{productDetails.rates.length > 0 ? (
|
||||||
|
<div className="overflow-x-auto">
|
||||||
|
<table className="min-w-full divide-y divide-gray-200">
|
||||||
|
<thead className="bg-gray-50">
|
||||||
|
<tr>
|
||||||
|
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Type</th>
|
||||||
|
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Rate</th>
|
||||||
|
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Unit</th>
|
||||||
|
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Notes</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody className="bg-white divide-y divide-gray-200">
|
||||||
|
{productDetails.rates.map((rate, index) => (
|
||||||
|
<tr key={index} className="hover:bg-gray-50">
|
||||||
|
<td className="px-6 py-4 whitespace-nowrap text-sm font-medium text-gray-900">
|
||||||
|
{rate.applicationType}
|
||||||
|
</td>
|
||||||
|
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-900">
|
||||||
|
{rate.rateAmount}
|
||||||
|
</td>
|
||||||
|
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-900">
|
||||||
|
{rate.rateUnit}
|
||||||
|
</td>
|
||||||
|
<td className="px-6 py-4 text-sm text-gray-900">
|
||||||
|
{rate.notes || 'No notes'}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<p className="text-gray-500 text-center py-8">No application rates defined for this product.</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div>
|
||||||
|
<h4 className="text-md font-semibold text-gray-800 mb-3">Spreader Settings ({productDetails.spreaderSettings.length})</h4>
|
||||||
|
{productDetails.spreaderSettings.length > 0 ? (
|
||||||
|
<div className="overflow-x-auto">
|
||||||
|
<table className="min-w-full divide-y divide-gray-200">
|
||||||
|
<thead className="bg-gray-50">
|
||||||
|
<tr>
|
||||||
|
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Equipment</th>
|
||||||
|
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Spreader</th>
|
||||||
|
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Setting</th>
|
||||||
|
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Custom Rate</th>
|
||||||
|
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Notes</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody className="bg-white divide-y divide-gray-200">
|
||||||
|
{productDetails.spreaderSettings.map((setting, index) => (
|
||||||
|
<tr key={index} className="hover:bg-gray-50">
|
||||||
|
<td className="px-6 py-4 whitespace-nowrap text-sm font-medium text-gray-900">
|
||||||
|
{setting.equipmentName}
|
||||||
|
</td>
|
||||||
|
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-900">
|
||||||
|
{setting.brandName} {setting.model}
|
||||||
|
</td>
|
||||||
|
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-900">
|
||||||
|
{setting.settingNumber || 'N/A'}
|
||||||
|
</td>
|
||||||
|
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-900">
|
||||||
|
{setting.customRate ? `${setting.customRate} ${setting.customRateUnit}` : 'Standard'}
|
||||||
|
</td>
|
||||||
|
<td className="px-6 py-4 text-sm text-gray-900">
|
||||||
|
{setting.notes || 'No notes'}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<p className="text-gray-500 text-center py-8">No spreader settings configured for this product.</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className="flex justify-end mt-6">
|
||||||
|
<button
|
||||||
|
onClick={() => {
|
||||||
|
setShowDetailsModal(false);
|
||||||
|
setSelectedProduct(null);
|
||||||
|
setProductDetails({ rates: [], spreaderSettings: [] });
|
||||||
|
}}
|
||||||
|
className="px-4 py-2 text-sm text-gray-700 bg-gray-200 rounded hover:bg-gray-300"
|
||||||
|
>
|
||||||
|
Close
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -214,6 +214,8 @@ export const adminAPI = {
|
|||||||
updateProduct: (id, productData) => apiClient.put(`/admin/products/${id}`, productData),
|
updateProduct: (id, productData) => apiClient.put(`/admin/products/${id}`, productData),
|
||||||
deleteProduct: (id) => apiClient.delete(`/admin/products/${id}`),
|
deleteProduct: (id) => apiClient.delete(`/admin/products/${id}`),
|
||||||
promoteUserProduct: (id) => apiClient.post(`/admin/products/user/${id}/promote`),
|
promoteUserProduct: (id) => apiClient.post(`/admin/products/user/${id}/promote`),
|
||||||
|
getProductRates: (id) => apiClient.get(`/admin/products/${id}/rates`),
|
||||||
|
getUserProductSpreaderSettings: (id) => apiClient.get(`/admin/products/user/${id}/spreader-settings`),
|
||||||
|
|
||||||
// Equipment management
|
// Equipment management
|
||||||
getAllUserEquipment: (params) => apiClient.get('/admin/equipment/user', { params }),
|
getAllUserEquipment: (params) => apiClient.get('/admin/equipment/user', { params }),
|
||||||
|
|||||||
Reference in New Issue
Block a user