update equipment stuff
This commit is contained in:
@@ -7,51 +7,72 @@ const { AppError } = require('../middleware/errorHandler');
|
||||
const router = express.Router();
|
||||
|
||||
// @route GET /api/nozzles/types
|
||||
// @desc Get all nozzle types
|
||||
// @desc Get all nozzle types with flow rate data
|
||||
// @access Private
|
||||
router.get('/types', async (req, res, next) => {
|
||||
try {
|
||||
const { manufacturer, droplet_size, spray_pattern } = req.query;
|
||||
const { manufacturer, droplet_size, spray_pattern, orifice_size } = req.query;
|
||||
|
||||
let query = 'SELECT * FROM nozzle_types WHERE 1=1';
|
||||
let query = `
|
||||
SELECT nt.*,
|
||||
json_agg(
|
||||
json_build_object(
|
||||
'pressure_psi', nfr.pressure_psi,
|
||||
'flow_rate_gpm', nfr.flow_rate_gpm
|
||||
) ORDER BY nfr.pressure_psi
|
||||
) FILTER (WHERE nfr.id IS NOT NULL) as flow_rates
|
||||
FROM nozzle_types nt
|
||||
LEFT JOIN nozzle_flow_rates nfr ON nt.id = nfr.nozzle_type_id
|
||||
WHERE 1=1
|
||||
`;
|
||||
const queryParams = [];
|
||||
|
||||
if (manufacturer) {
|
||||
queryParams.push(manufacturer);
|
||||
query += ` AND manufacturer ILIKE $${queryParams.length}`;
|
||||
query += ` AND nt.manufacturer ILIKE $${queryParams.length}`;
|
||||
}
|
||||
|
||||
if (droplet_size) {
|
||||
queryParams.push(droplet_size);
|
||||
query += ` AND droplet_size = $${queryParams.length}`;
|
||||
query += ` AND nt.droplet_size = $${queryParams.length}`;
|
||||
}
|
||||
|
||||
if (spray_pattern) {
|
||||
queryParams.push(spray_pattern);
|
||||
query += ` AND spray_pattern = $${queryParams.length}`;
|
||||
query += ` AND nt.spray_pattern = $${queryParams.length}`;
|
||||
}
|
||||
|
||||
query += ' ORDER BY manufacturer, name';
|
||||
if (orifice_size) {
|
||||
queryParams.push(orifice_size);
|
||||
query += ` AND nt.orifice_size = $${queryParams.length}`;
|
||||
}
|
||||
|
||||
query += ' GROUP BY nt.id ORDER BY nt.manufacturer, nt.name';
|
||||
|
||||
const result = await pool.query(query, queryParams);
|
||||
|
||||
// Group by manufacturer for easier frontend handling
|
||||
// Group by manufacturer and droplet size for easier frontend handling
|
||||
const nozzlesByManufacturer = result.rows.reduce((acc, nozzle) => {
|
||||
const manufacturer = nozzle.manufacturer || 'Unknown';
|
||||
if (!acc[manufacturer]) {
|
||||
acc[manufacturer] = [];
|
||||
acc[manufacturer] = {};
|
||||
}
|
||||
acc[manufacturer].push({
|
||||
if (!acc[manufacturer][nozzle.droplet_size]) {
|
||||
acc[manufacturer][nozzle.droplet_size] = [];
|
||||
}
|
||||
acc[manufacturer][nozzle.droplet_size].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,
|
||||
material: nozzle.material,
|
||||
threadSize: nozzle.thread_size,
|
||||
colorCode: nozzle.color_code,
|
||||
flowRates: nozzle.flow_rates || [],
|
||||
createdAt: nozzle.created_at
|
||||
});
|
||||
return acc;
|
||||
@@ -67,10 +88,12 @@ router.get('/types', async (req, res, next) => {
|
||||
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,
|
||||
material: nt.material,
|
||||
threadSize: nt.thread_size,
|
||||
colorCode: nt.color_code,
|
||||
flowRates: nt.flow_rates || [],
|
||||
createdAt: nt.created_at
|
||||
})),
|
||||
nozzlesByManufacturer
|
||||
@@ -81,6 +104,63 @@ router.get('/types', async (req, res, next) => {
|
||||
}
|
||||
});
|
||||
|
||||
// @route GET /api/nozzles/flow-rate/:nozzleTypeId/:pressurePsi
|
||||
// @desc Get flow rate for specific nozzle at specific pressure
|
||||
// @access Private
|
||||
router.get('/flow-rate/:nozzleTypeId/:pressurePsi', validateParams(idParamSchema), async (req, res, next) => {
|
||||
try {
|
||||
const { nozzleTypeId, pressurePsi } = req.params;
|
||||
|
||||
// Get exact match or interpolate between pressures
|
||||
const result = await pool.query(
|
||||
`SELECT nfr.pressure_psi, nfr.flow_rate_gpm, nt.name, nt.manufacturer
|
||||
FROM nozzle_flow_rates nfr
|
||||
JOIN nozzle_types nt ON nfr.nozzle_type_id = nt.id
|
||||
WHERE nfr.nozzle_type_id = $1
|
||||
ORDER BY ABS(nfr.pressure_psi - $2)
|
||||
LIMIT 2`,
|
||||
[nozzleTypeId, pressurePsi]
|
||||
);
|
||||
|
||||
if (result.rows.length === 0) {
|
||||
throw new AppError('Flow rate data not available for this nozzle', 404);
|
||||
}
|
||||
|
||||
let flowRate;
|
||||
if (result.rows.length === 1 || result.rows[0].pressure_psi === parseInt(pressurePsi)) {
|
||||
// Exact match or only one data point
|
||||
flowRate = parseFloat(result.rows[0].flow_rate_gpm);
|
||||
} else {
|
||||
// Interpolate between two points
|
||||
const p1 = result.rows[0];
|
||||
const p2 = result.rows[1];
|
||||
const pressure = parseInt(pressurePsi);
|
||||
|
||||
if (p1.pressure_psi === p2.pressure_psi) {
|
||||
flowRate = parseFloat(p1.flow_rate_gpm);
|
||||
} else {
|
||||
// Linear interpolation
|
||||
const slope = (p2.flow_rate_gpm - p1.flow_rate_gpm) / (p2.pressure_psi - p1.pressure_psi);
|
||||
flowRate = parseFloat(p1.flow_rate_gpm) + slope * (pressure - p1.pressure_psi);
|
||||
flowRate = Math.round(flowRate * 1000) / 1000; // Round to 3 decimal places
|
||||
}
|
||||
}
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: {
|
||||
nozzleTypeId: parseInt(nozzleTypeId),
|
||||
pressurePsi: parseInt(pressurePsi),
|
||||
flowRateGpm: flowRate,
|
||||
nozzleName: result.rows[0].name,
|
||||
manufacturer: result.rows[0].manufacturer
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
});
|
||||
|
||||
// @route GET /api/nozzles
|
||||
// @desc Get all user's nozzles
|
||||
// @access Private
|
||||
@@ -500,4 +580,250 @@ router.delete('/assignments/:assignmentId', validateParams(idParamSchema), async
|
||||
}
|
||||
});
|
||||
|
||||
// PUMP ASSIGNMENT ROUTES
|
||||
|
||||
// @route GET /api/nozzles/equipment/:equipmentId/pump
|
||||
// @desc Get pump assigned to sprayer
|
||||
// @access Private
|
||||
router.get('/equipment/:equipmentId/pump', 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 epa.*,
|
||||
pump_eq.custom_name as pump_name, pump_eq.manufacturer as pump_manufacturer,
|
||||
pump_eq.model as pump_model, pump_eq.max_gpm, pump_eq.max_psi
|
||||
FROM equipment_pump_assignments epa
|
||||
JOIN user_equipment pump_eq ON epa.pump_equipment_id = pump_eq.id
|
||||
WHERE epa.sprayer_equipment_id = $1 AND epa.is_active = true`,
|
||||
[equipmentId]
|
||||
);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: {
|
||||
pumpAssignment: result.rows.length > 0 ? {
|
||||
id: result.rows[0].id,
|
||||
sprayerEquipmentId: result.rows[0].sprayer_equipment_id,
|
||||
pumpEquipmentId: result.rows[0].pump_equipment_id,
|
||||
pumpName: result.rows[0].pump_name,
|
||||
pumpManufacturer: result.rows[0].pump_manufacturer,
|
||||
pumpModel: result.rows[0].pump_model,
|
||||
maxGpm: parseFloat(result.rows[0].max_gpm) || null,
|
||||
maxPsi: parseFloat(result.rows[0].max_psi) || null,
|
||||
assignedDate: result.rows[0].assigned_date,
|
||||
notes: result.rows[0].notes,
|
||||
isActive: result.rows[0].is_active
|
||||
} : null
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
});
|
||||
|
||||
// @route POST /api/nozzles/equipment/:equipmentId/pump
|
||||
// @desc Assign pump to sprayer
|
||||
// @access Private
|
||||
router.post('/equipment/:equipmentId/pump', validateParams(idParamSchema), async (req, res, next) => {
|
||||
try {
|
||||
const sprayerEquipmentId = req.params.equipmentId;
|
||||
const { pumpEquipmentId, notes } = req.body;
|
||||
|
||||
// Verify both equipment belong to user
|
||||
const equipmentCheck = await pool.query(
|
||||
'SELECT id, category_id FROM user_equipment WHERE id IN ($1, $2) AND user_id = $3',
|
||||
[sprayerEquipmentId, pumpEquipmentId, req.user.id]
|
||||
);
|
||||
|
||||
if (equipmentCheck.rows.length !== 2) {
|
||||
throw new AppError('Equipment not found', 404);
|
||||
}
|
||||
|
||||
// Deactivate any existing pump assignments for this sprayer
|
||||
await pool.query(
|
||||
'UPDATE equipment_pump_assignments SET is_active = false WHERE sprayer_equipment_id = $1',
|
||||
[sprayerEquipmentId]
|
||||
);
|
||||
|
||||
// Create new assignment
|
||||
const result = await pool.query(
|
||||
`INSERT INTO equipment_pump_assignments
|
||||
(sprayer_equipment_id, pump_equipment_id, notes)
|
||||
VALUES ($1, $2, $3)
|
||||
RETURNING *`,
|
||||
[sprayerEquipmentId, pumpEquipmentId, notes]
|
||||
);
|
||||
|
||||
const assignment = result.rows[0];
|
||||
|
||||
res.status(201).json({
|
||||
success: true,
|
||||
message: 'Pump assigned to sprayer successfully',
|
||||
data: {
|
||||
assignment: {
|
||||
id: assignment.id,
|
||||
sprayerEquipmentId: assignment.sprayer_equipment_id,
|
||||
pumpEquipmentId: assignment.pump_equipment_id,
|
||||
assignedDate: assignment.assigned_date,
|
||||
notes: assignment.notes,
|
||||
isActive: assignment.is_active
|
||||
}
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
});
|
||||
|
||||
// @route DELETE /api/nozzles/equipment/:equipmentId/pump
|
||||
// @desc Remove pump assignment from sprayer
|
||||
// @access Private
|
||||
router.delete('/equipment/:equipmentId/pump', validateParams(idParamSchema), async (req, res, next) => {
|
||||
try {
|
||||
const sprayerEquipmentId = req.params.equipmentId;
|
||||
|
||||
// Verify sprayer belongs to user
|
||||
const equipmentCheck = await pool.query(
|
||||
'SELECT id FROM user_equipment WHERE id = $1 AND user_id = $2',
|
||||
[sprayerEquipmentId, req.user.id]
|
||||
);
|
||||
|
||||
if (equipmentCheck.rows.length === 0) {
|
||||
throw new AppError('Equipment not found', 404);
|
||||
}
|
||||
|
||||
await pool.query(
|
||||
'UPDATE equipment_pump_assignments SET is_active = false WHERE sprayer_equipment_id = $1',
|
||||
[sprayerEquipmentId]
|
||||
);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: 'Pump assignment removed successfully'
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
});
|
||||
|
||||
// NOZZLE CONFIGURATION ROUTES
|
||||
|
||||
// @route GET /api/nozzles/equipment/:equipmentId/configurations
|
||||
// @desc Get nozzle configurations for equipment
|
||||
// @access Private
|
||||
router.get('/equipment/:equipmentId/configurations', 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 enc.*,
|
||||
COUNT(cna.id) as nozzle_count
|
||||
FROM equipment_nozzle_configurations enc
|
||||
LEFT JOIN configuration_nozzle_assignments cna ON enc.id = cna.configuration_id
|
||||
WHERE enc.user_equipment_id = $1
|
||||
GROUP BY enc.id
|
||||
ORDER BY enc.is_default DESC, enc.configuration_name`,
|
||||
[equipmentId]
|
||||
);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: {
|
||||
configurations: result.rows.map(config => ({
|
||||
id: config.id,
|
||||
userEquipmentId: config.user_equipment_id,
|
||||
configurationName: config.configuration_name,
|
||||
isDefault: config.is_default,
|
||||
nozzleCount: parseInt(config.nozzle_count),
|
||||
createdAt: config.created_at,
|
||||
updatedAt: config.updated_at
|
||||
}))
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
});
|
||||
|
||||
// @route GET /api/nozzles/configurations/:configId/details
|
||||
// @desc Get detailed nozzle configuration with assignments
|
||||
// @access Private
|
||||
router.get('/configurations/:configId/details', validateParams(idParamSchema), async (req, res, next) => {
|
||||
try {
|
||||
const configId = req.params.configId;
|
||||
|
||||
// Verify configuration belongs to user's equipment
|
||||
const configCheck = await pool.query(
|
||||
`SELECT enc.* FROM equipment_nozzle_configurations enc
|
||||
JOIN user_equipment ue ON enc.user_equipment_id = ue.id
|
||||
WHERE enc.id = $1 AND ue.user_id = $2`,
|
||||
[configId, req.user.id]
|
||||
);
|
||||
|
||||
if (configCheck.rows.length === 0) {
|
||||
throw new AppError('Configuration not found', 404);
|
||||
}
|
||||
|
||||
const result = await pool.query(
|
||||
`SELECT cna.*, nt.name, nt.manufacturer, nt.orifice_size, nt.spray_angle,
|
||||
nt.droplet_size, nt.spray_pattern, nt.color_code,
|
||||
nfr.flow_rate_gpm
|
||||
FROM configuration_nozzle_assignments cna
|
||||
JOIN nozzle_types nt ON cna.nozzle_type_id = nt.id
|
||||
LEFT JOIN nozzle_flow_rates nfr ON nt.id = nfr.nozzle_type_id AND nfr.pressure_psi = cna.operating_pressure_psi
|
||||
WHERE cna.configuration_id = $1
|
||||
ORDER BY cna.position`,
|
||||
[configId]
|
||||
);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: {
|
||||
configuration: configCheck.rows[0],
|
||||
assignments: result.rows.map(item => ({
|
||||
id: item.id,
|
||||
configurationId: item.configuration_id,
|
||||
nozzleTypeId: item.nozzle_type_id,
|
||||
position: item.position,
|
||||
quantity: item.quantity,
|
||||
operatingPressurePsi: item.operating_pressure_psi,
|
||||
notes: item.notes,
|
||||
nozzleName: item.name,
|
||||
manufacturer: item.manufacturer,
|
||||
orificeSize: item.orifice_size,
|
||||
sprayAngle: item.spray_angle,
|
||||
dropletSize: item.droplet_size,
|
||||
sprayPattern: item.spray_pattern,
|
||||
colorCode: item.color_code,
|
||||
flowRateGpm: parseFloat(item.flow_rate_gpm) || null,
|
||||
createdAt: item.created_at
|
||||
}))
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
269
database/migrations/fix_sprayer_pump_nozzle_relationships.sql
Normal file
269
database/migrations/fix_sprayer_pump_nozzle_relationships.sql
Normal file
@@ -0,0 +1,269 @@
|
||||
-- Fix Sprayer-Pump-Nozzle Relationships
|
||||
-- This creates proper equipment connections and comprehensive nozzle database
|
||||
|
||||
-- Step 1: Create equipment_pump_assignments table (sprayers can have pumps)
|
||||
CREATE TABLE IF NOT EXISTS equipment_pump_assignments (
|
||||
id SERIAL PRIMARY KEY,
|
||||
sprayer_equipment_id INTEGER REFERENCES user_equipment(id) ON DELETE CASCADE,
|
||||
pump_equipment_id INTEGER REFERENCES user_equipment(id) ON DELETE CASCADE,
|
||||
assigned_date DATE DEFAULT CURRENT_DATE,
|
||||
is_active BOOLEAN DEFAULT true,
|
||||
notes TEXT,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
UNIQUE(sprayer_equipment_id, pump_equipment_id)
|
||||
);
|
||||
|
||||
-- Step 2: Update nozzle_types table with comprehensive flow rate data
|
||||
DROP TABLE IF EXISTS nozzle_types CASCADE;
|
||||
CREATE TABLE nozzle_types (
|
||||
id SERIAL PRIMARY KEY,
|
||||
name VARCHAR(255) NOT NULL UNIQUE,
|
||||
manufacturer VARCHAR(100) NOT NULL,
|
||||
model VARCHAR(100),
|
||||
orifice_size VARCHAR(20) NOT NULL,
|
||||
spray_angle INTEGER NOT NULL,
|
||||
droplet_size VARCHAR(50) NOT NULL, -- fine, medium, coarse, very_coarse, extremely_coarse
|
||||
spray_pattern VARCHAR(50) NOT NULL, -- flat_fan, hollow_cone, full_cone, flooding
|
||||
material VARCHAR(50), -- ceramic, stainless_steel, hardened_steel, polymer
|
||||
thread_size VARCHAR(20), -- 1/4", 3/8", 1/2"
|
||||
color_code VARCHAR(20), -- TeeJet color coding
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
|
||||
-- Step 3: Create nozzle flow rate data table (pressure vs flow rate charts)
|
||||
CREATE TABLE IF NOT EXISTS nozzle_flow_rates (
|
||||
id SERIAL PRIMARY KEY,
|
||||
nozzle_type_id INTEGER REFERENCES nozzle_types(id) ON DELETE CASCADE,
|
||||
pressure_psi INTEGER NOT NULL,
|
||||
flow_rate_gpm DECIMAL(6, 3) NOT NULL,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
UNIQUE(nozzle_type_id, pressure_psi)
|
||||
);
|
||||
|
||||
-- Step 4: Insert comprehensive TeeJet nozzle database
|
||||
INSERT INTO nozzle_types (name, manufacturer, model, orifice_size, spray_angle, droplet_size, spray_pattern, material, thread_size, color_code) VALUES
|
||||
-- XR Extended Range Flat Fan Nozzles (Fine droplets)
|
||||
('XR8001', 'TeeJet', 'XR8001VK', '01', 80, 'fine', 'flat_fan', 'stainless_steel', '1/4"', 'Orange'),
|
||||
('XR8002', 'TeeJet', 'XR8002VK', '02', 80, 'fine', 'flat_fan', 'stainless_steel', '1/4"', 'Yellow'),
|
||||
('XR8003', 'TeeJet', 'XR8003VK', '03', 80, 'fine', 'flat_fan', 'stainless_steel', '1/4"', 'Blue'),
|
||||
('XR8004', 'TeeJet', 'XR8004VK', '04', 80, 'fine', 'flat_fan', 'stainless_steel', '1/4"', 'Red'),
|
||||
('XR8005', 'TeeJet', 'XR8005VK', '05', 80, 'fine', 'flat_fan', 'stainless_steel', '1/4"', 'Brown'),
|
||||
('XR8006', 'TeeJet', 'XR8006VK', '06', 80, 'fine', 'flat_fan', 'stainless_steel', '1/4"', 'Gray'),
|
||||
('XR8008', 'TeeJet', 'XR8008VK', '08', 80, 'fine', 'flat_fan', 'stainless_steel', '1/4"', 'White'),
|
||||
|
||||
-- AIXR Air Induction Extended Range (Medium droplets - drift reduction)
|
||||
('AIXR11001', 'TeeJet', 'AIXR11001', '01', 110, 'medium', 'flat_fan', 'polymer', '1/4"', 'Orange'),
|
||||
('AIXR11002', 'TeeJet', 'AIXR11002', '02', 110, 'medium', 'flat_fan', 'polymer', '1/4"', 'Yellow'),
|
||||
('AIXR11003', 'TeeJet', 'AIXR11003', '03', 110, 'medium', 'flat_fan', 'polymer', '1/4"', 'Blue'),
|
||||
('AIXR11004', 'TeeJet', 'AIXR11004', '04', 110, 'medium', 'flat_fan', 'polymer', '1/4"', 'Red'),
|
||||
('AIXR11005', 'TeeJet', 'AIXR11005', '05', 110, 'medium', 'flat_fan', 'polymer', '1/4"', 'Brown'),
|
||||
('AIXR11006', 'TeeJet', 'AIXR11006', '06', 110, 'medium', 'flat_fan', 'polymer', '1/4"', 'Gray'),
|
||||
('AIXR11008', 'TeeJet', 'AIXR11008', '08', 110, 'medium', 'flat_fan', 'polymer', '1/4"', 'White'),
|
||||
|
||||
-- TTI TurboDrop (Very coarse droplets - maximum drift reduction)
|
||||
('TTI11002', 'TeeJet', 'TTI11002', '02', 110, 'very_coarse', 'flat_fan', 'polymer', '1/4"', 'Yellow'),
|
||||
('TTI11003', 'TeeJet', 'TTI11003', '03', 110, 'very_coarse', 'flat_fan', 'polymer', '1/4"', 'Blue'),
|
||||
('TTI11004', 'TeeJet', 'TTI11004', '04', 110, 'very_coarse', 'flat_fan', 'polymer', '1/4"', 'Red'),
|
||||
('TTI11005', 'TeeJet', 'TTI11005', '05', 110, 'very_coarse', 'flat_fan', 'polymer', '1/4"', 'Brown'),
|
||||
('TTI11006', 'TeeJet', 'TTI11006', '06', 110, 'very_coarse', 'flat_fan', 'polymer', '1/4"', 'Gray'),
|
||||
|
||||
-- AI TeeJet Air Induction (Coarse droplets)
|
||||
('AI11002', 'TeeJet', 'AI11002-VS', '02', 110, 'coarse', 'flat_fan', 'stainless_steel', '1/4"', 'Yellow'),
|
||||
('AI11003', 'TeeJet', 'AI11003-VS', '03', 110, 'coarse', 'flat_fan', 'stainless_steel', '1/4"', 'Blue'),
|
||||
('AI11004', 'TeeJet', 'AI11004-VS', '04', 110, 'coarse', 'flat_fan', 'stainless_steel', '1/4"', 'Red'),
|
||||
('AI11005', 'TeeJet', 'AI11005-VS', '05', 110, 'coarse', 'flat_fan', 'stainless_steel', '1/4"', 'Brown'),
|
||||
|
||||
-- DG DropGuard (Extremely coarse droplets)
|
||||
('DG11002', 'TeeJet', 'DG11002-VS', '02', 110, 'extremely_coarse', 'flat_fan', 'stainless_steel', '1/4"', 'Yellow'),
|
||||
('DG11003', 'TeeJet', 'DG11003-VS', '03', 110, 'extremely_coarse', 'flat_fan', 'stainless_steel', '1/4"', 'Blue'),
|
||||
('DG11004', 'TeeJet', 'DG11004-VS', '04', 110, 'extremely_coarse', 'flat_fan', 'stainless_steel', '1/4"', 'Red'),
|
||||
|
||||
-- Hollow Cone Nozzles
|
||||
('D2-23', 'TeeJet', 'D2-23', '2', 80, 'fine', 'hollow_cone', 'hardened_steel', '1/4"', 'Yellow'),
|
||||
('D3-25', 'TeeJet', 'D3-25', '3', 80, 'fine', 'hollow_cone', 'hardened_steel', '1/4"', 'Blue'),
|
||||
('D4-25', 'TeeJet', 'D4-25', '4', 80, 'fine', 'hollow_cone', 'hardened_steel', '1/4"', 'Red'),
|
||||
|
||||
-- Full Cone Nozzles
|
||||
('TXA8002VK', 'TeeJet', 'TXA8002VK', '02', 80, 'medium', 'full_cone', 'stainless_steel', '1/4"', 'Yellow'),
|
||||
('TXA8004VK', 'TeeJet', 'TXA8004VK', '04', 80, 'medium', 'full_cone', 'stainless_steel', '1/4"', 'Red'),
|
||||
('TXA8006VK', 'TeeJet', 'TXA8006VK', '06', 80, 'medium', 'full_cone', 'stainless_steel', '1/4"', 'Gray');
|
||||
|
||||
-- Step 5: Insert flow rate data based on TeeJet charts
|
||||
-- XR Series Flow Rates (GPM at different PSI)
|
||||
INSERT INTO nozzle_flow_rates (nozzle_type_id, pressure_psi, flow_rate_gpm) VALUES
|
||||
-- XR8001
|
||||
((SELECT id FROM nozzle_types WHERE name = 'XR8001'), 15, 0.10),
|
||||
((SELECT id FROM nozzle_types WHERE name = 'XR8001'), 20, 0.11),
|
||||
((SELECT id FROM nozzle_types WHERE name = 'XR8001'), 30, 0.14),
|
||||
((SELECT id FROM nozzle_types WHERE name = 'XR8001'), 40, 0.16),
|
||||
((SELECT id FROM nozzle_types WHERE name = 'XR8001'), 60, 0.20),
|
||||
((SELECT id FROM nozzle_types WHERE name = 'XR8001'), 80, 0.23),
|
||||
|
||||
-- XR8002
|
||||
((SELECT id FROM nozzle_types WHERE name = 'XR8002'), 15, 0.20),
|
||||
((SELECT id FROM nozzle_types WHERE name = 'XR8002'), 20, 0.23),
|
||||
((SELECT id FROM nozzle_types WHERE name = 'XR8002'), 30, 0.28),
|
||||
((SELECT id FROM nozzle_types WHERE name = 'XR8002'), 40, 0.32),
|
||||
((SELECT id FROM nozzle_types WHERE name = 'XR8002'), 60, 0.39),
|
||||
((SELECT id FROM nozzle_types WHERE name = 'XR8002'), 80, 0.45),
|
||||
|
||||
-- XR8003
|
||||
((SELECT id FROM nozzle_types WHERE name = 'XR8003'), 15, 0.30),
|
||||
((SELECT id FROM nozzle_types WHERE name = 'XR8003'), 20, 0.35),
|
||||
((SELECT id FROM nozzle_types WHERE name = 'XR8003'), 30, 0.42),
|
||||
((SELECT id FROM nozzle_types WHERE name = 'XR8003'), 40, 0.49),
|
||||
((SELECT id FROM nozzle_types WHERE name = 'XR8003'), 60, 0.60),
|
||||
((SELECT id FROM nozzle_types WHERE name = 'XR8003'), 80, 0.69),
|
||||
|
||||
-- XR8004
|
||||
((SELECT id FROM nozzle_types WHERE name = 'XR8004'), 15, 0.40),
|
||||
((SELECT id FROM nozzle_types WHERE name = 'XR8004'), 20, 0.46),
|
||||
((SELECT id FROM nozzle_types WHERE name = 'XR8004'), 30, 0.56),
|
||||
((SELECT id FROM nozzle_types WHERE name = 'XR8004'), 40, 0.65),
|
||||
((SELECT id FROM nozzle_types WHERE name = 'XR8004'), 60, 0.80),
|
||||
((SELECT id FROM nozzle_types WHERE name = 'XR8004'), 80, 0.92),
|
||||
|
||||
-- XR8005
|
||||
((SELECT id FROM nozzle_types WHERE name = 'XR8005'), 15, 0.50),
|
||||
((SELECT id FROM nozzle_types WHERE name = 'XR8005'), 20, 0.58),
|
||||
((SELECT id FROM nozzle_types WHERE name = 'XR8005'), 30, 0.71),
|
||||
((SELECT id FROM nozzle_types WHERE name = 'XR8005'), 40, 0.82),
|
||||
((SELECT id FROM nozzle_types WHERE name = 'XR8005'), 60, 1.00),
|
||||
((SELECT id FROM nozzle_types WHERE name = 'XR8005'), 80, 1.15),
|
||||
|
||||
-- XR8006
|
||||
((SELECT id FROM nozzle_types WHERE name = 'XR8006'), 15, 0.60),
|
||||
((SELECT id FROM nozzle_types WHERE name = 'XR8006'), 20, 0.69),
|
||||
((SELECT id FROM nozzle_types WHERE name = 'XR8006'), 30, 0.85),
|
||||
((SELECT id FROM nozzle_types WHERE name = 'XR8006'), 40, 0.98),
|
||||
((SELECT id FROM nozzle_types WHERE name = 'XR8006'), 60, 1.20),
|
||||
((SELECT id FROM nozzle_types WHERE name = 'XR8006'), 80, 1.38),
|
||||
|
||||
-- XR8008
|
||||
((SELECT id FROM nozzle_types WHERE name = 'XR8008'), 15, 0.80),
|
||||
((SELECT id FROM nozzle_types WHERE name = 'XR8008'), 20, 0.92),
|
||||
((SELECT id FROM nozzle_types WHERE name = 'XR8008'), 30, 1.13),
|
||||
((SELECT id FROM nozzle_types WHERE name = 'XR8008'), 40, 1.30),
|
||||
((SELECT id FROM nozzle_types WHERE name = 'XR8008'), 60, 1.60),
|
||||
((SELECT id FROM nozzle_types WHERE name = 'XR8008'), 80, 1.84),
|
||||
|
||||
-- AIXR Series Flow Rates
|
||||
-- AIXR11002
|
||||
((SELECT id FROM nozzle_types WHERE name = 'AIXR11002'), 15, 0.20),
|
||||
((SELECT id FROM nozzle_types WHERE name = 'AIXR11002'), 20, 0.23),
|
||||
((SELECT id FROM nozzle_types WHERE name = 'AIXR11002'), 30, 0.28),
|
||||
((SELECT id FROM nozzle_types WHERE name = 'AIXR11002'), 40, 0.32),
|
||||
((SELECT id FROM nozzle_types WHERE name = 'AIXR11002'), 60, 0.39),
|
||||
((SELECT id FROM nozzle_types WHERE name = 'AIXR11002'), 80, 0.45),
|
||||
|
||||
-- AIXR11003
|
||||
((SELECT id FROM nozzle_types WHERE name = 'AIXR11003'), 15, 0.30),
|
||||
((SELECT id FROM nozzle_types WHERE name = 'AIXR11003'), 20, 0.35),
|
||||
((SELECT id FROM nozzle_types WHERE name = 'AIXR11003'), 30, 0.42),
|
||||
((SELECT id FROM nozzle_types WHERE name = 'AIXR11003'), 40, 0.49),
|
||||
((SELECT id FROM nozzle_types WHERE name = 'AIXR11003'), 60, 0.60),
|
||||
((SELECT id FROM nozzle_types WHERE name = 'AIXR11003'), 80, 0.69),
|
||||
|
||||
-- AIXR11004
|
||||
((SELECT id FROM nozzle_types WHERE name = 'AIXR11004'), 15, 0.40),
|
||||
((SELECT id FROM nozzle_types WHERE name = 'AIXR11004'), 20, 0.46),
|
||||
((SELECT id FROM nozzle_types WHERE name = 'AIXR11004'), 30, 0.56),
|
||||
((SELECT id FROM nozzle_types WHERE name = 'AIXR11004'), 40, 0.65),
|
||||
((SELECT id FROM nozzle_types WHERE name = 'AIXR11004'), 60, 0.80),
|
||||
((SELECT id FROM nozzle_types WHERE name = 'AIXR11004'), 80, 0.92),
|
||||
|
||||
-- AIXR11005
|
||||
((SELECT id FROM nozzle_types WHERE name = 'AIXR11005'), 15, 0.50),
|
||||
((SELECT id FROM nozzle_types WHERE name = 'AIXR11005'), 20, 0.58),
|
||||
((SELECT id FROM nozzle_types WHERE name = 'AIXR11005'), 30, 0.71),
|
||||
((SELECT id FROM nozzle_types WHERE name = 'AIXR11005'), 40, 0.82),
|
||||
((SELECT id FROM nozzle_types WHERE name = 'AIXR11005'), 60, 1.00),
|
||||
((SELECT id FROM nozzle_types WHERE name = 'AIXR11005'), 80, 1.15),
|
||||
|
||||
-- TTI Series Flow Rates (similar to AIXR but slightly different due to design)
|
||||
-- TTI11002
|
||||
((SELECT id FROM nozzle_types WHERE name = 'TTI11002'), 15, 0.18),
|
||||
((SELECT id FROM nozzle_types WHERE name = 'TTI11002'), 20, 0.21),
|
||||
((SELECT id FROM nozzle_types WHERE name = 'TTI11002'), 30, 0.26),
|
||||
((SELECT id FROM nozzle_types WHERE name = 'TTI11002'), 40, 0.30),
|
||||
((SELECT id FROM nozzle_types WHERE name = 'TTI11002'), 60, 0.37),
|
||||
((SELECT id FROM nozzle_types WHERE name = 'TTI11002'), 80, 0.42),
|
||||
|
||||
-- TTI11003
|
||||
((SELECT id FROM nozzle_types WHERE name = 'TTI11003'), 15, 0.28),
|
||||
((SELECT id FROM nozzle_types WHERE name = 'TTI11003'), 20, 0.32),
|
||||
((SELECT id FROM nozzle_types WHERE name = 'TTI11003'), 30, 0.39),
|
||||
((SELECT id FROM nozzle_types WHERE name = 'TTI11003'), 40, 0.45),
|
||||
((SELECT id FROM nozzle_types WHERE name = 'TTI11003'), 60, 0.55),
|
||||
((SELECT id FROM nozzle_types WHERE name = 'TTI11003'), 80, 0.64);
|
||||
|
||||
-- Step 6: Update user_nozzles to include current assignment info
|
||||
ALTER TABLE user_nozzles
|
||||
ADD COLUMN IF NOT EXISTS current_equipment_id INTEGER REFERENCES user_equipment(id),
|
||||
ADD COLUMN IF NOT EXISTS current_position VARCHAR(50),
|
||||
ADD COLUMN IF NOT EXISTS last_assigned_date DATE;
|
||||
|
||||
-- Step 7: Create equipment_nozzle_configurations table (for application planning)
|
||||
CREATE TABLE IF NOT EXISTS equipment_nozzle_configurations (
|
||||
id SERIAL PRIMARY KEY,
|
||||
user_equipment_id INTEGER REFERENCES user_equipment(id) ON DELETE CASCADE,
|
||||
configuration_name VARCHAR(255) NOT NULL,
|
||||
is_default BOOLEAN DEFAULT false,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
|
||||
-- Step 8: Create configuration_nozzle_assignments (specific nozzle setups)
|
||||
CREATE TABLE IF NOT EXISTS configuration_nozzle_assignments (
|
||||
id SERIAL PRIMARY KEY,
|
||||
configuration_id INTEGER REFERENCES equipment_nozzle_configurations(id) ON DELETE CASCADE,
|
||||
nozzle_type_id INTEGER REFERENCES nozzle_types(id) ON DELETE CASCADE,
|
||||
position VARCHAR(50) NOT NULL, -- boom_left_1, boom_right_1, center, etc.
|
||||
quantity INTEGER DEFAULT 1,
|
||||
operating_pressure_psi INTEGER,
|
||||
notes TEXT,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
UNIQUE(configuration_id, position)
|
||||
);
|
||||
|
||||
-- Step 9: Add triggers for updated_at
|
||||
CREATE OR REPLACE FUNCTION update_updated_at_column()
|
||||
RETURNS TRIGGER AS $$
|
||||
BEGIN
|
||||
NEW.updated_at = CURRENT_TIMESTAMP;
|
||||
RETURN NEW;
|
||||
END;
|
||||
$$ language 'plpgsql';
|
||||
|
||||
DO $$
|
||||
BEGIN
|
||||
IF NOT EXISTS (SELECT 1 FROM pg_trigger WHERE tgname = 'update_equipment_pump_assignments_updated_at') THEN
|
||||
CREATE TRIGGER update_equipment_pump_assignments_updated_at
|
||||
BEFORE UPDATE ON equipment_pump_assignments
|
||||
FOR EACH ROW
|
||||
EXECUTE FUNCTION update_updated_at_column();
|
||||
END IF;
|
||||
END $$;
|
||||
|
||||
DO $$
|
||||
BEGIN
|
||||
IF NOT EXISTS (SELECT 1 FROM pg_trigger WHERE tgname = 'update_equipment_nozzle_configurations_updated_at') THEN
|
||||
CREATE TRIGGER update_equipment_nozzle_configurations_updated_at
|
||||
BEFORE UPDATE ON equipment_nozzle_configurations
|
||||
FOR EACH ROW
|
||||
EXECUTE FUNCTION update_updated_at_column();
|
||||
END IF;
|
||||
END $$;
|
||||
|
||||
-- Step 10: Create indexes
|
||||
CREATE INDEX IF NOT EXISTS idx_equipment_pump_assignments_sprayer ON equipment_pump_assignments(sprayer_equipment_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_equipment_pump_assignments_pump ON equipment_pump_assignments(pump_equipment_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_nozzle_flow_rates_type ON nozzle_flow_rates(nozzle_type_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_nozzle_flow_rates_pressure ON nozzle_flow_rates(pressure_psi);
|
||||
CREATE INDEX IF NOT EXISTS idx_user_nozzles_equipment ON user_nozzles(current_equipment_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_configuration_nozzle_assignments_config ON configuration_nozzle_assignments(configuration_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_configuration_nozzle_assignments_nozzle ON configuration_nozzle_assignments(nozzle_type_id);
|
||||
|
||||
-- Migration completed successfully
|
||||
SELECT 'Sprayer-Pump-Nozzle relationship migration completed successfully!' as migration_status;
|
||||
@@ -24,6 +24,10 @@ const Equipment = () => {
|
||||
const [searchTerm, setSearchTerm] = useState('');
|
||||
const [showInactive, setShowInactive] = useState(false);
|
||||
const [activeTab, setActiveTab] = useState('all');
|
||||
const [showPumpAssignments, setShowPumpAssignments] = useState(false);
|
||||
const [showNozzleConfigs, setShowNozzleConfigs] = useState(false);
|
||||
const [selectedSprayerForPump, setSelectedSprayerForPump] = useState(null);
|
||||
const [selectedSprayerForNozzles, setSelectedSprayerForNozzles] = useState(null);
|
||||
|
||||
useEffect(() => {
|
||||
fetchData();
|
||||
@@ -106,6 +110,16 @@ const Equipment = () => {
|
||||
}
|
||||
};
|
||||
|
||||
const handleManagePumpAssignments = (sprayer) => {
|
||||
setSelectedSprayerForPump(sprayer);
|
||||
setShowPumpAssignments(true);
|
||||
};
|
||||
|
||||
const handleManageNozzleConfigs = (sprayer) => {
|
||||
setSelectedSprayerForNozzles(sprayer);
|
||||
setShowNozzleConfigs(true);
|
||||
};
|
||||
|
||||
// Filter equipment based on search term and active tab
|
||||
const filteredEquipment = equipment.filter(item => {
|
||||
const matchesSearch = searchTerm === '' ||
|
||||
@@ -149,6 +163,24 @@ const Equipment = () => {
|
||||
</div>
|
||||
|
||||
<div className="flex gap-1">
|
||||
{item.categoryName === 'Sprayer' && (
|
||||
<>
|
||||
<button
|
||||
onClick={() => handleManagePumpAssignments(item)}
|
||||
className="p-1 text-gray-400 hover:text-purple-600"
|
||||
title="Manage pump assignments"
|
||||
>
|
||||
<WrenchScrewdriverIcon className="h-4 w-4" />
|
||||
</button>
|
||||
<button
|
||||
onClick={() => handleManageNozzleConfigs(item)}
|
||||
className="p-1 text-gray-400 hover:text-green-600"
|
||||
title="Configure nozzles"
|
||||
>
|
||||
<EyeIcon className="h-4 w-4" />
|
||||
</button>
|
||||
</>
|
||||
)}
|
||||
<button
|
||||
onClick={() => handleEditEquipment(item)}
|
||||
className="p-1 text-gray-400 hover:text-blue-600"
|
||||
@@ -412,6 +444,31 @@ const Equipment = () => {
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Pump Assignment Modal */}
|
||||
{showPumpAssignments && selectedSprayerForPump && (
|
||||
<PumpAssignmentModal
|
||||
sprayer={selectedSprayerForPump}
|
||||
equipment={equipment.filter(e => e.categoryName === 'Pump')}
|
||||
onClose={() => {
|
||||
setShowPumpAssignments(false);
|
||||
setSelectedSprayerForPump(null);
|
||||
fetchData();
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Nozzle Configuration Modal */}
|
||||
{showNozzleConfigs && selectedSprayerForNozzles && (
|
||||
<NozzleConfigurationModal
|
||||
sprayer={selectedSprayerForNozzles}
|
||||
onClose={() => {
|
||||
setShowNozzleConfigs(false);
|
||||
setSelectedSprayerForNozzles(null);
|
||||
fetchData();
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -936,4 +993,333 @@ const EquipmentFormModal = ({ isEdit, equipment, categories, equipmentTypes, onS
|
||||
);
|
||||
};
|
||||
|
||||
// Pump Assignment Modal Component
|
||||
const PumpAssignmentModal = ({ sprayer, equipment, onClose }) => {
|
||||
const [assignments, setAssignments] = useState([]);
|
||||
const [availablePumps, setAvailablePumps] = useState([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [selectedPump, setSelectedPump] = useState('');
|
||||
|
||||
useEffect(() => {
|
||||
fetchPumpAssignments();
|
||||
}, [sprayer.id]);
|
||||
|
||||
const fetchPumpAssignments = async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
const [assignmentsResponse, availablePumpsResponse] = await Promise.all([
|
||||
nozzlesAPI.getPumpAssignments(sprayer.id),
|
||||
Promise.resolve({ data: { data: { pumps: equipment } } })
|
||||
]);
|
||||
|
||||
setAssignments(assignmentsResponse.data.data.assignments || []);
|
||||
setAvailablePumps(availablePumpsResponse.data.data.pumps || []);
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch pump assignments:', error);
|
||||
toast.error('Failed to load pump assignments');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleAssignPump = async () => {
|
||||
if (!selectedPump) return;
|
||||
|
||||
try {
|
||||
await nozzlesAPI.assignPump(sprayer.id, selectedPump);
|
||||
toast.success('Pump assigned successfully');
|
||||
setSelectedPump('');
|
||||
fetchPumpAssignments();
|
||||
} catch (error) {
|
||||
console.error('Failed to assign pump:', error);
|
||||
toast.error('Failed to assign pump');
|
||||
}
|
||||
};
|
||||
|
||||
const handleUnassignPump = async (assignmentId) => {
|
||||
try {
|
||||
await nozzlesAPI.unassignPump(assignmentId);
|
||||
toast.success('Pump unassigned successfully');
|
||||
fetchPumpAssignments();
|
||||
} catch (error) {
|
||||
console.error('Failed to unassign pump:', error);
|
||||
toast.error('Failed to unassign pump');
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
|
||||
<div className="bg-white rounded-lg p-6 w-full max-w-2xl max-h-[90vh] overflow-y-auto">
|
||||
<h3 className="text-lg font-semibold mb-4">
|
||||
Pump Assignments - {sprayer.customName || sprayer.typeName}
|
||||
</h3>
|
||||
|
||||
{loading ? (
|
||||
<LoadingSpinner />
|
||||
) : (
|
||||
<>
|
||||
{/* Add New Pump Assignment */}
|
||||
<div className="mb-6 p-4 bg-gray-50 rounded-lg">
|
||||
<h4 className="font-medium mb-3">Assign New Pump</h4>
|
||||
<div className="flex gap-3">
|
||||
<select
|
||||
className="input flex-1"
|
||||
value={selectedPump}
|
||||
onChange={(e) => setSelectedPump(e.target.value)}
|
||||
>
|
||||
<option value="">Select a pump...</option>
|
||||
{availablePumps.map(pump => (
|
||||
<option key={pump.id} value={pump.id}>
|
||||
{pump.customName || pump.typeName}
|
||||
{pump.manufacturer && ` - ${pump.manufacturer}`}
|
||||
{pump.maxGpm && ` (${pump.maxGpm} GPM)`}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
<button
|
||||
onClick={handleAssignPump}
|
||||
disabled={!selectedPump}
|
||||
className="btn-primary"
|
||||
>
|
||||
Assign
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Current Assignments */}
|
||||
<div className="space-y-3">
|
||||
<h4 className="font-medium">Current Pump Assignments</h4>
|
||||
{assignments.length === 0 ? (
|
||||
<p className="text-gray-500">No pumps assigned to this sprayer</p>
|
||||
) : (
|
||||
assignments.map(assignment => (
|
||||
<div key={assignment.id} className="flex justify-between items-center p-3 bg-gray-50 rounded-lg">
|
||||
<div>
|
||||
<div className="font-medium">
|
||||
{assignment.pump?.customName || assignment.pump?.typeName}
|
||||
</div>
|
||||
{assignment.pump?.manufacturer && (
|
||||
<div className="text-sm text-gray-600">
|
||||
{assignment.pump.manufacturer} {assignment.pump.model}
|
||||
</div>
|
||||
)}
|
||||
{assignment.pump?.maxGpm && (
|
||||
<div className="text-sm text-gray-600">
|
||||
Max Flow: {assignment.pump.maxGpm} GPM
|
||||
</div>
|
||||
)}
|
||||
<div className="text-xs text-gray-500">
|
||||
Assigned: {new Date(assignment.assignedDate).toLocaleDateString()}
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
onClick={() => handleUnassignPump(assignment.id)}
|
||||
className="text-red-600 hover:text-red-800"
|
||||
>
|
||||
<TrashIcon className="h-5 w-5" />
|
||||
</button>
|
||||
</div>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
<div className="flex gap-3 pt-6 mt-6 border-t">
|
||||
<button onClick={onClose} className="btn-secondary flex-1">
|
||||
Close
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
// Nozzle Configuration Modal Component
|
||||
const NozzleConfigurationModal = ({ sprayer, onClose }) => {
|
||||
const [configurations, setConfigurations] = useState([]);
|
||||
const [nozzleTypes, setNozzleTypes] = useState([]);
|
||||
const [userNozzles, setUserNozzles] = useState([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [showAddConfig, setShowAddConfig] = useState(false);
|
||||
const [configForm, setConfigForm] = useState({
|
||||
userNozzleId: '',
|
||||
position: '',
|
||||
quantityAssigned: 1
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
fetchNozzleConfigs();
|
||||
}, [sprayer.id]);
|
||||
|
||||
const fetchNozzleConfigs = async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
const [configsResponse, typesResponse, userNozzlesResponse] = await Promise.all([
|
||||
nozzlesAPI.getNozzleConfigurations(sprayer.id),
|
||||
nozzlesAPI.getNozzleTypes(),
|
||||
nozzlesAPI.getUserNozzles()
|
||||
]);
|
||||
|
||||
setConfigurations(configsResponse.data.data.configurations || []);
|
||||
setNozzleTypes(typesResponse.data.data.nozzleTypes || []);
|
||||
setUserNozzles(userNozzlesResponse.data.data.userNozzles || []);
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch nozzle configurations:', error);
|
||||
toast.error('Failed to load nozzle configurations');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleAddConfiguration = async () => {
|
||||
try {
|
||||
await nozzlesAPI.addNozzleConfiguration(sprayer.id, configForm);
|
||||
toast.success('Nozzle configuration added');
|
||||
setShowAddConfig(false);
|
||||
setConfigForm({ userNozzleId: '', position: '', quantityAssigned: 1 });
|
||||
fetchNozzleConfigs();
|
||||
} catch (error) {
|
||||
console.error('Failed to add configuration:', error);
|
||||
toast.error('Failed to add configuration');
|
||||
}
|
||||
};
|
||||
|
||||
const handleRemoveConfiguration = async (configId) => {
|
||||
try {
|
||||
await nozzlesAPI.removeNozzleConfiguration(configId);
|
||||
toast.success('Nozzle configuration removed');
|
||||
fetchNozzleConfigs();
|
||||
} catch (error) {
|
||||
console.error('Failed to remove configuration:', error);
|
||||
toast.error('Failed to remove configuration');
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
|
||||
<div className="bg-white rounded-lg p-6 w-full max-w-4xl max-h-[90vh] overflow-y-auto">
|
||||
<h3 className="text-lg font-semibold mb-4">
|
||||
Nozzle Configuration - {sprayer.customName || sprayer.typeName}
|
||||
</h3>
|
||||
|
||||
{loading ? (
|
||||
<LoadingSpinner />
|
||||
) : (
|
||||
<>
|
||||
{/* Add Configuration Button */}
|
||||
<div className="mb-6">
|
||||
<button
|
||||
onClick={() => setShowAddConfig(true)}
|
||||
className="btn-primary flex items-center gap-2"
|
||||
>
|
||||
<PlusIcon className="h-5 w-5" />
|
||||
Add Nozzle Configuration
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Add Configuration Form */}
|
||||
{showAddConfig && (
|
||||
<div className="mb-6 p-4 bg-gray-50 rounded-lg">
|
||||
<h4 className="font-medium mb-3">Add Nozzle Configuration</h4>
|
||||
<div className="grid grid-cols-3 gap-3">
|
||||
<select
|
||||
className="input"
|
||||
value={configForm.userNozzleId}
|
||||
onChange={(e) => setConfigForm({ ...configForm, userNozzleId: e.target.value })}
|
||||
>
|
||||
<option value="">Select nozzle...</option>
|
||||
{userNozzles.map(nozzle => (
|
||||
<option key={nozzle.id} value={nozzle.id}>
|
||||
{nozzle.nozzleType?.name} ({nozzle.quantity} available)
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
<input
|
||||
type="text"
|
||||
className="input"
|
||||
placeholder="Position (e.g., left, right, boom_1)"
|
||||
value={configForm.position}
|
||||
onChange={(e) => setConfigForm({ ...configForm, position: e.target.value })}
|
||||
/>
|
||||
<input
|
||||
type="number"
|
||||
className="input"
|
||||
placeholder="Quantity"
|
||||
min="1"
|
||||
value={configForm.quantityAssigned}
|
||||
onChange={(e) => setConfigForm({ ...configForm, quantityAssigned: parseInt(e.target.value) })}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex gap-3 mt-3">
|
||||
<button onClick={handleAddConfiguration} className="btn-primary">
|
||||
Add Configuration
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setShowAddConfig(false)}
|
||||
className="btn-secondary"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Current Configurations */}
|
||||
<div className="space-y-3">
|
||||
<h4 className="font-medium">Current Nozzle Configurations</h4>
|
||||
{configurations.length === 0 ? (
|
||||
<p className="text-gray-500">No nozzles configured for this sprayer</p>
|
||||
) : (
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
{configurations.map(config => (
|
||||
<div key={config.id} className="p-4 bg-gray-50 rounded-lg">
|
||||
<div className="flex justify-between items-start mb-2">
|
||||
<div className="font-medium">
|
||||
{config.userNozzle?.nozzleType?.name}
|
||||
</div>
|
||||
<button
|
||||
onClick={() => handleRemoveConfiguration(config.id)}
|
||||
className="text-red-600 hover:text-red-800"
|
||||
>
|
||||
<TrashIcon className="h-4 w-4" />
|
||||
</button>
|
||||
</div>
|
||||
<div className="text-sm text-gray-600 space-y-1">
|
||||
<div><strong>Position:</strong> {config.position}</div>
|
||||
<div><strong>Quantity:</strong> {config.quantityAssigned}</div>
|
||||
{config.userNozzle?.nozzleType?.manufacturer && (
|
||||
<div><strong>Manufacturer:</strong> {config.userNozzle.nozzleType.manufacturer}</div>
|
||||
)}
|
||||
{config.userNozzle?.nozzleType?.orificeSize && (
|
||||
<div><strong>Orifice:</strong> {config.userNozzle.nozzleType.orificeSize}</div>
|
||||
)}
|
||||
{config.userNozzle?.nozzleType?.sprayAngle && (
|
||||
<div><strong>Spray Angle:</strong> {config.userNozzle.nozzleType.sprayAngle}°</div>
|
||||
)}
|
||||
{config.userNozzle?.nozzleType?.dropletSize && (
|
||||
<div><strong>Droplet Size:</strong> {config.userNozzle.nozzleType.dropletSize}</div>
|
||||
)}
|
||||
<div className="text-xs text-gray-500">
|
||||
Assigned: {new Date(config.assignedDate).toLocaleDateString()}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
<div className="flex gap-3 pt-6 mt-6 border-t">
|
||||
<button onClick={onClose} className="btn-secondary flex-1">
|
||||
Close
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Equipment;
|
||||
@@ -109,8 +109,22 @@ export const nozzlesAPI = {
|
||||
create: (nozzleData) => apiClient.post('/nozzles', nozzleData),
|
||||
update: (id, nozzleData) => apiClient.put(`/nozzles/${id}`, nozzleData),
|
||||
delete: (id) => apiClient.delete(`/nozzles/${id}`),
|
||||
getNozzleTypes: () => apiClient.get('/nozzles/types'),
|
||||
getUserNozzles: () => apiClient.get('/nozzles/user'),
|
||||
calculateFlowRate: (nozzleId, pressure) => apiClient.get(`/nozzles/${nozzleId}/flow-rate`, { params: { pressure } }),
|
||||
|
||||
// Pump assignments
|
||||
getPumpAssignments: (sprayerId) => apiClient.get(`/nozzles/sprayer/${sprayerId}/pump-assignments`),
|
||||
assignPump: (sprayerId, pumpId) => apiClient.post(`/nozzles/sprayer/${sprayerId}/pump-assignments`, { pumpId }),
|
||||
unassignPump: (assignmentId) => apiClient.delete(`/nozzles/pump-assignments/${assignmentId}`),
|
||||
|
||||
// Nozzle configurations
|
||||
getNozzleConfigurations: (sprayerId) => apiClient.get(`/nozzles/sprayer/${sprayerId}/nozzle-configurations`),
|
||||
addNozzleConfiguration: (sprayerId, configData) => apiClient.post(`/nozzles/sprayer/${sprayerId}/nozzle-configurations`, configData),
|
||||
removeNozzleConfiguration: (configId) => apiClient.delete(`/nozzles/nozzle-configurations/${configId}`),
|
||||
|
||||
// Legacy endpoints for compatibility
|
||||
getTypes: (params) => apiClient.get('/nozzles/types', { params }),
|
||||
// Equipment-nozzle assignments
|
||||
getAssignments: (equipmentId) => apiClient.get(`/nozzles/equipment/${equipmentId}/assignments`),
|
||||
assignToEquipment: (equipmentId, assignmentData) => apiClient.post(`/nozzles/equipment/${equipmentId}/assignments`, assignmentData),
|
||||
removeAssignment: (assignmentId) => apiClient.delete(`/nozzles/assignments/${assignmentId}`),
|
||||
|
||||
Reference in New Issue
Block a user