diff --git a/src/app/projects/map/page.js b/src/app/projects/map/page.js
index 2ee90f3..23026dd 100644
--- a/src/app/projects/map/page.js
+++ b/src/app/projects/map/page.js
@@ -28,279 +28,901 @@ function ProjectsMapPageContent() {
registered: true,
in_progress_design: true,
in_progress_construction: true,
- in_progress_inspection: true,
- completed: false,
- cancelled: false,
+ fulfilled: true,
});
- const [selectedLayerName, setSelectedLayerName] = useState("OpenStreetMap");
+ const [activeBaseLayer, setActiveBaseLayer] = useState("OpenStreetMap");
+ const [activeOverlays, setActiveOverlays] = useState([]);
+ const [showLayerPanel, setShowLayerPanel] = useState(true);
+ const [currentTool, setCurrentTool] = useState("move"); // Current map tool
- // Check if we have a specific project ID from search params
- const projectId = searchParams.get("project");
+ // Status configuration with colors and labels
+ const statusConfig = {
+ registered: {
+ color: "#6B7280",
+ label: "Registered",
+ shortLabel: "Zarejestr.",
+ },
+ in_progress_design: {
+ color: "#3B82F6",
+ label: "In Progress (Design)",
+ shortLabel: "W real. (P)",
+ },
+ in_progress_construction: {
+ color: "#F59E0B",
+ label: "In Progress (Construction)",
+ shortLabel: "W real. (R)",
+ },
+ fulfilled: {
+ color: "#10B981",
+ label: "Completed",
+ shortLabel: "Zakończony",
+ },
+ };
- useEffect(() => {
- const fetchProjects = async () => {
- setLoading(true);
- try {
- const response = await fetch("/api/projects");
- if (response.ok) {
- const data = await response.json();
- setProjects(data);
+ // Toggle all status filters
+ const toggleAllFilters = () => {
+ const allActive = Object.values(statusFilters).every((value) => value);
+ const newState = allActive
+ ? Object.keys(statusFilters).reduce(
+ (acc, key) => ({ ...acc, [key]: false }),
+ {}
+ )
+ : Object.keys(statusFilters).reduce(
+ (acc, key) => ({ ...acc, [key]: true }),
+ {}
+ );
+ setStatusFilters(newState);
+ };
- // If we have a specific project ID, find it and center the map on it
- if (projectId) {
- const project = data.find(p => p.id === parseInt(projectId));
- if (project?.coordinates) {
- const coords = project.coordinates.split(",");
- if (coords.length === 2) {
- setMapCenter([parseFloat(coords[0]), parseFloat(coords[1])]);
- setMapZoom(15);
- }
- }
- }
- }
- } catch (error) {
- console.error("Error fetching projects:", error);
- } finally {
- setLoading(false);
- }
- };
-
- fetchProjects();
- }, [projectId]);
-
- const handleStatusFilterChange = (status) => {
- setStatusFilters(prev => ({
+ // Toggle status filter
+ const toggleStatusFilter = (status) => {
+ setStatusFilters((prev) => ({
...prev,
- [status]: !prev[status]
+ [status]: !prev[status],
}));
};
- const handleLayerChange = (layerName) => {
- setSelectedLayerName(layerName);
+ // Layer control functions
+ const handleBaseLayerChange = (layerName) => {
+ setActiveBaseLayer(layerName);
};
- // Filter projects based on status filters
- const filteredProjects = projects.filter(project => {
- if (!project?.coordinates) return false;
- const coords = project.coordinates.split(",");
- if (coords.length !== 2) return false;
-
- const lat = parseFloat(coords[0]);
- const lng = parseFloat(coords[1]);
- if (isNaN(lat) || isNaN(lng)) return false;
-
- return statusFilters[project.status] || false;
- });
+ const toggleOverlay = (layerName) => {
+ setActiveOverlays((prev) => {
+ if (prev.includes(layerName)) {
+ return prev.filter((name) => name !== layerName);
+ } else {
+ return [...prev, layerName];
+ }
+ });
+ };
- // Convert filtered projects to markers
- const markers = filteredProjects.map(project => {
- const coords = project.coordinates.split(",");
- const lat = parseFloat(coords[0]);
- const lng = parseFloat(coords[1]);
-
- return {
- position: [lat, lng],
- popup: `
-
-
${project.name}
-
- Status: ${project.status?.replace(/_/g, ' ')}
-
- ${project.contractor ? `
- Contractor: ${project.contractor}
-
` : ''}
- ${project.deadline ? `
- Deadline: ${new Date(project.deadline).toLocaleDateString()}
-
` : ''}
-
- View Project →
-
-
- `,
- data: project
+ const toggleLayerPanel = () => {
+ setShowLayerPanel(!showLayerPanel);
+ };
+
+ // Update URL with current map state (debounced to avoid too many updates)
+ const updateURL = (center, zoom) => {
+ const params = new URLSearchParams();
+ params.set("lat", center[0].toFixed(6));
+ params.set("lng", center[1].toFixed(6));
+ params.set("zoom", zoom.toString());
+
+ // Use replace to avoid cluttering browser history
+ router.replace(`/projects/map?${params.toString()}`, { scroll: false });
+ };
+
+ // Handle map view changes with debouncing
+ const handleMapViewChange = (center, zoom) => {
+ setMapCenter(center);
+ setMapZoom(zoom);
+
+ // Debounce URL updates to avoid too many history entries
+ clearTimeout(window.mapUpdateTimeout);
+ window.mapUpdateTimeout = setTimeout(() => {
+ updateURL(center, zoom);
+ }, 500); // Wait 500ms after the last move to update URL
+ };
+
+ // Hide navigation and ensure full-screen layout
+ useEffect(() => {
+ // Check for URL parameters for coordinates and zoom
+ const lat = searchParams.get("lat");
+ const lng = searchParams.get("lng");
+ const zoom = searchParams.get("zoom");
+
+ if (lat && lng) {
+ const latitude = parseFloat(lat);
+ const longitude = parseFloat(lng);
+ if (!isNaN(latitude) && !isNaN(longitude)) {
+ setMapCenter([latitude, longitude]);
+ }
+ }
+
+ if (zoom) {
+ const zoomLevel = parseInt(zoom);
+ if (!isNaN(zoomLevel) && zoomLevel >= 1 && zoomLevel <= 20) {
+ setMapZoom(zoomLevel);
+ }
+ }
+
+ // Hide navigation bar for full-screen experience
+ const nav = document.querySelector("nav");
+ if (nav) {
+ nav.style.display = "none";
+ }
+
+ // Prevent scrolling on body
+ document.body.style.overflow = "hidden";
+ document.documentElement.style.overflow = "hidden";
+
+ // Cleanup when leaving page
+ return () => {
+ if (nav) {
+ nav.style.display = "";
+ }
+ document.body.style.overflow = "";
+ document.documentElement.style.overflow = "";
+
+ // Clear any pending URL updates
+ if (window.mapUpdateTimeout) {
+ clearTimeout(window.mapUpdateTimeout);
+ }
};
- });
+ }, [searchParams]);
- const getStatusColor = (status) => {
- switch (status) {
- case 'registered': return 'bg-blue-100 text-blue-800';
- case 'in_progress_design': return 'bg-yellow-100 text-yellow-800';
- case 'in_progress_construction': return 'bg-orange-100 text-orange-800';
- case 'in_progress_inspection': return 'bg-purple-100 text-purple-800';
- case 'completed': return 'bg-green-100 text-green-800';
- case 'cancelled': return 'bg-red-100 text-red-800';
- default: return 'bg-gray-100 text-gray-800';
- }
- };
+ useEffect(() => {
+ fetch("/api/projects")
+ .then((res) => res.json())
+ .then((data) => {
+ setProjects(data);
- const getStatusDisplay = (status) => {
- switch (status) {
- case 'registered': return 'Registered';
- case 'in_progress_design': return 'In Progress - Design';
- case 'in_progress_construction': return 'In Progress - Construction';
- case 'in_progress_inspection': return 'In Progress - Inspection';
- case 'completed': return 'Completed';
- case 'cancelled': return 'Cancelled';
- default: return status;
- }
- };
+ // Only calculate center based on projects if no URL parameters are provided
+ const lat = searchParams.get("lat");
+ const lng = searchParams.get("lng");
- const exportGeoJSON = () => {
- const geojson = {
- type: "FeatureCollection",
- features: filteredProjects.map(project => {
- const coords = project.coordinates.split(",");
- const lat = parseFloat(coords[0]);
- const lng = parseFloat(coords[1]);
-
- return {
- type: "Feature",
- geometry: {
- type: "Point",
- coordinates: [lng, lat] // GeoJSON uses [lng, lat]
- },
- properties: {
- id: project.id,
- name: project.name,
- status: project.status,
- contractor: project.contractor || null,
- deadline: project.deadline || null,
- description: project.description || null,
- contract_id: project.contract_id || null
+ if (!lat || !lng) {
+ // Calculate center based on projects with coordinates
+ const projectsWithCoords = data.filter((p) => p.coordinates);
+ if (projectsWithCoords.length > 0) {
+ const avgLat =
+ projectsWithCoords.reduce((sum, p) => {
+ const [lat] = p.coordinates
+ .split(",")
+ .map((coord) => parseFloat(coord.trim()));
+ return sum + lat;
+ }, 0) / projectsWithCoords.length;
+
+ const avgLng =
+ projectsWithCoords.reduce((sum, p) => {
+ const [, lng] = p.coordinates
+ .split(",")
+ .map((coord) => parseFloat(coord.trim()));
+ return sum + lng;
+ }, 0) / projectsWithCoords.length;
+
+ setMapCenter([avgLat, avgLng]);
}
- };
+ }
+
+ setLoading(false);
})
- };
+ .catch((error) => {
+ console.error("Error fetching projects:", error);
+ setLoading(false);
+ });
+ }, [searchParams]);
- const blob = new Blob([JSON.stringify(geojson, null, 2)], { type: 'application/json' });
- const url = URL.createObjectURL(blob);
- const a = document.createElement('a');
- a.href = url;
- a.download = `projects_${new Date().toISOString().split('T')[0]}.geojson`;
- document.body.appendChild(a);
- a.click();
- document.body.removeChild(a);
- URL.revokeObjectURL(url);
- };
+ // Convert projects to map markers with filtering
+ const markers = projects
+ .filter((project) => project.coordinates)
+ .filter((project) => statusFilters[project.project_status] !== false)
+ .map((project) => {
+ const [lat, lng] = project.coordinates
+ .split(",")
+ .map((coord) => parseFloat(coord.trim()));
+ if (isNaN(lat) || isNaN(lng)) {
+ return null;
+ }
- if (loading) {
- return (
-
-
-
-
Loading projects...
-
-
- );
- }
+ const statusInfo =
+ statusConfig[project.project_status] || statusConfig.registered;
- return (
-
-
-
-
-
-
Projects Map
-
- Showing {filteredProjects.length} of {projects.length} projects with location data
-
+ return {
+ position: [lat, lng],
+ color: statusInfo.color,
+ popup: (
+
+
+
+ {project.project_name}
+
+ {project.project_number && (
+
+ {project.project_number}
+
+ )}
-
-
-
-
+ ),
+ };
+ })
+ .filter((marker) => marker !== null);
+
+ if (loading) {
+ return (
+
+
+
+
Loading projects map...
+
+ Preparing your full-screen map experience
+
+ );
+ }
+ return (
+
+ {/* Floating Header - Left Side */}
+
+ {/* Title Box */}
+
+
+
+ Projects Map
+
+
+ {markers.length} of {projects.length} projects with coordinates
+
+
{" "}
+
+
+ {/* Zoom Controls - Below Title */}
+
+
+ {
+ // This will be handled by the map component
+ const event = new CustomEvent("mapZoomIn");
+ window.dispatchEvent(event);
+ }}
+ title="Zoom In"
+ >
+ +
+
+ {
+ // This will be handled by the map component
+ const event = new CustomEvent("mapZoomOut");
+ window.dispatchEvent(event);
+ }}
+ title="Zoom Out"
+ >
+ −
+ {" "}
+
+
{" "}
+ {/* Tool Panel - Below Zoom Controls */}
+
+ {" "}
+
+ {" "}
+ {/* Move Tool */}
+
setCurrentTool("move")}
+ title="Move Tool (Pan Map)"
+ >
+
+
+ {/* Select Tool */}
+
setCurrentTool("select")}
+ title="Select Tool"
+ >
+
+
+ {/* Measure Tool */}
+
setCurrentTool("measure")}
+ title="Measure Distance"
+ >
+
+
+ {/* Draw Tool */}
+
setCurrentTool("draw")}
+ title="Draw/Markup"
+ >
+
+
+ {/* Pin/Marker Tool */}
+
setCurrentTool("pin")}
+ title="Add Pin/Marker"
+ >
+
+
+ {/* Area Tool */}
+
setCurrentTool("area")}
+ title="Measure Area"
+ >
+
+
+
+
+ {/* Layer Control Panel - Right Side */}
+
+ {/* Action Buttons */}
+
+
+
+
+ List View
+
+
+
+
+
+ Add Project
+
+
+
-
-
- {/* Filters Sidebar */}
-
-
-
Filters
-
-
+ {/* Layer Control Panel */}
+
+ {/* Layer Control Header */}
+
+
+
+
+
+ Map Layers
+
+
+ {1 + activeOverlays.length} active
+
+
+
+
+
{" "}
+ {/* Layer Control Content */}
+
+
+ {/* Base Layers Section */}
+
+
+
+ Base Maps
+
+
+ {mapLayers.base.map((layer, index) => (
+
+ ))}
+
+
+
+ {/* Overlay Layers Section */}
+ {mapLayers.overlays && mapLayers.overlays.length > 0 && (
-
Status
+
+
+ Overlay Layers
+
{" "}
- {Object.entries(statusFilters).map(([status, checked]) => (
-
-
-
-
Map Layer
-
-
-
+ )}
-
- {/* Legend */}
-
-
Legend
-
- {Object.entries(statusFilters).filter(([_, checked]) => checked).map(([status, _]) => (
-
-
-
{getStatusDisplay(status)}
-
- ))}
-
-
-
-
- {/* Map */}
-
-
+ {/* Status Filter Panel - Bottom Left */}
+
+
+
+
+ Filters:
+
+ {/* Toggle All Button */}
+
+
+
+
+ {Object.values(statusFilters).every((v) => v)
+ ? "Hide All"
+ : "Show All"}
+
+
+ {/* Individual Status Filters */}
+ {Object.entries(statusConfig).map(([status, config]) => {
+ const isActive = statusFilters[status];
+ const projectCount = projects.filter(
+ (p) => p.project_status === status && p.coordinates
+ ).length;
+
+ return (
+
toggleStatusFilter(status)}
+ className={`flex items-center gap-1 px-2 py-1 rounded text-xs font-medium transition-all duration-200 hover:bg-gray-100 ${
+ isActive ? "opacity-100 scale-100" : "opacity-40 scale-95"
+ }`}
+ title={`Toggle ${config.label} (${projectCount} projects)`}
+ >
+
+
+ {config.shortLabel}
+
+
+ ({projectCount})
+
+
+ );
+ })}{" "}
+
+
+
{" "}
+ {/* Status Panel - Bottom Left */}
+ {markers.length > 0 && (
+
+
+
+ Filters:
+
+
+ {/* Toggle All Button */}
+
+
+
+ {Object.values(statusFilters).every((v) => v)
+ ? "Hide All"
+ : "Show All"}
+
+
+
+ {/* Individual Status Filters */}
+ {Object.entries(statusConfig).map(([status, config]) => {
+ const isActive = statusFilters[status];
+ const projectCount = projects.filter(
+ (p) => p.project_status === status && p.coordinates
+ ).length;
+
+ return (
+
toggleStatusFilter(status)}
+ className={`flex items-center gap-1 px-2 py-1 rounded text-xs font-medium transition-all duration-200 hover:bg-gray-100 ${
+ isActive ? "opacity-100 scale-100" : "opacity-40 scale-95"
+ }`}
+ title={`Toggle ${config.label} (${projectCount} projects)`}
+ >
+
+
+ {config.shortLabel}
+
+
+ ({projectCount})
+
+
+ );
+ })}
+
+
+ )}{" "}
+ {/* Full Screen Map */}
+ {markers.length === 0 ? (
+
+
+
+
+ No projects with coordinates
+
+
+ Projects need coordinates to appear on the map. Add coordinates
+ when creating or editing projects.
+
+
+
+ View All Projects
+
+
+ Add Project
+
-
+ ) : (
+
+
+
+ )}{" "}
);
}