diff --git a/README.md b/README.md index 3d4a5dc..b6c2ba4 100644 --- a/README.md +++ b/README.md @@ -100,29 +100,42 @@ The application uses SQLite database which will be automatically initialized on ``` src/ ├── app/ # Next.js app router pages +│ ├── admin/ # Admin dashboard and user management │ ├── api/ # API routes +│ │ ├── admin/ # Admin-related endpoints (e.g., user management) │ │ ├── all-project-tasks/ # Get all project tasks endpoint +│ │ ├── audit-logs/ # Audit log endpoints +│ │ ├── auth/ # Authentication endpoints │ │ ├── contracts/ # Contract management endpoints │ │ ├── notes/ # Notes management endpoints │ │ ├── projects/ # Project management endpoints │ │ ├── project-tasks/ # Task management endpoints +│ │ ├── task-notes/ # Task-specific notes endpoints │ │ └── tasks/ # Task template endpoints +│ ├── auth/ # Authentication pages (login, etc.) │ ├── contracts/ # Contract pages │ ├── projects/ # Project pages +│ ├── project-tasks/ # Project-specific task pages │ └── tasks/ # Task management pages ├── components/ # Reusable React components -│ ├── ui/ # UI components (Button, Card, etc.) -│ ├── ContractForm.js # Contract form component -│ ├── NoteForm.js # Note form component -│ ├── ProjectForm.js # Project form component +│ ├── auth/ # Authentication-related components +│ ├── ui/ # UI components (Button, Card, etc.) +│ ├── AuditLogViewer.js # Component to view audit logs +│ ├── ContractForm.js # Contract form component +│ ├── NoteForm.js # Note form component +│ ├── ProjectForm.js # Project form component │ ├── ProjectTaskForm.js # Project task form component │ ├── ProjectTasksSection.js # Project tasks section component -│ ├── TaskForm.js # Task form component +│ ├── TaskForm.js # Task form component │ └── TaskTemplateForm.js # Task template form component -└── lib/ # Utility functions - ├── queries/ # Database query functions - ├── db.js # Database connection - └── init-db.js # Database initialization +├── lib/ # Utility functions +│ ├── queries/ # Database query functions +│ ├── auditLog.js # Audit logging utilities +│ ├── auth.js # Authentication helpers +│ ├── db.js # Database connection +│ ├── init-db.js # Database initialization +│ └── userManagement.js # User management functions +└── middleware.js # Next.js middleware for auth and routing ``` ## Available Scripts @@ -147,6 +160,9 @@ The application uses the following main tables: - **tasks** - Task templates - **project_tasks** - Tasks assigned to specific projects - **notes** - Project notes and updates +- **users** - User accounts and roles for authentication +- **sessions** - User session management +- **audit_logs** - Detailed logs for security and tracking ## API Endpoints @@ -188,6 +204,19 @@ The application uses the following main tables: - `POST /api/notes` - Create new note - `DELETE /api/notes` - Delete note +### Audit Logs + +- `GET /api/audit-logs` - Get all audit logs +- `POST /api/audit-logs/log` - Create a new audit log entry +- `GET /api/audit-logs/stats` - Get audit log statistics + +### Admin + +- `GET /api/admin/users` - Get all users +- `POST /api/admin/users` - Create a new user +- `PUT /api/admin/users/[id]` - Update a user +- `DELETE /api/admin/users/[id]` - Delete a user + ## Advanced Map Features This project includes a powerful map system for project locations, supporting multiple dynamic base layers: diff --git a/src/app/comprehensive-polish-map/page.js b/debug-disabled/comprehensive-polish-map/page.js similarity index 98% rename from src/app/comprehensive-polish-map/page.js rename to debug-disabled/comprehensive-polish-map/page.js index 3f096e6..130c7c2 100644 --- a/src/app/comprehensive-polish-map/page.js +++ b/debug-disabled/comprehensive-polish-map/page.js @@ -1,7 +1,15 @@ "use client"; import { useState } from 'react'; -import ComprehensivePolishMap from '../../components/ui/ComprehensivePolishMap'; +import dynamic from 'next/dynamic'; + +const ComprehensivePolishMap = dynamic( + () => import('../../components/ui/ComprehensivePolishMap'), + { + ssr: false, + loading: () =>
Loading map...
+ } +); export default function ComprehensivePolishMapPage() { const [selectedLocation, setSelectedLocation] = useState('krakow'); diff --git a/debug-disabled/debug-polish-orthophoto/layout.disabled.js b/debug-disabled/debug-polish-orthophoto/layout.disabled.js new file mode 100644 index 0000000..6ddd998 --- /dev/null +++ b/debug-disabled/debug-polish-orthophoto/layout.disabled.js @@ -0,0 +1,9 @@ +// Temporarily disabled debug pages during build +// These pages are for development/testing purposes only +// To re-enable, rename this file to layout.js + +export default function DebugLayout({ children }) { + return children; +} + +export const dynamic = 'force-dynamic'; diff --git a/src/app/debug-polish-orthophoto/page.js b/debug-disabled/debug-polish-orthophoto/page.js similarity index 93% rename from src/app/debug-polish-orthophoto/page.js rename to debug-disabled/debug-polish-orthophoto/page.js index 2722048..71aef92 100644 --- a/src/app/debug-polish-orthophoto/page.js +++ b/debug-disabled/debug-polish-orthophoto/page.js @@ -1,6 +1,16 @@ "use client"; -import DebugPolishOrthophotoMap from '../../components/ui/DebugPolishOrthophotoMap'; +import dynamic from 'next/dynamic'; + +const DebugPolishOrthophotoMap = dynamic( + () => import('../../components/ui/DebugPolishOrthophotoMap'), + { + ssr: false, + loading: () =>
Loading map...
+ } +); + +export const dynamicParams = true; export default function DebugPolishOrthophotoPage() { // Test marker in Poland @@ -100,4 +110,4 @@ export default function DebugPolishOrthophotoPage() { ); -} +} \ No newline at end of file diff --git a/src/app/test-improved-wmts/page.js b/debug-disabled/test-improved-wmts/page.js similarity index 93% rename from src/app/test-improved-wmts/page.js rename to debug-disabled/test-improved-wmts/page.js index 83e3efe..fe336ca 100644 --- a/src/app/test-improved-wmts/page.js +++ b/debug-disabled/test-improved-wmts/page.js @@ -1,6 +1,14 @@ "use client"; -import ImprovedPolishOrthophotoMap from '../../components/ui/ImprovedPolishOrthophotoMap'; +import dynamic from 'next/dynamic'; + +const ImprovedPolishOrthophotoMap = dynamic( + () => import('../../components/ui/ImprovedPolishOrthophotoMap'), + { + ssr: false, + loading: () =>
Loading map...
+ } +); export default function ImprovedPolishOrthophotoPage() { const testMarkers = [ diff --git a/src/app/test-polish-map/page.js b/debug-disabled/test-polish-map/page.js similarity index 94% rename from src/app/test-polish-map/page.js rename to debug-disabled/test-polish-map/page.js index 70956e4..df033d9 100644 --- a/src/app/test-polish-map/page.js +++ b/debug-disabled/test-polish-map/page.js @@ -1,8 +1,23 @@ "use client"; import { useState } from 'react'; -import PolishOrthophotoMap from '../../components/ui/PolishOrthophotoMap'; -import AdvancedPolishOrthophotoMap from '../../components/ui/AdvancedPolishOrthophotoMap'; +import dynamic from 'next/dynamic'; + +const PolishOrthophotoMap = dynamic( + () => import('../../components/ui/PolishOrthophotoMap'), + { + ssr: false, + loading: () =>
Loading map...
+ } +); + +const AdvancedPolishOrthophotoMap = dynamic( + () => import('../../components/ui/AdvancedPolishOrthophotoMap'), + { + ssr: false, + loading: () =>
Loading map...
+ } +); export default function PolishOrthophotoTestPage() { const [activeMap, setActiveMap] = useState('basic'); diff --git a/src/app/test-polish-orthophoto/page.js b/debug-disabled/test-polish-orthophoto/page.js similarity index 93% rename from src/app/test-polish-orthophoto/page.js rename to debug-disabled/test-polish-orthophoto/page.js index 640b0ff..fecc41a 100644 --- a/src/app/test-polish-orthophoto/page.js +++ b/debug-disabled/test-polish-orthophoto/page.js @@ -1,6 +1,14 @@ "use client"; -import PolishOrthophotoMap from '../../components/ui/PolishOrthophotoMap'; +import dynamic from 'next/dynamic'; + +const PolishOrthophotoMap = dynamic( + () => import('../../components/ui/PolishOrthophotoMap'), + { + ssr: false, + loading: () =>
Loading map...
+ } +); export default function TestPolishOrthophotoPage() { // Test markers - various locations in Poland diff --git a/src/app/auth/error/page.js b/src/app/auth/error/page.js index 5b2d3e3..757a2b0 100644 --- a/src/app/auth/error/page.js +++ b/src/app/auth/error/page.js @@ -1,8 +1,9 @@ 'use client' import { useSearchParams } from 'next/navigation' +import { Suspense } from 'react' -export default function AuthError() { +function AuthErrorContent() { const searchParams = useSearchParams() const error = searchParams.get('error') @@ -47,3 +48,18 @@ export default function AuthError() { ) } + +export default function AuthError() { + return ( + +
+
+

Loading...

+
+ + }> + +
+ ) +} diff --git a/src/app/auth/signin/page.js b/src/app/auth/signin/page.js index c6e9745..bbc45ac 100644 --- a/src/app/auth/signin/page.js +++ b/src/app/auth/signin/page.js @@ -1,11 +1,11 @@ "use client" -import { useState } from "react" +import { useState, Suspense } from "react" import { signIn, getSession } from "next-auth/react" import { useRouter } from "next/navigation" import { useSearchParams } from "next/navigation" -export default function SignIn() { +function SignInContent() { const [email, setEmail] = useState("") const [password, setPassword] = useState("") const [error, setError] = useState("") @@ -125,3 +125,18 @@ export default function SignIn() { ) } + +export default function SignIn() { + return ( + +
+
+

Loading...

+
+ + }> + +
+ ) +} \ No newline at end of file diff --git a/src/app/projects/[id]/page.js b/src/app/projects/[id]/page.js index 193d8a7..e01dea4 100644 --- a/src/app/projects/[id]/page.js +++ b/src/app/projects/[id]/page.js @@ -13,7 +13,7 @@ import { formatDate } from "@/lib/utils"; import PageContainer from "@/components/ui/PageContainer"; import PageHeader from "@/components/ui/PageHeader"; import ProjectStatusDropdown from "@/components/ProjectStatusDropdown"; -import ProjectMap from "@/components/ui/ProjectMap"; +import ClientProjectMap from "@/components/ui/ClientProjectMap"; export default async function ProjectViewPage({ params }) { const { id } = await params; @@ -435,7 +435,7 @@ export default async function ProjectViewPage({ params }) { - import("@/components/ui/LeafletMap"), { + ssr: false, + loading: () => ( +
+ Loading map... +
+ ), +}); + +export default function ProjectsMapPage() { + const searchParams = useSearchParams(); + const router = useRouter(); + const [projects, setProjects] = useState([]); + const [loading, setLoading] = useState(true); + const [mapCenter, setMapCenter] = useState([50.0614, 19.9366]); // Default to Krakow, Poland + const [mapZoom, setMapZoom] = useState(10); // Default zoom level + const [statusFilters, setStatusFilters] = useState({ + registered: true, + in_progress_design: true, + in_progress_construction: true, + fulfilled: true, + }); + const [activeBaseLayer, setActiveBaseLayer] = useState("OpenStreetMap"); + const [activeOverlays, setActiveOverlays] = useState([]); + const [showLayerPanel, setShowLayerPanel] = useState(true); + const [currentTool, setCurrentTool] = useState("move"); // Current map tool + + // 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", + }, + }; + + // 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); + }; + + // Toggle status filter + const toggleStatusFilter = (status) => { + setStatusFilters((prev) => ({ + ...prev, + [status]: !prev[status], + })); + }; + + // Layer control functions + const handleBaseLayerChange = (layerName) => { + setActiveBaseLayer(layerName); + }; + + const toggleOverlay = (layerName) => { + setActiveOverlays((prev) => { + if (prev.includes(layerName)) { + return prev.filter((name) => name !== layerName); + } else { + return [...prev, layerName]; + } + }); + }; + + 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]); + + useEffect(() => { + fetch("/api/projects") + .then((res) => res.json()) + .then((data) => { + setProjects(data); + + // Only calculate center based on projects if no URL parameters are provided + const lat = searchParams.get("lat"); + const lng = searchParams.get("lng"); + + 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]); + + // 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; + } + + const statusInfo = + statusConfig[project.project_status] || statusConfig.registered; + + return { + position: [lat, lng], + color: statusInfo.color, + popup: ( +
+
+

+ {project.project_name} +

+ {project.project_number && ( +
+ {project.project_number} +
+ )} +
+ +
+ {project.address && ( +
+ + + + +
+ + {project.address} + + {project.city && ( + , {project.city} + )} +
+
+ )} +
+ {project.wp && ( +
+ WP:{" "} + {project.wp} +
+ )} + {project.plot && ( +
+ Plot:{" "} + {project.plot} +
+ )} +
+ {project.project_status && ( +
+ Status: + + {statusInfo.shortLabel} + +
+ )} +
+ +
+ + + +
+
+ ), + }; + }) + .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 */} +
+
+ + {" "} +
+
{" "} + {/* Tool Panel - Below Zoom Controls */} +
+ {" "} +
+ {" "} + {/* Move Tool */} + + {/* Select Tool */} + + {/* Measure Tool */} + + {/* Draw Tool */} + + {/* Pin/Marker Tool */} + + {/* Area Tool */} + +
+
+ {/* Layer Control Panel - Right Side */} +
+ {/* Action Buttons */} +
+ + + + + + +
+ + {/* Layer Control Panel */} +
+ {/* Layer Control Header */} +
+ +
{" "} + {/* Layer Control Content */} +
+
+ {/* Base Layers Section */} +
+

+ + + + Base Maps +

+
+ {mapLayers.base.map((layer, index) => ( + + ))} +
+
+ + {/* Overlay Layers Section */} + {mapLayers.overlays && mapLayers.overlays.length > 0 && ( +
+

+ + + + Overlay Layers +

{" "} +
+ {mapLayers.overlays.map((layer, index) => ( + + ))} +
+
+ )} +
+
{" "} +
+
+ {/* Status Filter Panel - Bottom Left */} +
+
+
+ + Filters: + + {/* Toggle All Button */} + + {/* 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 ( + + ); + })}{" "} +
+
+
{" "} + {/* Status Panel - Bottom Left */} + {markers.length > 0 && ( +
+
+ + Filters: + + + {/* Toggle All Button */} + + + {/* 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 ( + + ); + })} +
+
+ )}{" "} + {/* 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. +

+
+ + + + + + +
+
+
+ ) : ( +
+ +
+ )}{" "} +
+ ); +} diff --git a/src/app/projects/map/page.js b/src/app/projects/map/page.js index 33e6751..2ee90f3 100644 --- a/src/app/projects/map/page.js +++ b/src/app/projects/map/page.js @@ -1,6 +1,6 @@ "use client"; -import React, { useEffect, useState } from "react"; +import React, { useEffect, useState, Suspense } from "react"; import Link from "next/link"; import dynamic from "next/dynamic"; import { useSearchParams, useRouter } from "next/navigation"; @@ -17,7 +17,7 @@ const DynamicMap = dynamic(() => import("@/components/ui/LeafletMap"), { ), }); -export default function ProjectsMapPage() { +function ProjectsMapPageContent() { const searchParams = useSearchParams(); const router = useRouter(); const [projects, setProjects] = useState([]); @@ -28,901 +28,294 @@ export default function ProjectsMapPage() { registered: true, in_progress_design: true, in_progress_construction: true, - fulfilled: true, + in_progress_inspection: true, + completed: false, + cancelled: false, }); - const [activeBaseLayer, setActiveBaseLayer] = useState("OpenStreetMap"); - const [activeOverlays, setActiveOverlays] = useState([]); - const [showLayerPanel, setShowLayerPanel] = useState(true); - const [currentTool, setCurrentTool] = useState("move"); // Current map tool + const [selectedLayerName, setSelectedLayerName] = useState("OpenStreetMap"); - // 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", - }, - }; + // Check if we have a specific project ID from search params + const projectId = searchParams.get("project"); - // 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); - }; + useEffect(() => { + const fetchProjects = async () => { + setLoading(true); + try { + const response = await fetch("/api/projects"); + if (response.ok) { + const data = await response.json(); + setProjects(data); - // Toggle status filter - const toggleStatusFilter = (status) => { - setStatusFilters((prev) => ({ + // 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 => ({ ...prev, - [status]: !prev[status], + [status]: !prev[status] })); }; - // Layer control functions - const handleBaseLayerChange = (layerName) => { - setActiveBaseLayer(layerName); + const handleLayerChange = (layerName) => { + setSelectedLayerName(layerName); }; - const toggleOverlay = (layerName) => { - setActiveOverlays((prev) => { - if (prev.includes(layerName)) { - return prev.filter((name) => name !== layerName); - } else { - return [...prev, 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 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); - } + // 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 }; - }, [searchParams]); + }); - useEffect(() => { - fetch("/api/projects") - .then((res) => res.json()) - .then((data) => { - setProjects(data); + 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'; + } + }; - // Only calculate center based on projects if no URL parameters are provided - const lat = searchParams.get("lat"); - const lng = searchParams.get("lng"); + 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; + } + }; - 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]); + 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 } - } - - setLoading(false); + }; }) - .catch((error) => { - console.error("Error fetching projects:", error); - setLoading(false); - }); - }, [searchParams]); + }; - // 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; - } - - const statusInfo = - statusConfig[project.project_status] || statusConfig.registered; - - return { - position: [lat, lng], - color: statusInfo.color, - popup: ( -
-
-

- {project.project_name} -

- {project.project_number && ( -
- {project.project_number} -
- )} -
- -
- {project.address && ( -
- - - - -
- - {project.address} - - {project.city && ( - , {project.city} - )} -
-
- )} -
- {project.wp && ( -
- WP:{" "} - {project.wp} -
- )} - {project.plot && ( -
- Plot:{" "} - {project.plot} -
- )} -
- {project.project_status && ( -
- Status: - - {statusInfo.shortLabel} - -
- )} -
- -
- - - -
-
- ), - }; - }) - .filter((marker) => marker !== null); + 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); + }; if (loading) { return ( -
+
-
-

Loading projects map...

-

- Preparing your full-screen map experience -

+
+

Loading projects...

); } + return ( -
- {/* Floating Header - Left Side */} -
- {/* Title Box */} -
-
-

- Projects Map -

-
- {markers.length} of {projects.length} projects with coordinates +
+
+
+
+
+

Projects Map

+

+ Showing {filteredProjects.length} of {projects.length} projects with location data +

-
{" "} -
-
- {/* Zoom Controls - Below Title */} -
-
- - {" "} -
-
{" "} - {/* Tool Panel - Below Zoom Controls */} -
- {" "} -
- {" "} - {/* Move Tool */} - - {/* Select Tool */} - - {/* Measure Tool */} - - {/* Draw Tool */} - - {/* Pin/Marker Tool */} - - {/* Area Tool */} - -
-
- {/* Layer Control Panel - Right Side */} -
- {/* Action Buttons */} -
- - - - - - -
- - {/* Layer Control Panel */} -
- {/* Layer Control Header */} -
- -
{" "} - {/* Layer Control Content */} -
-
- {/* Base Layers Section */} -
-

- - - - Base Maps -

-
- {mapLayers.base.map((layer, index) => ( - - ))} -
-
+ Export GeoJSON + + + + +
+
+
+
- {/* Overlay Layers Section */} - {mapLayers.overlays && mapLayers.overlays.length > 0 && ( +
+
+ {/* Filters Sidebar */} +
+
+

Filters

+ +
-

- - - - Overlay Layers -

{" "} +

Status

- {mapLayers.overlays.map((layer, index) => ( -
- )} + +
+

Map Layer

+ +
+
-
{" "} + + {/* Legend */} +
+

Legend

+
+ {Object.entries(statusFilters).filter(([_, checked]) => checked).map(([status, _]) => ( +
+
+ {getStatusDisplay(status)} +
+ ))} +
+
+
+ + {/* Map */} +
+
+
+ +
+
+
- {/* Status Filter Panel - Bottom Left */} -
-
-
- - Filters: - - {/* Toggle All Button */} - - {/* 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 ( - - ); - })}{" "} -
-
-
{" "} - {/* Status Panel - Bottom Left */} - {markers.length > 0 && ( -
-
- - Filters: - - - {/* Toggle All Button */} - - - {/* 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 ( - - ); - })} -
-
- )}{" "} - {/* 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. -

-
- - - - - - -
-
-
- ) : ( -
- -
- )}{" "}
); } + +export default function ProjectsMapPage() { + return ( + +
+
+

Loading map...

+
+
+ }> + + + ); +} diff --git a/src/components/ui/ClientProjectMap.js b/src/components/ui/ClientProjectMap.js new file mode 100644 index 0000000..ff5adf9 --- /dev/null +++ b/src/components/ui/ClientProjectMap.js @@ -0,0 +1,15 @@ +"use client"; + +import dynamic from "next/dynamic"; + +const ProjectMap = dynamic( + () => import("@/components/ui/ProjectMap"), + { + ssr: false, + loading: () =>
Loading map...
+ } +); + +export default function ClientProjectMap(props) { + return ; +}