diff --git a/backend/src/routes/equipment.js b/backend/src/routes/equipment.js index 7646f41..fa8b333 100644 --- a/backend/src/routes/equipment.js +++ b/backend/src/routes/equipment.js @@ -167,6 +167,15 @@ router.get('/', async (req, res, next) => { material: item.material, colorCode: item.color_code, quantityOwned: item.quantity_owned, + // Sprinkler fields + sprinklerMount: item.sprinkler_mount, + sprinklerHeadType: item.sprinkler_head_type, + sprinklerGpm: item.sprinkler_gpm ? parseFloat(item.sprinkler_gpm) : null, + sprinklerThrowFeet: item.sprinkler_throw_feet ? parseFloat(item.sprinkler_throw_feet) : null, + sprinklerDegrees: item.sprinkler_degrees, + sprinklerLengthFeet: item.sprinkler_length_feet ? parseFloat(item.sprinkler_length_feet) : null, + sprinklerWidthFeet: item.sprinkler_width_feet ? parseFloat(item.sprinkler_width_feet) : null, + sprinklerCoverageSqft: item.sprinkler_coverage_sqft ? parseFloat(item.sprinkler_coverage_sqft) : null, // General fields purchaseDate: item.purchase_date, purchasePrice: parseFloat(item.purchase_price) || null, @@ -297,7 +306,16 @@ router.get('/:id', validateParams(idParamSchema), async (req, res, next) => { notes: item.notes, isActive: item.is_active, createdAt: item.created_at, - updatedAt: item.updated_at + updatedAt: item.updated_at, + // Sprinkler fields + sprinklerMount: item.sprinkler_mount, + sprinklerHeadType: item.sprinkler_head_type, + sprinklerGpm: item.sprinkler_gpm ? parseFloat(item.sprinkler_gpm) : null, + sprinklerThrowFeet: item.sprinkler_throw_feet ? parseFloat(item.sprinkler_throw_feet) : null, + sprinklerDegrees: item.sprinkler_degrees, + sprinklerLengthFeet: item.sprinkler_length_feet ? parseFloat(item.sprinkler_length_feet) : null, + sprinklerWidthFeet: item.sprinkler_width_feet ? parseFloat(item.sprinkler_width_feet) : null, + sprinklerCoverageSqft: item.sprinkler_coverage_sqft ? parseFloat(item.sprinkler_coverage_sqft) : null } } }); @@ -405,8 +423,9 @@ router.post('/', async (req, res, next) => { tool_type, working_width_inches, pump_type, max_gpm, max_psi, power_source, orifice_size, spray_angle, flow_rate_gpm, droplet_size, spray_pattern, pressure_range_psi, thread_size, material, color_code, quantity_owned, - 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, $29, $30, $31, $32, $33, $34, $35, $36, $37, $38) + purchase_date, purchase_price, notes, + sprinkler_mount, sprinkler_head_type, sprinkler_gpm, sprinkler_throw_feet, sprinkler_degrees, sprinkler_length_feet, sprinkler_width_feet) + 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, $29, $30, $31, $32, $33, $34, $35, $36, $37, $38, $39, $40, $41, $42, $43, $44, $45) RETURNING *`, [ req.user.id, equipmentTypeId, finalCategoryId, customName, manufacturer, model, @@ -416,7 +435,8 @@ router.post('/', async (req, res, next) => { toolType, workingWidthInches, pumpType, maxGpm, maxPsi, powerSource, orificeSize, sprayAngle, flowRateGpm, dropletSize, sprayPattern, pressureRangePsi, threadSize, material, colorCode, quantityOwned, - purchaseDate, purchasePrice, notes + purchaseDate, purchasePrice, notes, + sprinklerMount || null, sprinklerHeadType || null, sprinklerGpm || null, sprinklerThrowFeet || null, sprinklerDegrees || null, sprinklerLengthFeet || null, sprinklerWidthFeet || null ] ); @@ -533,6 +553,7 @@ router.put('/:id', validateParams(idParamSchema), async (req, res, next) => { pump_type = $21, max_gpm = $22, max_psi = $23, power_source = $24, orifice_size = $25, spray_angle = $26, flow_rate_gpm = $27, droplet_size = $28, spray_pattern = $29, pressure_range_psi = $30, thread_size = $31, material = $32, color_code = $33, quantity_owned = $34, purchase_date = $35, purchase_price = $36, notes = $37, is_active = $38, + sprinkler_mount = $40, sprinkler_head_type = $41, sprinkler_gpm = $42, sprinkler_throw_feet = $43, sprinkler_degrees = $44, sprinkler_length_feet = $45, sprinkler_width_feet = $46, updated_at = CURRENT_TIMESTAMP WHERE id = $39 RETURNING *`, @@ -545,7 +566,8 @@ router.put('/:id', validateParams(idParamSchema), async (req, res, next) => { pumpType, maxGpm, maxPsi, powerSource, orificeSize, sprayAngle, flowRateGpm, dropletSize, sprayPattern, pressureRangePsi, threadSize, material, colorCode, quantityOwned, purchaseDate, purchasePrice, notes, isActive !== undefined ? isActive : true, - equipmentId + equipmentId, + sprinklerMount || null, sprinklerHeadType || null, sprinklerGpm || null, sprinklerThrowFeet || null, sprinklerDegrees || null, sprinklerLengthFeet || null, sprinklerWidthFeet || null ] ); @@ -645,4 +667,4 @@ router.delete('/:id', validateParams(idParamSchema), async (req, res, next) => { } }); -module.exports = router; \ No newline at end of file +module.exports = router; diff --git a/backend/src/routes/watering.js b/backend/src/routes/watering.js index b329a74..1248de3 100644 --- a/backend/src/routes/watering.js +++ b/backend/src/routes/watering.js @@ -91,15 +91,14 @@ router.post('/plans/:id/points', async (req,res,next)=>{ const ins = await pool.query( `INSERT INTO watering_plan_points (plan_id, sequence, lat, lng, duration_minutes, sprinkler_mount, sprinkler_head_type, - sprinkler_gpm, sprinkler_throw_feet, sprinkler_degrees, sprinkler_length_feet, sprinkler_width_feet, coverage_sqft) - VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13) RETURNING *`, + sprinkler_gpm, sprinkler_throw_feet, sprinkler_degrees, sprinkler_length_feet, sprinkler_width_feet, coverage_sqft, sprinkler_heading_degrees) + VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13,$14) RETURNING *`, [planId, sequence, payload.lat, payload.lng, payload.durationMinutes||0, payload.mountType||null, payload.sprinklerHeadType||null, payload.gpm||null, payload.throwFeet||null, payload.degrees||null, - payload.lengthFeet||null, payload.widthFeet||null, coverage] + payload.lengthFeet||null, payload.widthFeet||null, coverage, payload.headingDegrees||null] ); res.status(201).json({ success:true, data:{ point: ins.rows[0] }}); } catch (e) { next(e); } }); module.exports = router; - diff --git a/database/migrations/V12__watering_point_heading.sql b/database/migrations/V12__watering_point_heading.sql new file mode 100644 index 0000000..3deae66 --- /dev/null +++ b/database/migrations/V12__watering_point_heading.sql @@ -0,0 +1,6 @@ +-- Add heading for sprinkler sectors on watering plan points +ALTER TABLE watering_plan_points + ADD COLUMN IF NOT EXISTS sprinkler_heading_degrees INTEGER; + +SELECT 'Added sprinkler_heading_degrees to watering_plan_points' as migration_status; + diff --git a/frontend/src/components/Layout/Layout.js b/frontend/src/components/Layout/Layout.js index 7792a8d..48a7159 100644 --- a/frontend/src/components/Layout/Layout.js +++ b/frontend/src/components/Layout/Layout.js @@ -66,6 +66,12 @@ const Layout = ({ children }) => { icon: CalendarDaysIcon, iconSolid: CalendarIconSolid, }, + { + name: 'Watering', + href: '/watering', + icon: CloudIcon, + iconSolid: CloudIconSolid, + }, { name: 'Mowing', href: '/mowing', @@ -84,6 +90,12 @@ const Layout = ({ children }) => { icon: CloudIcon, iconSolid: CloudIconSolid, }, + { + name: 'Watering', + href: '/watering', + icon: CloudIcon, + iconSolid: CloudIconSolid, + }, ]; const adminNavigation = [ diff --git a/frontend/src/pages/Equipment/Equipment.js b/frontend/src/pages/Equipment/Equipment.js index 5bcf957..dfe1e88 100644 --- a/frontend/src/pages/Equipment/Equipment.js +++ b/frontend/src/pages/Equipment/Equipment.js @@ -246,6 +246,18 @@ const Equipment = () => { {item.boomSections &&
Boom Sections: {item.boomSections}
} > ); + case 'sprinkler': + return ( + <> + {item.sprinklerMount &&Mount: {item.sprinklerMount.replace('_',' ')}
} + {item.sprinklerHeadType &&Head: {item.sprinklerHeadType.replace('_',' ')}
} + {item.sprinklerGpm &&GPM: {item.sprinklerGpm}
} + {item.sprinklerThrowFeet &&Throw: {item.sprinklerThrowFeet} ft
} + {item.sprinklerDegrees &&Degrees: {item.sprinklerDegrees}°
} + {item.sprinklerLengthFeet &&Length: {item.sprinklerLengthFeet} ft
} + {item.sprinklerWidthFeet &&Width: {item.sprinklerWidthFeet} ft
} + > + ); case 'pump': return ( <> @@ -281,6 +293,7 @@ const Equipment = () => { 'Mower': 'bg-green-100 text-green-800', 'Spreader': 'bg-orange-100 text-orange-800', 'Sprayer': 'bg-blue-100 text-blue-800', + 'Sprinkler': 'bg-cyan-100 text-cyan-800', 'Nozzle': 'bg-teal-100 text-teal-800', 'Pump': 'bg-purple-100 text-purple-800', 'Aerator': 'bg-yellow-100 text-yellow-800', @@ -529,6 +542,14 @@ const EquipmentFormModal = ({ isEdit, equipment, categories, equipmentTypes, onS material: equipment?.material || '', colorCode: equipment?.colorCode || '', quantityOwned: equipment?.quantityOwned || 1, + // Sprinkler fields + sprinklerMount: equipment?.sprinklerMount || 'above_ground', + sprinklerHeadType: equipment?.sprinklerHeadType || 'rotor_impact', + sprinklerGpm: equipment?.sprinklerGpm || '', + sprinklerThrowFeet: equipment?.sprinklerThrowFeet || '', + sprinklerDegrees: equipment?.sprinklerDegrees || 360, + sprinklerLengthFeet: equipment?.sprinklerLengthFeet || '', + sprinklerWidthFeet: equipment?.sprinklerWidthFeet || '', // General fields purchaseDate: equipment?.purchaseDate || '', purchasePrice: equipment?.purchasePrice || '', @@ -587,6 +608,14 @@ const EquipmentFormModal = ({ isEdit, equipment, categories, equipmentTypes, onS material: formData.material || null, colorCode: formData.colorCode || null, quantityOwned: formData.quantityOwned ? parseInt(formData.quantityOwned) : null, + // Sprinkler fields + sprinklerMount: formData.sprinklerMount || null, + sprinklerHeadType: formData.sprinklerHeadType || null, + sprinklerGpm: formData.sprinklerGpm ? parseFloat(formData.sprinklerGpm) : null, + sprinklerThrowFeet: formData.sprinklerThrowFeet ? parseFloat(formData.sprinklerThrowFeet) : null, + sprinklerDegrees: formData.sprinklerDegrees ? parseInt(formData.sprinklerDegrees) : null, + sprinklerLengthFeet: formData.sprinklerLengthFeet ? parseFloat(formData.sprinklerLengthFeet) : null, + sprinklerWidthFeet: formData.sprinklerWidthFeet ? parseFloat(formData.sprinklerWidthFeet) : null, purchaseDate: formData.purchaseDate || null, purchasePrice: formData.purchasePrice ? parseFloat(formData.purchasePrice) : null, notes: formData.notes || null, @@ -782,6 +811,56 @@ const EquipmentFormModal = ({ isEdit, equipment, categories, equipmentTypes, onS > ); + case 'sprinkler': + return ( + <> +