sdgsdgf
This commit is contained in:
@@ -102,6 +102,15 @@ router.post('/plans/:id/points', async (req,res,next)=>{
|
|||||||
const seqrs = await pool.query('SELECT COALESCE(MAX(sequence),0)+1 as next FROM watering_plan_points WHERE plan_id=$1',[planId]);
|
const seqrs = await pool.query('SELECT COALESCE(MAX(sequence),0)+1 as next FROM watering_plan_points WHERE plan_id=$1',[planId]);
|
||||||
const sequence = seqrs.rows[0].next;
|
const sequence = seqrs.rows[0].next;
|
||||||
const payload = req.body || {};
|
const payload = req.body || {};
|
||||||
|
let equipment_id = null, equipment_name = null;
|
||||||
|
if (payload.equipmentId) {
|
||||||
|
// verify ownership, otherwise ignore silently
|
||||||
|
const eq = await pool.query('SELECT id, custom_name, manufacturer, model FROM user_equipment WHERE id=$1 AND user_id=$2', [payload.equipmentId, req.user.id]);
|
||||||
|
if (eq.rows.length) {
|
||||||
|
equipment_id = eq.rows[0].id;
|
||||||
|
equipment_name = payload.equipmentName || eq.rows[0].custom_name || `${eq.rows[0].manufacturer||''} ${eq.rows[0].model||''}`.trim() || null;
|
||||||
|
}
|
||||||
|
}
|
||||||
const point = {
|
const point = {
|
||||||
sprinkler_head_type: payload.sprinklerHeadType,
|
sprinkler_head_type: payload.sprinklerHeadType,
|
||||||
sprinkler_throw_feet: payload.throwFeet,
|
sprinkler_throw_feet: payload.throwFeet,
|
||||||
@@ -113,11 +122,13 @@ 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_heading_degrees)
|
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 *`,
|
equipment_id, equipment_name)
|
||||||
|
VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13,$14,$15,$16) 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.headingDegrees||null]
|
payload.lengthFeet||null, payload.widthFeet||null, coverage, payload.headingDegrees||null,
|
||||||
|
equipment_id, equipment_name]
|
||||||
);
|
);
|
||||||
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); }
|
||||||
@@ -158,10 +169,12 @@ router.put('/points/:id', async (req, res, next) => {
|
|||||||
sprinkler_length_feet = COALESCE($9, sprinkler_length_feet),
|
sprinkler_length_feet = COALESCE($9, sprinkler_length_feet),
|
||||||
sprinkler_width_feet = COALESCE($10, sprinkler_width_feet),
|
sprinkler_width_feet = COALESCE($10, sprinkler_width_feet),
|
||||||
sprinkler_heading_degrees = COALESCE($11, sprinkler_heading_degrees),
|
sprinkler_heading_degrees = COALESCE($11, sprinkler_heading_degrees),
|
||||||
coverage_sqft = COALESCE($12, coverage_sqft)
|
coverage_sqft = COALESCE($12, coverage_sqft),
|
||||||
WHERE id=$13 RETURNING *`,
|
equipment_id = COALESCE($13, equipment_id),
|
||||||
|
equipment_name = COALESCE($14, equipment_name)
|
||||||
|
WHERE id=$15 RETURNING *`,
|
||||||
[p.lat, p.lng, p.durationMinutes, p.mountType, p.sprinklerHeadType, p.gpm,
|
[p.lat, p.lng, p.durationMinutes, p.mountType, p.sprinklerHeadType, p.gpm,
|
||||||
p.throwFeet, p.degrees, p.lengthFeet, p.widthFeet, p.headingDegrees, coverage || null, pointId]
|
p.throwFeet, p.degrees, p.lengthFeet, p.widthFeet, p.headingDegrees, coverage || null, p.equipmentId || null, p.equipmentName || null, pointId]
|
||||||
);
|
);
|
||||||
res.json({ success:true, data:{ point: rs.rows[0] }});
|
res.json({ success:true, data:{ point: rs.rows[0] }});
|
||||||
} catch (e) { next(e); }
|
} catch (e) { next(e); }
|
||||||
@@ -182,4 +195,44 @@ router.delete('/points/:id', async (req,res,next)=>{
|
|||||||
} catch (e) { next(e); }
|
} catch (e) { next(e); }
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// DELETE /api/watering/plans/:id
|
||||||
|
router.delete('/plans/:id', async (req, res, next) => {
|
||||||
|
try {
|
||||||
|
const planId = req.params.id;
|
||||||
|
const own = await pool.query('SELECT id FROM watering_plans WHERE id=$1 AND user_id=$2', [planId, req.user.id]);
|
||||||
|
if (own.rows.length === 0) throw new AppError('Plan not found', 404);
|
||||||
|
await pool.query('DELETE FROM watering_plans WHERE id=$1', [planId]);
|
||||||
|
res.json({ success: true });
|
||||||
|
} catch (e) { next(e); }
|
||||||
|
});
|
||||||
|
|
||||||
module.exports = router;
|
module.exports = router;
|
||||||
|
// Duplicate a watering plan with all points
|
||||||
|
router.post('/plans/:id/duplicate', async (req, res, next) => {
|
||||||
|
try {
|
||||||
|
const planId = req.params.id;
|
||||||
|
const own = await pool.query('SELECT * FROM watering_plans WHERE id=$1 AND user_id=$2', [planId, req.user.id]);
|
||||||
|
if (own.rows.length === 0) throw new AppError('Plan not found', 404);
|
||||||
|
const src = own.rows[0];
|
||||||
|
const newName = (req.body && req.body.name) || `${src.name} (Copy ${new Date().toLocaleDateString()})`;
|
||||||
|
const ins = await pool.query(
|
||||||
|
`INSERT INTO watering_plans(user_id, property_id, name, notes)
|
||||||
|
VALUES ($1,$2,$3,$4) RETURNING *`,
|
||||||
|
[req.user.id, src.property_id, newName, src.notes]
|
||||||
|
);
|
||||||
|
const newPlan = ins.rows[0];
|
||||||
|
// Copy points
|
||||||
|
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, sprinkler_heading_degrees, equipment_id, equipment_name)
|
||||||
|
SELECT $1, 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_heading_degrees, equipment_id, equipment_name
|
||||||
|
FROM watering_plan_points WHERE plan_id=$2`,
|
||||||
|
[newPlan.id, planId]
|
||||||
|
);
|
||||||
|
res.status(201).json({ success: true, data: { plan: newPlan } });
|
||||||
|
} catch (e) { next(e); }
|
||||||
|
});
|
||||||
|
|||||||
7
database/migrations/V13__watering_point_equipment.sql
Normal file
7
database/migrations/V13__watering_point_equipment.sql
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
-- Link watering plan points to saved equipment and store name snapshot
|
||||||
|
ALTER TABLE watering_plan_points
|
||||||
|
ADD COLUMN IF NOT EXISTS equipment_id INTEGER REFERENCES user_equipment(id) ON DELETE SET NULL,
|
||||||
|
ADD COLUMN IF NOT EXISTS equipment_name VARCHAR(255);
|
||||||
|
|
||||||
|
SELECT 'Added equipment_id and equipment_name to watering_plan_points' as migration_status;
|
||||||
|
|
||||||
@@ -222,7 +222,9 @@ const Watering = () => {
|
|||||||
degrees: base.degrees,
|
degrees: base.degrees,
|
||||||
lengthFeet: base.lengthFeet,
|
lengthFeet: base.lengthFeet,
|
||||||
widthFeet: base.widthFeet,
|
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);
|
const r = await wateringAPI.addPlanPoint(p.id, payload);
|
||||||
setPoints(prev => [...prev, r.data?.data?.point]);
|
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 className="text-xs">{Number(pt.lat).toFixed(5)}, {Number(pt.lng).toFixed(5)}</span>
|
||||||
</span>
|
</span>
|
||||||
<span className="text-[11px] text-gray-600">
|
<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_throw_feet ? ` • ${Number(pt.sprinkler_throw_feet)}ft` : ''}
|
||||||
{pt.sprinkler_degrees ? ` • ${pt.sprinkler_degrees}°` : ''}
|
{pt.sprinkler_degrees ? ` • ${pt.sprinkler_degrees}°` : ''}
|
||||||
</span>
|
</span>
|
||||||
@@ -343,6 +345,9 @@ const Watering = () => {
|
|||||||
<div className="mt-3 border-t pt-3 space-y-2">
|
<div className="mt-3 border-t pt-3 space-y-2">
|
||||||
<div className="font-medium">Selected Details</div>
|
<div className="font-medium">Selected Details</div>
|
||||||
<div className="text-xs text-gray-700">
|
<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
|
Type: {editForm.sprinklerHeadType || '-'} • GPM: {editForm.gpm || '-'} • Duration: {editForm.durationMinutes||0}m
|
||||||
</div>
|
</div>
|
||||||
{(editForm.sprinklerHeadType==='rotor_impact' || editForm.sprinklerHeadType==='spray_fixed') ? (
|
{(editForm.sprinklerHeadType==='rotor_impact' || editForm.sprinklerHeadType==='spray_fixed') ? (
|
||||||
@@ -367,6 +372,33 @@ const Watering = () => {
|
|||||||
const pl = await wateringAPI.getPlans({ property_id: selectedProperty.id });
|
const pl = await wateringAPI.getPlans({ property_id: selectedProperty.id });
|
||||||
setPlans(pl.data?.data?.plans || []); setPoints([]);
|
setPlans(pl.data?.data?.plans || []); setPoints([]);
|
||||||
}}>New</button>
|
}}>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>
|
</div>
|
||||||
{plan && (
|
{plan && (
|
||||||
<div className="flex gap-2 mt-2">
|
<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>
|
<option key={s.id} value={s.id}>{s.customName || `${s.manufacturer||''} ${s.model||''}`.trim()}</option>
|
||||||
))}
|
))}
|
||||||
</select>
|
</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>
|
<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>
|
||||||
@@ -452,6 +496,35 @@ const Watering = () => {
|
|||||||
{selectedPoint && editForm && (
|
{selectedPoint && editForm && (
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<div className="font-medium">Edit Selected</div>
|
<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') ? (
|
{(editForm.sprinklerHeadType==='rotor_impact' || editForm.sprinklerHeadType==='spray_fixed') ? (
|
||||||
<>
|
<>
|
||||||
<label className="text-xs">Degrees</label>
|
<label className="text-xs">Degrees</label>
|
||||||
@@ -537,7 +610,9 @@ const Watering = () => {
|
|||||||
<li key={`lg-${pt.id}`} className="flex items-center gap-2">
|
<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 style={{backgroundColor: pointColor(pt)}} className="inline-block w-3 h-3 rounded-full"></span>
|
||||||
<span className="font-mono">#{pt.sequence}</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>
|
</li>
|
||||||
))}
|
))}
|
||||||
</ul>
|
</ul>
|
||||||
@@ -566,7 +641,7 @@ const Watering = () => {
|
|||||||
>
|
>
|
||||||
<Popup>
|
<Popup>
|
||||||
<div className="space-y-2 text-sm">
|
<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') && (
|
{(pt.sprinkler_head_type==='rotor_impact' || pt.sprinkler_head_type==='spray_fixed') && (
|
||||||
<>
|
<>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
@@ -666,3 +741,51 @@ const Watering = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export default 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>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|||||||
@@ -230,6 +230,8 @@ export const wateringAPI = {
|
|||||||
getPlans: (params) => apiClient.get('/watering/plans', { params }),
|
getPlans: (params) => apiClient.get('/watering/plans', { params }),
|
||||||
updatePlan: (planId, payload) => apiClient.put(`/watering/plans/${planId}`, payload),
|
updatePlan: (planId, payload) => apiClient.put(`/watering/plans/${planId}`, payload),
|
||||||
createPlan: (payload) => apiClient.post('/watering/plans', 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`),
|
getPlanPoints: (planId) => apiClient.get(`/watering/plans/${planId}/points`),
|
||||||
addPlanPoint: (planId, payload) => apiClient.post(`/watering/plans/${planId}/points`, payload),
|
addPlanPoint: (planId, payload) => apiClient.post(`/watering/plans/${planId}/points`, payload),
|
||||||
updatePoint: (pointId, payload) => apiClient.put(`/watering/points/${pointId}`, payload),
|
updatePoint: (pointId, payload) => apiClient.put(`/watering/points/${pointId}`, payload),
|
||||||
|
|||||||
Reference in New Issue
Block a user