From e5275398eae3fe8fb0a9a7286b8fb35081910506 Mon Sep 17 00:00:00 2001 From: Jake Kasper Date: Fri, 29 Aug 2025 08:41:43 -0400 Subject: [PATCH] admin --- backend/src/routes/admin.js | 98 ++++++++++-- frontend/src/pages/Admin/AdminProducts.js | 173 +++++++++++++++++++++- frontend/src/services/api.js | 2 + 3 files changed, 256 insertions(+), 17 deletions(-) diff --git a/backend/src/routes/admin.js b/backend/src/routes/admin.js index 2bb9140..596c6bc 100644 --- a/backend/src/routes/admin.js +++ b/backend/src/routes/admin.js @@ -859,12 +859,11 @@ router.get('/products/user/:id/spreader-settings', validateParams(idParamSchema) 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 + SELECT pss.*, ue.custom_name as equipment_name, ue.manufacturer, ue.model as equipment_model 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 + LEFT JOIN user_equipment ue ON pss.equipment_id = ue.id WHERE pss.user_product_id = $1 - ORDER BY ue.custom_name, ss.brand_name + ORDER BY ue.custom_name NULLS LAST, pss.spreader_brand, pss.spreader_model NULLS LAST, pss.setting_value `, [userProductId]); res.json({ @@ -873,14 +872,14 @@ router.get('/products/user/:id/spreader-settings', validateParams(idParamSchema) spreaderSettings: result.rows.map(setting => ({ id: setting.id, userProductId: setting.user_product_id, - userEquipmentId: setting.user_equipment_id, + equipmentId: setting.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, + equipmentManufacturer: setting.manufacturer, + equipmentModel: setting.equipment_model, + spreaderBrand: setting.spreader_brand, + spreaderModel: setting.spreader_model, + settingValue: setting.setting_value, + rateDescription: setting.rate_description, notes: setting.notes, createdAt: setting.created_at })) @@ -891,4 +890,81 @@ router.get('/products/user/:id/spreader-settings', validateParams(idParamSchema) } }); +// @route POST /api/admin/products/user/:id/spreader-settings +// @desc Add spreader setting for a user product +// @access Private (Admin) +router.post('/products/user/:id/spreader-settings', validateParams(idParamSchema), async (req, res, next) => { + try { + const userProductId = req.params.id; + const { equipmentId, spreaderBrand, spreaderModel, settingValue, rateDescription, notes } = req.body; + + // Verify the user product exists + const productCheck = await pool.query( + 'SELECT id FROM user_products WHERE id = $1', + [userProductId] + ); + + if (productCheck.rows.length === 0) { + throw new AppError('User product not found', 404); + } + + const result = await pool.query(` + INSERT INTO product_spreader_settings + (user_product_id, equipment_id, spreader_brand, spreader_model, setting_value, rate_description, notes) + VALUES ($1, $2, $3, $4, $5, $6, $7) + RETURNING * + `, [userProductId, equipmentId, spreaderBrand, spreaderModel, settingValue, rateDescription, notes]); + + const setting = result.rows[0]; + + res.status(201).json({ + success: true, + message: 'Spreader setting added successfully', + data: { + spreaderSetting: { + id: setting.id, + userProductId: setting.user_product_id, + equipmentId: setting.equipment_id, + spreaderBrand: setting.spreader_brand, + spreaderModel: setting.spreader_model, + settingValue: setting.setting_value, + rateDescription: setting.rate_description, + notes: setting.notes, + createdAt: setting.created_at + } + } + }); + } catch (error) { + next(error); + } +}); + +// @route DELETE /api/admin/products/user/spreader-settings/:id +// @desc Delete spreader setting +// @access Private (Admin) +router.delete('/products/user/spreader-settings/:id', validateParams(idParamSchema), async (req, res, next) => { + try { + const settingId = req.params.id; + + // Check if setting exists + const settingCheck = await pool.query( + 'SELECT id FROM product_spreader_settings WHERE id = $1', + [settingId] + ); + + if (settingCheck.rows.length === 0) { + throw new AppError('Spreader setting not found', 404); + } + + await pool.query('DELETE FROM product_spreader_settings WHERE id = $1', [settingId]); + + res.json({ + success: true, + message: 'Spreader setting deleted successfully' + }); + } 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 e8d4f16..c899070 100644 --- a/frontend/src/pages/Admin/AdminProducts.js +++ b/frontend/src/pages/Admin/AdminProducts.js @@ -29,6 +29,15 @@ const AdminProducts = () => { const [showDeleteModal, setShowDeleteModal] = useState(false); const [showDetailsModal, setShowDetailsModal] = useState(false); const [productDetails, setProductDetails] = useState({ rates: [], spreaderSettings: [] }); + const [showAddSettingForm, setShowAddSettingForm] = useState(false); + const [newSettingData, setNewSettingData] = useState({ + equipmentId: '', + spreaderBrand: '', + spreaderModel: '', + settingValue: '', + rateDescription: '', + notes: '' + }); const [formData, setFormData] = useState({ name: '', brand: '', @@ -150,6 +159,40 @@ const AdminProducts = () => { } }; + const handleAddSpreaderSetting = async (e) => { + e.preventDefault(); + try { + await adminAPI.addUserProductSpreaderSetting(selectedProduct.id, newSettingData); + toast.success('Spreader setting added successfully'); + setShowAddSettingForm(false); + setNewSettingData({ + equipmentId: '', + spreaderBrand: '', + spreaderModel: '', + settingValue: '', + rateDescription: '', + notes: '' + }); + // Refresh the settings + showProductDetails(selectedProduct); + } catch (error) { + console.error('Failed to add spreader setting:', error); + toast.error('Failed to add spreader setting'); + } + }; + + const handleDeleteSpreaderSetting = async (settingId) => { + try { + await adminAPI.deleteUserProductSpreaderSetting(settingId); + toast.success('Spreader setting deleted successfully'); + // Refresh the settings + showProductDetails(selectedProduct); + } catch (error) { + console.error('Failed to delete spreader setting:', error); + toast.error('Failed to delete spreader setting'); + } + }; + const resetForm = () => { setFormData({ name: '', @@ -697,29 +740,39 @@ const AdminProducts = () => { Equipment Spreader - Setting - Custom Rate + Setting Value + Rate Description Notes + Actions {productDetails.spreaderSettings.map((setting, index) => ( - {setting.equipmentName} + {setting.equipmentName || 'Generic Equipment'} - {setting.brandName} {setting.model} + {setting.spreaderBrand} {setting.spreaderModel} - {setting.settingNumber || 'N/A'} + {setting.settingValue || 'N/A'} - {setting.customRate ? `${setting.customRate} ${setting.customRateUnit}` : 'Standard'} + {setting.rateDescription || 'Not specified'} {setting.notes || 'No notes'} + + + ))} @@ -728,6 +781,105 @@ const AdminProducts = () => { ) : (

No spreader settings configured for this product.

)} + + {/* Add Setting Button */} +
+ {!showAddSettingForm ? ( + + ) : ( +
+
Add New Spreader Setting
+
+
+
+ + setNewSettingData({ ...newSettingData, spreaderBrand: e.target.value })} + className="w-full px-2 py-1 text-sm border border-gray-300 rounded focus:outline-none focus:ring-1 focus:ring-blue-500" + placeholder="e.g. Scotts, TruGreen" + /> +
+
+ + setNewSettingData({ ...newSettingData, spreaderModel: e.target.value })} + className="w-full px-2 py-1 text-sm border border-gray-300 rounded focus:outline-none focus:ring-1 focus:ring-blue-500" + placeholder="e.g. EdgeGuard, DLX" + /> +
+
+
+
+ + setNewSettingData({ ...newSettingData, settingValue: e.target.value })} + className="w-full px-2 py-1 text-sm border border-gray-300 rounded focus:outline-none focus:ring-1 focus:ring-blue-500" + placeholder="e.g. 3.5, M, Large" + /> +
+
+ + setNewSettingData({ ...newSettingData, rateDescription: e.target.value })} + className="w-full px-2 py-1 text-sm border border-gray-300 rounded focus:outline-none focus:ring-1 focus:ring-blue-500" + placeholder="e.g. 2.5 lbs/1000 sq ft" + /> +
+
+
+ +