Files
HRM-System/resources/js/components/LocationPicker.tsx

216 lines
7.0 KiB
TypeScript

import React, { useEffect, useState } from 'react';
import { MapContainer, TileLayer, Marker, Circle, useMapEvents, useMap } from 'react-leaflet';
import 'leaflet/dist/leaflet.css';
import L from 'leaflet';
import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
import { LocateFixed } from 'lucide-react';
// Fix for default marker icon in leaflet
delete (L.Icon.Default.prototype as any)._getIconUrl;
L.Icon.Default.mergeOptions({
iconRetinaUrl: 'https://cdnjs.cloudflare.com/ajax/libs/leaflet/1.7.1/images/marker-icon-2x.png',
iconUrl: 'https://cdnjs.cloudflare.com/ajax/libs/leaflet/1.7.1/images/marker-icon.png',
shadowUrl: 'https://cdnjs.cloudflare.com/ajax/libs/leaflet/1.7.1/images/marker-shadow.png',
});
interface LocationPickerProps {
latitude: number | null;
longitude: number | null;
radius: number | null;
onChange: (lat: number | null, lng: number | null, radius: number | null) => void;
disabled?: boolean;
}
function LocationMarker({ position, radius, onPositionChange, onRadiusChange }: any) {
const [handlePosition, setHandlePosition] = useState<L.LatLng | null>(null);
useMapEvents({
click(e) {
if (onPositionChange) {
onPositionChange(e.latlng.lat, e.latlng.lng);
}
},
});
useEffect(() => {
if (position && radius) {
const center = L.latLng(position);
// approximate 1 meter in degrees (very rough, better to use leaflet's projection, but this is just for UI handle placement)
// Actually, a simple offset: 1 degree lat is ~111km, so 1 meter is ~ 1/111000 degrees.
const offsetLng = radius / (111320 * Math.cos(center.lat * Math.PI / 180));
setHandlePosition(L.latLng(center.lat, center.lng + offsetLng));
} else {
setHandlePosition(null);
}
}, [position, radius]);
const handleDrag = (e: any) => {
if (position && onRadiusChange) {
const center = L.latLng(position);
const newDistance = center.distanceTo(e.latlng);
onRadiusChange(Math.round(newDistance));
}
};
return position === null ? null : (
<>
<Marker position={position} />
<Circle
center={position}
radius={radius}
pathOptions={{ color: 'blue', fillColor: 'blue', fillOpacity: 0.2 }}
/>
{handlePosition && (
<Marker
position={handlePosition}
draggable={true}
eventHandlers={{
drag: handleDrag,
dragend: handleDrag
}}
icon={L.divIcon({
className: 'custom-handle-icon',
html: '<div style="width: 12px; height: 12px; background: blue; border-radius: 50%; border: 2px solid white; box-shadow: 0 0 4px rgba(0,0,0,0.5);"></div>',
iconSize: [12, 12],
iconAnchor: [6, 6]
})}
/>
)}
</>
);
}
export default function LocationPicker({ latitude, longitude, radius, onChange, disabled }: LocationPickerProps) {
const mapRef = React.useRef<L.Map>(null);
const [locating, setLocating] = useState(false);
const [position, setPosition] = useState<L.LatLngExpression | null>(
latitude && longitude ? [latitude, longitude] : null
);
const [localRadius, setLocalRadius] = useState(radius || 100);
useEffect(() => {
if (latitude && longitude) {
setPosition([latitude, longitude]);
}
}, [latitude, longitude]);
useEffect(() => {
if (radius) {
setLocalRadius(radius);
}
}, [radius]);
const handlePositionChange = (lat: number, lng: number) => {
if (disabled) return;
setPosition([lat, lng]);
onChange(lat, lng, localRadius);
};
const handleRadiusUpdate = (newRadius: number) => {
if (disabled) return;
setLocalRadius(newRadius);
if (position) {
const lat = Array.isArray(position) ? position[0] : (position as any).lat;
const lng = Array.isArray(position) ? position[1] : (position as any).lng;
onChange(lat, lng, newRadius);
}
};
const handleRadiusChange = (e: React.ChangeEvent<HTMLInputElement>) => {
if (disabled) return;
const newRadius = Number(e.target.value);
handleRadiusUpdate(newRadius);
};
const handleClear = () => {
if (disabled) return;
setPosition(null);
onChange(null, null, localRadius);
}
const handleLocate = () => {
setLocating(true);
navigator.geolocation.getCurrentPosition(
(pos) => {
const { latitude, longitude } = pos.coords;
if (mapRef.current) {
mapRef.current.flyTo([latitude, longitude], 15);
}
handlePositionChange(latitude, longitude);
setLocating(false);
},
(err) => {
console.error("Geolocation error:", err);
setLocating(false);
},
{ enableHighAccuracy: true }
);
};
const defaultCenter = [51.505, -0.09] as L.LatLngExpression; // London fallback
return (
<div className="space-y-4">
<div className="flex justify-between items-center">
<Label>Branch Location</Label>
{!disabled && position && (
<button type="button" onClick={handleClear} className="text-xs text-red-500 hover:underline">
Clear Location
</button>
)}
</div>
<div className="h-[300px] w-full rounded-md border border-gray-300 overflow-hidden relative z-0">
{!disabled && (
<button
type="button"
onClick={handleLocate}
title="Find My Location"
disabled={locating}
className="absolute top-2 right-2 z-10 bg-white hover:bg-gray-100 text-gray-700 p-2 rounded shadow-md border border-gray-300 flex items-center justify-center transition-colors disabled:opacity-50"
>
<LocateFixed className={`h-5 w-5 ${locating ? 'animate-pulse text-blue-500' : ''}`} />
</button>
)}
<MapContainer
ref={mapRef}
center={position || defaultCenter}
zoom={13}
scrollWheelZoom={true}
style={{ height: '100%', width: '100%', zIndex: 0 }}
>
<TileLayer
attribution='&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors'
url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png"
/>
<LocationMarker
position={position}
radius={localRadius}
onPositionChange={handlePositionChange}
onRadiusChange={handleRadiusUpdate}
/>
</MapContainer>
</div>
<div className="flex gap-4">
<div className="w-1/3">
<Label className="text-xs">Radius (meters)</Label>
<Input
type="number"
value={localRadius}
onChange={handleRadiusChange}
disabled={disabled || !position}
min={10}
step={10}
/>
</div>
<div className="w-2/3 flex items-end">
<p className="text-xs text-gray-500 mb-2">Drag the blue dot on the circle edge to adjust radius, or enter meters manually.</p>
</div>
</div>
</div>
);
}