lawnmowing/src/components/SitePlan.tsx

304 lines
11 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import React, { useState, useRef, useEffect } from 'react';
import { MapPin, Upload, X, Eye, EyeOff } from './Icons';
import { Zone } from '../types/zone';
interface ZoneMarker {
id: number;
x: number; // Percentage from left
y: number; // Percentage from top
}
interface SitePlanProps {
zones: Zone[];
onZoneSelect: (zone: Zone) => void;
selectedZoneId?: number;
}
const SitePlan: React.FC<SitePlanProps> = ({ zones, onZoneSelect, selectedZoneId }) => {
const [sitePlanImage, setSitePlanImage] = useState<string | null>(
localStorage.getItem('sitePlanImage')
);
const [zoneMarkers, setZoneMarkers] = useState<ZoneMarker[]>(() => {
const saved = localStorage.getItem('zoneMarkers');
return saved ? JSON.parse(saved) : [];
});
const [isEditMode, setIsEditMode] = useState(false);
const [showMarkers, setShowMarkers] = useState(true);
const fileInputRef = useRef<HTMLInputElement>(null);
const imageRef = useRef<HTMLImageElement>(null);
// Save markers to localStorage whenever they change
useEffect(() => {
localStorage.setItem('zoneMarkers', JSON.stringify(zoneMarkers));
}, [zoneMarkers]);
const handleImageUpload = (e: React.ChangeEvent<HTMLInputElement>) => {
const file = e.target.files?.[0];
if (file) {
const reader = new FileReader();
reader.onload = (e) => {
const imageUrl = e.target?.result as string;
setSitePlanImage(imageUrl);
localStorage.setItem('sitePlanImage', imageUrl);
};
reader.readAsDataURL(file);
}
};
const handleImageClick = (e: React.MouseEvent<HTMLImageElement>) => {
if (!isEditMode || !imageRef.current) return;
const rect = imageRef.current.getBoundingClientRect();
const x = ((e.clientX - rect.left) / rect.width) * 100;
const y = ((e.clientY - rect.top) / rect.height) * 100;
// Find a zone that doesn't have a marker yet
const unmarkedZone = zones.find(zone =>
!zoneMarkers.some(marker => marker.id === zone.id)
);
if (unmarkedZone) {
const newMarker: ZoneMarker = {
id: unmarkedZone.id,
x,
y
};
setZoneMarkers(prev => [...prev, newMarker]);
}
};
const removeMarker = (zoneId: number) => {
setZoneMarkers(prev => prev.filter(marker => marker.id !== zoneId));
};
const clearSitePlan = () => {
if (window.confirm('Are you sure you want to remove the site plan and all markers?')) {
setSitePlanImage(null);
setZoneMarkers([]);
localStorage.removeItem('sitePlanImage');
localStorage.removeItem('zoneMarkers');
setIsEditMode(false);
}
};
const getZoneById = (id: number) => zones.find(zone => zone.id === id);
const getMarkerColor = (zone: Zone) => {
switch (zone.status) {
case 'overdue':
return 'bg-red-500 border-red-600 shadow-red-200';
case 'due':
return 'bg-orange-500 border-orange-600 shadow-orange-200';
default:
return 'bg-green-500 border-green-600 shadow-green-200';
}
};
if (!sitePlanImage) {
return (
<div className="bg-white rounded-lg shadow-md p-8">
<div className="text-center">
<div className="mb-6">
<MapPin className="mx-auto h-16 w-16 text-gray-400" />
<h3 className="mt-4 text-lg font-semibold text-gray-900">План участка</h3>
<p className="mt-2 text-gray-600">
Загрузите изображение участка, чтобы визуализировать расположение зон
</p>
</div>
<button
onClick={() => fileInputRef.current?.click()}
className="bg-blue-600 hover:bg-blue-700 text-white px-6 py-3 rounded-lg flex items-center gap-2 mx-auto transition-colors duration-200"
>
<Upload className="h-5 w-5" />
Загрузка плана участка
</button>
<input
ref={fileInputRef}
type="file"
accept="image/*"
onChange={handleImageUpload}
className="hidden"
/>
<p className="mt-4 text-sm text-gray-500">
Поддерживаемые форматы: JPG, PNG, GIF (максимально 10MB)
</p>
</div>
</div>
);
}
return (
<div className="bg-white rounded-lg shadow-md overflow-hidden">
{/* Header */}
<div className="p-4 border-b bg-gray-50">
<div className="flex items-center justify-between">
<div className="flex items-center gap-3">
<MapPin className="h-6 w-6 text-blue-600" />
<h3 className="text-lg font-semibold text-gray-900">План участка</h3>
<span className="text-sm text-gray-500">
({zoneMarkers.length} of {zones.length} zones marked)
</span>
</div>
<div className="flex items-center gap-2">
<button
onClick={() => setShowMarkers(!showMarkers)}
className={`p-2 rounded-md transition-colors duration-200 ${
showMarkers
? 'bg-blue-100 text-blue-600'
: 'bg-gray-100 text-gray-600 hover:bg-gray-200'
}`}
title={showMarkers ? 'Hide markers' : 'Show markers'}
>
{showMarkers ? <Eye className="h-4 w-4" /> : <EyeOff className="h-4 w-4" />}
</button>
<button
onClick={() => setIsEditMode(!isEditMode)}
className={`px-3 py-2 rounded-md text-sm font-medium transition-colors duration-200 ${
isEditMode
? 'bg-green-100 text-green-700 hover:bg-green-200'
: 'bg-gray-100 text-gray-700 hover:bg-gray-200'
}`}
>
{isEditMode ? 'Done Editing' : 'Edit Markers'}
</button>
<button
onClick={() => fileInputRef.current?.click()}
className="px-3 py-2 bg-blue-100 text-blue-700 rounded-md text-sm font-medium hover:bg-blue-200 transition-colors duration-200"
>
Change Image
</button>
<button
onClick={clearSitePlan}
className="p-2 text-gray-600 hover:text-red-600 hover:bg-red-50 rounded-md transition-colors duration-200"
title="Remove site plan"
>
<X className="h-4 w-4" />
</button>
</div>
</div>
{isEditMode && (
<div className="mt-3 p-3 bg-blue-50 rounded-md">
<p className="text-sm text-blue-800">
<strong>Edit Mode:</strong> Click on the image to place markers for zones.
Zones without markers: {zones.filter(zone => !zoneMarkers.some(marker => marker.id === zone.id)).map(zone => zone.name).join(', ') || 'None'}
</p>
</div>
)}
</div>
{/* Site Plan Image */}
<div className="relative">
<img
ref={imageRef}
src={sitePlanImage}
alt="План участка"
className={`w-full h-auto max-h-[600px] object-contain ${
isEditMode ? 'cursor-crosshair' : 'cursor-default'
}`}
onClick={handleImageClick}
/>
{/* Zone Markers */}
{showMarkers && zoneMarkers.map(marker => {
const zone = getZoneById(marker.id);
if (!zone) return null;
return (
<div
key={marker.id}
className="absolute transform -translate-x-1/2 -translate-y-1/2 group"
style={{
left: `${marker.x}%`,
top: `${marker.y}%`
}}
>
{/* Marker */}
<button
onClick={(e) => {
e.stopPropagation();
if (!isEditMode) {
onZoneSelect(zone);
}
}}
className={`w-6 h-6 rounded-full border-2 shadow-lg transition-all duration-200 hover:scale-110 ${
getMarkerColor(zone)
} ${
selectedZoneId === zone.id
? 'ring-4 ring-blue-300 scale-110'
: ''
} ${
isEditMode ? 'cursor-pointer' : 'cursor-pointer hover:shadow-xl'
}`}
>
<span className="sr-only">{zone.name}</span>
</button>
{/* Tooltip */}
<div className="absolute bottom-full left-1/2 transform -translate-x-1/2 mb-2 opacity-0 group-hover:opacity-100 transition-opacity duration-200 pointer-events-none z-10">
<div className="bg-gray-900 text-white text-xs rounded-md px-2 py-1 whitespace-nowrap">
<div className="font-medium">{zone.name}</div>
<div className="text-gray-300">
{zone.isOverdue ? 'Срок прошел' : zone.isDueToday ? 'Срок - сегодня' : `${zone.daysUntilNext} дней`}
</div>
<div className="absolute top-full left-1/2 transform -translate-x-1/2 border-4 border-transparent border-t-gray-900"></div>
</div>
</div>
{/* Remove button in edit mode */}
{isEditMode && (
<button
onClick={(e) => {
e.stopPropagation();
removeMarker(zone.id);
}}
className="absolute -top-2 -right-2 w-5 h-5 bg-red-500 text-white rounded-full text-xs hover:bg-red-600 transition-colors duration-200 flex items-center justify-center"
>
×
</button>
)}
</div>
);
})}
</div>
{/* Legend */}
{showMarkers && zoneMarkers.length > 0 && (
<div className="p-4 border-t bg-gray-50">
<h4 className="text-sm font-medium text-gray-900 mb-3">Zone Status Legend</h4>
<div className="flex flex-wrap gap-4 text-sm">
<div className="flex items-center gap-2">
<div className="w-3 h-3 rounded-full bg-green-500 border border-green-600"></div>
<span className="text-gray-700">Up to date</span>
</div>
<div className="flex items-center gap-2">
<div className="w-3 h-3 rounded-full bg-orange-500 border border-orange-600"></div>
<span className="text-gray-700">Срок - сегодня</span>
</div>
<div className="flex items-center gap-2">
<div className="w-3 h-3 rounded-full bg-red-500 border border-red-600"></div>
<span className="text-gray-700">Overdue</span>
</div>
</div>
</div>
)}
<input
ref={fileInputRef}
type="file"
accept="image/*"
onChange={handleImageUpload}
className="hidden"
/>
</div>
);
};
export default SitePlan;