mowing and properties
This commit is contained in:
@@ -83,8 +83,15 @@ router.post('/sessions', validateRequest(mowingSessionSchema), async (req, res,
|
|||||||
// GET /api/mowing/sessions - list sessions for user
|
// GET /api/mowing/sessions - list sessions for user
|
||||||
router.get('/sessions', async (req, res, next) => {
|
router.get('/sessions', async (req, res, next) => {
|
||||||
try {
|
try {
|
||||||
|
const { property_id } = req.query;
|
||||||
|
const params = [req.user.id];
|
||||||
|
let where = 'ms.user_id=$1';
|
||||||
|
if (property_id) {
|
||||||
|
params.push(property_id);
|
||||||
|
where += ` AND p.id=$${params.length}`;
|
||||||
|
}
|
||||||
const result = await pool.query(
|
const result = await pool.query(
|
||||||
`SELECT ms.*, p.name as property_name, ue.custom_name as equipment_name,
|
`SELECT ms.*, p.name as property_name, ue.custom_name as equipment_name, ue.cutting_width_inches,
|
||||||
STRING_AGG(ls.name, ', ') as section_names,
|
STRING_AGG(ls.name, ', ') as section_names,
|
||||||
SUM(ls.area) as total_area
|
SUM(ls.area) as total_area
|
||||||
FROM mowing_sessions ms
|
FROM mowing_sessions ms
|
||||||
@@ -92,11 +99,11 @@ router.get('/sessions', async (req, res, next) => {
|
|||||||
LEFT JOIN user_equipment ue ON ms.equipment_id=ue.id
|
LEFT JOIN user_equipment ue ON ms.equipment_id=ue.id
|
||||||
JOIN mowing_session_sections lss ON lss.session_id = ms.id
|
JOIN mowing_session_sections lss ON lss.session_id = ms.id
|
||||||
JOIN lawn_sections ls ON lss.lawn_section_id = ls.id
|
JOIN lawn_sections ls ON lss.lawn_section_id = ls.id
|
||||||
WHERE ms.user_id=$1
|
WHERE ${where}
|
||||||
GROUP BY ms.id, p.name, ue.custom_name
|
GROUP BY ms.id, p.name, ue.custom_name
|
||||||
ORDER BY ms.created_at DESC
|
ORDER BY ms.created_at DESC
|
||||||
LIMIT 200`,
|
LIMIT 200`,
|
||||||
[req.user.id]
|
params
|
||||||
);
|
);
|
||||||
res.json({ success: true, data: { sessions: result.rows } });
|
res.json({ success: true, data: { sessions: result.rows } });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -109,7 +116,7 @@ router.get('/sessions/:id', async (req, res, next) => {
|
|||||||
try {
|
try {
|
||||||
const { id } = req.params;
|
const { id } = req.params;
|
||||||
const sessionRes = await pool.query(
|
const sessionRes = await pool.query(
|
||||||
`SELECT ms.*, p.name as property_name, ue.custom_name as equipment_name
|
`SELECT ms.*, p.name as property_name, ue.custom_name as equipment_name, ue.cutting_width_inches
|
||||||
FROM mowing_sessions ms
|
FROM mowing_sessions ms
|
||||||
JOIN properties p ON ms.property_id=p.id
|
JOIN properties p ON ms.property_id=p.id
|
||||||
LEFT JOIN user_equipment ue ON ms.equipment_id=ue.id
|
LEFT JOIN user_equipment ue ON ms.equipment_id=ue.id
|
||||||
@@ -218,8 +225,15 @@ router.put('/plans/:id/status', async (req, res, next) => {
|
|||||||
// Alias /logs to sessions to be consistent with Applications
|
// Alias /logs to sessions to be consistent with Applications
|
||||||
router.get('/logs', async (req, res, next) => {
|
router.get('/logs', async (req, res, next) => {
|
||||||
try {
|
try {
|
||||||
|
const { property_id } = req.query;
|
||||||
|
const params = [req.user.id];
|
||||||
|
let where = 'ms.user_id=$1';
|
||||||
|
if (property_id) {
|
||||||
|
params.push(property_id);
|
||||||
|
where += ` AND p.id=$${params.length}`;
|
||||||
|
}
|
||||||
const rs = await pool.query(
|
const rs = await pool.query(
|
||||||
`SELECT ms.*, p.name as property_name, ue.custom_name as equipment_name,
|
`SELECT ms.*, p.name as property_name, ue.custom_name as equipment_name, ue.cutting_width_inches,
|
||||||
STRING_AGG(ls.name, ', ') as section_names,
|
STRING_AGG(ls.name, ', ') as section_names,
|
||||||
SUM(ls.area) as total_area
|
SUM(ls.area) as total_area
|
||||||
FROM mowing_sessions ms
|
FROM mowing_sessions ms
|
||||||
@@ -227,10 +241,10 @@ router.get('/logs', async (req, res, next) => {
|
|||||||
LEFT JOIN user_equipment ue ON ms.equipment_id=ue.id
|
LEFT JOIN user_equipment ue ON ms.equipment_id=ue.id
|
||||||
JOIN mowing_session_sections lss ON lss.session_id = ms.id
|
JOIN mowing_session_sections lss ON lss.session_id = ms.id
|
||||||
JOIN lawn_sections ls ON lss.lawn_section_id = ls.id
|
JOIN lawn_sections ls ON lss.lawn_section_id = ls.id
|
||||||
WHERE ms.user_id=$1
|
WHERE ${where}
|
||||||
GROUP BY ms.id, p.name, ue.custom_name
|
GROUP BY ms.id, p.name, ue.custom_name
|
||||||
ORDER BY ms.created_at DESC
|
ORDER BY ms.created_at DESC
|
||||||
LIMIT 200`, [req.user.id]);
|
LIMIT 200`, params);
|
||||||
res.json({ success: true, data: { logs: rs.rows } });
|
res.json({ success: true, data: { logs: rs.rows } });
|
||||||
} catch (error) { next(error); }
|
} catch (error) { next(error); }
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -373,11 +373,10 @@ const History = () => {
|
|||||||
const meters = (log.total_distance_meters || log.gpsTrack?.totalDistance || log.gps_track?.totalDistance || 0) || 0;
|
const meters = (log.total_distance_meters || log.gpsTrack?.totalDistance || log.gps_track?.totalDistance || 0) || 0;
|
||||||
const totalDistanceFeet = meters * 3.28084;
|
const totalDistanceFeet = meters * 3.28084;
|
||||||
const plannedArea = Number(log.total_area || 0);
|
const plannedArea = Number(log.total_area || 0);
|
||||||
if (totalDistanceFeet === 0 || plannedArea === 0) return 0;
|
const widthInches = parseFloat(log.cutting_width_inches || 0);
|
||||||
let equipmentWidth = 6; // default mower deck width in ft
|
const widthFeet = isNaN(widthInches) ? 0 : (widthInches / 12);
|
||||||
const name = (log.equipment_name || '').toLowerCase();
|
if (totalDistanceFeet === 0 || plannedArea === 0 || widthFeet === 0) return 0;
|
||||||
if (name.includes('30"') || name.includes('30 in')) equipmentWidth = 2.5;
|
const theoreticalCoverageArea = totalDistanceFeet * widthFeet;
|
||||||
const theoreticalCoverageArea = totalDistanceFeet * equipmentWidth;
|
|
||||||
return Math.min(100, Math.round((theoreticalCoverageArea / plannedArea) * 100));
|
return Math.min(100, Math.round((theoreticalCoverageArea / plannedArea) * 100));
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -11,7 +11,10 @@ import {
|
|||||||
SwatchIcon,
|
SwatchIcon,
|
||||||
PencilIcon
|
PencilIcon
|
||||||
} from '@heroicons/react/24/outline';
|
} from '@heroicons/react/24/outline';
|
||||||
import { propertiesAPI } from '../../services/api';
|
import { propertiesAPI, applicationsAPI, mowingAPI } from '../../services/api';
|
||||||
|
import ApplicationViewModal from '../../components/Applications/ApplicationViewModal';
|
||||||
|
import MowingSessionViewModal from '../../components/Mowing/MowingSessionViewModal';
|
||||||
|
import { EyeIcon, CalendarIcon, WrenchScrewdriverIcon } from '@heroicons/react/24/outline';
|
||||||
import LoadingSpinner from '../../components/UI/LoadingSpinner';
|
import LoadingSpinner from '../../components/UI/LoadingSpinner';
|
||||||
import toast from 'react-hot-toast';
|
import toast from 'react-hot-toast';
|
||||||
import 'leaflet/dist/leaflet.css';
|
import 'leaflet/dist/leaflet.css';
|
||||||
@@ -302,10 +305,45 @@ const PropertyDetail = () => {
|
|||||||
const [editingSection, setEditingSection] = useState(null);
|
const [editingSection, setEditingSection] = useState(null);
|
||||||
const [showEditModal, setShowEditModal] = useState(false);
|
const [showEditModal, setShowEditModal] = useState(false);
|
||||||
|
|
||||||
|
// Recent history state for this property
|
||||||
|
const [completedApplications, setCompletedApplications] = useState([]);
|
||||||
|
const [applicationLogs, setApplicationLogs] = useState([]);
|
||||||
|
const [mowingLogs, setMowingLogs] = useState([]);
|
||||||
|
const [historyLoading, setHistoryLoading] = useState(false);
|
||||||
|
const [viewingApplication, setViewingApplication] = useState(null);
|
||||||
|
const [viewingMowingSession, setViewingMowingSession] = useState(null);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
fetchPropertyDetails();
|
fetchPropertyDetails();
|
||||||
}, [id]); // eslint-disable-line react-hooks/exhaustive-deps
|
}, [id]); // eslint-disable-line react-hooks/exhaustive-deps
|
||||||
|
|
||||||
|
// Load recent history when property is available
|
||||||
|
useEffect(() => {
|
||||||
|
if (!property?.id) return;
|
||||||
|
const loadHistory = async () => {
|
||||||
|
try {
|
||||||
|
setHistoryLoading(true);
|
||||||
|
const [completedRes, archivedRes, logsRes, mowRes] = await Promise.all([
|
||||||
|
applicationsAPI.getPlans({ status: 'completed', property_id: property.id }),
|
||||||
|
applicationsAPI.getPlans({ status: 'archived', property_id: property.id }),
|
||||||
|
applicationsAPI.getLogs({ property_id: property.id }),
|
||||||
|
mowingAPI.getLogs({ property_id: property.id })
|
||||||
|
]);
|
||||||
|
const completedPlans = completedRes.data?.data?.plans || [];
|
||||||
|
const archivedPlans = archivedRes.data?.data?.plans || [];
|
||||||
|
setCompletedApplications([...(completedPlans||[]), ...(archivedPlans||[])]);
|
||||||
|
setApplicationLogs(logsRes.data?.data?.logs || []);
|
||||||
|
setMowingLogs(mowRes.data?.data?.logs || []);
|
||||||
|
} catch (e) {
|
||||||
|
console.warn('Failed to load property history', e?.response?.data || e.message);
|
||||||
|
setCompletedApplications([]);
|
||||||
|
setApplicationLogs([]);
|
||||||
|
setMowingLogs([]);
|
||||||
|
} finally { setHistoryLoading(false); }
|
||||||
|
};
|
||||||
|
loadHistory();
|
||||||
|
}, [property?.id]);
|
||||||
|
|
||||||
// Handle keyboard shortcuts for polygon drawing
|
// Handle keyboard shortcuts for polygon drawing
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const handleKeyPress = (e) => {
|
const handleKeyPress = (e) => {
|
||||||
@@ -494,6 +532,55 @@ const PropertyDetail = () => {
|
|||||||
return lawnSections.reduce((total, section) => total + section.area, 0);
|
return lawnSections.reduce((total, section) => total + section.area, 0);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// History helpers
|
||||||
|
const getApplicationType = (app) => {
|
||||||
|
if (!app.productDetails || app.productDetails.length === 0) return null;
|
||||||
|
return app.productDetails[0].type;
|
||||||
|
};
|
||||||
|
|
||||||
|
const calculateCoverage = (application, log) => {
|
||||||
|
if (!log?.gpsTrack?.points && !log?.gps_track?.points) return 0;
|
||||||
|
const storedMeters = typeof (log.gpsTrack?.totalDistance || log.gps_track?.totalDistance) === 'number' ? (log.gpsTrack?.totalDistance || log.gps_track?.totalDistance) : 0;
|
||||||
|
const totalDistanceFeet = storedMeters * 3.28084;
|
||||||
|
const plannedArea = application.totalSectionArea || application.total_section_area || 0;
|
||||||
|
if (totalDistanceFeet === 0 || plannedArea === 0) return 0;
|
||||||
|
let equipmentWidth = 4;
|
||||||
|
const equipmentName = (application.equipmentName || '').toLowerCase();
|
||||||
|
if (equipmentName.includes('spreader')) equipmentWidth = 12;
|
||||||
|
else if (equipmentName.includes('sprayer')) equipmentWidth = 20;
|
||||||
|
else if (equipmentName.includes('mower')) equipmentWidth = 6;
|
||||||
|
const theoreticalCoverageArea = totalDistanceFeet * equipmentWidth;
|
||||||
|
return Math.min(Math.round((theoreticalCoverageArea / plannedArea) * 100), 100);
|
||||||
|
};
|
||||||
|
|
||||||
|
const calculateMowingCoverage = (log) => {
|
||||||
|
const meters = (log.total_distance_meters || log.gpsTrack?.totalDistance || log.gps_track?.totalDistance || 0) || 0;
|
||||||
|
const totalDistanceFeet = meters * 3.28084;
|
||||||
|
const plannedArea = Number(log.total_area || 0);
|
||||||
|
const widthInches = parseFloat(log.cutting_width_inches || 0);
|
||||||
|
const widthFeet = isNaN(widthInches) ? 0 : (widthInches / 12);
|
||||||
|
if (totalDistanceFeet === 0 || plannedArea === 0 || widthFeet === 0) return 0;
|
||||||
|
const theoreticalCoverageArea = totalDistanceFeet * widthFeet;
|
||||||
|
return Math.min(100, Math.round((theoreticalCoverageArea / plannedArea) * 100));
|
||||||
|
};
|
||||||
|
|
||||||
|
const unifiedHistory = (() => {
|
||||||
|
const apps = (completedApplications||[]).map((application) => {
|
||||||
|
const log = applicationLogs.find((l) => l.planId === application.id);
|
||||||
|
const dateStr = log?.applicationDate || application.plannedDate || application.updatedAt || application.createdAt;
|
||||||
|
const date = dateStr ? new Date(dateStr) : new Date(0);
|
||||||
|
return { kind: 'application', date, application, log };
|
||||||
|
});
|
||||||
|
const mows = (mowingLogs||[]).map((log) => {
|
||||||
|
const dateStr = log.session_date || log.created_at;
|
||||||
|
const date = dateStr ? new Date(dateStr) : new Date(0);
|
||||||
|
return { kind: 'mowing', date, log };
|
||||||
|
});
|
||||||
|
const items = [...apps, ...mows];
|
||||||
|
items.sort((a,b)=> b.date - a.date);
|
||||||
|
return items.slice(0, 10);
|
||||||
|
})();
|
||||||
|
|
||||||
if (loading) {
|
if (loading) {
|
||||||
return (
|
return (
|
||||||
<div className="p-6">
|
<div className="p-6">
|
||||||
@@ -704,6 +791,95 @@ const PropertyDetail = () => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Recent History */}
|
||||||
|
<div className="mt-8">
|
||||||
|
<h2 className="text-lg font-semibold mb-3">Recent History</h2>
|
||||||
|
{historyLoading ? (
|
||||||
|
<div className="card p-4"><div className="text-gray-600">Loading…</div></div>
|
||||||
|
) : unifiedHistory.length === 0 ? (
|
||||||
|
<div className="card p-4 text-gray-600">No history yet for this property.</div>
|
||||||
|
) : (
|
||||||
|
<div className="grid gap-4">
|
||||||
|
{unifiedHistory.map((item) => {
|
||||||
|
if (item.kind === 'application') {
|
||||||
|
const application = item.application; const log = item.log;
|
||||||
|
return (
|
||||||
|
<div key={`app-${application.id}`} className="bg-white p-6 rounded-lg shadow">
|
||||||
|
<div className="flex justify-between items-start">
|
||||||
|
<div className="flex-1">
|
||||||
|
<div className="flex items-center justify-between mb-4">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<h3 className="text-lg font-semibold text-gray-900">{application.propertyName} - {application.sectionNames}</h3>
|
||||||
|
<span className="px-2 py-0.5 text-xs rounded-full bg-indigo-100 text-indigo-800">Application</span>
|
||||||
|
</div>
|
||||||
|
<span className={`px-3 py-1 text-sm font-medium rounded-full ${application.status==='archived'?'bg-gray-100 text-gray-800':'bg-green-100 text-green-800'}`}>{application.status==='archived'?'Archived':'Completed'}</span>
|
||||||
|
</div>
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-3 gap-4 mb-4">
|
||||||
|
<div className="flex items-center text-sm text-gray-600"><CalendarIcon className="h-4 w-4 mr-2" />{new Date(item.date).toLocaleString()}</div>
|
||||||
|
<div className="flex items-center text-sm text-gray-600"><MapPinIcon className="h-4 w-4 mr-2" />{application.propertyName}</div>
|
||||||
|
<div className="flex items-center text-sm text-gray-600"><WrenchScrewdriverIcon className="h-4 w-4 mr-2" />{application.equipmentName}</div>
|
||||||
|
</div>
|
||||||
|
{log?.gpsTrack && (
|
||||||
|
<div className="grid grid-cols-2 md:grid-cols-4 gap-2 mb-4">
|
||||||
|
<div className="bg-blue-50 p-2 rounded text-center">
|
||||||
|
<div className="text-xs text-blue-600 font-medium">Duration</div>
|
||||||
|
<div className="text-sm font-bold text-blue-900">{Math.round((log.gpsTrack?.duration||0)/60)} min</div>
|
||||||
|
</div>
|
||||||
|
<div className="bg-green-50 p-2 rounded text-center">
|
||||||
|
<div className="text-xs text-green-600 font-medium">GPS Points</div>
|
||||||
|
<div className="text-sm font-bold text-green-900">{log.gpsTrack?.points?.length || 0}</div>
|
||||||
|
</div>
|
||||||
|
<div className="bg-purple-50 p-2 rounded text-center">
|
||||||
|
<div className="text-xs text-purple-600 font-medium">Distance</div>
|
||||||
|
<div className="text-sm font-bold text-purple-900">{Math.round(log.gpsTrack?.totalDistance||0)} ft</div>
|
||||||
|
</div>
|
||||||
|
<div className="bg-orange-50 p-2 rounded text-center">
|
||||||
|
<div className="text-xs text-orange-600 font-medium">Coverage</div>
|
||||||
|
<div className="text-sm font-bold text-orange-900">{calculateCoverage(application, log)}%</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<button onClick={()=> setViewingApplication(application)} className="p-2 text-indigo-600 hover:text-indigo-800 hover:bg-indigo-50 rounded" title="View details"><EyeIcon className="h-5 w-5" /></button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
const log = item.log;
|
||||||
|
const durationMin = Math.round((log.duration_seconds || log.durationSeconds || log.gpsTrack?.duration || log.gps_track?.duration || 0)/60);
|
||||||
|
const distFeet = Math.round(((log.total_distance_meters || log.gpsTrack?.totalDistance || log.gps_track?.totalDistance || 0)*3.28084)||0);
|
||||||
|
return (
|
||||||
|
<div key={`mow-${log.id}`} className="bg-white p-6 rounded-lg shadow">
|
||||||
|
<div className="flex justify-between items-start">
|
||||||
|
<div className="flex-1">
|
||||||
|
<div className="flex items-center justify-between mb-4">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<h3 className="text-lg font-semibold text-gray-900">{log.property_name} - {log.section_names}</h3>
|
||||||
|
<span className="px-2 py-0.5 text-xs rounded-full bg-green-100 text-green-800">Mowing</span>
|
||||||
|
</div>
|
||||||
|
<span className="px-3 py-1 text-sm font-medium rounded-full bg-green-100 text-green-800">Completed</span>
|
||||||
|
</div>
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-3 gap-4 mb-4">
|
||||||
|
<div className="flex items-center text-sm text-gray-600"><CalendarIcon className="h-4 w-4 mr-2" />{new Date(item.date).toLocaleString()}</div>
|
||||||
|
<div className="flex items-center text-sm text-gray-600"><MapPinIcon className="h-4 w-4 mr-2" />{log.property_name}</div>
|
||||||
|
<div className="flex items-center text-sm text-gray-600"><WrenchScrewdriverIcon className="h-4 w-4 mr-2" />{log.equipment_name || 'Mower'}</div>
|
||||||
|
</div>
|
||||||
|
<div className="grid grid-cols-2 md:grid-cols-4 gap-2 mb-4">
|
||||||
|
<div className="bg-blue-50 p-2 rounded text-center"><div className="text-xs text-blue-600 font-medium">Duration</div><div className="text-sm font-bold text-blue-900">{durationMin} min</div></div>
|
||||||
|
<div className="bg-green-50 p-2 rounded text-center"><div className="text-xs text-green-600 font-medium">GPS Points</div><div className="text-sm font-bold text-green-900">{log.gpsTrack?.points?.length || log.gps_track?.points?.length || 0}</div></div>
|
||||||
|
<div className="bg-purple-50 p-2 rounded text-center"><div className="text-xs text-purple-600 font-medium">Distance</div><div className="text-sm font-bold text-purple-900">{distFeet} ft</div></div>
|
||||||
|
<div className="bg-orange-50 p-2 rounded text-center"><div className="text-xs text-orange-600 font-medium">Coverage</div><div className="text-sm font-bold text-orange-900">{calculateMowingCoverage(log)}%</div></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<button onClick={()=> setViewingMowingSession(log)} className="p-2 text-indigo-600 hover:text-indigo-800 hover:bg-indigo-50 rounded" title="View mowing session"><EyeIcon className="h-5 w-5" /></button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* Name Modal */}
|
{/* Name Modal */}
|
||||||
{showNameModal && (
|
{showNameModal && (
|
||||||
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center" style={{ zIndex: 9999 }}>
|
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center" style={{ zIndex: 9999 }}>
|
||||||
@@ -806,8 +982,23 @@ const PropertyDetail = () => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{/* View Modals for History */}
|
||||||
|
{viewingApplication && (
|
||||||
|
<ApplicationViewModal
|
||||||
|
application={viewingApplication}
|
||||||
|
propertyDetails={property}
|
||||||
|
onClose={() => setViewingApplication(null)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{viewingMowingSession && (
|
||||||
|
<MowingSessionViewModal
|
||||||
|
session={viewingMowingSession}
|
||||||
|
onClose={() => setViewingMowingSession(null)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default PropertyDetail;
|
export default PropertyDetail;
|
||||||
|
|||||||
@@ -244,7 +244,7 @@ export const mowingAPI = {
|
|||||||
updatePlanStatus: (id, status) => apiClient.put(`/mowing/plans/${id}/status`, { status }),
|
updatePlanStatus: (id, status) => apiClient.put(`/mowing/plans/${id}/status`, { status }),
|
||||||
// Logs/Sessions
|
// Logs/Sessions
|
||||||
createLog: (data) => apiClient.post('/mowing/sessions', data),
|
createLog: (data) => apiClient.post('/mowing/sessions', data),
|
||||||
getLogs: () => apiClient.get('/mowing/logs'),
|
getLogs: (params) => apiClient.get('/mowing/logs', { params }),
|
||||||
getSession: (id) => apiClient.get(`/mowing/sessions/${id}`),
|
getSession: (id) => apiClient.get(`/mowing/sessions/${id}`),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user