equipment stuff
This commit is contained in:
@@ -10,6 +10,7 @@ const authRoutes = require('./routes/auth');
|
||||
const userRoutes = require('./routes/users');
|
||||
const propertyRoutes = require('./routes/properties');
|
||||
const equipmentRoutes = require('./routes/equipment');
|
||||
const nozzleRoutes = require('./routes/nozzles');
|
||||
const productRoutes = require('./routes/products');
|
||||
const applicationRoutes = require('./routes/applications');
|
||||
const weatherRoutes = require('./routes/weather');
|
||||
@@ -82,6 +83,7 @@ app.use('/api/auth', authLimiter, authRoutes);
|
||||
app.use('/api/users', authenticateToken, userRoutes);
|
||||
app.use('/api/properties', authenticateToken, propertyRoutes);
|
||||
app.use('/api/equipment', authenticateToken, equipmentRoutes);
|
||||
app.use('/api/nozzles', authenticateToken, nozzleRoutes);
|
||||
app.use('/api/products', authenticateToken, productRoutes);
|
||||
app.use('/api/applications', authenticateToken, applicationRoutes);
|
||||
app.use('/api/weather', authenticateToken, weatherRoutes);
|
||||
|
||||
@@ -1,28 +1,66 @@
|
||||
const express = require('express');
|
||||
const pool = require('../config/database');
|
||||
const { validateRequest, validateParams } = require('../utils/validation');
|
||||
const { equipmentSchema, idParamSchema } = require('../utils/validation');
|
||||
const { idParamSchema } = require('../utils/validation');
|
||||
const { AppError } = require('../middleware/errorHandler');
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
// @route GET /api/equipment/categories
|
||||
// @desc Get all equipment categories
|
||||
// @access Private
|
||||
router.get('/categories', async (req, res, next) => {
|
||||
try {
|
||||
const result = await pool.query(
|
||||
'SELECT * FROM equipment_categories ORDER BY name'
|
||||
);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: {
|
||||
categories: result.rows
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
});
|
||||
|
||||
// @route GET /api/equipment/types
|
||||
// @desc Get all equipment types
|
||||
// @access Private
|
||||
router.get('/types', async (req, res, next) => {
|
||||
try {
|
||||
const result = await pool.query(
|
||||
'SELECT * FROM equipment_types ORDER BY category, name'
|
||||
);
|
||||
const { category_id } = req.query;
|
||||
|
||||
let query = `
|
||||
SELECT et.*, ec.name as category_name, ec.description as category_description
|
||||
FROM equipment_types et
|
||||
JOIN equipment_categories ec ON et.category_id = ec.id
|
||||
`;
|
||||
const queryParams = [];
|
||||
|
||||
if (category_id) {
|
||||
query += ' WHERE et.category_id = $1';
|
||||
queryParams.push(category_id);
|
||||
}
|
||||
|
||||
query += ' ORDER BY ec.name, et.name';
|
||||
|
||||
const result = await pool.query(query, queryParams);
|
||||
|
||||
const equipmentByCategory = result.rows.reduce((acc, equipment) => {
|
||||
if (!acc[equipment.category]) {
|
||||
acc[equipment.category] = [];
|
||||
const categoryName = equipment.category_name;
|
||||
if (!acc[categoryName]) {
|
||||
acc[categoryName] = [];
|
||||
}
|
||||
acc[equipment.category].push({
|
||||
acc[categoryName].push({
|
||||
id: equipment.id,
|
||||
name: equipment.name,
|
||||
category: equipment.category,
|
||||
manufacturer: equipment.manufacturer,
|
||||
model: equipment.model,
|
||||
categoryId: equipment.category_id,
|
||||
categoryName: equipment.category_name,
|
||||
createdAt: equipment.created_at
|
||||
});
|
||||
return acc;
|
||||
@@ -31,7 +69,16 @@ router.get('/types', async (req, res, next) => {
|
||||
res.json({
|
||||
success: true,
|
||||
data: {
|
||||
equipmentTypes: result.rows,
|
||||
equipmentTypes: result.rows.map(et => ({
|
||||
id: et.id,
|
||||
name: et.name,
|
||||
manufacturer: et.manufacturer,
|
||||
model: et.model,
|
||||
categoryId: et.category_id,
|
||||
categoryName: et.category_name,
|
||||
categoryDescription: et.category_description,
|
||||
createdAt: et.created_at
|
||||
})),
|
||||
equipmentByCategory
|
||||
}
|
||||
});
|
||||
@@ -45,14 +92,31 @@ router.get('/types', async (req, res, next) => {
|
||||
// @access Private
|
||||
router.get('/', async (req, res, next) => {
|
||||
try {
|
||||
const result = await pool.query(
|
||||
`SELECT ue.*, et.name as type_name, et.category
|
||||
FROM user_equipment ue
|
||||
JOIN equipment_types et ON ue.equipment_type_id = et.id
|
||||
WHERE ue.user_id = $1
|
||||
ORDER BY et.category, et.name`,
|
||||
[req.user.id]
|
||||
);
|
||||
const { category_id, is_active } = req.query;
|
||||
|
||||
let query = `
|
||||
SELECT ue.*, et.name as type_name, et.manufacturer as type_manufacturer, et.model as type_model,
|
||||
ec.name as category_name
|
||||
FROM user_equipment ue
|
||||
LEFT JOIN equipment_types et ON ue.equipment_type_id = et.id
|
||||
LEFT JOIN equipment_categories ec ON ue.category_id = ec.id
|
||||
WHERE ue.user_id = $1
|
||||
`;
|
||||
const queryParams = [req.user.id];
|
||||
|
||||
if (category_id) {
|
||||
queryParams.push(category_id);
|
||||
query += ` AND ue.category_id = $${queryParams.length}`;
|
||||
}
|
||||
|
||||
if (is_active !== undefined) {
|
||||
queryParams.push(is_active === 'true');
|
||||
query += ` AND ue.is_active = $${queryParams.length}`;
|
||||
}
|
||||
|
||||
query += ' ORDER BY ec.name, COALESCE(ue.custom_name, et.name)';
|
||||
|
||||
const result = await pool.query(query, queryParams);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
@@ -60,14 +124,43 @@ router.get('/', async (req, res, next) => {
|
||||
equipment: result.rows.map(item => ({
|
||||
id: item.id,
|
||||
equipmentTypeId: item.equipment_type_id,
|
||||
categoryId: item.category_id,
|
||||
typeName: item.type_name,
|
||||
category: item.category,
|
||||
typeManufacturer: item.type_manufacturer,
|
||||
typeModel: item.type_model,
|
||||
categoryName: item.category_name,
|
||||
customName: item.custom_name,
|
||||
tankSize: parseFloat(item.tank_size),
|
||||
pumpGpm: parseFloat(item.pump_gpm),
|
||||
nozzleGpm: parseFloat(item.nozzle_gpm),
|
||||
nozzleCount: item.nozzle_count,
|
||||
spreaderWidth: parseFloat(item.spreader_width),
|
||||
manufacturer: item.manufacturer,
|
||||
model: item.model,
|
||||
// Spreader fields
|
||||
capacityLbs: parseFloat(item.capacity_lbs) || null,
|
||||
spreaderType: item.spreader_type,
|
||||
spreadWidth: parseFloat(item.spread_width) || null,
|
||||
// Sprayer fields
|
||||
tankSizeGallons: parseFloat(item.tank_size_gallons) || null,
|
||||
sprayerType: item.sprayer_type,
|
||||
sprayWidthFeet: parseFloat(item.spray_width_feet) || null,
|
||||
pumpGpm: parseFloat(item.pump_gpm) || null,
|
||||
pumpPsi: parseFloat(item.pump_psi) || null,
|
||||
boomSections: item.boom_sections,
|
||||
// Mower fields
|
||||
mowerStyle: item.mower_style,
|
||||
cuttingWidthInches: parseFloat(item.cutting_width_inches) || null,
|
||||
engineHp: parseFloat(item.engine_hp) || null,
|
||||
fuelType: item.fuel_type,
|
||||
// Tool fields
|
||||
toolType: item.tool_type,
|
||||
workingWidthInches: parseFloat(item.working_width_inches) || null,
|
||||
// Pump fields
|
||||
pumpType: item.pump_type,
|
||||
maxGpm: parseFloat(item.max_gpm) || null,
|
||||
maxPsi: parseFloat(item.max_psi) || null,
|
||||
powerSource: item.power_source,
|
||||
// General fields
|
||||
purchaseDate: item.purchase_date,
|
||||
purchasePrice: parseFloat(item.purchase_price) || null,
|
||||
notes: item.notes,
|
||||
isActive: item.is_active,
|
||||
createdAt: item.created_at,
|
||||
updatedAt: item.updated_at
|
||||
}))
|
||||
@@ -86,9 +179,11 @@ router.get('/:id', validateParams(idParamSchema), async (req, res, next) => {
|
||||
const equipmentId = req.params.id;
|
||||
|
||||
const result = await pool.query(
|
||||
`SELECT ue.*, et.name as type_name, et.category
|
||||
`SELECT ue.*, et.name as type_name, et.manufacturer as type_manufacturer, et.model as type_model,
|
||||
ec.name as category_name
|
||||
FROM user_equipment ue
|
||||
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 equipment_categories ec ON ue.category_id = ec.id
|
||||
WHERE ue.id = $1 AND ue.user_id = $2`,
|
||||
[equipmentId, req.user.id]
|
||||
);
|
||||
@@ -105,14 +200,43 @@ router.get('/:id', validateParams(idParamSchema), async (req, res, next) => {
|
||||
equipment: {
|
||||
id: item.id,
|
||||
equipmentTypeId: item.equipment_type_id,
|
||||
categoryId: item.category_id,
|
||||
typeName: item.type_name,
|
||||
category: item.category,
|
||||
typeManufacturer: item.type_manufacturer,
|
||||
typeModel: item.type_model,
|
||||
categoryName: item.category_name,
|
||||
customName: item.custom_name,
|
||||
tankSize: parseFloat(item.tank_size),
|
||||
pumpGpm: parseFloat(item.pump_gpm),
|
||||
nozzleGpm: parseFloat(item.nozzle_gpm),
|
||||
nozzleCount: item.nozzle_count,
|
||||
spreaderWidth: parseFloat(item.spreader_width),
|
||||
manufacturer: item.manufacturer,
|
||||
model: item.model,
|
||||
// Spreader fields
|
||||
capacityLbs: parseFloat(item.capacity_lbs) || null,
|
||||
spreaderType: item.spreader_type,
|
||||
spreadWidth: parseFloat(item.spread_width) || null,
|
||||
// Sprayer fields
|
||||
tankSizeGallons: parseFloat(item.tank_size_gallons) || null,
|
||||
sprayerType: item.sprayer_type,
|
||||
sprayWidthFeet: parseFloat(item.spray_width_feet) || null,
|
||||
pumpGpm: parseFloat(item.pump_gpm) || null,
|
||||
pumpPsi: parseFloat(item.pump_psi) || null,
|
||||
boomSections: item.boom_sections,
|
||||
// Mower fields
|
||||
mowerStyle: item.mower_style,
|
||||
cuttingWidthInches: parseFloat(item.cutting_width_inches) || null,
|
||||
engineHp: parseFloat(item.engine_hp) || null,
|
||||
fuelType: item.fuel_type,
|
||||
// Tool fields
|
||||
toolType: item.tool_type,
|
||||
workingWidthInches: parseFloat(item.working_width_inches) || null,
|
||||
// Pump fields
|
||||
pumpType: item.pump_type,
|
||||
maxGpm: parseFloat(item.max_gpm) || null,
|
||||
maxPsi: parseFloat(item.max_psi) || null,
|
||||
powerSource: item.power_source,
|
||||
// General fields
|
||||
purchaseDate: item.purchase_date,
|
||||
purchasePrice: parseFloat(item.purchase_price) || null,
|
||||
notes: item.notes,
|
||||
isActive: item.is_active,
|
||||
createdAt: item.created_at,
|
||||
updatedAt: item.updated_at
|
||||
}
|
||||
@@ -126,47 +250,99 @@ router.get('/:id', validateParams(idParamSchema), async (req, res, next) => {
|
||||
// @route POST /api/equipment
|
||||
// @desc Add new equipment
|
||||
// @access Private
|
||||
router.post('/', validateRequest(equipmentSchema), async (req, res, next) => {
|
||||
router.post('/', async (req, res, next) => {
|
||||
try {
|
||||
const {
|
||||
equipmentTypeId,
|
||||
customName,
|
||||
tankSize,
|
||||
pumpGpm,
|
||||
nozzleGpm,
|
||||
nozzleCount,
|
||||
spreaderWidth
|
||||
const {
|
||||
equipmentTypeId,
|
||||
categoryId,
|
||||
customName,
|
||||
manufacturer,
|
||||
model,
|
||||
// Spreader specific fields
|
||||
capacityLbs,
|
||||
spreaderType,
|
||||
spreadWidth,
|
||||
// Sprayer specific fields
|
||||
tankSizeGallons,
|
||||
sprayerType,
|
||||
sprayWidthFeet,
|
||||
pumpGpm,
|
||||
pumpPsi,
|
||||
boomSections,
|
||||
// Mower specific fields
|
||||
mowerStyle,
|
||||
cuttingWidthInches,
|
||||
engineHp,
|
||||
fuelType,
|
||||
// Tool specific fields
|
||||
toolType,
|
||||
workingWidthInches,
|
||||
// Pump specific fields
|
||||
pumpType,
|
||||
maxGpm,
|
||||
maxPsi,
|
||||
powerSource,
|
||||
// General fields
|
||||
purchaseDate,
|
||||
purchasePrice,
|
||||
notes
|
||||
} = req.body;
|
||||
|
||||
// Verify equipment type exists
|
||||
const typeCheck = await pool.query(
|
||||
'SELECT id, name, category FROM equipment_types WHERE id = $1',
|
||||
[equipmentTypeId]
|
||||
);
|
||||
|
||||
if (typeCheck.rows.length === 0) {
|
||||
throw new AppError('Equipment type not found', 404);
|
||||
// Validate required fields
|
||||
if (!categoryId && !equipmentTypeId) {
|
||||
throw new AppError('Either category or equipment type is required', 400);
|
||||
}
|
||||
|
||||
const equipmentType = typeCheck.rows[0];
|
||||
// If equipmentTypeId is provided, verify it exists and get category
|
||||
let finalCategoryId = categoryId;
|
||||
let equipmentType = null;
|
||||
|
||||
if (equipmentTypeId) {
|
||||
const typeCheck = await pool.query(
|
||||
'SELECT id, name, category_id, manufacturer, model FROM equipment_types WHERE id = $1',
|
||||
[equipmentTypeId]
|
||||
);
|
||||
|
||||
// Validate required fields based on equipment type
|
||||
if (equipmentType.category === 'sprayer') {
|
||||
if (!tankSize || !pumpGpm || !nozzleGpm || !nozzleCount) {
|
||||
throw new AppError('Tank size, pump GPM, nozzle GPM, and nozzle count are required for sprayers', 400);
|
||||
if (typeCheck.rows.length === 0) {
|
||||
throw new AppError('Equipment type not found', 404);
|
||||
}
|
||||
|
||||
equipmentType = typeCheck.rows[0];
|
||||
finalCategoryId = equipmentType.category_id;
|
||||
}
|
||||
|
||||
if (equipmentType.category === 'spreader' && !spreaderWidth) {
|
||||
throw new AppError('Spreader width is required for spreaders', 400);
|
||||
// Verify category exists
|
||||
if (finalCategoryId) {
|
||||
const categoryCheck = await pool.query(
|
||||
'SELECT id, name FROM equipment_categories WHERE id = $1',
|
||||
[finalCategoryId]
|
||||
);
|
||||
|
||||
if (categoryCheck.rows.length === 0) {
|
||||
throw new AppError('Category not found', 404);
|
||||
}
|
||||
}
|
||||
|
||||
const result = await pool.query(
|
||||
`INSERT INTO user_equipment
|
||||
(user_id, equipment_type_id, custom_name, tank_size, pump_gpm, nozzle_gpm, nozzle_count, spreader_width)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7, $8)
|
||||
(user_id, equipment_type_id, category_id, custom_name, manufacturer, model,
|
||||
capacity_lbs, spreader_type, spread_width,
|
||||
tank_size_gallons, sprayer_type, spray_width_feet, pump_gpm, pump_psi, boom_sections,
|
||||
mower_style, cutting_width_inches, engine_hp, fuel_type,
|
||||
tool_type, working_width_inches,
|
||||
pump_type, max_gpm, max_psi, power_source,
|
||||
purchase_date, purchase_price, notes)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19, $20, $21, $22, $23, $24, $25, $26, $27, $28)
|
||||
RETURNING *`,
|
||||
[req.user.id, equipmentTypeId, customName, tankSize, pumpGpm, nozzleGpm, nozzleCount, spreaderWidth]
|
||||
[
|
||||
req.user.id, equipmentTypeId, finalCategoryId, customName, manufacturer, model,
|
||||
capacityLbs, spreaderType, spreadWidth,
|
||||
tankSizeGallons, sprayerType, sprayWidthFeet, pumpGpm, pumpPsi, boomSections,
|
||||
mowerStyle, cuttingWidthInches, engineHp, fuelType,
|
||||
toolType, workingWidthInches,
|
||||
pumpType, maxGpm, maxPsi, powerSource,
|
||||
purchaseDate, purchasePrice, notes
|
||||
]
|
||||
);
|
||||
|
||||
const equipment = result.rows[0];
|
||||
@@ -178,14 +354,33 @@ router.post('/', validateRequest(equipmentSchema), async (req, res, next) => {
|
||||
equipment: {
|
||||
id: equipment.id,
|
||||
equipmentTypeId: equipment.equipment_type_id,
|
||||
typeName: equipmentType.name,
|
||||
category: equipmentType.category,
|
||||
categoryId: equipment.category_id,
|
||||
customName: equipment.custom_name,
|
||||
tankSize: parseFloat(equipment.tank_size),
|
||||
pumpGpm: parseFloat(equipment.pump_gpm),
|
||||
nozzleGpm: parseFloat(equipment.nozzle_gpm),
|
||||
nozzleCount: equipment.nozzle_count,
|
||||
spreaderWidth: parseFloat(equipment.spreader_width),
|
||||
manufacturer: equipment.manufacturer,
|
||||
model: equipment.model,
|
||||
capacityLbs: parseFloat(equipment.capacity_lbs) || null,
|
||||
spreaderType: equipment.spreader_type,
|
||||
spreadWidth: parseFloat(equipment.spread_width) || null,
|
||||
tankSizeGallons: parseFloat(equipment.tank_size_gallons) || null,
|
||||
sprayerType: equipment.sprayer_type,
|
||||
sprayWidthFeet: parseFloat(equipment.spray_width_feet) || null,
|
||||
pumpGpm: parseFloat(equipment.pump_gpm) || null,
|
||||
pumpPsi: parseFloat(equipment.pump_psi) || null,
|
||||
boomSections: equipment.boom_sections,
|
||||
mowerStyle: equipment.mower_style,
|
||||
cuttingWidthInches: parseFloat(equipment.cutting_width_inches) || null,
|
||||
engineHp: parseFloat(equipment.engine_hp) || null,
|
||||
fuelType: equipment.fuel_type,
|
||||
toolType: equipment.tool_type,
|
||||
workingWidthInches: parseFloat(equipment.working_width_inches) || null,
|
||||
pumpType: equipment.pump_type,
|
||||
maxGpm: parseFloat(equipment.max_gpm) || null,
|
||||
maxPsi: parseFloat(equipment.max_psi) || null,
|
||||
powerSource: equipment.power_source,
|
||||
purchaseDate: equipment.purchase_date,
|
||||
purchasePrice: parseFloat(equipment.purchase_price) || null,
|
||||
notes: equipment.notes,
|
||||
isActive: equipment.is_active,
|
||||
createdAt: equipment.created_at,
|
||||
updatedAt: equipment.updated_at
|
||||
}
|
||||
@@ -199,17 +394,38 @@ router.post('/', validateRequest(equipmentSchema), async (req, res, next) => {
|
||||
// @route PUT /api/equipment/:id
|
||||
// @desc Update equipment
|
||||
// @access Private
|
||||
router.put('/:id', validateParams(idParamSchema), validateRequest(equipmentSchema), async (req, res, next) => {
|
||||
router.put('/:id', validateParams(idParamSchema), async (req, res, next) => {
|
||||
try {
|
||||
const equipmentId = req.params.id;
|
||||
const {
|
||||
equipmentTypeId,
|
||||
customName,
|
||||
tankSize,
|
||||
pumpGpm,
|
||||
nozzleGpm,
|
||||
nozzleCount,
|
||||
spreaderWidth
|
||||
const {
|
||||
equipmentTypeId,
|
||||
categoryId,
|
||||
customName,
|
||||
manufacturer,
|
||||
model,
|
||||
capacityLbs,
|
||||
spreaderType,
|
||||
spreadWidth,
|
||||
tankSizeGallons,
|
||||
sprayerType,
|
||||
sprayWidthFeet,
|
||||
pumpGpm,
|
||||
pumpPsi,
|
||||
boomSections,
|
||||
mowerStyle,
|
||||
cuttingWidthInches,
|
||||
engineHp,
|
||||
fuelType,
|
||||
toolType,
|
||||
workingWidthInches,
|
||||
pumpType,
|
||||
maxGpm,
|
||||
maxPsi,
|
||||
powerSource,
|
||||
purchaseDate,
|
||||
purchasePrice,
|
||||
notes,
|
||||
isActive
|
||||
} = req.body;
|
||||
|
||||
// Check if equipment exists and belongs to user
|
||||
@@ -222,36 +438,28 @@ router.put('/:id', validateParams(idParamSchema), validateRequest(equipmentSchem
|
||||
throw new AppError('Equipment not found', 404);
|
||||
}
|
||||
|
||||
// Verify equipment type exists
|
||||
const typeCheck = await pool.query(
|
||||
'SELECT id, name, category FROM equipment_types WHERE id = $1',
|
||||
[equipmentTypeId]
|
||||
);
|
||||
|
||||
if (typeCheck.rows.length === 0) {
|
||||
throw new AppError('Equipment type not found', 404);
|
||||
}
|
||||
|
||||
const equipmentType = typeCheck.rows[0];
|
||||
|
||||
// Validate required fields based on equipment type
|
||||
if (equipmentType.category === 'sprayer') {
|
||||
if (!tankSize || !pumpGpm || !nozzleGpm || !nozzleCount) {
|
||||
throw new AppError('Tank size, pump GPM, nozzle GPM, and nozzle count are required for sprayers', 400);
|
||||
}
|
||||
}
|
||||
|
||||
if (equipmentType.category === 'spreader' && !spreaderWidth) {
|
||||
throw new AppError('Spreader width is required for spreaders', 400);
|
||||
}
|
||||
|
||||
const result = await pool.query(
|
||||
`UPDATE user_equipment
|
||||
SET equipment_type_id = $1, custom_name = $2, tank_size = $3, pump_gpm = $4,
|
||||
nozzle_gpm = $5, nozzle_count = $6, spreader_width = $7, updated_at = CURRENT_TIMESTAMP
|
||||
WHERE id = $8
|
||||
SET equipment_type_id = $1, category_id = $2, custom_name = $3, manufacturer = $4, model = $5,
|
||||
capacity_lbs = $6, spreader_type = $7, spread_width = $8,
|
||||
tank_size_gallons = $9, sprayer_type = $10, spray_width_feet = $11, pump_gpm = $12, pump_psi = $13, boom_sections = $14,
|
||||
mower_style = $15, cutting_width_inches = $16, engine_hp = $17, fuel_type = $18,
|
||||
tool_type = $19, working_width_inches = $20,
|
||||
pump_type = $21, max_gpm = $22, max_psi = $23, power_source = $24,
|
||||
purchase_date = $25, purchase_price = $26, notes = $27, is_active = $28,
|
||||
updated_at = CURRENT_TIMESTAMP
|
||||
WHERE id = $29
|
||||
RETURNING *`,
|
||||
[equipmentTypeId, customName, tankSize, pumpGpm, nozzleGpm, nozzleCount, spreaderWidth, equipmentId]
|
||||
[
|
||||
equipmentTypeId, categoryId, customName, manufacturer, model,
|
||||
capacityLbs, spreaderType, spreadWidth,
|
||||
tankSizeGallons, sprayerType, sprayWidthFeet, pumpGpm, pumpPsi, boomSections,
|
||||
mowerStyle, cuttingWidthInches, engineHp, fuelType,
|
||||
toolType, workingWidthInches,
|
||||
pumpType, maxGpm, maxPsi, powerSource,
|
||||
purchaseDate, purchasePrice, notes, isActive !== undefined ? isActive : true,
|
||||
equipmentId
|
||||
]
|
||||
);
|
||||
|
||||
const equipment = result.rows[0];
|
||||
@@ -263,14 +471,33 @@ router.put('/:id', validateParams(idParamSchema), validateRequest(equipmentSchem
|
||||
equipment: {
|
||||
id: equipment.id,
|
||||
equipmentTypeId: equipment.equipment_type_id,
|
||||
typeName: equipmentType.name,
|
||||
category: equipmentType.category,
|
||||
categoryId: equipment.category_id,
|
||||
customName: equipment.custom_name,
|
||||
tankSize: parseFloat(equipment.tank_size),
|
||||
pumpGpm: parseFloat(equipment.pump_gpm),
|
||||
nozzleGpm: parseFloat(equipment.nozzle_gpm),
|
||||
nozzleCount: equipment.nozzle_count,
|
||||
spreaderWidth: parseFloat(equipment.spreader_width),
|
||||
manufacturer: equipment.manufacturer,
|
||||
model: equipment.model,
|
||||
capacityLbs: parseFloat(equipment.capacity_lbs) || null,
|
||||
spreaderType: equipment.spreader_type,
|
||||
spreadWidth: parseFloat(equipment.spread_width) || null,
|
||||
tankSizeGallons: parseFloat(equipment.tank_size_gallons) || null,
|
||||
sprayerType: equipment.sprayer_type,
|
||||
sprayWidthFeet: parseFloat(equipment.spray_width_feet) || null,
|
||||
pumpGpm: parseFloat(equipment.pump_gpm) || null,
|
||||
pumpPsi: parseFloat(equipment.pump_psi) || null,
|
||||
boomSections: equipment.boom_sections,
|
||||
mowerStyle: equipment.mower_style,
|
||||
cuttingWidthInches: parseFloat(equipment.cutting_width_inches) || null,
|
||||
engineHp: parseFloat(equipment.engine_hp) || null,
|
||||
fuelType: equipment.fuel_type,
|
||||
toolType: equipment.tool_type,
|
||||
workingWidthInches: parseFloat(equipment.working_width_inches) || null,
|
||||
pumpType: equipment.pump_type,
|
||||
maxGpm: parseFloat(equipment.max_gpm) || null,
|
||||
maxPsi: parseFloat(equipment.max_psi) || null,
|
||||
powerSource: equipment.power_source,
|
||||
purchaseDate: equipment.purchase_date,
|
||||
purchasePrice: parseFloat(equipment.purchase_price) || null,
|
||||
notes: equipment.notes,
|
||||
isActive: equipment.is_active,
|
||||
createdAt: equipment.created_at,
|
||||
updatedAt: equipment.updated_at
|
||||
}
|
||||
@@ -321,128 +548,4 @@ router.delete('/:id', validateParams(idParamSchema), async (req, res, next) => {
|
||||
}
|
||||
});
|
||||
|
||||
// @route GET /api/equipment/:id/calculations
|
||||
// @desc Get application calculations for equipment
|
||||
// @access Private
|
||||
router.get('/:id/calculations', validateParams(idParamSchema), async (req, res, next) => {
|
||||
try {
|
||||
const equipmentId = req.params.id;
|
||||
const { area, rateAmount, rateUnit } = req.query;
|
||||
|
||||
if (!area || !rateAmount || !rateUnit) {
|
||||
throw new AppError('Area, rate amount, and rate unit are required for calculations', 400);
|
||||
}
|
||||
|
||||
// Get equipment details
|
||||
const equipmentResult = await pool.query(
|
||||
`SELECT ue.*, et.category
|
||||
FROM user_equipment ue
|
||||
JOIN equipment_types et ON ue.equipment_type_id = et.id
|
||||
WHERE ue.id = $1 AND ue.user_id = $2`,
|
||||
[equipmentId, req.user.id]
|
||||
);
|
||||
|
||||
if (equipmentResult.rows.length === 0) {
|
||||
throw new AppError('Equipment not found', 404);
|
||||
}
|
||||
|
||||
const equipment = equipmentResult.rows[0];
|
||||
const targetArea = parseFloat(area);
|
||||
const rate = parseFloat(rateAmount);
|
||||
|
||||
let calculations = {};
|
||||
|
||||
if (equipment.category === 'sprayer') {
|
||||
// Liquid application calculations
|
||||
const tankSize = parseFloat(equipment.tank_size);
|
||||
const pumpGpm = parseFloat(equipment.pump_gpm);
|
||||
const nozzleGpm = parseFloat(equipment.nozzle_gpm);
|
||||
const nozzleCount = parseInt(equipment.nozzle_count);
|
||||
|
||||
// Calculate total nozzle output
|
||||
const totalNozzleGpm = nozzleGpm * nozzleCount;
|
||||
|
||||
let productAmount, waterAmount, targetSpeed;
|
||||
|
||||
if (rateUnit.includes('gal/1000sqft') || rateUnit.includes('gal/acre')) {
|
||||
// Gallons per area - calculate water volume needed
|
||||
const multiplier = rateUnit.includes('acre') ? targetArea / 43560 : targetArea / 1000;
|
||||
waterAmount = rate * multiplier;
|
||||
productAmount = 0; // Pure water application
|
||||
} else if (rateUnit.includes('oz/gal/1000sqft')) {
|
||||
// Ounces per gallon per 1000 sqft
|
||||
const waterGallonsNeeded = targetArea / 1000; // 1 gallon per 1000 sqft default
|
||||
productAmount = rate * waterGallonsNeeded;
|
||||
waterAmount = waterGallonsNeeded * 128; // Convert to ounces
|
||||
} else {
|
||||
// Default liquid calculation
|
||||
productAmount = rate * (targetArea / 1000);
|
||||
waterAmount = tankSize * 128; // Tank capacity in ounces
|
||||
}
|
||||
|
||||
// Calculate target speed (assuming 20 foot spray width as default)
|
||||
const sprayWidth = 20; // feet
|
||||
const minutesToCover = waterAmount / (totalNozzleGpm * 128); // Convert GPM to oz/min
|
||||
const distanceFeet = targetArea / sprayWidth;
|
||||
targetSpeed = (distanceFeet / minutesToCover) * (60 / 5280); // Convert to MPH
|
||||
|
||||
calculations = {
|
||||
productAmount: Math.round(productAmount * 100) / 100,
|
||||
waterAmount: Math.round(waterAmount * 100) / 100,
|
||||
targetSpeed: Math.round(targetSpeed * 100) / 100,
|
||||
tankCount: Math.ceil(waterAmount / (tankSize * 128)),
|
||||
applicationType: 'liquid',
|
||||
unit: rateUnit.includes('oz') ? 'oz' : 'gal'
|
||||
};
|
||||
} else if (equipment.category === 'spreader') {
|
||||
// Granular application calculations
|
||||
const spreaderWidth = parseFloat(equipment.spreader_width);
|
||||
|
||||
let productAmount, targetSpeed;
|
||||
|
||||
if (rateUnit.includes('lbs/1000sqft')) {
|
||||
productAmount = rate * (targetArea / 1000);
|
||||
} else if (rateUnit.includes('lbs/acre')) {
|
||||
productAmount = rate * (targetArea / 43560);
|
||||
} else {
|
||||
productAmount = rate * (targetArea / 1000); // Default to per 1000 sqft
|
||||
}
|
||||
|
||||
// Calculate target speed (assuming 3 MPH walking speed as baseline)
|
||||
const baselineSpeed = 3; // MPH
|
||||
const minutesToSpread = 60; // Assume 1 hour coverage
|
||||
const distanceFeet = targetArea / spreaderWidth;
|
||||
targetSpeed = (distanceFeet / (minutesToSpread * 60)) * (60 / 5280); // Convert to MPH
|
||||
|
||||
calculations = {
|
||||
productAmount: Math.round(productAmount * 100) / 100,
|
||||
targetSpeed: Math.round(targetSpeed * 100) / 100,
|
||||
applicationType: 'granular',
|
||||
unit: 'lbs',
|
||||
coverageTime: Math.round((targetArea / (spreaderWidth * baselineSpeed * 5280 / 60)) * 100) / 100
|
||||
};
|
||||
}
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: {
|
||||
calculations,
|
||||
equipment: {
|
||||
id: equipment.id,
|
||||
category: equipment.category,
|
||||
tankSize: equipment.tank_size,
|
||||
spreaderWidth: equipment.spreader_width
|
||||
},
|
||||
inputs: {
|
||||
area: targetArea,
|
||||
rate: rate,
|
||||
unit: rateUnit
|
||||
}
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
503
backend/src/routes/nozzles.js
Normal file
503
backend/src/routes/nozzles.js
Normal 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;
|
||||
Reference in New Issue
Block a user