watering 2
This commit is contained in:
@@ -167,6 +167,15 @@ router.get('/', async (req, res, next) => {
|
|||||||
material: item.material,
|
material: item.material,
|
||||||
colorCode: item.color_code,
|
colorCode: item.color_code,
|
||||||
quantityOwned: item.quantity_owned,
|
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
|
// General fields
|
||||||
purchaseDate: item.purchase_date,
|
purchaseDate: item.purchase_date,
|
||||||
purchasePrice: parseFloat(item.purchase_price) || null,
|
purchasePrice: parseFloat(item.purchase_price) || null,
|
||||||
@@ -297,7 +306,16 @@ router.get('/:id', validateParams(idParamSchema), async (req, res, next) => {
|
|||||||
notes: item.notes,
|
notes: item.notes,
|
||||||
isActive: item.is_active,
|
isActive: item.is_active,
|
||||||
createdAt: item.created_at,
|
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,
|
tool_type, working_width_inches,
|
||||||
pump_type, max_gpm, max_psi, power_source,
|
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,
|
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)
|
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)
|
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 *`,
|
RETURNING *`,
|
||||||
[
|
[
|
||||||
req.user.id, equipmentTypeId, finalCategoryId, customName, manufacturer, model,
|
req.user.id, equipmentTypeId, finalCategoryId, customName, manufacturer, model,
|
||||||
@@ -416,7 +435,8 @@ router.post('/', async (req, res, next) => {
|
|||||||
toolType, workingWidthInches,
|
toolType, workingWidthInches,
|
||||||
pumpType, maxGpm, maxPsi, powerSource,
|
pumpType, maxGpm, maxPsi, powerSource,
|
||||||
orificeSize, sprayAngle, flowRateGpm, dropletSize, sprayPattern, pressureRangePsi, threadSize, material, colorCode, quantityOwned,
|
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,
|
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,
|
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,
|
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
|
updated_at = CURRENT_TIMESTAMP
|
||||||
WHERE id = $39
|
WHERE id = $39
|
||||||
RETURNING *`,
|
RETURNING *`,
|
||||||
@@ -545,7 +566,8 @@ router.put('/:id', validateParams(idParamSchema), async (req, res, next) => {
|
|||||||
pumpType, maxGpm, maxPsi, powerSource,
|
pumpType, maxGpm, maxPsi, powerSource,
|
||||||
orificeSize, sprayAngle, flowRateGpm, dropletSize, sprayPattern, pressureRangePsi, threadSize, material, colorCode, quantityOwned,
|
orificeSize, sprayAngle, flowRateGpm, dropletSize, sprayPattern, pressureRangePsi, threadSize, material, colorCode, quantityOwned,
|
||||||
purchaseDate, purchasePrice, notes, isActive !== undefined ? isActive : true,
|
purchaseDate, purchasePrice, notes, isActive !== undefined ? isActive : true,
|
||||||
equipmentId
|
equipmentId,
|
||||||
|
sprinklerMount || null, sprinklerHeadType || null, sprinklerGpm || null, sprinklerThrowFeet || null, sprinklerDegrees || null, sprinklerLengthFeet || null, sprinklerWidthFeet || null
|
||||||
]
|
]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -91,15 +91,14 @@ router.post('/plans/:id/points', async (req,res,next)=>{
|
|||||||
const ins = await pool.query(
|
const ins = await pool.query(
|
||||||
`INSERT INTO watering_plan_points
|
`INSERT INTO watering_plan_points
|
||||||
(plan_id, sequence, lat, lng, duration_minutes, sprinkler_mount, sprinkler_head_type,
|
(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)
|
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) RETURNING *`,
|
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,
|
[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.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] }});
|
res.status(201).json({ success:true, data:{ point: ins.rows[0] }});
|
||||||
} catch (e) { next(e); }
|
} catch (e) { next(e); }
|
||||||
});
|
});
|
||||||
|
|
||||||
module.exports = router;
|
module.exports = router;
|
||||||
|
|
||||||
|
|||||||
6
database/migrations/V12__watering_point_heading.sql
Normal file
6
database/migrations/V12__watering_point_heading.sql
Normal file
@@ -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;
|
||||||
|
|
||||||
@@ -66,6 +66,12 @@ const Layout = ({ children }) => {
|
|||||||
icon: CalendarDaysIcon,
|
icon: CalendarDaysIcon,
|
||||||
iconSolid: CalendarIconSolid,
|
iconSolid: CalendarIconSolid,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: 'Watering',
|
||||||
|
href: '/watering',
|
||||||
|
icon: CloudIcon,
|
||||||
|
iconSolid: CloudIconSolid,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: 'Mowing',
|
name: 'Mowing',
|
||||||
href: '/mowing',
|
href: '/mowing',
|
||||||
@@ -84,6 +90,12 @@ const Layout = ({ children }) => {
|
|||||||
icon: CloudIcon,
|
icon: CloudIcon,
|
||||||
iconSolid: CloudIconSolid,
|
iconSolid: CloudIconSolid,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: 'Watering',
|
||||||
|
href: '/watering',
|
||||||
|
icon: CloudIcon,
|
||||||
|
iconSolid: CloudIconSolid,
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
const adminNavigation = [
|
const adminNavigation = [
|
||||||
|
|||||||
@@ -246,6 +246,18 @@ const Equipment = () => {
|
|||||||
{item.boomSections && <p><strong>Boom Sections:</strong> {item.boomSections}</p>}
|
{item.boomSections && <p><strong>Boom Sections:</strong> {item.boomSections}</p>}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
case 'sprinkler':
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{item.sprinklerMount && <p><strong>Mount:</strong> {item.sprinklerMount.replace('_',' ')}</p>}
|
||||||
|
{item.sprinklerHeadType && <p><strong>Head:</strong> {item.sprinklerHeadType.replace('_',' ')}</p>}
|
||||||
|
{item.sprinklerGpm && <p><strong>GPM:</strong> {item.sprinklerGpm}</p>}
|
||||||
|
{item.sprinklerThrowFeet && <p><strong>Throw:</strong> {item.sprinklerThrowFeet} ft</p>}
|
||||||
|
{item.sprinklerDegrees && <p><strong>Degrees:</strong> {item.sprinklerDegrees}°</p>}
|
||||||
|
{item.sprinklerLengthFeet && <p><strong>Length:</strong> {item.sprinklerLengthFeet} ft</p>}
|
||||||
|
{item.sprinklerWidthFeet && <p><strong>Width:</strong> {item.sprinklerWidthFeet} ft</p>}
|
||||||
|
</>
|
||||||
|
);
|
||||||
case 'pump':
|
case 'pump':
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@@ -281,6 +293,7 @@ const Equipment = () => {
|
|||||||
'Mower': 'bg-green-100 text-green-800',
|
'Mower': 'bg-green-100 text-green-800',
|
||||||
'Spreader': 'bg-orange-100 text-orange-800',
|
'Spreader': 'bg-orange-100 text-orange-800',
|
||||||
'Sprayer': 'bg-blue-100 text-blue-800',
|
'Sprayer': 'bg-blue-100 text-blue-800',
|
||||||
|
'Sprinkler': 'bg-cyan-100 text-cyan-800',
|
||||||
'Nozzle': 'bg-teal-100 text-teal-800',
|
'Nozzle': 'bg-teal-100 text-teal-800',
|
||||||
'Pump': 'bg-purple-100 text-purple-800',
|
'Pump': 'bg-purple-100 text-purple-800',
|
||||||
'Aerator': 'bg-yellow-100 text-yellow-800',
|
'Aerator': 'bg-yellow-100 text-yellow-800',
|
||||||
@@ -529,6 +542,14 @@ const EquipmentFormModal = ({ isEdit, equipment, categories, equipmentTypes, onS
|
|||||||
material: equipment?.material || '',
|
material: equipment?.material || '',
|
||||||
colorCode: equipment?.colorCode || '',
|
colorCode: equipment?.colorCode || '',
|
||||||
quantityOwned: equipment?.quantityOwned || 1,
|
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
|
// General fields
|
||||||
purchaseDate: equipment?.purchaseDate || '',
|
purchaseDate: equipment?.purchaseDate || '',
|
||||||
purchasePrice: equipment?.purchasePrice || '',
|
purchasePrice: equipment?.purchasePrice || '',
|
||||||
@@ -587,6 +608,14 @@ const EquipmentFormModal = ({ isEdit, equipment, categories, equipmentTypes, onS
|
|||||||
material: formData.material || null,
|
material: formData.material || null,
|
||||||
colorCode: formData.colorCode || null,
|
colorCode: formData.colorCode || null,
|
||||||
quantityOwned: formData.quantityOwned ? parseInt(formData.quantityOwned) : 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,
|
purchaseDate: formData.purchaseDate || null,
|
||||||
purchasePrice: formData.purchasePrice ? parseFloat(formData.purchasePrice) : null,
|
purchasePrice: formData.purchasePrice ? parseFloat(formData.purchasePrice) : null,
|
||||||
notes: formData.notes || null,
|
notes: formData.notes || null,
|
||||||
@@ -782,6 +811,56 @@ const EquipmentFormModal = ({ isEdit, equipment, categories, equipmentTypes, onS
|
|||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
case 'sprinkler':
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div className="grid grid-cols-2 gap-4">
|
||||||
|
<div>
|
||||||
|
<label className="label">Mount *</label>
|
||||||
|
<select className="input" value={formData.sprinklerMount} onChange={(e)=> setFormData({ ...formData, sprinklerMount: e.target.value })}>
|
||||||
|
<option value="in_ground">In‑Ground</option>
|
||||||
|
<option value="above_ground">Above‑Ground</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="label">Head Type *</label>
|
||||||
|
<select className="input" value={formData.sprinklerHeadType} onChange={(e)=> setFormData({ ...formData, sprinklerHeadType: e.target.value })}>
|
||||||
|
<option value="rotor_impact">Rotor/Impact</option>
|
||||||
|
<option value="oscillating_fan">Oscillating/Fan</option>
|
||||||
|
<option value="spray_fixed">Spray (Fixed)</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{(formData.sprinklerHeadType === 'rotor_impact' || formData.sprinklerHeadType === 'spray_fixed') && (
|
||||||
|
<div className="grid grid-cols-3 gap-4">
|
||||||
|
<div>
|
||||||
|
<label className="label">GPM</label>
|
||||||
|
<input type="number" step="0.1" className="input" value={formData.sprinklerGpm} onChange={(e)=> setFormData({ ...formData, sprinklerGpm: e.target.value })} />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="label">Throw (ft)</label>
|
||||||
|
<input type="number" step="0.1" className="input" value={formData.sprinklerThrowFeet} onChange={(e)=> setFormData({ ...formData, sprinklerThrowFeet: e.target.value })} />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="label">Degrees</label>
|
||||||
|
<input type="number" className="input" value={formData.sprinklerDegrees} onChange={(e)=> setFormData({ ...formData, sprinklerDegrees: e.target.value })} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{formData.sprinklerHeadType === 'oscillating_fan' && (
|
||||||
|
<div className="grid grid-cols-2 gap-4">
|
||||||
|
<div>
|
||||||
|
<label className="label">Length (ft)</label>
|
||||||
|
<input type="number" step="0.1" className="input" value={formData.sprinklerLengthFeet} onChange={(e)=> setFormData({ ...formData, sprinklerLengthFeet: e.target.value })} />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="label">Width (ft)</label>
|
||||||
|
<input type="number" step="0.1" className="input" value={formData.sprinklerWidthFeet} onChange={(e)=> setFormData({ ...formData, sprinklerWidthFeet: e.target.value })} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
|
||||||
case 'pump':
|
case 'pump':
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import React, { useEffect, useMemo, useState } from 'react';
|
import React, { useEffect, useMemo, useState } from 'react';
|
||||||
import { MapContainer, TileLayer, Polygon, Marker, Circle, Rectangle, useMapEvents } from 'react-leaflet';
|
import { MapContainer, TileLayer, Polygon, Marker, Circle, Rectangle, useMapEvents } from 'react-leaflet';
|
||||||
import 'leaflet/dist/leaflet.css';
|
import 'leaflet/dist/leaflet.css';
|
||||||
import { propertiesAPI, wateringAPI } from '../../services/api';
|
import { propertiesAPI, wateringAPI, equipmentAPI } from '../../services/api';
|
||||||
import toast from 'react-hot-toast';
|
import toast from 'react-hot-toast';
|
||||||
|
|
||||||
const SprinklerPlacement = ({ onPlace }) => {
|
const SprinklerPlacement = ({ onPlace }) => {
|
||||||
@@ -43,14 +43,25 @@ const Watering = () => {
|
|||||||
const [guideIndex, setGuideIndex] = useState(0);
|
const [guideIndex, setGuideIndex] = useState(0);
|
||||||
const [currentPos, setCurrentPos] = useState(null);
|
const [currentPos, setCurrentPos] = useState(null);
|
||||||
const [watchId, setWatchId] = useState(null);
|
const [watchId, setWatchId] = useState(null);
|
||||||
|
const [sprinklers, setSprinklers] = useState([]);
|
||||||
|
const [selectedSprinklerId, setSelectedSprinklerId] = useState('');
|
||||||
|
|
||||||
useEffect(() => { (async () => {
|
useEffect(() => { (async () => {
|
||||||
try { const r = await propertiesAPI.getAll(); setProperties(r.data?.data?.properties||[]); }
|
try {
|
||||||
|
const [pr, eq] = await Promise.all([propertiesAPI.getAll(), equipmentAPI.getAll()]);
|
||||||
|
setProperties(pr.data?.data?.properties||[]);
|
||||||
|
const list = (eq.data?.data?.equipment||[]).filter(e => (e.categoryName||'').toLowerCase()==='sprinkler');
|
||||||
|
setSprinklers(list);
|
||||||
|
}
|
||||||
catch(e){ toast.error('Failed to load properties'); }
|
catch(e){ toast.error('Failed to load properties'); }
|
||||||
})(); }, []);
|
})(); }, []);
|
||||||
|
|
||||||
const loadProperty = async (pid) => {
|
const loadProperty = async (pid) => {
|
||||||
try { const r = await propertiesAPI.getById(pid); const p = r.data?.data?.property; setSelectedProperty(p); setSections(p.sections||[]); }
|
try {
|
||||||
|
const r = await propertiesAPI.getById(pid); const p = r.data?.data?.property; setSelectedProperty(p);
|
||||||
|
const secs = (p.sections||[]).map(s => ({ ...s, polygonData: typeof s.polygonData === 'string' ? JSON.parse(s.polygonData) : s.polygonData }));
|
||||||
|
setSections(secs);
|
||||||
|
}
|
||||||
catch(e){ toast.error('Failed to load property'); }
|
catch(e){ toast.error('Failed to load property'); }
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -64,15 +75,26 @@ const Watering = () => {
|
|||||||
const onPlace = async (latlng) => {
|
const onPlace = async (latlng) => {
|
||||||
const p = await ensurePlan(); if (!p) return;
|
const p = await ensurePlan(); if (!p) return;
|
||||||
try {
|
try {
|
||||||
|
const eq = sprinklers.find(s=> s.id === (selectedSprinklerId? parseInt(selectedSprinklerId): -1));
|
||||||
|
const base = eq ? {
|
||||||
|
mount: eq.sprinklerMount || sprinklerForm.mount,
|
||||||
|
type: eq.sprinklerHeadType || sprinklerForm.type,
|
||||||
|
gpm: eq.sprinklerGpm || sprinklerForm.gpm,
|
||||||
|
throwFeet: eq.sprinklerThrowFeet || sprinklerForm.throwFeet,
|
||||||
|
degrees: sprinklerForm.degrees,
|
||||||
|
lengthFeet: eq.sprinklerLengthFeet || sprinklerForm.lengthFeet,
|
||||||
|
widthFeet: eq.sprinklerWidthFeet || sprinklerForm.widthFeet
|
||||||
|
} : sprinklerForm;
|
||||||
const payload = {
|
const payload = {
|
||||||
lat: latlng[0], lng: latlng[1], durationMinutes: sprinklerForm.durationMinutes,
|
lat: latlng[0], lng: latlng[1], durationMinutes: sprinklerForm.durationMinutes,
|
||||||
mountType: sprinklerForm.mount,
|
mountType: base.mount,
|
||||||
sprinklerHeadType: sprinklerForm.type,
|
sprinklerHeadType: base.type,
|
||||||
gpm: sprinklerForm.gpm,
|
gpm: base.gpm,
|
||||||
throwFeet: sprinklerForm.throwFeet,
|
throwFeet: base.throwFeet,
|
||||||
degrees: sprinklerForm.degrees,
|
degrees: base.degrees,
|
||||||
lengthFeet: sprinklerForm.lengthFeet,
|
lengthFeet: base.lengthFeet,
|
||||||
widthFeet: sprinklerForm.widthFeet
|
widthFeet: base.widthFeet,
|
||||||
|
headingDegrees: sprinklerForm.headingDegrees || 0
|
||||||
};
|
};
|
||||||
const r = await wateringAPI.addPlanPoint(p.id, payload);
|
const r = await wateringAPI.addPlanPoint(p.id, payload);
|
||||||
setPoints(prev => [...prev, r.data?.data?.point]);
|
setPoints(prev => [...prev, r.data?.data?.point]);
|
||||||
@@ -82,8 +104,11 @@ const Watering = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const center = useMemo(() => {
|
const center = useMemo(() => {
|
||||||
if (selectedProperty?.latitude && selectedProperty?.longitude) return [selectedProperty.latitude, selectedProperty.longitude];
|
if (selectedProperty?.latitude && selectedProperty?.longitude) return [Number(selectedProperty.latitude), Number(selectedProperty.longitude)];
|
||||||
if (sections?.length){ const s=sections[0]; const c=s.polygonData?.coordinates?.[0]?.[0]; if (c) return [c[0], c[1]]; }
|
if (sections?.length){
|
||||||
|
const pts = sections.flatMap(s => (s.polygonData?.coordinates?.[0]||[]));
|
||||||
|
if (pts.length){ const lat = pts.reduce((a,p)=> a + Number(p[0]), 0) / pts.length; const lng = pts.reduce((a,p)=> a + Number(p[1]), 0) / pts.length; return [lat, lng]; }
|
||||||
|
}
|
||||||
return [39.8,-98.6];
|
return [39.8,-98.6];
|
||||||
}, [selectedProperty, sections]);
|
}, [selectedProperty, sections]);
|
||||||
|
|
||||||
@@ -106,6 +131,48 @@ const Watering = () => {
|
|||||||
return (2*R*Math.atan2(Math.sqrt(A),Math.sqrt(1-A)))*3.28084;
|
return (2*R*Math.atan2(Math.sqrt(A),Math.sqrt(1-A)))*3.28084;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Build sector polygon (approx) for Leaflet
|
||||||
|
const sectorPolygon = (center, radiusFeet, startDeg, endDeg, steps=60) => {
|
||||||
|
const [clat, clng] = [Number(center.lat), Number(center.lng)];
|
||||||
|
const Rlat = 111320; // meters per degree lat
|
||||||
|
const Rlng = Math.cos(clat*Math.PI/180)*111320; // meters per degree lng
|
||||||
|
const rm = radiusFeet*0.3048; // meters
|
||||||
|
const pts = [];
|
||||||
|
const start = startDeg*Math.PI/180; const end = endDeg*Math.PI/180;
|
||||||
|
const span = end - start;
|
||||||
|
const n = Math.max(8, Math.round(steps*Math.abs(span)/(2*Math.PI)));
|
||||||
|
pts.push([clat, clng]);
|
||||||
|
for (let i=0;i<=n;i++){
|
||||||
|
const ang = start + (span*i/n);
|
||||||
|
const dx = rm * Math.cos(ang);
|
||||||
|
const dy = rm * Math.sin(ang);
|
||||||
|
const lat = clat + (dy / Rlat);
|
||||||
|
const lng = clng + (dx / Rlng);
|
||||||
|
pts.push([lat, lng]);
|
||||||
|
}
|
||||||
|
pts.push([clat, clng]);
|
||||||
|
return pts;
|
||||||
|
};
|
||||||
|
|
||||||
|
const coverageSqft = useMemo(() => {
|
||||||
|
const type = sprinklerForm.type;
|
||||||
|
if (type==='rotor_impact' || type==='spray_fixed') {
|
||||||
|
const r = Number(sprinklerForm.throwFeet||0); const deg=Number(sprinklerForm.degrees||360);
|
||||||
|
return Math.PI * r * r * (deg/360);
|
||||||
|
}
|
||||||
|
if (type==='oscillating_fan') {
|
||||||
|
return Number(sprinklerForm.lengthFeet||0) * Number(sprinklerForm.widthFeet||0);
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}, [sprinklerForm]);
|
||||||
|
const [targetInches, setTargetInches] = useState(0.5);
|
||||||
|
const suggestMinutes = useMemo(() => {
|
||||||
|
const gpm = Number(sprinklerForm.gpm||0);
|
||||||
|
if (!gpm || !coverageSqft || !targetInches) return 0;
|
||||||
|
const gallonsNeeded = coverageSqft * targetInches * 0.623; // gal per sqft-inch
|
||||||
|
return Math.ceil((gallonsNeeded / gpm));
|
||||||
|
}, [sprinklerForm.gpm, coverageSqft, targetInches]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="p-6">
|
<div className="p-6">
|
||||||
<h1 className="text-2xl font-bold mb-4">Watering - Sprinklers</h1>
|
<h1 className="text-2xl font-bold mb-4">Watering - Sprinklers</h1>
|
||||||
@@ -120,6 +187,26 @@ const Watering = () => {
|
|||||||
</div>
|
</div>
|
||||||
<div className="card space-y-2">
|
<div className="card space-y-2">
|
||||||
<div className="font-medium">Sprinkler Settings</div>
|
<div className="font-medium">Sprinkler Settings</div>
|
||||||
|
<div className="text-sm">Choose Saved Sprinkler</div>
|
||||||
|
<select className="input" value={selectedSprinklerId} onChange={(e)=> {
|
||||||
|
const val=e.target.value; setSelectedSprinklerId(val);
|
||||||
|
const eq = sprinklers.find(s=> s.id === parseInt(val));
|
||||||
|
if (eq) {
|
||||||
|
setSprinklerForm(sf=> ({...sf,
|
||||||
|
mount: eq.sprinklerMount || sf.mount,
|
||||||
|
type: eq.sprinklerHeadType || sf.type,
|
||||||
|
gpm: eq.sprinklerGpm || sf.gpm,
|
||||||
|
throwFeet: eq.sprinklerThrowFeet || sf.throwFeet,
|
||||||
|
lengthFeet: eq.sprinklerLengthFeet || sf.lengthFeet,
|
||||||
|
widthFeet: eq.sprinklerWidthFeet || sf.widthFeet
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
}}>
|
||||||
|
<option value="">-- none --</option>
|
||||||
|
{sprinklers.map(s=> (
|
||||||
|
<option key={s.id} value={s.id}>{s.customName || `${s.manufacturer||''} ${s.model||''}`.trim()}</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
<div className="text-sm">Mount</div>
|
<div className="text-sm">Mount</div>
|
||||||
<select className="input" value={sprinklerForm.mount} onChange={e=> setSprinklerForm({...sprinklerForm, mount:e.target.value})}>
|
<select className="input" value={sprinklerForm.mount} onChange={e=> setSprinklerForm({...sprinklerForm, mount:e.target.value})}>
|
||||||
<option value="in_ground">In‑Ground</option>
|
<option value="in_ground">In‑Ground</option>
|
||||||
@@ -139,6 +226,8 @@ const Watering = () => {
|
|||||||
<input type="number" step="0.1" className="input" value={sprinklerForm.throwFeet} onChange={e=> setSprinklerForm({...sprinklerForm, throwFeet: parseFloat(e.target.value)})} />
|
<input type="number" step="0.1" className="input" value={sprinklerForm.throwFeet} onChange={e=> setSprinklerForm({...sprinklerForm, throwFeet: parseFloat(e.target.value)})} />
|
||||||
<div className="text-sm">Degrees (0‑360)</div>
|
<div className="text-sm">Degrees (0‑360)</div>
|
||||||
<input type="number" className="input" value={sprinklerForm.degrees} onChange={e=> setSprinklerForm({...sprinklerForm, degrees: parseInt(e.target.value||'0',10)})} />
|
<input type="number" className="input" value={sprinklerForm.degrees} onChange={e=> setSprinklerForm({...sprinklerForm, degrees: parseInt(e.target.value||'0',10)})} />
|
||||||
|
<div className="text-sm">Heading (0‑359, 0 = East)</div>
|
||||||
|
<input type="number" className="input" value={sprinklerForm.headingDegrees||0} onChange={e=> setSprinklerForm({...sprinklerForm, headingDegrees: (parseInt(e.target.value||'0',10)||0)%360})} />
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
@@ -148,6 +237,10 @@ const Watering = () => {
|
|||||||
<input type="number" step="0.1" className="input" value={sprinklerForm.widthFeet} onChange={e=> setSprinklerForm({...sprinklerForm, widthFeet: parseFloat(e.target.value)})} />
|
<input type="number" step="0.1" className="input" value={sprinklerForm.widthFeet} onChange={e=> setSprinklerForm({...sprinklerForm, widthFeet: parseFloat(e.target.value)})} />
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
<div className="text-sm">Target Depth (inches)</div>
|
||||||
|
<input type="number" step="0.1" className="input" value={targetInches} onChange={e=> setTargetInches(parseFloat(e.target.value||'0'))} />
|
||||||
|
<div className="text-xs text-gray-600">Suggested runtime: {suggestMinutes} minutes {sprinklerForm.gpm? '' : '(enter GPM to calculate)'}
|
||||||
|
</div>
|
||||||
<div className="text-sm">Run Duration (minutes)</div>
|
<div className="text-sm">Run Duration (minutes)</div>
|
||||||
<input type="number" className="input" value={sprinklerForm.durationMinutes} onChange={e=> setSprinklerForm({...sprinklerForm, durationMinutes: parseInt(e.target.value||'0',10)})} />
|
<input type="number" className="input" value={sprinklerForm.durationMinutes} onChange={e=> setSprinklerForm({...sprinklerForm, durationMinutes: parseInt(e.target.value||'0',10)})} />
|
||||||
<button className="btn-primary w-full" disabled={!selectedProperty} onClick={()=> setPlacing(true)}>Place Sprinkler on Map</button>
|
<button className="btn-primary w-full" disabled={!selectedProperty} onClick={()=> setPlacing(true)}>Place Sprinkler on Map</button>
|
||||||
@@ -198,7 +291,11 @@ const Watering = () => {
|
|||||||
<React.Fragment key={pt.id}>
|
<React.Fragment key={pt.id}>
|
||||||
<Marker position={[pt.lat, pt.lng]} />
|
<Marker position={[pt.lat, pt.lng]} />
|
||||||
{cov?.kind==='circle' && (
|
{cov?.kind==='circle' && (
|
||||||
<Circle center={[pt.lat, pt.lng]} radius={cov.radius*0.3048} pathOptions={{ color:'#2563eb', fillOpacity:0.2 }} />
|
cov.degrees && cov.degrees < 360 ? (
|
||||||
|
<Polygon positions={sectorPolygon({lat:Number(pt.lat),lng:Number(pt.lng)}, cov.radius, (pt.sprinkler_heading_degrees||0), (pt.sprinkler_heading_degrees||0)+cov.degrees)} pathOptions={{ color:'#2563eb', fillOpacity:0.2 }} />
|
||||||
|
) : (
|
||||||
|
<Circle center={[pt.lat, pt.lng]} radius={cov.radius*0.3048} pathOptions={{ color:'#2563eb', fillOpacity:0.2 }} />
|
||||||
|
)
|
||||||
)}
|
)}
|
||||||
{cov?.kind==='rect' && (
|
{cov?.kind==='rect' && (
|
||||||
<Rectangle bounds={[[pt.lat - (cov.width/2)*0.00000274, pt.lng - (cov.length/2)*0.0000036], [pt.lat + (cov.width/2)*0.00000274, pt.lng + (cov.length/2)*0.0000036]]} pathOptions={{ color:'#2563eb', fillOpacity:0.2 }} />
|
<Rectangle bounds={[[pt.lat - (cov.width/2)*0.00000274, pt.lng - (cov.length/2)*0.0000036], [pt.lat + (cov.width/2)*0.00000274, pt.lng + (cov.length/2)*0.0000036]]} pathOptions={{ color:'#2563eb', fillOpacity:0.2 }} />
|
||||||
|
|||||||
Reference in New Issue
Block a user