boundary entries updates
This commit is contained in:
191
frontend/src/pages/Admin/AdminProperties.js
Normal file
191
frontend/src/pages/Admin/AdminProperties.js
Normal file
@@ -0,0 +1,191 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { adminAPI } from '../../services/api';
|
||||
import LoadingSpinner from '../../components/UI/LoadingSpinner';
|
||||
import { MapContainer, TileLayer, Polygon, Marker, Popup } from 'react-leaflet';
|
||||
import { Icon } from 'leaflet';
|
||||
|
||||
const AdminProperties = () => {
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [properties, setProperties] = useState([]);
|
||||
const [search, setSearch] = useState('');
|
||||
const [selected, setSelected] = useState(null);
|
||||
const [editingSection, setEditingSection] = useState(null);
|
||||
const [editName, setEditName] = useState('');
|
||||
const [editGrass, setEditGrass] = useState('');
|
||||
const [editingGeomId, setEditingGeomId] = useState(null);
|
||||
const [editedCoords, setEditedCoords] = useState([]);
|
||||
|
||||
const load = async () => {
|
||||
try{
|
||||
setLoading(true);
|
||||
const rs = await adminAPI.getProperties({ search });
|
||||
setProperties(rs.data?.data?.properties || []);
|
||||
}finally{ setLoading(false);} }
|
||||
|
||||
useEffect(()=>{ load(); }, [search]);
|
||||
|
||||
const openProperty = async (id) => {
|
||||
const rs = await adminAPI.getProperty(id);
|
||||
setSelected(rs.data?.data?.property || null);
|
||||
};
|
||||
|
||||
const beginEditSection = (s) => { setEditingSection(s); setEditName(s.name); setEditGrass(s.grassType || (s.grassTypes||[]).join(', ')); };
|
||||
const beginEditGeometry = (s) => { setEditingGeomId(s.id); setEditedCoords(s.polygonData?.coordinates?.[0] || []); };
|
||||
const saveSection = async () => {
|
||||
const grassTypes = editGrass.split(',').map(x=>x.trim()).filter(Boolean);
|
||||
const payload = { name: editName, area: editingSection.area, polygonData: editingSection.polygonData, grassType: grassTypes.join(', '), grassTypes };
|
||||
await adminAPI.updateSectionAdmin(selected.id, editingSection.id, payload);
|
||||
// refresh selected property
|
||||
await openProperty(selected.id);
|
||||
setEditingSection(null);
|
||||
};
|
||||
const saveGeometry = async () => {
|
||||
const section = (selected.sections||[]).find(s=> s.id===editingGeomId);
|
||||
const poly = { ...(section.polygonData||{}), coordinates: [editedCoords] };
|
||||
await adminAPI.updateSectionAdmin(selected.id, editingGeomId, { name: section.name, area: section.area, polygonData: poly, grassType: section.grassType, grassTypes: section.grassTypes });
|
||||
await openProperty(selected.id);
|
||||
setEditingGeomId(null);
|
||||
setEditedCoords([]);
|
||||
};
|
||||
const cancelGeometry = () => { setEditingGeomId(null); setEditedCoords([]); };
|
||||
|
||||
if (loading) return (<div className="p-6"><LoadingSpinner /></div>);
|
||||
|
||||
return (
|
||||
<div className="p-6">
|
||||
<div className="flex justify-between items-center mb-4">
|
||||
<h1 className="text-2xl font-bold">Admin: Properties</h1>
|
||||
<input className="input w-64" placeholder="Search by name/address/email" value={search} onChange={(e)=> setSearch(e.target.value)} />
|
||||
</div>
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
||||
<div className="card">
|
||||
<h3 className="font-semibold mb-2">All Properties</h3>
|
||||
<div className="max-h-[60vh] overflow-auto divide-y">
|
||||
{properties.map(p=> (
|
||||
<div key={p.id} className={`p-3 hover:bg-gray-50 cursor-pointer ${selected?.id===p.id? 'bg-blue-50':''}`} onClick={()=> openProperty(p.id)}>
|
||||
<div className="flex justify-between items-center">
|
||||
<div>
|
||||
<div className="font-medium">{p.name}</div>
|
||||
<div className="text-xs text-gray-600">{p.userEmail}</div>
|
||||
</div>
|
||||
<div className="text-sm text-gray-700">{p.sectionCount} sections • {(p.calculatedArea||0).toLocaleString()} sq ft</div>
|
||||
</div>
|
||||
<div className="text-xs text-gray-500">{p.address}</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="card">
|
||||
{!selected ? (
|
||||
<div className="text-gray-500">Select a property to view details</div>
|
||||
) : (
|
||||
<div>
|
||||
<div className="flex justify-between items-center mb-2">
|
||||
<div>
|
||||
<h3 className="font-semibold">{selected.name}</h3>
|
||||
<div className="text-xs text-gray-600">{selected.userEmail}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="h-64 rounded overflow-hidden mb-3">
|
||||
<MapContainer center={[selected.latitude||39.8, selected.longitude||-98.6]} zoom={16} style={{height:'100%', width:'100%'}}>
|
||||
<TileLayer attribution='© Esri' url="https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}" />
|
||||
{(selected.sections||[]).map(s=> (
|
||||
<Polygon key={s.id} positions={s.polygonData?.coordinates?.[0]||[]} pathOptions={{ color: s.id===editingGeomId? '#f59e0b':'#10b981', weight: 2, fillOpacity: .2 }} />
|
||||
))}
|
||||
{editingGeomId && editedCoords.map((c, idx)=> (
|
||||
<Marker
|
||||
key={`ed-${idx}`}
|
||||
position={c}
|
||||
draggable
|
||||
eventHandlers={{ dragend: (e)=> {
|
||||
const { lat, lng } = e.target.getLatLng();
|
||||
setEditedCoords(prev=> prev.map((p,i)=> i===idx? [lat,lng]: p));
|
||||
}}}
|
||||
icon={new Icon({
|
||||
iconUrl: 'data:image/svg+xml;base64,' + btoa(`
|
||||
<svg width="12" height="12" viewBox="0 0 12 12" xmlns="http://www.w3.org/2000/svg">
|
||||
<circle cx="6" cy="6" r="5" fill="#f59e0b" stroke="white" stroke-width="2"/>
|
||||
</svg>
|
||||
`),
|
||||
iconSize: [12,12], iconAnchor: [6,6]
|
||||
})}
|
||||
>
|
||||
<Popup>
|
||||
<div className="text-xs">Point {idx+1}</div>
|
||||
</Popup>
|
||||
</Marker>
|
||||
))}
|
||||
</MapContainer>
|
||||
</div>
|
||||
{editingGeomId && (
|
||||
<div className="mb-3 flex gap-2">
|
||||
<button className="btn-primary" onClick={saveGeometry}>Save Geometry</button>
|
||||
<button className="btn-secondary" onClick={cancelGeometry}>Cancel</button>
|
||||
</div>
|
||||
)}
|
||||
<h4 className="font-medium mb-2">Sections</h4>
|
||||
<div className="space-y-2 max-h-[40vh] overflow-auto">
|
||||
{(selected.sections||[]).map(s=> (
|
||||
<div key={s.id} className="p-2 border rounded flex justify-between items-center">
|
||||
<div>
|
||||
<div className="font-medium text-sm">{s.name}</div>
|
||||
<div className="text-xs text-gray-600">{(s.area||0).toLocaleString()} sq ft</div>
|
||||
<div className="mt-1 flex items-center gap-2">
|
||||
{s.captureMethod && (
|
||||
<span className={`px-1.5 py-0.5 text-[10px] rounded ${s.captureMethod==='gps_points'?'bg-blue-100 text-blue-800': s.captureMethod==='gps_trace'?'bg-yellow-100 text-yellow-800':'bg-gray-100 text-gray-700'}`}>{s.captureMethod}</span>
|
||||
)}
|
||||
{s.captureMeta?.pointsCount && (
|
||||
<span className="text-[10px] text-gray-600">{s.captureMeta.pointsCount} pts</span>
|
||||
)}
|
||||
{s.captureMeta?.accuracyLast && (
|
||||
<span className="text-[10px] text-gray-600">±{Math.round(s.captureMeta.accuracyLast)} m</span>
|
||||
)}
|
||||
{s.captureMeta?.totalDistanceMeters && (
|
||||
<span className="text-[10px] text-gray-600">{Math.round(s.captureMeta.totalDistanceMeters*3.28084)} ft walked</span>
|
||||
)}
|
||||
</div>
|
||||
{(s.grassTypes?.length>0 || s.grassType) && (
|
||||
<div className="mt-1 flex flex-wrap gap-1">
|
||||
{(s.grassTypes?.length? s.grassTypes : (s.grassType||'').split(',').map(x=>x.trim()).filter(Boolean)).map((g,i)=>(<span key={i} className="px-1.5 py-0.5 text-[10px] rounded bg-green-100 text-green-800">{g}</span>))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
<button className="btn-secondary" onClick={()=> beginEditSection(s)}>Edit</button>
|
||||
<button className="btn-secondary" onClick={()=> beginEditGeometry(s)}>Edit Geometry</button>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{editingSection && (
|
||||
<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-lg">
|
||||
<h3 className="font-semibold mb-3">Edit Section</h3>
|
||||
<div className="space-y-3">
|
||||
<div>
|
||||
<label className="label">Name</label>
|
||||
<input className="input" value={editName} onChange={(e)=> setEditName(e.target.value)} />
|
||||
</div>
|
||||
<div>
|
||||
<label className="label">Grass Types (comma separated)</label>
|
||||
<input className="input" value={editGrass} onChange={(e)=> setEditGrass(e.target.value)} placeholder="e.g., Kentucky Bluegrass, Perennial Ryegrass" />
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-4 flex justify-end gap-2">
|
||||
<button className="btn-secondary" onClick={()=> setEditingSection(null)}>Cancel</button>
|
||||
<button className="btn-primary" onClick={saveSection}>Save</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default AdminProperties;
|
||||
Reference in New Issue
Block a user