mowing
This commit is contained in:
33
frontend/src/components/Icons/GrassIcon.js
Normal file
33
frontend/src/components/Icons/GrassIcon.js
Normal file
@@ -0,0 +1,33 @@
|
||||
import React from 'react';
|
||||
|
||||
// Simple grass-shaped icons that follow currentColor
|
||||
export const GrassIconOutline = (props) => (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="1.5"
|
||||
className={props.className}
|
||||
aria-hidden="true"
|
||||
>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M3 20h18" />
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M5 20v-5m0 0 2 2m-2-2-2 2M9 20v-6m0 0 2 2m-2-2-2 2M13 20v-5m0 0 2 2m-2-2-2 2M17 20v-6m0 0 2 2m-2-2-2 2" />
|
||||
</svg>
|
||||
);
|
||||
|
||||
export const GrassIconSolid = (props) => (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 24 24"
|
||||
fill="currentColor"
|
||||
className={props.className}
|
||||
aria-hidden="true"
|
||||
>
|
||||
<rect x="3" y="19" width="18" height="2" rx="0.5" />
|
||||
<path d="M6 14v6h2v-4l2 2v2h2v-4l2 2v2h2v-4l2 2v2h2v-1.5l-2.5-2.5L22 16v-1.5l-2.5-2.5L22 12V10l-3 3-3-3v3l-2.5-2.5L13 12v3l-2.5-2.5L9 14v4H7v-3l-2 2v1H4v-1.5l2-2.5-2-1.5V14z" />
|
||||
</svg>
|
||||
);
|
||||
|
||||
export default GrassIconOutline;
|
||||
|
||||
@@ -24,6 +24,7 @@ import {
|
||||
ClockIcon as ClockIconSolid,
|
||||
CloudIcon as CloudIconSolid,
|
||||
} from '@heroicons/react/24/solid';
|
||||
import { GrassIconOutline, GrassIconSolid } from '../Icons/GrassIcon';
|
||||
|
||||
import { useAuth } from '../../hooks/useAuth';
|
||||
import LoadingSpinner from '../UI/LoadingSpinner';
|
||||
@@ -68,8 +69,8 @@ const Layout = ({ children }) => {
|
||||
{
|
||||
name: 'Mowing',
|
||||
href: '/mowing',
|
||||
icon: ClockIcon,
|
||||
iconSolid: ClockIconSolid,
|
||||
icon: GrassIconOutline,
|
||||
iconSolid: GrassIconSolid,
|
||||
},
|
||||
{
|
||||
name: 'History',
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import React, { useEffect, useMemo, useState } from 'react';
|
||||
import { propertiesAPI, equipmentAPI, mowingAPI } from '../../services/api';
|
||||
import toast from 'react-hot-toast';
|
||||
import { XMarkIcon, MapPinIcon, WrenchScrewdriverIcon } from '@heroicons/react/24/outline';
|
||||
import PropertyMap from '../Maps/PropertyMap';
|
||||
|
||||
const directionOptions = [
|
||||
{ value: 'N_S', label: 'North to South' },
|
||||
@@ -14,6 +16,7 @@ const MowingPlanModal = ({ onClose, onCreated }) => {
|
||||
const [properties, setProperties] = useState([]);
|
||||
const [mowers, setMowers] = useState([]);
|
||||
const [sections, setSections] = useState([]);
|
||||
const [selectedPropertyDetails, setSelectedPropertyDetails] = useState(null);
|
||||
const [propertyId, setPropertyId] = useState('');
|
||||
const [lawnSectionIds, setLawnSectionIds] = useState([]);
|
||||
const [equipmentId, setEquipmentId] = useState('');
|
||||
@@ -36,14 +39,41 @@ const MowingPlanModal = ({ onClose, onCreated }) => {
|
||||
|
||||
useEffect(() => {
|
||||
const loadSections = async () => {
|
||||
if (!propertyId) { setSections([]); return; }
|
||||
try { const r = await propertiesAPI.getById(propertyId); setSections(r.data.data.property.sections || []);} catch {
|
||||
if (!propertyId) { setSections([]); setSelectedPropertyDetails(null); return; }
|
||||
try {
|
||||
const r = await propertiesAPI.getById(propertyId);
|
||||
const prop = r.data.data.property;
|
||||
setSelectedPropertyDetails(prop);
|
||||
setSections(prop.sections || []);
|
||||
} catch {
|
||||
setSelectedPropertyDetails(null);
|
||||
setSections([]);
|
||||
}
|
||||
};
|
||||
loadSections();
|
||||
}, [propertyId]);
|
||||
|
||||
// Map center similar to ApplicationPlanModal
|
||||
const mapCenter = useMemo(() => {
|
||||
if (selectedPropertyDetails?.latitude && selectedPropertyDetails?.longitude) {
|
||||
return [selectedPropertyDetails.latitude, selectedPropertyDetails.longitude];
|
||||
}
|
||||
if (selectedPropertyDetails?.sections?.length > 0) {
|
||||
let totalLat = 0, totalLng = 0, count = 0;
|
||||
selectedPropertyDetails.sections.forEach(section => {
|
||||
let polygonData = section.polygonData;
|
||||
if (typeof polygonData === 'string') {
|
||||
try { polygonData = JSON.parse(polygonData); } catch { return; }
|
||||
}
|
||||
if (polygonData?.coordinates?.[0]) {
|
||||
polygonData.coordinates[0].forEach(([lat, lng]) => { totalLat += lat; totalLng += lng; count++; });
|
||||
}
|
||||
});
|
||||
if (count > 0) return [totalLat / count, totalLng / count];
|
||||
}
|
||||
return [39.8283, -98.5795];
|
||||
}, [selectedPropertyDetails]);
|
||||
|
||||
const create = async () => {
|
||||
try {
|
||||
if (!propertyId || lawnSectionIds.length === 0 || !equipmentId) { toast.error('Missing fields'); return; }
|
||||
@@ -56,54 +86,168 @@ const MowingPlanModal = ({ onClose, onCreated }) => {
|
||||
|
||||
return (
|
||||
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
|
||||
<div className="bg-white rounded-lg p-6 w-full max-w-3xl max-h-[90vh] overflow-y-auto">
|
||||
<div className="flex justify-between items-center mb-4">
|
||||
<h3 className="text-xl font-semibold">New Mowing Plan</h3>
|
||||
<button onClick={onClose} className="text-gray-500 hover:text-gray-700">✕</button>
|
||||
<div className="bg-white rounded-lg p-6 w-full max-w-6xl mx-4 max-h-[90vh] overflow-y-auto">
|
||||
{/* Header */}
|
||||
<div className="flex justify-between items-center mb-6">
|
||||
<h2 className="text-2xl font-bold text-gray-900">Plan New Mowing</h2>
|
||||
<button onClick={onClose} className="text-gray-400 hover:text-gray-600">
|
||||
<XMarkIcon className="h-6 w-6" />
|
||||
</button>
|
||||
</div>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
||||
{/* Left Column */}
|
||||
<div className="space-y-6">
|
||||
{/* Property */}
|
||||
<div>
|
||||
<label className="block text-sm mb-1">Property</label>
|
||||
<select className="w-full border rounded px-2 py-2" value={propertyId} onChange={(e)=>{setPropertyId(e.target.value); setLawnSectionIds([]);}}>
|
||||
<option value="">Select…</option>
|
||||
{properties.map(p=> <option key={p.id} value={p.id}>{p.name}</option>)}
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||
<MapPinIcon className="h-4 w-4 inline mr-1" />
|
||||
Property
|
||||
</label>
|
||||
<select
|
||||
value={propertyId}
|
||||
onChange={(e) => { setPropertyId(e.target.value); setLawnSectionIds([]); }}
|
||||
className="w-full border border-gray-300 rounded px-3 py-2"
|
||||
>
|
||||
<option value="">Select a property</option>
|
||||
{properties.map(p => (
|
||||
<option key={p.id} value={p.id}>{p.name} {p.address ? `- ${p.address}` : ''}</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
{/* Planned Date */}
|
||||
<div>
|
||||
<label className="block text-sm mb-1">Planned Date</label>
|
||||
<input type="date" value={plannedDate} onChange={(e)=> setPlannedDate(e.target.value)} className="w-full border rounded px-2 py-2" />
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">Planned Date</label>
|
||||
<input
|
||||
type="date"
|
||||
value={plannedDate}
|
||||
onChange={(e) => setPlannedDate(e.target.value)}
|
||||
className="w-full border border-gray-300 rounded px-3 py-2"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Mower */}
|
||||
<div>
|
||||
<label className="block text-sm mb-1">Areas</label>
|
||||
<select multiple className="w-full border rounded px-2 py-2 h-32" value={lawnSectionIds} onChange={(e)=> setLawnSectionIds(Array.from(e.target.selectedOptions).map(o=>o.value))}>
|
||||
{sections.map(s=> <option key={s.id} value={s.id}>{s.name}</option>)}
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||
<WrenchScrewdriverIcon className="h-4 w-4 inline mr-1" />
|
||||
Mower
|
||||
</label>
|
||||
<select
|
||||
value={equipmentId}
|
||||
onChange={(e) => setEquipmentId(e.target.value)}
|
||||
className="w-full border border-gray-300 rounded px-3 py-2"
|
||||
>
|
||||
<option value="">Select mower</option>
|
||||
{mowers.map(m => (
|
||||
<option key={m.id} value={m.id}>
|
||||
{(m.customName || m.manufacturer || 'Mower')} {m.model || ''}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
{/* Cut height and direction */}
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label className="block text-sm mb-1">Mower</label>
|
||||
<select className="w-full border rounded px-2 py-2" value={equipmentId} onChange={(e)=> setEquipmentId(e.target.value)}>
|
||||
<option value="">Select…</option>
|
||||
{mowers.map(m=> <option key={m.id} value={m.id}>{m.customName || m.manufacturer} {m.model}</option>)}
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">Cut Height (in)</label>
|
||||
<input
|
||||
type="number"
|
||||
step="0.25"
|
||||
value={cutHeightInches}
|
||||
onChange={(e) => setCutHeightInches(e.target.value)}
|
||||
className="w-full border border-gray-300 rounded px-3 py-2"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">Direction</label>
|
||||
<select
|
||||
value={direction}
|
||||
onChange={(e) => setDirection(e.target.value)}
|
||||
className="w-full border border-gray-300 rounded px-3 py-2"
|
||||
>
|
||||
{directionOptions.map(d => (
|
||||
<option key={d.value} value={d.value}>{d.label}</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Notes */}
|
||||
<div>
|
||||
<label className="block text-sm mb-1">Cut Height (in)</label>
|
||||
<input type="number" step="0.25" className="w-full border rounded px-2 py-2" value={cutHeightInches} onChange={(e)=> setCutHeightInches(e.target.value)} />
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">Notes (Optional)</label>
|
||||
<textarea
|
||||
rows={3}
|
||||
value={notes}
|
||||
onChange={(e) => setNotes(e.target.value)}
|
||||
className="w-full border border-gray-300 rounded px-3 py-2"
|
||||
placeholder="Add any notes or special instructions..."
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Right Column */}
|
||||
<div className="space-y-4">
|
||||
{/* Area selection */}
|
||||
<div className="border rounded-lg p-4 bg-gray-50">
|
||||
<h4 className="font-medium text-gray-900 mb-3">Select Areas to Mow</h4>
|
||||
{sections.length > 0 ? (
|
||||
<div className="space-y-2 max-h-40 overflow-auto pr-1">
|
||||
{sections.map((s) => (
|
||||
<label key={s.id} className="flex items-center text-sm">
|
||||
<input
|
||||
type="checkbox"
|
||||
className="mr-2"
|
||||
checked={lawnSectionIds.includes(s.id.toString()) || lawnSectionIds.includes(s.id)}
|
||||
onChange={(e) => {
|
||||
const id = s.id.toString();
|
||||
setLawnSectionIds(prev =>
|
||||
e.target.checked ? [...prev, id] : prev.filter(x => x.toString() !== id)
|
||||
);
|
||||
}}
|
||||
/>
|
||||
{s.name} {s.area ? `(${Number(s.area).toLocaleString()} sq ft)` : ''}
|
||||
</label>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<p className="text-sm text-gray-500">Select a property to view areas</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Map */}
|
||||
<div>
|
||||
<label className="block text-sm mb-1">Direction</label>
|
||||
<select className="w-full border rounded px-2 py-2" value={direction} onChange={(e)=> setDirection(e.target.value)}>
|
||||
{directionOptions.map(d=> <option key={d.value} value={d.value}>{d.label}</option>)}
|
||||
</select>
|
||||
<h4 className="font-medium text-gray-900 mb-2">Property Map</h4>
|
||||
<div className="h-72">
|
||||
{propertyId ? (
|
||||
<PropertyMap
|
||||
center={mapCenter}
|
||||
property={selectedPropertyDetails}
|
||||
sections={selectedPropertyDetails?.sections || []}
|
||||
selectedSections={lawnSectionIds.map(Number)}
|
||||
onSectionClick={(area) => {
|
||||
setLawnSectionIds(prev => {
|
||||
const idStr = area.id.toString();
|
||||
return prev.includes(idStr) ? prev.filter(x => x !== idStr) : [...prev, idStr];
|
||||
});
|
||||
}}
|
||||
editable={false}
|
||||
className="h-72 w-full"
|
||||
/>
|
||||
) : (
|
||||
<div className="h-full w-full flex items-center justify-center bg-gray-100 rounded">
|
||||
<p className="text-gray-500">Select a property to view areas</p>
|
||||
</div>
|
||||
<div className="md:col-span-2">
|
||||
<label className="block text-sm mb-1">Notes</label>
|
||||
<textarea className="w-full border rounded px-2 py-2" rows={3} value={notes} onChange={(e)=> setNotes(e.target.value)} />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-4 flex justify-end gap-2">
|
||||
<button className="btn-secondary" onClick={onClose}>Cancel</button>
|
||||
<button className="btn-primary" onClick={create}>Create Plan</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Actions */}
|
||||
<div className="flex justify-end gap-3 mt-6 pt-6 border-t">
|
||||
<button className="px-4 py-2 text-gray-600 hover:text-gray-800" onClick={onClose}>Cancel</button>
|
||||
<button className="px-6 py-2 bg-blue-600 text-white rounded hover:bg-blue-700" onClick={create}>Create Plan</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -111,4 +255,3 @@ const MowingPlanModal = ({ onClose, onCreated }) => {
|
||||
};
|
||||
|
||||
export default MowingPlanModal;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user