admin
This commit is contained in:
@@ -859,12 +859,11 @@ router.get('/products/user/:id/spreader-settings', validateParams(idParamSchema)
|
|||||||
const userProductId = req.params.id;
|
const userProductId = req.params.id;
|
||||||
|
|
||||||
const result = await pool.query(`
|
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
|
FROM product_spreader_settings pss
|
||||||
JOIN user_equipment ue ON pss.user_equipment_id = ue.id
|
LEFT JOIN user_equipment ue ON pss.equipment_id = ue.id
|
||||||
LEFT JOIN spreader_settings ss ON pss.spreader_setting_id = ss.id
|
|
||||||
WHERE pss.user_product_id = $1
|
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]);
|
`, [userProductId]);
|
||||||
|
|
||||||
res.json({
|
res.json({
|
||||||
@@ -873,14 +872,14 @@ router.get('/products/user/:id/spreader-settings', validateParams(idParamSchema)
|
|||||||
spreaderSettings: result.rows.map(setting => ({
|
spreaderSettings: result.rows.map(setting => ({
|
||||||
id: setting.id,
|
id: setting.id,
|
||||||
userProductId: setting.user_product_id,
|
userProductId: setting.user_product_id,
|
||||||
userEquipmentId: setting.user_equipment_id,
|
equipmentId: setting.equipment_id,
|
||||||
equipmentName: setting.equipment_name,
|
equipmentName: setting.equipment_name,
|
||||||
spreaderSettingId: setting.spreader_setting_id,
|
equipmentManufacturer: setting.manufacturer,
|
||||||
brandName: setting.brand_name,
|
equipmentModel: setting.equipment_model,
|
||||||
model: setting.model,
|
spreaderBrand: setting.spreader_brand,
|
||||||
settingNumber: setting.setting_number,
|
spreaderModel: setting.spreader_model,
|
||||||
customRate: setting.custom_rate ? parseFloat(setting.custom_rate) : null,
|
settingValue: setting.setting_value,
|
||||||
customRateUnit: setting.custom_rate_unit,
|
rateDescription: setting.rate_description,
|
||||||
notes: setting.notes,
|
notes: setting.notes,
|
||||||
createdAt: setting.created_at
|
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;
|
module.exports = router;
|
||||||
@@ -29,6 +29,15 @@ const AdminProducts = () => {
|
|||||||
const [showDeleteModal, setShowDeleteModal] = useState(false);
|
const [showDeleteModal, setShowDeleteModal] = useState(false);
|
||||||
const [showDetailsModal, setShowDetailsModal] = useState(false);
|
const [showDetailsModal, setShowDetailsModal] = useState(false);
|
||||||
const [productDetails, setProductDetails] = useState({ rates: [], spreaderSettings: [] });
|
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({
|
const [formData, setFormData] = useState({
|
||||||
name: '',
|
name: '',
|
||||||
brand: '',
|
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 = () => {
|
const resetForm = () => {
|
||||||
setFormData({
|
setFormData({
|
||||||
name: '',
|
name: '',
|
||||||
@@ -697,29 +740,39 @@ const AdminProducts = () => {
|
|||||||
<tr>
|
<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">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">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">Setting Value</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">Rate Description</th>
|
||||||
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Notes</th>
|
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Notes</th>
|
||||||
|
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Actions</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody className="bg-white divide-y divide-gray-200">
|
<tbody className="bg-white divide-y divide-gray-200">
|
||||||
{productDetails.spreaderSettings.map((setting, index) => (
|
{productDetails.spreaderSettings.map((setting, index) => (
|
||||||
<tr key={index} className="hover:bg-gray-50">
|
<tr key={index} className="hover:bg-gray-50">
|
||||||
<td className="px-6 py-4 whitespace-nowrap text-sm font-medium text-gray-900">
|
<td className="px-6 py-4 whitespace-nowrap text-sm font-medium text-gray-900">
|
||||||
{setting.equipmentName}
|
{setting.equipmentName || 'Generic Equipment'}
|
||||||
</td>
|
</td>
|
||||||
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-900">
|
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-900">
|
||||||
{setting.brandName} {setting.model}
|
{setting.spreaderBrand} {setting.spreaderModel}
|
||||||
</td>
|
</td>
|
||||||
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-900">
|
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-900">
|
||||||
{setting.settingNumber || 'N/A'}
|
{setting.settingValue || 'N/A'}
|
||||||
</td>
|
</td>
|
||||||
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-900">
|
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-900">
|
||||||
{setting.customRate ? `${setting.customRate} ${setting.customRateUnit}` : 'Standard'}
|
{setting.rateDescription || 'Not specified'}
|
||||||
</td>
|
</td>
|
||||||
<td className="px-6 py-4 text-sm text-gray-900">
|
<td className="px-6 py-4 text-sm text-gray-900">
|
||||||
{setting.notes || 'No notes'}
|
{setting.notes || 'No notes'}
|
||||||
</td>
|
</td>
|
||||||
|
<td className="px-6 py-4 whitespace-nowrap text-sm font-medium">
|
||||||
|
<button
|
||||||
|
onClick={() => handleDeleteSpreaderSetting(setting.id)}
|
||||||
|
className="text-red-600 hover:text-red-900"
|
||||||
|
title="Delete Setting"
|
||||||
|
>
|
||||||
|
<TrashIcon className="h-4 w-4" />
|
||||||
|
</button>
|
||||||
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
))}
|
))}
|
||||||
</tbody>
|
</tbody>
|
||||||
@@ -728,6 +781,105 @@ const AdminProducts = () => {
|
|||||||
) : (
|
) : (
|
||||||
<p className="text-gray-500 text-center py-8">No spreader settings configured for this product.</p>
|
<p className="text-gray-500 text-center py-8">No spreader settings configured for this product.</p>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{/* Add Setting Button */}
|
||||||
|
<div className="mt-4">
|
||||||
|
{!showAddSettingForm ? (
|
||||||
|
<button
|
||||||
|
onClick={() => setShowAddSettingForm(true)}
|
||||||
|
className="btn-primary flex items-center text-sm"
|
||||||
|
>
|
||||||
|
<PlusIcon className="h-4 w-4 mr-1" />
|
||||||
|
Add Spreader Setting
|
||||||
|
</button>
|
||||||
|
) : (
|
||||||
|
<div className="border rounded-lg p-4 bg-gray-50">
|
||||||
|
<h5 className="text-sm font-semibold text-gray-800 mb-3">Add New Spreader Setting</h5>
|
||||||
|
<form onSubmit={handleAddSpreaderSetting} className="space-y-3">
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-3">
|
||||||
|
<div>
|
||||||
|
<label className="block text-xs font-medium text-gray-700 mb-1">Spreader Brand</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
required
|
||||||
|
value={newSettingData.spreaderBrand}
|
||||||
|
onChange={(e) => 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"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="block text-xs font-medium text-gray-700 mb-1">Spreader Model</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={newSettingData.spreaderModel}
|
||||||
|
onChange={(e) => 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"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-3">
|
||||||
|
<div>
|
||||||
|
<label className="block text-xs font-medium text-gray-700 mb-1">Setting Value</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={newSettingData.settingValue}
|
||||||
|
onChange={(e) => 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"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="block text-xs font-medium text-gray-700 mb-1">Rate Description</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={newSettingData.rateDescription}
|
||||||
|
onChange={(e) => 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"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="block text-xs font-medium text-gray-700 mb-1">Notes</label>
|
||||||
|
<textarea
|
||||||
|
value={newSettingData.notes}
|
||||||
|
onChange={(e) => setNewSettingData({ ...newSettingData, notes: 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"
|
||||||
|
rows="2"
|
||||||
|
placeholder="Additional notes or instructions"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="flex justify-end space-x-2">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => {
|
||||||
|
setShowAddSettingForm(false);
|
||||||
|
setNewSettingData({
|
||||||
|
equipmentId: '',
|
||||||
|
spreaderBrand: '',
|
||||||
|
spreaderModel: '',
|
||||||
|
settingValue: '',
|
||||||
|
rateDescription: '',
|
||||||
|
notes: ''
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
className="px-3 py-1 text-xs text-gray-700 bg-gray-200 rounded hover:bg-gray-300"
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
className="px-3 py-1 text-xs text-white bg-blue-600 rounded hover:bg-blue-700"
|
||||||
|
>
|
||||||
|
Add Setting
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
@@ -737,6 +889,15 @@ const AdminProducts = () => {
|
|||||||
setShowDetailsModal(false);
|
setShowDetailsModal(false);
|
||||||
setSelectedProduct(null);
|
setSelectedProduct(null);
|
||||||
setProductDetails({ rates: [], spreaderSettings: [] });
|
setProductDetails({ rates: [], spreaderSettings: [] });
|
||||||
|
setShowAddSettingForm(false);
|
||||||
|
setNewSettingData({
|
||||||
|
equipmentId: '',
|
||||||
|
spreaderBrand: '',
|
||||||
|
spreaderModel: '',
|
||||||
|
settingValue: '',
|
||||||
|
rateDescription: '',
|
||||||
|
notes: ''
|
||||||
|
});
|
||||||
}}
|
}}
|
||||||
className="px-4 py-2 text-sm text-gray-700 bg-gray-200 rounded hover:bg-gray-300"
|
className="px-4 py-2 text-sm text-gray-700 bg-gray-200 rounded hover:bg-gray-300"
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -216,6 +216,8 @@ export const adminAPI = {
|
|||||||
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`),
|
getProductRates: (id) => apiClient.get(`/admin/products/${id}/rates`),
|
||||||
getUserProductSpreaderSettings: (id) => apiClient.get(`/admin/products/user/${id}/spreader-settings`),
|
getUserProductSpreaderSettings: (id) => apiClient.get(`/admin/products/user/${id}/spreader-settings`),
|
||||||
|
addUserProductSpreaderSetting: (id, settingData) => apiClient.post(`/admin/products/user/${id}/spreader-settings`, settingData),
|
||||||
|
deleteUserProductSpreaderSetting: (id) => apiClient.delete(`/admin/products/user/spreader-settings/${id}`),
|
||||||
|
|
||||||
// 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