From cb160f21cc05f3c07d822894c997bf11037a0a3b Mon Sep 17 00:00:00 2001 From: Jake Kasper Date: Fri, 29 Aug 2025 08:34:25 -0400 Subject: [PATCH] admin --- backend/src/routes/admin.js | 85 +++++++++++-- frontend/src/pages/Admin/AdminProducts.js | 144 +++++++++++++++++++++- frontend/src/services/api.js | 2 + 3 files changed, 222 insertions(+), 9 deletions(-) diff --git a/backend/src/routes/admin.js b/backend/src/routes/admin.js index 682d364..2bb9140 100644 --- a/backend/src/routes/admin.js +++ b/backend/src/routes/admin.js @@ -711,17 +711,12 @@ router.get('/equipment/user', async (req, res, next) => { const result = await pool.query(` 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, - COUNT(DISTINCT nc.id) as nozzle_configurations, - COUNT(DISTINCT app.id) as usage_count + ec.name as category_name, et.name as type_name, et.manufacturer as type_manufacturer, et.model as type_model FROM user_equipment ue JOIN users u ON ue.user_id = u.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 nozzle_configurations nc ON ue.id = nc.sprayer_id - LEFT JOIN application_plans app ON ue.id = app.equipment_id ${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 `, queryParams); @@ -756,8 +751,6 @@ router.get('/equipment/user', async (req, res, next) => { serialNumber: equipment.serial_number, notes: equipment.notes, isActive: equipment.is_active, - nozzleConfigurations: parseInt(equipment.nozzle_configurations), - usageCount: parseInt(equipment.usage_count), createdAt: equipment.created_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; \ No newline at end of file diff --git a/frontend/src/pages/Admin/AdminProducts.js b/frontend/src/pages/Admin/AdminProducts.js index 43ec220..e8d4f16 100644 --- a/frontend/src/pages/Admin/AdminProducts.js +++ b/frontend/src/pages/Admin/AdminProducts.js @@ -7,7 +7,8 @@ import { TrashIcon, ExclamationTriangleIcon, TagIcon, - ArrowUpIcon + ArrowUpIcon, + InformationCircleIcon } from '@heroicons/react/24/outline'; import { adminAPI, productsAPI } from '../../services/api'; import LoadingSpinner from '../../components/UI/LoadingSpinner'; @@ -26,6 +27,8 @@ const AdminProducts = () => { const [showCreateModal, setShowCreateModal] = useState(false); const [showEditModal, setShowEditModal] = useState(false); const [showDeleteModal, setShowDeleteModal] = useState(false); + const [showDetailsModal, setShowDetailsModal] = useState(false); + const [productDetails, setProductDetails] = useState({ rates: [], spreaderSettings: [] }); const [formData, setFormData] = useState({ name: '', 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 = () => { setFormData({ name: '', @@ -508,6 +535,13 @@ const AdminProducts = () => {
+ {!product.isShared && (
)} + + {/* Product Details Modal */} + {showDetailsModal && selectedProduct && ( +
+
+
+

+ {selectedProduct.isShared ? 'Application Rates' : 'Spreader Settings'} - {selectedProduct.name || selectedProduct.customName} +

+ + {selectedProduct.isShared ? ( +
+

Application Rates ({productDetails.rates.length})

+ {productDetails.rates.length > 0 ? ( +
+ + + + + + + + + + + {productDetails.rates.map((rate, index) => ( + + + + + + + ))} + +
TypeRateUnitNotes
+ {rate.applicationType} + + {rate.rateAmount} + + {rate.rateUnit} + + {rate.notes || 'No notes'} +
+
+ ) : ( +

No application rates defined for this product.

+ )} +
+ ) : ( +
+

Spreader Settings ({productDetails.spreaderSettings.length})

+ {productDetails.spreaderSettings.length > 0 ? ( +
+ + + + + + + + + + + + {productDetails.spreaderSettings.map((setting, index) => ( + + + + + + + + ))} + +
EquipmentSpreaderSettingCustom RateNotes
+ {setting.equipmentName} + + {setting.brandName} {setting.model} + + {setting.settingNumber || 'N/A'} + + {setting.customRate ? `${setting.customRate} ${setting.customRateUnit}` : 'Standard'} + + {setting.notes || 'No notes'} +
+
+ ) : ( +

No spreader settings configured for this product.

+ )} +
+ )} + +
+ +
+
+
+
+ )} ); }; diff --git a/frontend/src/services/api.js b/frontend/src/services/api.js index 7da4c1b..2e644a7 100644 --- a/frontend/src/services/api.js +++ b/frontend/src/services/api.js @@ -214,6 +214,8 @@ export const adminAPI = { 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`), + getProductRates: (id) => apiClient.get(`/admin/products/${id}/rates`), + getUserProductSpreaderSettings: (id) => apiClient.get(`/admin/products/user/${id}/spreader-settings`), // Equipment management getAllUserEquipment: (params) => apiClient.get('/admin/equipment/user', { params }),