equipment stuff

This commit is contained in:
Jake Kasper
2025-08-22 09:17:22 -04:00
parent f95422325c
commit b7ceed70a5
7 changed files with 2137 additions and 260 deletions

View File

@@ -0,0 +1,503 @@
const express = require('express');
const pool = require('../config/database');
const { validateRequest, validateParams } = require('../utils/validation');
const { idParamSchema } = require('../utils/validation');
const { AppError } = require('../middleware/errorHandler');
const router = express.Router();
// @route GET /api/nozzles/types
// @desc Get all nozzle types
// @access Private
router.get('/types', async (req, res, next) => {
try {
const { manufacturer, droplet_size, spray_pattern } = req.query;
let query = 'SELECT * FROM nozzle_types WHERE 1=1';
const queryParams = [];
if (manufacturer) {
queryParams.push(manufacturer);
query += ` AND manufacturer ILIKE $${queryParams.length}`;
}
if (droplet_size) {
queryParams.push(droplet_size);
query += ` AND droplet_size = $${queryParams.length}`;
}
if (spray_pattern) {
queryParams.push(spray_pattern);
query += ` AND spray_pattern = $${queryParams.length}`;
}
query += ' ORDER BY manufacturer, name';
const result = await pool.query(query, queryParams);
// Group by manufacturer for easier frontend handling
const nozzlesByManufacturer = result.rows.reduce((acc, nozzle) => {
const manufacturer = nozzle.manufacturer || 'Unknown';
if (!acc[manufacturer]) {
acc[manufacturer] = [];
}
acc[manufacturer].push({
id: nozzle.id,
name: nozzle.name,
manufacturer: nozzle.manufacturer,
model: nozzle.model,
orificeSize: nozzle.orifice_size,
sprayAngle: nozzle.spray_angle,
flowRateGpm: parseFloat(nozzle.flow_rate_gpm),
dropletSize: nozzle.droplet_size,
sprayPattern: nozzle.spray_pattern,
pressureRangePsi: nozzle.pressure_range_psi,
createdAt: nozzle.created_at
});
return acc;
}, {});
res.json({
success: true,
data: {
nozzleTypes: result.rows.map(nt => ({
id: nt.id,
name: nt.name,
manufacturer: nt.manufacturer,
model: nt.model,
orificeSize: nt.orifice_size,
sprayAngle: nt.spray_angle,
flowRateGpm: parseFloat(nt.flow_rate_gpm),
dropletSize: nt.droplet_size,
sprayPattern: nt.spray_pattern,
pressureRangePsi: nt.pressure_range_psi,
createdAt: nt.created_at
})),
nozzlesByManufacturer
}
});
} catch (error) {
next(error);
}
});
// @route GET /api/nozzles
// @desc Get all user's nozzles
// @access Private
router.get('/', async (req, res, next) => {
try {
const result = await pool.query(
`SELECT un.*, nt.name as type_name, nt.manufacturer as type_manufacturer,
nt.model as type_model, nt.orifice_size, nt.spray_angle, nt.flow_rate_gpm,
nt.droplet_size, nt.spray_pattern, nt.pressure_range_psi
FROM user_nozzles un
LEFT JOIN nozzle_types nt ON un.nozzle_type_id = nt.id
WHERE un.user_id = $1
ORDER BY nt.manufacturer, nt.name, un.custom_name`,
[req.user.id]
);
res.json({
success: true,
data: {
nozzles: result.rows.map(item => ({
id: item.id,
nozzleTypeId: item.nozzle_type_id,
typeName: item.type_name,
typeManufacturer: item.type_manufacturer,
typeModel: item.type_model,
orificeSize: item.orifice_size,
sprayAngle: item.spray_angle,
flowRateGpm: parseFloat(item.flow_rate_gpm),
dropletSize: item.droplet_size,
sprayPattern: item.spray_pattern,
pressureRangePsi: item.pressure_range_psi,
customName: item.custom_name,
quantity: item.quantity,
condition: item.condition,
purchaseDate: item.purchase_date,
notes: item.notes,
createdAt: item.created_at,
updatedAt: item.updated_at
}))
}
});
} catch (error) {
next(error);
}
});
// @route GET /api/nozzles/:id
// @desc Get single user nozzle
// @access Private
router.get('/:id', validateParams(idParamSchema), async (req, res, next) => {
try {
const nozzleId = req.params.id;
const result = await pool.query(
`SELECT un.*, nt.name as type_name, nt.manufacturer as type_manufacturer,
nt.model as type_model, nt.orifice_size, nt.spray_angle, nt.flow_rate_gpm,
nt.droplet_size, nt.spray_pattern, nt.pressure_range_psi
FROM user_nozzles un
LEFT JOIN nozzle_types nt ON un.nozzle_type_id = nt.id
WHERE un.id = $1 AND un.user_id = $2`,
[nozzleId, req.user.id]
);
if (result.rows.length === 0) {
throw new AppError('Nozzle not found', 404);
}
const item = result.rows[0];
res.json({
success: true,
data: {
nozzle: {
id: item.id,
nozzleTypeId: item.nozzle_type_id,
typeName: item.type_name,
typeManufacturer: item.type_manufacturer,
typeModel: item.type_model,
orificeSize: item.orifice_size,
sprayAngle: item.spray_angle,
flowRateGpm: parseFloat(item.flow_rate_gpm),
dropletSize: item.droplet_size,
sprayPattern: item.spray_pattern,
pressureRangePsi: item.pressure_range_psi,
customName: item.custom_name,
quantity: item.quantity,
condition: item.condition,
purchaseDate: item.purchase_date,
notes: item.notes,
createdAt: item.created_at,
updatedAt: item.updated_at
}
}
});
} catch (error) {
next(error);
}
});
// @route POST /api/nozzles
// @desc Add new nozzle to user's inventory
// @access Private
router.post('/', async (req, res, next) => {
try {
const {
nozzleTypeId,
customName,
quantity,
condition,
purchaseDate,
notes
} = req.body;
// Validate nozzle type exists if provided
if (nozzleTypeId) {
const typeCheck = await pool.query(
'SELECT id, name, manufacturer FROM nozzle_types WHERE id = $1',
[nozzleTypeId]
);
if (typeCheck.rows.length === 0) {
throw new AppError('Nozzle type not found', 404);
}
}
// Either nozzleTypeId or customName is required
if (!nozzleTypeId && !customName) {
throw new AppError('Either nozzle type or custom name is required', 400);
}
const result = await pool.query(
`INSERT INTO user_nozzles
(user_id, nozzle_type_id, custom_name, quantity, condition, purchase_date, notes)
VALUES ($1, $2, $3, $4, $5, $6, $7)
RETURNING *`,
[req.user.id, nozzleTypeId, customName, quantity || 1, condition || 'good', purchaseDate, notes]
);
const nozzle = result.rows[0];
res.status(201).json({
success: true,
message: 'Nozzle added successfully',
data: {
nozzle: {
id: nozzle.id,
nozzleTypeId: nozzle.nozzle_type_id,
customName: nozzle.custom_name,
quantity: nozzle.quantity,
condition: nozzle.condition,
purchaseDate: nozzle.purchase_date,
notes: nozzle.notes,
createdAt: nozzle.created_at,
updatedAt: nozzle.updated_at
}
}
});
} catch (error) {
next(error);
}
});
// @route PUT /api/nozzles/:id
// @desc Update user nozzle
// @access Private
router.put('/:id', validateParams(idParamSchema), async (req, res, next) => {
try {
const nozzleId = req.params.id;
const {
nozzleTypeId,
customName,
quantity,
condition,
purchaseDate,
notes
} = req.body;
// Check if nozzle exists and belongs to user
const checkResult = await pool.query(
'SELECT id FROM user_nozzles WHERE id = $1 AND user_id = $2',
[nozzleId, req.user.id]
);
if (checkResult.rows.length === 0) {
throw new AppError('Nozzle not found', 404);
}
const result = await pool.query(
`UPDATE user_nozzles
SET nozzle_type_id = $1, custom_name = $2, quantity = $3, condition = $4,
purchase_date = $5, notes = $6, updated_at = CURRENT_TIMESTAMP
WHERE id = $7
RETURNING *`,
[nozzleTypeId, customName, quantity, condition, purchaseDate, notes, nozzleId]
);
const nozzle = result.rows[0];
res.json({
success: true,
message: 'Nozzle updated successfully',
data: {
nozzle: {
id: nozzle.id,
nozzleTypeId: nozzle.nozzle_type_id,
customName: nozzle.custom_name,
quantity: nozzle.quantity,
condition: nozzle.condition,
purchaseDate: nozzle.purchase_date,
notes: nozzle.notes,
createdAt: nozzle.created_at,
updatedAt: nozzle.updated_at
}
}
});
} catch (error) {
next(error);
}
});
// @route DELETE /api/nozzles/:id
// @desc Delete user nozzle
// @access Private
router.delete('/:id', validateParams(idParamSchema), async (req, res, next) => {
try {
const nozzleId = req.params.id;
// Check if nozzle exists and belongs to user
const checkResult = await pool.query(
'SELECT id FROM user_nozzles WHERE id = $1 AND user_id = $2',
[nozzleId, req.user.id]
);
if (checkResult.rows.length === 0) {
throw new AppError('Nozzle not found', 404);
}
// Check if nozzle is assigned to any equipment
const assignmentCheck = await pool.query(
'SELECT COUNT(*) as count FROM equipment_nozzle_assignments WHERE user_nozzle_id = $1',
[nozzleId]
);
if (parseInt(assignmentCheck.rows[0].count) > 0) {
throw new AppError('Cannot delete nozzle that is assigned to equipment', 400);
}
await pool.query('DELETE FROM user_nozzles WHERE id = $1', [nozzleId]);
res.json({
success: true,
message: 'Nozzle deleted successfully'
});
} catch (error) {
next(error);
}
});
// @route GET /api/nozzles/equipment/:equipmentId/assignments
// @desc Get nozzle assignments for specific equipment
// @access Private
router.get('/equipment/:equipmentId/assignments', validateParams(idParamSchema), async (req, res, next) => {
try {
const equipmentId = req.params.equipmentId;
// Verify equipment belongs to user
const equipmentCheck = await pool.query(
'SELECT id FROM user_equipment WHERE id = $1 AND user_id = $2',
[equipmentId, req.user.id]
);
if (equipmentCheck.rows.length === 0) {
throw new AppError('Equipment not found', 404);
}
const result = await pool.query(
`SELECT ena.*, un.custom_name as nozzle_custom_name, un.quantity as nozzle_total_quantity,
nt.name as nozzle_type_name, nt.manufacturer, nt.model, nt.orifice_size,
nt.spray_angle, nt.flow_rate_gpm, nt.droplet_size, nt.spray_pattern
FROM equipment_nozzle_assignments ena
JOIN user_nozzles un ON ena.user_nozzle_id = un.id
LEFT JOIN nozzle_types nt ON un.nozzle_type_id = nt.id
WHERE ena.user_equipment_id = $1
ORDER BY ena.position, nt.manufacturer, nt.name`,
[equipmentId]
);
res.json({
success: true,
data: {
assignments: result.rows.map(item => ({
id: item.id,
userEquipmentId: item.user_equipment_id,
userNozzleId: item.user_nozzle_id,
position: item.position,
quantityAssigned: item.quantity_assigned,
assignedDate: item.assigned_date,
nozzleCustomName: item.nozzle_custom_name,
nozzleTotalQuantity: item.nozzle_total_quantity,
nozzleTypeName: item.nozzle_type_name,
manufacturer: item.manufacturer,
model: item.model,
orificeSize: item.orifice_size,
sprayAngle: item.spray_angle,
flowRateGpm: parseFloat(item.flow_rate_gpm),
dropletSize: item.droplet_size,
sprayPattern: item.spray_pattern,
createdAt: item.created_at
}))
}
});
} catch (error) {
next(error);
}
});
// @route POST /api/nozzles/equipment/:equipmentId/assignments
// @desc Assign nozzle to equipment
// @access Private
router.post('/equipment/:equipmentId/assignments', validateParams(idParamSchema), async (req, res, next) => {
try {
const equipmentId = req.params.equipmentId;
const { userNozzleId, position, quantityAssigned } = req.body;
// Verify equipment belongs to user
const equipmentCheck = await pool.query(
'SELECT id FROM user_equipment WHERE id = $1 AND user_id = $2',
[equipmentId, req.user.id]
);
if (equipmentCheck.rows.length === 0) {
throw new AppError('Equipment not found', 404);
}
// Verify nozzle belongs to user
const nozzleCheck = await pool.query(
'SELECT id, quantity FROM user_nozzles WHERE id = $1 AND user_id = $2',
[userNozzleId, req.user.id]
);
if (nozzleCheck.rows.length === 0) {
throw new AppError('Nozzle not found', 404);
}
const nozzle = nozzleCheck.rows[0];
// Check if enough nozzles are available
const assignedCount = await pool.query(
'SELECT COALESCE(SUM(quantity_assigned), 0) as total_assigned FROM equipment_nozzle_assignments WHERE user_nozzle_id = $1',
[userNozzleId]
);
const totalAssigned = parseInt(assignedCount.rows[0].total_assigned);
const requestedQuantity = quantityAssigned || 1;
if (totalAssigned + requestedQuantity > nozzle.quantity) {
throw new AppError(`Not enough nozzles available. Available: ${nozzle.quantity - totalAssigned}, Requested: ${requestedQuantity}`, 400);
}
const result = await pool.query(
`INSERT INTO equipment_nozzle_assignments
(user_equipment_id, user_nozzle_id, position, quantity_assigned)
VALUES ($1, $2, $3, $4)
RETURNING *`,
[equipmentId, userNozzleId, position || 'center', requestedQuantity]
);
const assignment = result.rows[0];
res.status(201).json({
success: true,
message: 'Nozzle assigned to equipment successfully',
data: {
assignment: {
id: assignment.id,
userEquipmentId: assignment.user_equipment_id,
userNozzleId: assignment.user_nozzle_id,
position: assignment.position,
quantityAssigned: assignment.quantity_assigned,
assignedDate: assignment.assigned_date,
createdAt: assignment.created_at
}
}
});
} catch (error) {
next(error);
}
});
// @route DELETE /api/nozzles/assignments/:assignmentId
// @desc Remove nozzle assignment from equipment
// @access Private
router.delete('/assignments/:assignmentId', validateParams(idParamSchema), async (req, res, next) => {
try {
const assignmentId = req.params.assignmentId;
// Verify assignment belongs to user's equipment
const checkResult = await pool.query(
`SELECT ena.id FROM equipment_nozzle_assignments ena
JOIN user_equipment ue ON ena.user_equipment_id = ue.id
WHERE ena.id = $1 AND ue.user_id = $2`,
[assignmentId, req.user.id]
);
if (checkResult.rows.length === 0) {
throw new AppError('Assignment not found', 404);
}
await pool.query('DELETE FROM equipment_nozzle_assignments WHERE id = $1', [assignmentId]);
res.json({
success: true,
message: 'Nozzle assignment removed successfully'
});
} catch (error) {
next(error);
}
});
module.exports = router;