"use client"; /* eslint-disable @typescript-eslint/no-explicit-any, react-hooks/set-state-in-effect */ import { useState, useMemo, useEffect } from "react"; import dynamic from "next/dynamic"; import { AppShell } from "@/components/app-shell"; import { MapPageHeader } from "@/components/maps/map-page-header"; import { GasStationStatsCards } from "@/components/gas-station/gas-station-stats-cards"; import { GasStationDirectory } from "@/components/gas-station/gas-station-directory"; import { GasStationInspector } from "@/components/gas-station/gas-station-inspector"; import { PertaminaPriceTable } from "@/components/gas-station/pertamina-price-table"; import { GasStationHud } from "@/components/gas-station/gas-station-hud"; import { GasStationDisclosureCard } from "@/components/ui/collection-grid-disclosure"; import type { UserMarker } from "@/components/leaflet-map"; import { FuelIcon } from "@/components/ui/phosphor-icons"; import { StationPlacementDialog } from "@/components/gas-station/station-placement-dialog"; import { StationEditDialog } from "@/components/gas-station/station-edit-dialog"; import { toast } from "sonner"; const PLACING_LABELS: Record = { "charging-station": "Pengisi Daya EV", "gas-pump": "SPBU", wrench: "Bengkel", }; const LeafletMap = dynamic(() => import("@/components/leaflet-map"), { ssr: false }); const BACKEND_URL = process.env.NEXT_PUBLIC_BACKEND_URL ?? "http://localhost:4000"; export default function GasStationMapPage() { const [selectedStation, setSelectedStation] = useState(null); const [searchQuery, setSearchQuery] = useState(""); const [brandFilter, setBrandFilter] = useState("All"); const [fuelPriceDisplay, setFuelPriceDisplay] = useState<"ron92" | "ron95" | "diesel">("ron95"); const [evOnlyFilter, setEvOnlyFilter] = useState(false); const [useLeaflet, setUseLeaflet] = useState(false); const [mapStyle, setMapStyle] = useState<"street" | "satellite">("street"); const [userMarkers, setUserMarkers] = useState([]); const [placingMarkerType, setPlacingMarkerType] = useState(null); const [dragDropEnabled, setDragDropEnabled] = useState(false); // Custom Marker Dialog State const [pendingClick, setPendingClick] = useState<{ lat: number; lng: number } | null>(null); const [pendingMarkerType, setPendingMarkerType] = useState(null); const [editingMarker, setEditingMarker] = useState(null); const [hydrated, setHydrated] = useState(false); const [refreshKey, setRefreshKey] = useState(0); const [fuelPrices, setFuelPrices] = useState(null); const [fuelPricesLoading, setFuelPricesLoading] = useState(true); // Fetch dynamic Pertamina fuel prices useEffect(() => { setFuelPricesLoading(true); fetch(`${BACKEND_URL}/gas-station/fuel-prices`) .then((res) => res.json()) .then((data) => { if (data && data.succeeded) { setFuelPrices(data); } }) .catch((err) => console.error("Gagal memuat harga BBM:", err)) .finally(() => setFuelPricesLoading(false)); }, []); const filteredStations = useMemo(() => { return userMarkers.filter((st) => { const matchesSearch = st.name.toLowerCase().includes(searchQuery.toLowerCase()); const matchesBrand = brandFilter === "All" || (st.type === "gas-pump" && st.meta?.poi_type === brandFilter); const matchesEv = !evOnlyFilter || st.type === "charging-station"; return matchesSearch && matchesBrand && matchesEv; }); }, [userMarkers, searchQuery, brandFilter, evOnlyFilter]); const activeSelectedStation = useMemo(() => { if (filteredStations.length === 0) return null; const found = filteredStations.find((st) => st.id === selectedStation?.id); if (found) return found; return filteredStations[0]; }, [filteredStations, selectedStation]); // Load existing markers when page loads or refreshKey changes useEffect(() => { const token = localStorage.getItem("auth_token"); const headers: Record = token ? { Authorization: `Bearer ${token}` } : {}; fetch(`${BACKEND_URL}/gas-station/markers`, { headers }) .then((r) => r.json()) .then((rows) => { if (Array.isArray(rows)) { const mapped = rows.map((row: any) => { let finalPoiType = ""; let finalNotes = ""; if (row.marker_category === "charging-station") { finalPoiType = row.sub_type; finalNotes = row.status ? `Status: ${row.status}` : ""; } else if (row.marker_category === "gas-pump") { finalPoiType = row.brand; finalNotes = row.operating_hours ? `Jam: ${row.operating_hours}` : ""; } else if (row.marker_category === "wrench") { finalPoiType = row.sub_type; finalNotes = row.status; } return { id: `gsm-${row.id}`, lat: parseFloat(row.latitude), lng: parseFloat(row.longitude), type: row.marker_category, name: row.name, meta: { poi_type: finalPoiType, notes: finalNotes, gas_types: row.gas_types || undefined, }, }; }); setUserMarkers(mapped); } }) .catch((err) => { console.error("Gagal memuat markers:", err); }); }, [refreshKey]); // Session storage synchronization (identical pattern to poverty) useEffect(() => { setUseLeaflet(sessionStorage.getItem("gas_useLeaflet") === "true"); setMapStyle((sessionStorage.getItem("gas_mapStyle") as "street" | "satellite") ?? "street"); setBrandFilter(sessionStorage.getItem("gas_brandFilter") ?? "All"); setFuelPriceDisplay((sessionStorage.getItem("gas_fuelPriceDisplay") as "ron92" | "ron95" | "diesel") ?? "ron95"); setHydrated(true); }, []); useEffect(() => { if (hydrated) sessionStorage.setItem("gas_useLeaflet", String(useLeaflet)); }, [useLeaflet, hydrated]); useEffect(() => { if (hydrated) sessionStorage.setItem("gas_mapStyle", mapStyle); }, [mapStyle, hydrated]); useEffect(() => { if (hydrated) sessionStorage.setItem("gas_brandFilter", brandFilter); }, [brandFilter, hydrated]); useEffect(() => { if (hydrated) sessionStorage.setItem("gas_fuelPriceDisplay", fuelPriceDisplay); }, [fuelPriceDisplay, hydrated]); // FULL BLEED MAP LAYOUT if (useLeaflet) { return ( setUseLeaflet(false)}>
{/* Leaflet Map */} { const found = userMarkers.find((st) => st.id === id); if (found) setSelectedStation(found); }} fuelPriceDisplay={fuelPriceDisplay} mapStyle={mapStyle} userMarkers={userMarkers} onMarkerClick={(m) => { if (["charging-station", "gas-pump", "wrench"].includes(m.type)) { setEditingMarker(m); } }} onMapClick={(lat, lng) => { if (placingMarkerType) { setPendingClick({ lat, lng }); setPendingMarkerType(placingMarkerType); setPlacingMarkerType(null); } }} dragDropEnabled={dragDropEnabled} onMarkerDragEnd={handleMarkerDragEnd} /> {/* Placing Marker Banner */} {placingMarkerType && (
Menempatkan {PLACING_LABELS[placingMarkerType] ?? placingMarkerType.replace("-", " ")}... Klik pada peta untuk menentukan posisi
)} {/* Floating Filters HUD */} setUseLeaflet(false)} /> {/* Floating Collection Grid Disclosure */}
{/* Placement Dialog */} { if (!v) { setPendingClick(null); setPendingMarkerType(null); } }} pendingClick={pendingClick} markerType={pendingMarkerType} onConfirm={(newMarker) => { setUserMarkers((prev) => [...prev, newMarker]); setPendingClick(null); setPendingMarkerType(null); setRefreshKey((k) => k + 1); }} /> {/* Edit Dialog */} { if (!v) setEditingMarker(null); }} marker={editingMarker} onUpdated={(updated) => { setUserMarkers((prev) => prev.map((m) => m.id === updated.id ? updated : m)); setEditingMarker(null); setRefreshKey((k) => k + 1); }} onDeleted={(deleted) => { setUserMarkers((prev) => prev.filter((m) => m.id !== deleted.id)); setEditingMarker(null); setRefreshKey((k) => k + 1); }} />
); } async function handleMarkerDragEnd(marker: UserMarker, newLat: number, newLng: number) { const dbId = marker.id.split("-").slice(1).join("-"); const token = localStorage.getItem("auth_token"); const headers = { "Content-Type": "application/json", ...(token ? { Authorization: `Bearer ${token}` } : {}), }; try { const res = await fetch(`${BACKEND_URL}/gas-station/markers/${dbId}`, { method: "PUT", headers, body: JSON.stringify({ latitude: newLat, longitude: newLng }), }); if (!res.ok) throw new Error("Gagal memperbarui posisi di server"); setUserMarkers((prev) => prev.map((m) => m.id === marker.id ? { ...m, lat: newLat, lng: newLng } : m ) ); toast.success(`Posisi "${marker.name}" berhasil diperbarui`); setRefreshKey((k) => k + 1); } catch (err) { console.error(err); toast.error(err instanceof Error ? err.message : "Gagal memindahkan penanda"); } } // STANDARD DASHBOARD LAYOUT return (
{/* Top Header Card */} setUseLeaflet(true)} buttonText="Peta Stasiun" icon={} /> {/* Unified Grid Layout */}
{/* Row 1: 4 stats cards (rendered inside GasStationStatsCards as direct children) */} {/* Row 2: Directory */}
{/* Row 2: Inspector (right column) */}
{/* Row 3: Pricing Table (full width) */}
); }