This commit is contained in:
Jake Kasper
2025-09-05 10:45:01 -04:00
parent e691172a9c
commit e5946157f0
4 changed files with 195 additions and 10 deletions

View File

@@ -222,7 +222,9 @@ const Watering = () => {
degrees: base.degrees,
lengthFeet: base.lengthFeet,
widthFeet: base.widthFeet,
headingDegrees: sprinklerForm.headingDegrees || 0
headingDegrees: sprinklerForm.headingDegrees || 0,
equipmentId: eq ? eq.id : null,
equipmentName: eq ? (eq.customName || `${eq.manufacturer||''} ${eq.model||''}`.trim()) : null
};
const r = await wateringAPI.addPlanPoint(p.id, payload);
setPoints(prev => [...prev, r.data?.data?.point]);
@@ -331,7 +333,7 @@ const Watering = () => {
<span className="text-xs">{Number(pt.lat).toFixed(5)}, {Number(pt.lng).toFixed(5)}</span>
</span>
<span className="text-[11px] text-gray-600">
{(pt.sprinkler_head_type||'-').replace('_',' ')}
{pt.equipment_name || (pt.sprinkler_head_type||'-').replace('_',' ')}
{pt.sprinkler_throw_feet ? `${Number(pt.sprinkler_throw_feet)}ft` : ''}
{pt.sprinkler_degrees ? `${pt.sprinkler_degrees}°` : ''}
</span>
@@ -343,6 +345,9 @@ const Watering = () => {
<div className="mt-3 border-t pt-3 space-y-2">
<div className="font-medium">Selected Details</div>
<div className="text-xs text-gray-700">
{selectedPoint?.equipment_name ? (<>
<span className="font-semibold">{selectedPoint.equipment_name}</span>
</>) : null}
Type: {editForm.sprinklerHeadType || '-'} GPM: {editForm.gpm || '-'} Duration: {editForm.durationMinutes||0}m
</div>
{(editForm.sprinklerHeadType==='rotor_impact' || editForm.sprinklerHeadType==='spray_fixed') ? (
@@ -367,6 +372,33 @@ const Watering = () => {
const pl = await wateringAPI.getPlans({ property_id: selectedProperty.id });
setPlans(pl.data?.data?.plans || []); setPoints([]);
}}>New</button>
{plan && (
<button className="btn-secondary" onClick={async ()=>{
const name = `${plan.name} (Copy ${new Date().toLocaleDateString()})`;
const rs = await wateringAPI.duplicatePlan(plan.id, { name });
const np = rs.data?.data?.plan; setPlan(np);
const pl = await wateringAPI.getPlans({ property_id: selectedProperty.id });
setPlans(pl.data?.data?.plans || []);
const pts = await wateringAPI.getPlanPoints(np.id);
setPoints(pts.data?.data?.points || []);
}}>Duplicate</button>
)}
{plan && (
<button className="btn-secondary text-red-600 border-red-300" onClick={async ()=>{
if (!window.confirm('Delete this plan and all points?')) return;
await wateringAPI.deletePlan(plan.id);
const pl = await wateringAPI.getPlans({ property_id: selectedProperty.id });
const list = pl.data?.data?.plans || [];
setPlans(list);
setPlan(list[0] || null);
if (list[0]){
const rs = await wateringAPI.getPlanPoints(list[0].id);
setPoints(rs.data?.data?.points || []);
} else {
setPoints([]);
}
}}>Delete</button>
)}
</div>
{plan && (
<div className="flex gap-2 mt-2">
@@ -410,6 +442,18 @@ const Watering = () => {
<option key={s.id} value={s.id}>{s.customName || `${s.manufacturer||''} ${s.model||''}`.trim()}</option>
))}
</select>
{(!selectedSprinklerId) && (
<SaveSprinklerInline
current={sprinklerForm}
onSaved={async (newEq)=>{
// refresh list
const eq = await equipmentAPI.getAll();
const list = (eq.data?.data?.equipment||[]).filter(e => (e.categoryName||'').toLowerCase()==='sprinkler');
setSprinklers(list);
setSelectedSprinklerId(String(newEq.id));
}}
/>
)}
<div className="text-sm">Mount</div>
<select className="input" value={sprinklerForm.mount} onChange={e=> setSprinklerForm({...sprinklerForm, mount:e.target.value})}>
<option value="in_ground">InGround</option>
@@ -452,6 +496,35 @@ const Watering = () => {
{selectedPoint && editForm && (
<div className="space-y-2">
<div className="font-medium">Edit Selected</div>
<div>
<label className="text-xs">Saved Sprinkler</label>
<div className="flex gap-2 items-center">
<select className="input flex-1" value={selectedPoint?.equipment_id || ''} onChange={async (e)=>{
const val = e.target.value;
if (!val) {
await updatePointField(selectedPointId, { equipmentId: null, equipmentName: null });
return;
}
const eq = sprinklers.find(s=> s.id === parseInt(val));
if (eq) {
await updatePointField(selectedPointId, {
equipmentId: eq.id,
equipmentName: eq.customName || `${eq.manufacturer||''} ${eq.model||''}`.trim()
});
}
}}>
<option value="">-- none --</option>
{sprinklers.map(s=> (
<option key={s.id} value={s.id}>{s.customName || `${s.manufacturer||''} ${s.model||''}`.trim()}</option>
))}
</select>
{selectedPoint?.equipment_id && (
<button className="btn-secondary" onClick={async ()=>{
await updatePointField(selectedPointId, { equipmentId: null, equipmentName: null });
}}>Unlink</button>
)}
</div>
</div>
{(editForm.sprinklerHeadType==='rotor_impact' || editForm.sprinklerHeadType==='spray_fixed') ? (
<>
<label className="text-xs">Degrees</label>
@@ -537,7 +610,9 @@ const Watering = () => {
<li key={`lg-${pt.id}`} className="flex items-center gap-2">
<span style={{backgroundColor: pointColor(pt)}} className="inline-block w-3 h-3 rounded-full"></span>
<span className="font-mono">#{pt.sequence}</span>
<span className="text-gray-700">{(pt.sprinkler_head_type||'-').replace('_',' ')}</span>
<span className="text-gray-700">{pt.equipment_name || (pt.sprinkler_head_type||'-').replace('_',' ')}</span>
{pt.sprinkler_gpm ? (<span className="text-gray-600"> {Number(pt.sprinkler_gpm)} gpm</span>) : null}
{pt.duration_minutes ? (<span className="text-gray-600"> {pt.duration_minutes} min</span>) : null}
</li>
))}
</ul>
@@ -566,7 +641,7 @@ const Watering = () => {
>
<Popup>
<div className="space-y-2 text-sm">
<div className="font-medium">Adjust Sprinkler</div>
<div className="font-medium">{pt.equipment_name || 'Adjust Sprinkler'}</div>
{(pt.sprinkler_head_type==='rotor_impact' || pt.sprinkler_head_type==='spray_fixed') && (
<>
<div className="flex items-center gap-2">
@@ -666,3 +741,51 @@ const Watering = () => {
};
export default Watering;
// Inline component to save current sprinkler settings as user equipment
const SaveSprinklerInline = ({ current, onSaved }) => {
const [name, setName] = useState('');
const [saving, setSaving] = useState(false);
const save = async () => {
if (!name.trim()) { toast.error('Enter a name'); return; }
setSaving(true);
try {
// Find Sprinkler category id
const cats = await equipmentAPI.getCategories();
const list = cats.data?.data?.categories || [];
const spr = list.find(c => (c.name||'').toLowerCase() === 'sprinkler');
if (!spr) { toast.error('Sprinkler category not found'); setSaving(false); return; }
const payload = {
categoryId: spr.id,
customName: name.trim(),
// store sprinkler-specific fields
sprinklerMount: current.mount,
sprinklerHeadType: current.type,
sprinklerGpm: current.gpm,
sprinklerThrowFeet: current.throwFeet,
sprinklerDegrees: current.degrees,
sprinklerLengthFeet: current.lengthFeet,
sprinklerWidthFeet: current.widthFeet,
notes: 'Saved from Watering settings',
};
const rs = await equipmentAPI.create(payload);
const eq = rs.data?.data?.equipment;
toast.success('Sprinkler saved');
onSaved && onSaved(eq);
setName('');
} catch(e){
toast.error('Failed to save sprinkler');
} finally { setSaving(false); }
};
return (
<div className="mt-2 p-2 border rounded bg-gray-50 space-y-2">
<div className="text-sm">Save current settings as equipment</div>
<div className="flex gap-2">
<input className="input flex-1" placeholder="Name (e.g., Front Yard Rotor)" value={name} onChange={e=> setName(e.target.value)} />
<button className="btn-secondary" disabled={saving} onClick={save}>{saving? 'Saving...':'Save'}</button>
</div>
</div>
);
};

View File

@@ -230,6 +230,8 @@ export const wateringAPI = {
getPlans: (params) => apiClient.get('/watering/plans', { params }),
updatePlan: (planId, payload) => apiClient.put(`/watering/plans/${planId}`, payload),
createPlan: (payload) => apiClient.post('/watering/plans', payload),
deletePlan: (planId) => apiClient.delete(`/watering/plans/${planId}`),
duplicatePlan: (planId, payload) => apiClient.post(`/watering/plans/${planId}/duplicate`, payload),
getPlanPoints: (planId) => apiClient.get(`/watering/plans/${planId}/points`),
addPlanPoint: (planId, payload) => apiClient.post(`/watering/plans/${planId}/points`, payload),
updatePoint: (pointId, payload) => apiClient.put(`/watering/points/${pointId}`, payload),