diff --git a/sigap-website/app/(pages)/(admin)/dashboard/crime-management/crime-overview/action.ts b/sigap-website/app/(pages)/(admin)/dashboard/crime-management/crime-overview/action.ts index 5f41f50..2961a3a 100644 --- a/sigap-website/app/(pages)/(admin)/dashboard/crime-management/crime-overview/action.ts +++ b/sigap-website/app/(pages)/(admin)/dashboard/crime-management/crime-overview/action.ts @@ -1,6 +1,12 @@ 'use server'; -import { ICrimes, ICrimesByYearAndMonth } from '@/app/_utils/types/crimes'; +import { createClient } from '@/app/_utils/supabase/client'; + +import { + ICrimes, + ICrimesByYearAndMonth, + IDistanceResult, +} from '@/app/_utils/types/crimes'; import { getInjection } from '@/di/container'; import db from '@/prisma/db'; import { @@ -304,3 +310,38 @@ export async function getCrimeByYearAndMonth( } ); } + +/** + * Calculate distances between units and incidents using PostGIS + * @param unitId Optional unit code to filter by specific unit + * @param districtId Optional district ID to filter by specific district + * @returns Array of distance calculations between units and incidents + */ +export async function calculateDistances( + unitId?: string, + districtId?: string +): Promise { + const supabase = createClient(); + + try { + const { data, error } = await supabase.rpc( + 'calculate_unit_incident_distances', + { + unit_id: unitId || null, + district_id: districtId || null, + } + ); + + if (error) { + console.error('Error calculating distances:', error); + return []; + } + + return data || []; + } catch (error) { + console.error('Failed to calculate distances:', error); + return []; + } +} + + diff --git a/sigap-website/app/_components/map/layers/layers.tsx b/sigap-website/app/_components/map/layers/layers.tsx index 5104d0b..186d573 100644 --- a/sigap-website/app/_components/map/layers/layers.tsx +++ b/sigap-website/app/_components/map/layers/layers.tsx @@ -228,8 +228,9 @@ export default function Layers({ // District fill should only be visible for incidents and clusters const showDistrictFill = activeControl === "incidents" || activeControl === "clusters"; - // Only show incident markers for incidents and clusters views - exclude timeline mode - const showIncidentMarkers = (activeControl === "incidents" || activeControl === "clusters") + // Show incident markers for incidents, clusters, AND units modes + // But hide for heatmap and timeline + const showIncidentMarkers = activeControl !== "heatmap" && activeControl !== "timeline"; return ( <> @@ -270,7 +271,7 @@ export default function Layers({ useAllData={useAllData} /> - {/* Units Layer */} + {/* Units Layer - always show incidents when Units is active */} - {/* Unclustered Points Layer - explicitly hide in timeline mode */} + {/* Unclustered Points Layer - now show for both incidents and units modes */} { + // Skip the query when no entity is selected + return useQuery({ + queryKey: ['distance-incidents', entityId, isUnit, districtId], + queryFn: async () => { + if (!entityId) return []; + const unitId = isUnit ? entityId : undefined; + const result = await calculateDistances(unitId, districtId); + return result; + }, + enabled: !!entityId, // Only run query when there's an entityId + staleTime: 5 * 60 * 1000, // Consider data fresh for 5 minutes + gcTime: 10 * 60 * 1000, // Keep cache for 10 minutes + }); +}; + export default function UnitsLayer({ crimes, units = [], @@ -25,6 +47,20 @@ export default function UnitsLayer({ const [loadedUnits, setLoadedUnits] = useState([]) const loadedUnitsRef = useRef([]) + // For popups + const [selectedUnit, setSelectedUnit] = useState(null) + const [selectedIncident, setSelectedIncident] = useState(null) + const [selectedEntityId, setSelectedEntityId] = useState() + const [isUnitSelected, setIsUnitSelected] = useState(false) + const [selectedDistrictId, setSelectedDistrictId] = useState() + + // Use react-query for distance data + const { data: distances = [], isLoading: isLoadingDistances } = useDistanceData( + selectedEntityId, + isUnitSelected, + selectedDistrictId + ); + // Use either provided units or loaded units const unitsData = useMemo(() => { return units.length > 0 ? units : (loadedUnits || []) @@ -74,6 +110,44 @@ export default function UnitsLayer({ } }, [unitsData]) + // Process incident data to GeoJSON format + const incidentsGeoJSON = useMemo(() => { + const features: any[] = []; + + crimes.forEach(crime => { + crime.crime_incidents.forEach(incident => { + // Skip incidents without location data or filtered by category + if ( + !incident.locations?.latitude || + !incident.locations?.longitude || + (filterCategory !== "all" && incident.crime_categories.name !== filterCategory) + ) return; + + features.push({ + type: "Feature" as const, + properties: { + id: incident.id, + description: incident.description || "No description", + category: incident.crime_categories.name, + date: incident.timestamp, + district: crime.districts?.name || "", + district_id: crime.district_id, + categoryColor: categoryColorMap[incident.crime_categories.name] || '#22c55e', + }, + geometry: { + type: "Point" as const, + coordinates: [incident.locations.longitude, incident.locations.latitude] + } + }); + }); + }); + + return { + type: "FeatureCollection" as const, + features + }; + }, [crimes, filterCategory, categoryColorMap]); + // Create lines between units and incidents within their districts const connectionLinesGeoJSON = useMemo(() => { if (!unitsData.length || !crimes.length) return { @@ -143,7 +217,6 @@ export default function UnitsLayer({ } }, [unitsData, crimes, filterCategory, categoryColorMap]) - // Map click handler code and the rest remains the same... useEffect(() => { if (!map || !visible) return @@ -155,22 +228,16 @@ export default function UnitsLayer({ if (!properties) return - // Create a popup for the unit - const popup = new mapboxgl.Popup() - .setLngLat(feature.geometry.type === 'Point' ? - (feature.geometry as any).coordinates as [number, number] : - [0, 0]) // Fallback coordinates if not a Point geometry - .setHTML(` -
-

${properties.name}

-

${properties.type}

-

${properties.address || 'No address provided'}

-

Staff: ${properties.staff_count || 'N/A'}

-

Phone: ${properties.phone || 'N/A'}

-

District: ${properties.district || 'N/A'}

-
- `) - .addTo(map) + // Find the unit in our data + const unit = unitsData.find(u => u.code_unit === properties.id); + if (!unit) return; + + // Set the selected unit and query parameters + setSelectedUnit(unit); + setSelectedIncident(null); // Clear any selected incident + setSelectedEntityId(properties.id); + setIsUnitSelected(true); + setSelectedDistrictId(properties.district_id); // Highlight the connected lines for this unit if (map.getLayer('units-connection-lines')) { @@ -180,15 +247,49 @@ export default function UnitsLayer({ properties.id ]) } - - // When popup closes, reset the lines filter - popup.on('close', () => { - if (map.getLayer('units-connection-lines')) { - map.setFilter('units-connection-lines', ['has', 'unit_id']) - } - }) } + const handleIncidentClick = (e: mapboxgl.MapMouseEvent & { features?: mapboxgl.MapboxGeoJSONFeature[] }) => { + if (!e.features || e.features.length === 0) return; + + const feature = e.features[0]; + const properties = feature.properties; + + if (!properties) return; + + // Create incident object from properties + const incident = { + id: properties.id, + category: properties.category, + description: properties.description, + date: properties.date, + district: properties.district, + district_id: properties.district_id, + }; + + // Set the selected incident and query parameters + setSelectedIncident({ + ...incident, + latitude: feature.geometry.type === 'Point' ? + (feature.geometry as any).coordinates[1] : 0, + longitude: feature.geometry.type === 'Point' ? + (feature.geometry as any).coordinates[0] : 0 + }); + setSelectedUnit(null); + setSelectedEntityId(properties.id); + setIsUnitSelected(false); + setSelectedDistrictId(properties.district_id); + + // Highlight the connected lines for this incident + if (map.getLayer('units-connection-lines')) { + map.setFilter('units-connection-lines', [ + '==', + ['get', 'incident_id'], + properties.id + ]); + } + }; + // Define event handlers that can be referenced for both adding and removing const handleMouseEnter = () => { map.getCanvas().style.cursor = 'pointer' @@ -207,14 +308,41 @@ export default function UnitsLayer({ map.on('mouseleave', 'units-points', handleMouseLeave) } + // Add click event for incidents-points layer + if (map.getLayer('incidents-points')) { + map.on('click', 'incidents-points', handleIncidentClick) + + // Change cursor on hover + map.on('mouseenter', 'incidents-points', handleMouseEnter) + map.on('mouseleave', 'incidents-points', handleMouseLeave) + } + return () => { if (map.getLayer('units-points')) { map.off('click', 'units-points', handleUnitClick) map.off('mouseenter', 'units-points', handleMouseEnter) map.off('mouseleave', 'units-points', handleMouseLeave) } + + if (map.getLayer('incidents-points')) { + map.off('click', 'incidents-points', handleIncidentClick) + map.off('mouseenter', 'incidents-points', handleMouseEnter) + map.off('mouseleave', 'incidents-points', handleMouseLeave) + } } - }, [map, visible]) + }, [map, visible, unitsData]) + + // Reset map filters when popup is closed + const handleClosePopup = () => { + setSelectedUnit(null); + setSelectedIncident(null); + setSelectedEntityId(undefined); + setSelectedDistrictId(undefined); + + if (map && map.getLayer('units-connection-lines')) { + map.setFilter('units-connection-lines', ['has', 'unit_id']); + } + }; if (!visible) return null @@ -255,6 +383,22 @@ export default function UnitsLayer({ /> + {/* Incidents Points */} + + + + {/* Connection Lines */} + + {/* Custom Unit Popup */} + {selectedUnit && ( + + )} + + {/* Custom Incident Popup */} + {selectedIncident && ( + + )} ) } diff --git a/sigap-website/app/_components/map/pop-up/distance-info.tsx b/sigap-website/app/_components/map/pop-up/distance-info.tsx new file mode 100644 index 0000000..d27ce2a --- /dev/null +++ b/sigap-website/app/_components/map/pop-up/distance-info.tsx @@ -0,0 +1,108 @@ +"use client" + +import { calculateDistances } from '@/app/(pages)/(admin)/dashboard/crime-management/crime-overview/action' +import { useState, useEffect } from 'react' +import { Skeleton } from '../../ui/skeleton' +import { formatDistance } from '@/app/_utils/map' +import { useQuery } from '@tanstack/react-query' + + +interface DistanceInfoProps { + unitId?: string + districtId?: string + className?: string +} + +export default function DistanceInfo({ unitId, districtId, className = '' }: DistanceInfoProps) { + + const { data, isLoading } = useQuery({ + queryKey: ['calculate-distances', unitId, districtId], + queryFn: () => calculateDistances(unitId, districtId), + }) + + if (isLoading) { + return ( +
+

Distance Information

+ + + +
+ ) + } + + if (!data) { + return ( +
+

Distance Information

+

No distance data available

+
+ ) + } + + if (!data.length) { + return ( +
+

Distance Information

+

No distance data available

+
+ ) + } + + // Group by unit if we're showing multiple units + const unitGroups = !unitId ? + data.reduce((acc, item) => { + if (!acc[item.unit_code]) { + acc[item.unit_code] = { + name: item.unit_name, + incidents: [] + } + } + acc[item.unit_code].incidents.push(item) + return acc + }, {} as Record) : + null + + return ( +
+

Distance Information

+ + {unitId ? ( + // Single unit view +
+

{data[0]?.unit_name || 'Selected Unit'}

+
    + {data.map(item => ( +
  • + {item.category_name} + {formatDistance(item.distance_meters)} +
  • + ))} +
+
+ ) : ( + // Multi-unit view (grouped) +
+ {unitGroups && Object.entries(unitGroups).map(([code, unit]) => ( +
+

{unit.name}

+
    + {unit.incidents.slice(0, 3).map(item => ( +
  • + {item.category_name} + {formatDistance(item.distance_meters)} +
  • + ))} + {unit.incidents.length > 3 && ( +
  • + + {unit.incidents.length - 3} more incidents +
  • + )} +
+
+ ))} +
+ )} +
+ ) +} diff --git a/sigap-website/app/_components/map/pop-up/district-popup.tsx b/sigap-website/app/_components/map/pop-up/district-popup.tsx index 3560cc6..2de1015 100644 --- a/sigap-website/app/_components/map/pop-up/district-popup.tsx +++ b/sigap-website/app/_components/map/pop-up/district-popup.tsx @@ -8,7 +8,7 @@ import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/app/_components/ui/t import { Button } from "@/app/_components/ui/button" import { getMonthName } from "@/app/_utils/common" import { BarChart, Users, Home, AlertTriangle, ChevronRight, Building, Calendar, X } from 'lucide-react' -import type { DistrictFeature } from "../layers/district-layer" +import { IDistrictFeature } from "@/app/_utils/types/map" // Helper function to format numbers function formatNumber(num?: number): string { @@ -29,7 +29,7 @@ interface DistrictPopupProps { longitude: number latitude: number onClose: () => void - district: DistrictFeature + district: IDistrictFeature year?: string month?: string filterCategory?: string | "all" diff --git a/sigap-website/app/_components/map/pop-up/incident-popup.tsx b/sigap-website/app/_components/map/pop-up/incident-popup.tsx new file mode 100644 index 0000000..e12caf0 --- /dev/null +++ b/sigap-website/app/_components/map/pop-up/incident-popup.tsx @@ -0,0 +1,180 @@ +"use client" + +import { Popup } from "react-map-gl/mapbox" +import { Badge } from "@/app/_components/ui/badge" +import { Card } from "@/app/_components/ui/card" +import { Separator } from "@/app/_components/ui/separator" +import { Button } from "@/app/_components/ui/button" +import { MapPin, AlertTriangle, Calendar, Clock, Bookmark, Navigation, X, FileText } from "lucide-react" +import { IDistanceResult } from "@/app/_utils/types/crimes" +import { ScrollArea } from "@/app/_components/ui/scroll-area" +import { Skeleton } from "@/app/_components/ui/skeleton" + +interface IncidentPopupProps { + longitude: number + latitude: number + onClose: () => void + incident: { + id: string + category?: string + description?: string + date?: Date | string + district?: string + district_id?: string + } + distances?: IDistanceResult[] + isLoadingDistances?: boolean +} + +export default function IncidentPopup({ + longitude, + latitude, + onClose, + incident, + distances = [], + isLoadingDistances = false +}: IncidentPopupProps) { + + const formatDate = (date?: Date | string) => { + if (!date) return "Unknown date" + return new Date(date).toLocaleDateString() + } + + const formatTime = (date?: Date | string) => { + if (!date) return "Unknown time" + return new Date(date).toLocaleTimeString() + } + + // Format distance to be more readable + const formatDistance = (meters: number) => { + if (meters < 1000) { + return `${meters.toFixed(0)} m`; + } else { + return `${(meters / 1000).toFixed(2)} km`; + } + } + + return ( + + +
+ {/* Custom close button */} + + +
+

+ + {incident.category || "Unknown Incident"} +

+
+ + {incident.description && ( +
+

+ + {incident.description} +

+
+ )} + + + +
+ {incident.district && ( +
+

District

+

+ + {incident.district} +

+
+ )} + + {incident.date && ( + <> +
+

Date

+

+ + {formatDate(incident.date)} +

+
+
+

Time

+

+ + {formatTime(incident.date)} +

+
+ + )} +
+ + {/* Distances to police units section */} + + +
+

Nearby Police Units

+ + {isLoadingDistances ? ( +
+ + + +
+ ) : distances.length > 0 ? ( + +
+ {distances.map((item) => ( +
+
+

{item.unit_name || "Unknown Unit"}

+

+ {item.unit_type || "Police Unit"} +

+
+ + {formatDistance(item.distance_meters)} + +
+ ))} +
+
+ ) : ( +

+ No police units data available +

+ )} +
+ +
+

+ + Coordinates: {latitude.toFixed(6)}, {longitude.toFixed(6)} +

+

ID: {incident.id}

+
+
+
+
+ ) +} diff --git a/sigap-website/app/_components/map/pop-up/unit-popup.tsx b/sigap-website/app/_components/map/pop-up/unit-popup.tsx new file mode 100644 index 0000000..58297ad --- /dev/null +++ b/sigap-website/app/_components/map/pop-up/unit-popup.tsx @@ -0,0 +1,167 @@ +"use client" + +import { Popup } from "react-map-gl/mapbox" +import { Badge } from "@/app/_components/ui/badge" +import { Card } from "@/app/_components/ui/card" +import { Separator } from "@/app/_components/ui/separator" +import { Button } from "@/app/_components/ui/button" +import { Phone, Building, MapPin, Navigation, X, Shield, Compass, Map, Building2 } from "lucide-react" +import { IDistanceResult } from "@/app/_utils/types/crimes" +import { ScrollArea } from "@/app/_components/ui/scroll-area" +import { Skeleton } from "@/app/_components/ui/skeleton" + +interface UnitPopupProps { + longitude: number + latitude: number + onClose: () => void + unit: { + id: string + name: string + type?: string + address?: string + phone?: string + district?: string + district_id?: string + } + distances?: IDistanceResult[] + isLoadingDistances?: boolean +} + +export default function UnitPopup({ + longitude, + latitude, + onClose, + unit, + distances = [], + isLoadingDistances = false +}: UnitPopupProps) { + + // Format distance to be more readable + const formatDistance = (meters: number) => { + if (meters < 1000) { + return `${meters.toFixed(0)} m`; + } else { + return `${(meters / 1000).toFixed(2)} km`; + } + } + + return ( + + +
+ {/* Custom close button */} + + +
+

+ + {unit.name || "Police Unit"} +

+ + {unit.type || "Unit"} + +
+ +
+ {unit.address && ( +
+

Address

+

+ + {unit.address} +

+
+ )} + + {unit.phone && ( +
+

Contact

+

+ + {unit.phone} +

+
+ )} + + {unit.district && ( +
+

District

+

+ + {unit.district} +

+
+ )} +
+ + {/* Distances to incidents section */} + + +
+

+ + Nearby Incidents +

+ + {isLoadingDistances ? ( +
+ + + +
+ ) : distances.length > 0 ? ( + +
+ {distances.map((item) => ( +
+
+

{item.category_name || "Unknown"}

+

+ {item.incident_description || "No description"} +

+
+ + {formatDistance(item.distance_meters)} + +
+ ))} +
+
+ ) : ( +

+ No incident data available +

+ )} +
+ +
+

+ + Coordinates: {latitude.toFixed(6)}, {longitude.toFixed(6)} +

+

ID: {unit.id}

+
+
+
+
+ ) +} diff --git a/sigap-website/app/_styles/globals.css b/sigap-website/app/_styles/globals.css index c8088e0..83f62e1 100644 --- a/sigap-website/app/_styles/globals.css +++ b/sigap-website/app/_styles/globals.css @@ -178,4 +178,6 @@ background-position: center; pointer-events: auto; background-image: url(data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiIHN0YW5kYWxvbmU9Im5vIj8+PHN2ZyAgIHhtbG5zOmRjPSJodHRwOi8vcHVybC5vcmcvZGMvZWxlbWVudHMvMS4xLyIgICB4bWxuczpjYz0iaHR0cDovL2NyZWF0aXZlY29tbW9ucy5vcmcvbnMjIiAgIHhtbG5zOnJkZj0iaHR0cDovL3d3dy53My5vcmcvMTk5OS8wMi8yMi1yZGYtc3ludGF4LW5zIyIgICB4bWxuczpzdmc9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiAgIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgICB4bWxuczpzb2RpcG9kaT0iaHR0cDovL3NvZGlwb2RpLnNvdXJjZWZvcmdlLm5ldC9EVEQvc29kaXBvZGktMC5kdGQiICAgeG1sbnM6aW5rc2NhcGU9Imh0dHA6Ly93d3cuaW5rc2NhcGUub3JnL25hbWVzcGFjZXMvaW5rc2NhcGUiICAgd2lkdGg9IjIwIiAgIGhlaWdodD0iMjAiICAgdmlld0JveD0iMCAwIDIwIDIwIiAgIGlkPSJzdmcxOTE2NyIgICB2ZXJzaW9uPSIxLjEiICAgaW5rc2NhcGU6dmVyc2lvbj0iMC45MStkZXZlbCtvc3htZW51IHIxMjkxMSIgICBzb2RpcG9kaTpkb2NuYW1lPSJtYXJrZXIuc3ZnIj4gIDxkZWZzICAgICBpZD0iZGVmczE5MTY5IiAvPiAgPHNvZGlwb2RpOm5hbWVkdmlldyAgICAgaWQ9ImJhc2UiICAgICBwYWdlY29sb3I9IiNmZmZmZmYiICAgICBib3JkZXJjb2xvcj0iIzY2NjY2NiIgICAgIGJvcmRlcm9wYWNpdHk9IjEuMCIgICAgIGlua3NjYXBlOnBhZ2VvcGFjaXR5PSIwLjAiICAgICBpbmtzY2FwZTpwYWdlc2hhZG93PSIyIiAgICAgaW5rc2NhcGU6em9vbT0iMTYiICAgICBpbmtzY2FwZTpjeD0iMTQuMTY0MjUzIiAgICAgaW5rc2NhcGU6Y3k9IjguODg1NzIiICAgICBpbmtzY2FwZTpkb2N1bWVudC11bml0cz0icHgiICAgICBpbmtzY2FwZTpjdXJyZW50LWxheWVyPSJsYXllcjEiICAgICBzaG93Z3JpZD0iZmFsc2UiICAgICB1bml0cz0icHgiICAgICBpbmtzY2FwZTp3aW5kb3ctd2lkdGg9IjEyODAiICAgICBpbmtzY2FwZTp3aW5kb3ctaGVpZ2h0PSI3NTEiICAgICBpbmtzY2FwZTp3aW5kb3cteD0iMjA4IiAgICAgaW5rc2NhcGU6d2luZG93LXk9IjE5MCIgICAgIGlua3NjYXBlOndpbmRvdy1tYXhpbWl6ZWQ9IjAiICAgICBpbmtzY2FwZTpvYmplY3Qtbm9kZXM9InRydWUiPiAgICA8aW5rc2NhcGU6Z3JpZCAgICAgICB0eXBlPSJ4eWdyaWQiICAgICAgIGlkPSJncmlkMTk3MTUiIC8+ICA8L3NvZGlwb2RpOm5hbWVkdmlldz4gIDxtZXRhZGF0YSAgICAgaWQ9Im1ldGFkYXRhMTkxNzIiPiAgICA8cmRmOlJERj4gICAgICA8Y2M6V29yayAgICAgICAgIHJkZjphYm91dD0iIj4gICAgICAgIDxkYzpmb3JtYXQ+aW1hZ2Uvc3ZnK3htbDwvZGM6Zm9ybWF0PiAgICAgICAgPGRjOnR5cGUgICAgICAgICAgIHJkZjpyZXNvdXJjZT0iaHR0cDovL3B1cmwub3JnL2RjL2RjbWl0eXBlL1N0aWxsSW1hZ2UiIC8+ICAgICAgICA8ZGM6dGl0bGUgLz4gICAgICA8L2NjOldvcms+ICAgIDwvcmRmOlJERj4gIDwvbWV0YWRhdGE+ICA8ZyAgICAgaW5rc2NhcGU6bGFiZWw9IkxheWVyIDEiICAgICBpbmtzY2FwZTpncm91cG1vZGU9ImxheWVyIiAgICAgaWQ9ImxheWVyMSIgICAgIHRyYW5zZm9ybT0idHJhbnNsYXRlKDAsLTEwMzIuMzYyMikiPiAgICA8cGF0aCAgICAgICBzdHlsZT0iY29sb3I6IzAwMDAwMDtjbGlwLXJ1bGU6bm9uemVybztkaXNwbGF5OmlubGluZTtvdmVyZmxvdzp2aXNpYmxlO3Zpc2liaWxpdHk6dmlzaWJsZTtvcGFjaXR5OjE7aXNvbGF0aW9uOmF1dG87bWl4LWJsZW5kLW1vZGU6bm9ybWFsO2NvbG9yLWludGVycG9sYXRpb246c1JHQjtjb2xvci1pbnRlcnBvbGF0aW9uLWZpbHRlcnM6bGluZWFyUkdCO3NvbGlkLWNvbG9yOiMwMDAwMDA7c29saWQtb3BhY2l0eToxO2ZpbGw6IzAwMDAwMDtmaWxsLW9wYWNpdHk6MTtmaWxsLXJ1bGU6ZXZlbm9kZDtzdHJva2U6bm9uZTtzdHJva2Utd2lkdGg6MjtzdHJva2UtbGluZWNhcDpyb3VuZDtzdHJva2UtbGluZWpvaW46cm91bmQ7c3Ryb2tlLW1pdGVybGltaXQ6NDtzdHJva2UtZGFzaGFycmF5Om5vbmU7c3Ryb2tlLWRhc2hvZmZzZXQ6MDtzdHJva2Utb3BhY2l0eToxO21hcmtlcjpub25lO2NvbG9yLXJlbmRlcmluZzphdXRvO2ltYWdlLXJlbmRlcmluZzphdXRvO3NoYXBlLXJlbmRlcmluZzphdXRvO3RleHQtcmVuZGVyaW5nOmF1dG87ZW5hYmxlLWJhY2tncm91bmQ6YWNjdW11bGF0ZSIgICAgICAgZD0ibSAzNiwxMDQwLjM2MjIgYyA2ZS02LDMuMzA5MyAtNS45ODg2MTIsMTAgLTUuOTg4NjEyLDEwIDAsMCAtNS45OTg3NzYsLTYuNjY4IC02LjAxMTM0NSwtOS45NzcyIC0wLjAxMjU3LC0zLjMwOTIgMi42NTY1NzYsLTYuMDAzOSA1Ljk2NTc5MiwtNi4wMjI3IDMuMzA5MTg5LC0wLjAxOSA2LjAwODg0LDIuNjQ1MiA2LjAzMzk5Miw1Ljk1NDMiICAgICAgIGlkPSJwYXRoMTI1NjEiICAgICAgIGlua3NjYXBlOmNvbm5lY3Rvci1jdXJ2YXR1cmU9IjAiICAgICAgIHNvZGlwb2RpOm5vZGV0eXBlcz0iY2Nzc2MiIC8+ICAgIDxwYXRoICAgICAgIHN0eWxlPSJjb2xvcjojMDAwMDAwO2NsaXAtcnVsZTpub256ZXJvO2Rpc3BsYXk6aW5saW5lO292ZXJmbG93OnZpc2libGU7dmlzaWJpbGl0eTp2aXNpYmxlO29wYWNpdHk6MTtpc29sYXRpb246YXV0bzttaXgtYmxlbmQtbW9kZTpub3JtYWw7Y29sb3ItaW50ZXJwb2xhdGlvbjpzUkdCO2NvbG9yLWludGVycG9sYXRpb24tZmlsdGVyczpsaW5lYXJSR0I7c29saWQtY29sb3I6IzAwMDAwMDtzb2xpZC1vcGFjaXR5OjE7ZmlsbDojZmZmZmZmO2ZpbGwtb3BhY2l0eToxO2ZpbGwtcnVsZTpldmVub2RkO3N0cm9rZTpub25lO3N0cm9rZS13aWR0aDoyO3N0cm9rZS1saW5lY2FwOnJvdW5kO3N0cm9rZS1saW5lam9pbjpyb3VuZDtzdHJva2UtbWl0ZXJsaW1pdDo0O3N0cm9rZS1kYXNoYXJyYXk6bm9uZTtzdHJva2UtZGFzaG9mZnNldDowO3N0cm9rZS1vcGFjaXR5OjE7bWFya2VyOm5vbmU7Y29sb3ItcmVuZGVyaW5nOmF1dG87aW1hZ2UtcmVuZGVyaW5nOmF1dG87c2hhcGUtcmVuZGVyaW5nOmF1dG87dGV4dC1yZW5kZXJpbmc6YXV0bztlbmFibGUtYmFja2dyb3VuZDphY2N1bXVsYXRlIiAgICAgICBkPSJtIDM0LjAwMDExNSwxMDQwLjM2MjIgYyAtNWUtNiwyLjIwNjIgLTMuOTkyNTIzLDcuMDAwMSAtMy45OTI1MjMsNy4wMDAxIDAsMCAtMy45OTkyOTEsLTQuNzc4NyAtNC4wMDc2NzksLTYuOTg0OSAtMC4wMDg0LC0yLjIwNjIgMS43NzEwODIsLTQuMDAyNyAzLjk3NzMxLC00LjAxNTMgMi4yMDYyMSwtMC4wMTMgNC4wMDYwMzcsMS43NjM1IDQuMDIyNzc3LDMuOTY5NyIgICAgICAgaWQ9InBhdGgxMjU2MyIgICAgICAgaW5rc2NhcGU6Y29ubmVjdG9yLWN1cnZhdHVyZT0iMCIgICAgICAgc29kaXBvZGk6bm9kZXR5cGVzPSJjY2NzYyIgLz4gICAgPHBhdGggICAgICAgc3R5bGU9ImNvbG9yOiMwMDAwMDA7Y2xpcC1ydWxlOm5vbnplcm87ZGlzcGxheTppbmxpbmU7b3ZlcmZsb3c6dmlzaWJsZTt2aXNpYmlsaXR5OnZpc2libGU7b3BhY2l0eToxO2lzb2xhdGlvbjphdXRvO21peC1ibGVuZC1tb2RlOm5vcm1hbDtjb2xvci1pbnRlcnBvbGF0aW9uOnNSR0I7Y29sb3ItaW50ZXJwb2xhdGlvbi1maWx0ZXJzOmxpbmVhclJHQjtzb2xpZC1jb2xvcjojMDAwMDAwO3NvbGlkLW9wYWNpdHk6MTtmaWxsOiMwMDAwMDA7ZmlsbC1vcGFjaXR5OjE7ZmlsbC1ydWxlOmV2ZW5vZGQ7c3Ryb2tlOm5vbmU7c3Ryb2tlLXdpZHRoOjI7c3Ryb2tlLWxpbmVjYXA6cm91bmQ7c3Ryb2tlLWxpbmVqb2luOnJvdW5kO3N0cm9rZS1taXRlcmxpbWl0OjQ7c3Ryb2tlLWRhc2hhcnJheTpub25lO3N0cm9rZS1kYXNob2Zmc2V0OjA7c3Ryb2tlLW9wYWNpdHk6MTttYXJrZXI6bm9uZTtjb2xvci1yZW5kZXJpbmc6YXV0bztpbWFnZS1yZW5kZXJpbmc6YXV0bztzaGFwZS1yZW5kZXJpbmc6YXV0bzt0ZXh0LXJlbmRlcmluZzphdXRvO2VuYWJsZS1iYWNrZ3JvdW5kOmFjY3VtdWxhdGUiICAgICAgIGQ9Ik0gOS45NjY3OTY5LDEwMTQuMzYyMiBDIDYuNjU3NTgwOSwxMDE0LjM4MSAzLjk4NzQzLDEwMTcuMDc2NCA0LDEwMjAuMzg1NiBjIDAuMDEyNTY5LDMuMzA5MiA2LjAxMTcxOSw4Ljk3NjYgNi4wMTE3MTksOC45NzY2IDAsMCA1Ljk4ODI4NywtNS42OTA3IDUuOTg4MjgxLC05IGwgMCwtMC4wNDUgYyAtMC4wMjUxNSwtMy4zMDkxIC0yLjcyNDAxNCwtNS45NzQxIC02LjAzMzIwMzEsLTUuOTU1MSB6IG0gMC4wMDk3NywyIGMgMi4yMDYyMDYxLC0wLjAxMyA0LjAwNjY5MzEsMS43NjI2IDQuMDIzNDMzMSwzLjk2ODggbCAwLDAuMDMxIGMgLTVlLTYsMi4yMDYyIC0zLjk5MjE4OCw2IC0zLjk5MjE4OCw2IDAsMCAtMy45OTk0MjQsLTMuNzc4MiAtNC4wMDc4MTIsLTUuOTg0NCAtMC4wMDg0LC0yLjIwNjIgMS43NzAzMzQ1LC00LjAwMyAzLjk3NjU2MjUsLTQuMDE1NiB6IiAgICAgICBpZD0icGF0aDEyNTY4IiAgICAgICBpbmtzY2FwZTpjb25uZWN0b3ItY3VydmF0dXJlPSIwIiAgICAgICBzb2RpcG9kaTpub2RldHlwZXM9ImNzY3NjY2Njc2NzYyIgLz4gICAgPHBhdGggICAgICAgc3R5bGU9Im9wYWNpdHk6MTtmaWxsOiMwMDAwMDA7ZmlsbC1vcGFjaXR5OjE7c3Ryb2tlOm5vbmU7c3Ryb2tlLXdpZHRoOjI7c3Ryb2tlLWxpbmVjYXA6YnV0dDtzdHJva2UtbGluZWpvaW46YmV2ZWw7c3Ryb2tlLW1pdGVybGltaXQ6NDtzdHJva2UtZGFzaGFycmF5Om5vbmU7c3Ryb2tlLWRhc2hvZmZzZXQ6MDtzdHJva2Utb3BhY2l0eToxO21hcmtlcjpub25lIiAgICAgICBkPSJNIDEwIDIgQyA2LjY4NjI5MiAyIDQgNC42ODYzIDQgOCBDIDQgMTEuMzEzNyAxMCAxNyAxMCAxNyBDIDEwIDE3IDE2IDExLjMxMzcgMTYgOCBDIDE2IDQuNjg2MyAxMy4zMTM3MDggMiAxMCAyIHogTSAxMCA0IEMgMTIuMDcxMDY4IDQgMTMuNzUgNS42Nzg5IDEzLjc1IDcuNzUgQyAxMy43NSA5LjIwNTMyNzggMTEuOTMxMTEgMTEuNjQ0MzkzIDEwLjgzMDA3OCAxMyBMIDkuMTY5OTIxOSAxMyBDIDguMDY4ODkwMyAxMS42NDQzOTMgNi4yNSA5LjIwNTMyNzggNi4yNSA3Ljc1IEMgNi4yNSA1LjY3ODkgNy45Mjg5MzIgNCAxMCA0IHogIiAgICAgICB0cmFuc2Zvcm09InRyYW5zbGF0ZSgwLDEwMzIuMzYyMikiICAgICAgIGlkPSJwYXRoMTczMDUiIC8+ICA8L2c+PC9zdmc+); -} \ No newline at end of file +} + + diff --git a/sigap-website/app/_utils/common.ts b/sigap-website/app/_utils/common.ts index 05fae91..987a19d 100644 --- a/sigap-website/app/_utils/common.ts +++ b/sigap-website/app/_utils/common.ts @@ -15,9 +15,11 @@ import { districtsGeoJson } from '../../prisma/data/geojson/jember/districts'; // Used to track generated IDs const usedIdRegistry = new Set(); -// Add type definition for global counter + +// Add type definition for global counter and registry declare global { var __idCounter: number; + var __idRegistry: Record; } /** @@ -392,30 +394,12 @@ function formatDateV2(date: Date, formatStr: string): string { * @param {boolean} options.upperCase - Convert result to uppercase * @returns {string} - Generated custom ID */ -/** - * Universal Custom ID Generator - * Creates structured, readable IDs for any system or entity - * - * @param {Object} options - Configuration options - * @param {string} options.prefix - Primary identifier prefix (e.g., "CRIME", "USER", "INVOICE") - * @param {Object} options.segments - Collection of ID segments to include - * @param {string[]} options.segments.codes - Array of short codes (e.g., region codes, department codes) - * @param {number} options.segments.year - Year to include in the ID - * @param {number} options.segments.sequentialDigits - Number of digits for sequential number - * @param {boolean} options.segments.includeDate - Whether to include current date - * @param {string} options.segments.dateFormat - Format for date (e.g., "yyyy-MM-dd", "dd/MM/yyyy") - * @param {boolean} options.segments.includeTime - Whether to include timestamp - * @param {string} options.format - Custom format string for ID structure - * @param {string} options.separator - Character to separate ID components - * @param {boolean} options.upperCase - Convert result to uppercase - * @returns {string} - Generated custom ID - */ -export function generateId( +export async function generateId( options: { prefix?: string; segments?: { codes?: string[]; - year?: number | boolean; // Year diubah menjadi number | boolean + year?: number | boolean; sequentialDigits?: number; includeDate?: boolean; dateFormat?: string; @@ -429,10 +413,10 @@ export function generateId( uniquenessStrategy?: 'uuid' | 'timestamp' | 'counter' | 'hash'; retryOnCollision?: boolean; maxRetries?: number; + storage?: 'memory' | 'localStorage' | 'database'; + tableName?: string; // Added table name for database interactions } = {} -): string { - // Jika uniquenessStrategy tidak diatur dan randomSequence = false, - // gunakan counter sebagai strategi default +): Promise { if (!options.uniquenessStrategy && options.randomSequence === false) { options.uniquenessStrategy = 'counter'; } @@ -441,7 +425,7 @@ export function generateId( prefix: options.prefix || 'ID', segments: { codes: options.segments?.codes || [], - year: options.segments?.year, // Akan diproses secara kondisional nanti + year: options.segments?.year, sequentialDigits: options.segments?.sequentialDigits || 6, includeDate: options.segments?.includeDate ?? false, dateFormat: options.segments?.dateFormat || 'yyyyMMdd', @@ -455,22 +439,21 @@ export function generateId( uniquenessStrategy: options.uniquenessStrategy || 'timestamp', retryOnCollision: options.retryOnCollision ?? true, maxRetries: options.maxRetries || 10, + storage: options.storage || 'memory', + tableName: options.tableName }; - // Initialize global counter if not exists if (typeof globalThis.__idCounter === 'undefined') { globalThis.__idCounter = 0; } const now = new Date(); - // Generate date string if needed let dateString = ''; if (config.segments.includeDate) { dateString = format(now, config.segments.dateFormat); } - // Generate time string if needed let timeString = ''; if (config.segments.includeTime) { timeString = format(now, 'HHmmss'); @@ -479,7 +462,6 @@ export function generateId( } } - // Generate sequential number based on uniqueness strategy let sequentialNum: string; try { switch (config.uniquenessStrategy) { @@ -491,9 +473,34 @@ export function generateId( sequentialNum = sequentialNum.slice(-config.segments.sequentialDigits); break; case 'counter': - sequentialNum = (++globalThis.__idCounter) - .toString() - .padStart(config.segments.sequentialDigits, '0'); + const lastId = await getLastId(config.prefix, { + separator: config.separator, + storage: config.storage, + tableName: config.tableName + }); + + let counterStart = 0; + + if (lastId !== null) { + const parts = lastId.split(config.separator); + const lastPart = parts[parts.length - 1]; + + if (/^\d+$/.test(lastPart)) { + counterStart = parseInt(lastPart, 10); + } else { + for (let i = parts.length - 1; i >= 0; i--) { + if (/^\d+$/.test(parts[i])) { + counterStart = parseInt(parts[i], 10); + break; + } + } + } + } + + const nextCounter = counterStart + 1; + globalThis.__idCounter = nextCounter; + + sequentialNum = nextCounter.toString().padStart(config.segments.sequentialDigits, '0'); break; case 'hash': const hashSource = `${now.getTime()}-${JSON.stringify(options)}-${Math.random()}`; @@ -513,39 +520,59 @@ export function generateId( .toString() .padStart(config.segments.sequentialDigits, '0'); } else { - sequentialNum = (++globalThis.__idCounter) - .toString() - .padStart(config.segments.sequentialDigits, '0'); + const lastId = await getLastId(config.prefix, { + separator: config.separator, + storage: config.storage, + tableName: config.tableName + }); + + let counterStart = 0; + + if (lastId !== null) { + const parts = lastId.split(config.separator); + const lastPart = parts[parts.length - 1]; + + if (/^\d+$/.test(lastPart)) { + counterStart = parseInt(lastPart, 10); + } else { + for (let i = parts.length - 1; i >= 0; i--) { + if (/^\d+$/.test(parts[i])) { + counterStart = parseInt(parts[i], 10); + break; + } + } + } + } + + const nextCounter = counterStart + 1; + globalThis.__idCounter = nextCounter; + + sequentialNum = nextCounter.toString().padStart(config.segments.sequentialDigits, '0'); } } } catch (error) { console.error('Error generating sequential number:', error); - // Fallback to timestamp strategy if other methods fail sequentialNum = `${now.getTime()}`.slice(-config.segments.sequentialDigits); } - // Determine if year should be included and what value to use let yearValue = null; - if (config.segments.year !== undefined || config.segments.year != false) { + if (config.segments.year !== undefined && config.segments.year !== false) { if (typeof config.segments.year === 'number') { yearValue = String(config.segments.year); } else if (config.segments.year === true) { yearValue = format(now, 'yyyy'); } - // if year is false, yearValue remains null and won't be included } else { - // Default behavior (backward compatibility) yearValue = format(now, 'yyyy'); } - // Prepare components for ID assembly const components = { prefix: config.prefix, codes: config.segments.codes.length > 0 ? config.segments.codes.join(config.separator) : '', - year: yearValue, // Added the year value to components + year: yearValue, sequence: sequentialNum, date: dateString, time: timeString, @@ -553,7 +580,6 @@ export function generateId( let result: string; - // Use custom format if provided if (config.format) { let customID = config.format; for (const [key, value] of Object.entries(components)) { @@ -565,10 +591,8 @@ export function generateId( ); } } - // Remove unused placeholders customID = customID.replace(/{[^}]+}/g, ''); - // Clean up separators const escapedSeparator = config.separator.replace( /[-\/\\^$*+?.()|[\]{}]/g, '\\$&' @@ -582,7 +606,6 @@ export function generateId( result = config.upperCase ? customID.toUpperCase() : customID; } else { - // Assemble ID from parts const parts = []; if (components.prefix) parts.push(components.prefix); if (components.codes) parts.push(components.codes); @@ -595,7 +618,6 @@ export function generateId( if (config.upperCase) result = result.toUpperCase(); } - // Handle collisions if required if (config.retryOnCollision) { let retryCount = 0; let originalResult = result; @@ -607,7 +629,6 @@ export function generateId( result = `${originalResult}${config.separator}${suffix}`; } catch (error) { console.error('Error generating collision suffix:', error); - // Simple fallback if crypto fails result = `${originalResult}${config.separator}${Date.now().toString(36)}`; } } @@ -619,7 +640,6 @@ export function generateId( } } - // Register the ID and maintain registry size usedIdRegistry.add(result); if (usedIdRegistry.size > 10000) { const entriesToKeep = Array.from(usedIdRegistry).slice(-5000); @@ -627,31 +647,161 @@ export function generateId( entriesToKeep.forEach((id) => usedIdRegistry.add(id)); } + await updateLastId(config.prefix, result, { + storage: config.storage, + tableName: config.tableName + }); + return result.trim(); } + /** - * Gets the last ID from a specified table and column. - * @param tableName - The name of the table to query. - * @param columnName - The column containing the IDs. - * @returns The last ID as a string, or null if no records exist. + * Retrieves the last generated ID for a specific prefix + * Used by generateId to determine the next sequential number + * + * @param {string} prefix - The prefix to look up (e.g., "INVOICE", "USER") + * @param {Object} options - Additional options + * @param {string} options.separator - The separator used in IDs (must match generateId) + * @param {boolean} options.extractFullSequence - Whether to extract the full sequence or just the numeric part + * @param {string} options.storage - Storage method ('memory', 'localStorage', 'database') + * @returns {string|null} - Returns the last ID or null if none exists */ export async function getLastId( - tableName: string, - columnName: string + prefix: string, + options: { + separator?: string; + extractFullSequence?: boolean; + storage?: 'memory' | 'localStorage' | 'database'; + tableName?: string; + } = {} ): Promise { - try { - const result = await db.$queryRawUnsafe( - `SELECT ${columnName} FROM ${tableName} ORDER BY ${columnName} DESC LIMIT 1` - ); + const config = { + separator: options.separator || '-', + extractFullSequence: options.extractFullSequence ?? false, + storage: options.storage || 'memory', + tableName: options.tableName + }; - if (Array.isArray(result) && result.length > 0) { - return result[0][columnName]; - } - } catch (error) { - console.error('Error fetching last ID:', error); + if (typeof globalThis.__idRegistry === 'undefined') { + globalThis.__idRegistry = {}; } - return null; + const normalizedPrefix = prefix.toUpperCase(); + + let lastId: string | null = null; + + switch (config.storage) { + case 'localStorage': + try { + const storedIds = localStorage.getItem('customIdRegistry'); + if (storedIds) { + const registry = JSON.parse(storedIds); + lastId = registry[normalizedPrefix] || null; + } + } catch (error) { + console.error('Error accessing localStorage:', error); + lastId = globalThis.__idRegistry[normalizedPrefix] || null; + } + break; + + case 'database': + if (!config.tableName) { + console.warn('Table name not provided for database storage. Falling back to memory storage.'); + lastId = globalThis.__idRegistry[normalizedPrefix] || null; + break; + } + + try { + const result = await db.$queryRawUnsafe<{id: string}[]>( + `SELECT id FROM ${config.tableName} + WHERE id LIKE $1 + ORDER BY id DESC + LIMIT 1`, + `${normalizedPrefix}%` + ); + + if (result && result.length > 0) { + lastId = result[0].id; + } + } catch (error) { + console.error(`Error querying database for last ID in ${config.tableName}:`, error); + lastId = globalThis.__idRegistry[normalizedPrefix] || null; + } + break; + + case 'memory': + default: + lastId = globalThis.__idRegistry[normalizedPrefix] || null; + break; + } + + return lastId; +} + +/** + * Updates the last ID record for a specific prefix + * Should be called by generateId after creating a new ID + * + * @param {string} prefix - The prefix to update + * @param {string} id - The newly generated ID to store + * @param {Object} options - Additional options (matching getLastId options) + */ +export async function updateLastId( + prefix: string, + id: string, + options: { + storage?: 'memory' | 'localStorage' | 'database'; + tableName?: string; + } = {} +): Promise { + const config = { + storage: options.storage || 'memory', + tableName: options.tableName + }; + + if (typeof globalThis.__idRegistry === 'undefined') { + globalThis.__idRegistry = {}; + } + + const normalizedPrefix = prefix.toUpperCase(); + + switch (config.storage) { + case 'localStorage': + try { + let registry: Record = {}; + const storedIds = localStorage.getItem('customIdRegistry'); + + if (storedIds) { + registry = JSON.parse(storedIds) as Record; + } + + registry[normalizedPrefix] = id; + localStorage.setItem('customIdRegistry', JSON.stringify(registry)); + + globalThis.__idRegistry[normalizedPrefix] = id; + } catch (error) { + console.error('Error updating localStorage:', error); + globalThis.__idRegistry[normalizedPrefix] = id; + } + break; + + case 'database': + globalThis.__idRegistry[normalizedPrefix] = id; + + if (config.tableName) { + try { + // Optional: Update a dedicated ID tracking table if you have one + } catch (error) { + console.error(`Error updating ID registry in database:`, error); + } + } + break; + + case 'memory': + default: + globalThis.__idRegistry[normalizedPrefix] = id; + break; + } } /** @@ -777,7 +927,6 @@ export const getDistrictName = (districtId: string): string => { export function formatNumber(num?: number): string { if (num === undefined || num === null) return "N/A"; - // If number is in the thousands, abbreviate if (num >= 1_000_000) { return (num / 1_000_000).toFixed(1) + 'M'; } @@ -786,7 +935,6 @@ export function formatNumber(num?: number): string { return (num / 1_000).toFixed(1) + 'K'; } - // Otherwise, format with commas return num.toLocaleString(); } diff --git a/sigap-website/app/_utils/map.ts b/sigap-website/app/_utils/map.ts index e34ef6f..0601df5 100644 --- a/sigap-website/app/_utils/map.ts +++ b/sigap-website/app/_utils/map.ts @@ -250,3 +250,16 @@ export const processDistrictFeature = ( isFocused: true, }; }; + +/** + * Format distance in a human-readable way + * @param meters Distance in meters + * @returns Formatted distance string + */ +export function formatDistance(meters: number): string { + if (meters < 1000) { + return `${Math.round(meters)} m`; + } else { + return `${(meters / 1000).toFixed(1)} km`; + } +} \ No newline at end of file diff --git a/sigap-website/app/_utils/types/crimes.ts b/sigap-website/app/_utils/types/crimes.ts index 904ae1b..da1090d 100644 --- a/sigap-website/app/_utils/types/crimes.ts +++ b/sigap-website/app/_utils/types/crimes.ts @@ -76,3 +76,18 @@ export interface ICrimesByYearAndMonth extends crimes { }; }[]; } + +export interface IDistanceResult { + unit_code: string; + unit_name: string; + unit_type: string; + unit_lat: number; + unit_lng: number; + incident_id: number; + incident_description: string; + incident_lat: number; + incident_lng: number; + category_name: string; + district_name: string; + distance_meters: number; +} \ No newline at end of file diff --git a/sigap-website/package-lock.json b/sigap-website/package-lock.json index 4e8cdf6..81235b1 100644 --- a/sigap-website/package-lock.json +++ b/sigap-website/package-lock.json @@ -60,6 +60,8 @@ "recharts": "^2.15.2", "resend": "^4.1.2", "sonner": "^2.0.1", + "three": "^0.176.0", + "threebox-plugin": "^2.2.7", "uuid": "^11.1.0", "vaul": "^1.1.2", "zod": "^3.24.2", @@ -72,6 +74,7 @@ "@types/node": "^22.10.2", "@types/react": "^19.0.2", "@types/react-dom": "19.0.2", + "@types/three": "^0.176.0", "@types/uuid": "^10.0.0", "postcss": "8.4.49", "prisma": "^6.4.1", @@ -429,6 +432,13 @@ "@jridgewell/sourcemap-codec": "^1.4.10" } }, + "node_modules/@dimforge/rapier3d-compat": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/@dimforge/rapier3d-compat/-/rapier3d-compat-0.12.0.tgz", + "integrity": "sha512-uekIGetywIgopfD97oDL5PfeezkFpNhwlzlaEYNOA0N6ghdsOvh/HYjSMek5Q2O1PYvRSDFcqFVJl4r4ZBwOow==", + "dev": true, + "license": "Apache-2.0" + }, "node_modules/@emnapi/runtime": { "version": "1.3.1", "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.3.1.tgz", @@ -7207,6 +7217,13 @@ "url": "https://opencollective.com/turf" } }, + "node_modules/@tweenjs/tween.js": { + "version": "23.1.3", + "resolved": "https://registry.npmjs.org/@tweenjs/tween.js/-/tween.js-23.1.3.tgz", + "integrity": "sha512-vJmvvwFxYuGnF2axRtPYocag6Clbb5YS7kLL+SO/TeVFzHqDIWrNKYtcsPMibjDx9O+bu+psAy9NKfWklassUA==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/cacheable-request": { "version": "6.0.3", "resolved": "https://registry.npmjs.org/@types/cacheable-request/-/cacheable-request-6.0.3.tgz", @@ -7506,6 +7523,13 @@ "integrity": "sha512-UE7oxhQLLd9gub6JKIAhDq06T0F6FnztwMNRvYgjeQSBeMc1ZG/tA47EwfduvkuQS8apbkM/lpLpWsaCeYsXVg==", "license": "MIT" }, + "node_modules/@types/stats.js": { + "version": "0.17.3", + "resolved": "https://registry.npmjs.org/@types/stats.js/-/stats.js-0.17.3.tgz", + "integrity": "sha512-pXNfAD3KHOdif9EQXZ9deK82HVNaXP5ZIF5RP2QG6OQFNTaY2YIetfrE9t528vEreGQvEPRDDc8muaoYeK0SxQ==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/supercluster": { "version": "7.1.3", "resolved": "https://registry.npmjs.org/@types/supercluster/-/supercluster-7.1.3.tgz", @@ -7524,6 +7548,22 @@ "@types/node": "*" } }, + "node_modules/@types/three": { + "version": "0.176.0", + "resolved": "https://registry.npmjs.org/@types/three/-/three-0.176.0.tgz", + "integrity": "sha512-FwfPXxCqOtP7EdYMagCFePNKoG1AGBDUEVKtluv2BTVRpSt7b+X27xNsirPCTCqY1pGYsPUzaM3jgWP7dXSxlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@dimforge/rapier3d-compat": "^0.12.0", + "@tweenjs/tween.js": "~23.1.3", + "@types/stats.js": "*", + "@types/webxr": "*", + "@webgpu/types": "*", + "fflate": "~0.8.2", + "meshoptimizer": "~0.18.1" + } + }, "node_modules/@types/uuid": { "version": "10.0.0", "resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-10.0.0.tgz", @@ -7531,6 +7571,13 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/webxr": { + "version": "0.5.22", + "resolved": "https://registry.npmjs.org/@types/webxr/-/webxr-0.5.22.tgz", + "integrity": "sha512-Vr6Stjv5jPRqH690f5I5GLjVk8GSsoQSYJ2FVd/3jJF7KaqfwPi3ehfBS96mlQ2kPCwZaX6U0rG2+NGHBKkA/A==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/ws": { "version": "8.5.14", "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.5.14.tgz", @@ -7850,6 +7897,13 @@ "@xtuc/long": "4.2.2" } }, + "node_modules/@webgpu/types": { + "version": "0.1.60", + "resolved": "https://registry.npmjs.org/@webgpu/types/-/types-0.1.60.tgz", + "integrity": "sha512-8B/tdfRFKdrnejqmvq95ogp8tf52oZ51p3f4QD5m5Paey/qlX4Rhhy5Y8tgFMi7Ms70HzcMMw3EQjH/jdhTwlA==", + "dev": true, + "license": "BSD-3-Clause" + }, "node_modules/@xtuc/ieee754": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/@xtuc/ieee754/-/ieee754-1.2.0.tgz", @@ -9960,6 +10014,13 @@ "reusify": "^1.0.4" } }, + "node_modules/fflate": { + "version": "0.8.2", + "resolved": "https://registry.npmjs.org/fflate/-/fflate-0.8.2.tgz", + "integrity": "sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A==", + "dev": true, + "license": "MIT" + }, "node_modules/file-entry-cache": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", @@ -11270,6 +11331,13 @@ "node": ">= 8" } }, + "node_modules/meshoptimizer": { + "version": "0.18.1", + "resolved": "https://registry.npmjs.org/meshoptimizer/-/meshoptimizer-0.18.1.tgz", + "integrity": "sha512-ZhoIoL7TNV4s5B6+rx5mC//fw8/POGyNxS/DZyCJeiZ12ScLfVwRE/GfsxwiTkMYYD5DmK2/JXnEVXqL4rF+Sw==", + "dev": true, + "license": "MIT" + }, "node_modules/micromatch": { "version": "4.0.8", "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", @@ -14824,6 +14892,18 @@ "node": ">=0.8" } }, + "node_modules/three": { + "version": "0.176.0", + "resolved": "https://registry.npmjs.org/three/-/three-0.176.0.tgz", + "integrity": "sha512-PWRKYWQo23ojf9oZSlRGH8K09q7nRSWx6LY/HF/UUrMdYgN9i1e2OwJYHoQjwc6HF/4lvvYLC5YC1X8UJL2ZpA==", + "license": "MIT" + }, + "node_modules/threebox-plugin": { + "version": "2.2.7", + "resolved": "https://registry.npmjs.org/threebox-plugin/-/threebox-plugin-2.2.7.tgz", + "integrity": "sha512-H87Nm4w1PfisHPHzavTGXlwIoJpx2+QU57GooQYIhF51lsg+U5A0KGf3Jrv/HWsLCGOwV2BTnv7UTLfpO1EccQ==", + "license": "MIT" + }, "node_modules/tiny-invariant": { "version": "1.3.3", "resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.3.3.tgz", diff --git a/sigap-website/package.json b/sigap-website/package.json index b346039..f8db003 100644 --- a/sigap-website/package.json +++ b/sigap-website/package.json @@ -66,6 +66,8 @@ "recharts": "^2.15.2", "resend": "^4.1.2", "sonner": "^2.0.1", + "three": "^0.176.0", + "threebox-plugin": "^2.2.7", "uuid": "^11.1.0", "vaul": "^1.1.2", "zod": "^3.24.2", @@ -78,6 +80,7 @@ "@types/node": "^22.10.2", "@types/react": "^19.0.2", "@types/react-dom": "19.0.2", + "@types/three": "^0.176.0", "@types/uuid": "^10.0.0", "postcss": "8.4.49", "prisma": "^6.4.1", diff --git a/sigap-website/prisma/backups/20250421113117_remote_schema.sql b/sigap-website/prisma/backups/20250421113117_remote_schema.sql new file mode 100644 index 0000000..ac69912 --- /dev/null +++ b/sigap-website/prisma/backups/20250421113117_remote_schema.sql @@ -0,0 +1,1559 @@ + + +SET statement_timeout = 0; +SET lock_timeout = 0; +SET idle_in_transaction_session_timeout = 0; +SET client_encoding = 'UTF8'; +SET standard_conforming_strings = on; +SELECT pg_catalog.set_config('search_path', '', false); +SET check_function_bodies = false; +SET xmloption = content; +SET client_min_messages = warning; +SET row_security = off; + + +CREATE SCHEMA IF NOT EXISTS "gis"; + + +ALTER SCHEMA "gis" OWNER TO "postgres"; + + +CREATE EXTENSION IF NOT EXISTS "pgsodium"; + + + + + + +COMMENT ON SCHEMA "public" IS 'standard public schema'; + + + +CREATE EXTENSION IF NOT EXISTS "pg_graphql" WITH SCHEMA "graphql"; + + + + + + +CREATE EXTENSION IF NOT EXISTS "pg_stat_statements" WITH SCHEMA "extensions"; + + + + + + +CREATE EXTENSION IF NOT EXISTS "pgcrypto" WITH SCHEMA "extensions"; + + + + + + +CREATE EXTENSION IF NOT EXISTS "pgjwt" WITH SCHEMA "extensions"; + + + + + + +CREATE EXTENSION IF NOT EXISTS "postgis" WITH SCHEMA "gis"; + + + + + + +CREATE EXTENSION IF NOT EXISTS "supabase_vault" WITH SCHEMA "vault"; + + + + + + +CREATE EXTENSION IF NOT EXISTS "uuid-ossp" WITH SCHEMA "extensions"; + + + + + + +CREATE TYPE "public"."crime_rates" AS ENUM ( + 'low', + 'medium', + 'high', + 'critical' +); + + +-- ALTER TYPE "public"."crime_rates" OWNER TO "prisma"; + + +CREATE TYPE "public"."crime_status" AS ENUM ( + 'open', + 'closed', + 'under_investigation', + 'resolved', + 'unresolved' +); + + +-- ALTER TYPE "public"."crime_status" OWNER TO "prisma"; + + +CREATE TYPE "public"."session_status" AS ENUM ( + 'active', + 'completed' +); + + +-- ALTER TYPE "public"."session_status" OWNER TO "prisma"; + + +CREATE TYPE "public"."status_contact_messages" AS ENUM ( + 'new', + 'read', + 'replied', + 'closed' +); + + +-- ALTER TYPE "public"."status_contact_messages" OWNER TO "prisma"; + + +CREATE TYPE "public"."unit_type" AS ENUM ( + 'polda', + 'polsek', + 'polres', + 'other' +); + + +-- ALTER TYPE "public"."unit_type" OWNER TO "prisma"; + + +CREATE OR REPLACE FUNCTION "gis"."update_land_area"() RETURNS "trigger" + LANGUAGE "plpgsql" + AS $$ +BEGIN + NEW.land_area := ROUND((ST_Area(NEW.geometry::geography) / 1000000.0)::numeric, 2); + RETURN NEW; +END; +$$; + + +ALTER FUNCTION "gis"."update_land_area"() OWNER TO "postgres"; + + +CREATE OR REPLACE FUNCTION "public"."generate_username"("email" "text") RETURNS "text" + LANGUAGE "plpgsql" SECURITY DEFINER + AS $$ +DECLARE + result_username TEXT; + username_base TEXT; + random_number INTEGER; + username_exists BOOLEAN; +BEGIN + -- Extract the part before @ from email + username_base := split_part(email, '@', 1); + + -- Remove any special characters and replace with underscore + username_base := regexp_replace(username_base, '[^a-zA-Z0-9]', '_', 'g'); + + -- Generate a random number between 100 and 9999 + random_number := floor(random() * 9900 + 100)::integer; + + -- Combine the base and random number + result_username := username_base || random_number; + + -- Check if username already exists + LOOP + SELECT EXISTS(SELECT 1 FROM public.profiles WHERE username = result_username) INTO username_exists; + + IF NOT username_exists THEN + EXIT; + END IF; + + -- Generate a different random number + random_number := floor(random() * 9900 + 100)::integer; + result_username := username_base || random_number; + END LOOP; + + RETURN result_username; +END; +$$; + + +ALTER FUNCTION "public"."generate_username"("email" "text") OWNER TO "postgres"; + + +CREATE OR REPLACE FUNCTION "public"."handle_new_user"() RETURNS "trigger" + LANGUAGE "plpgsql" SECURITY DEFINER + AS $$ +DECLARE + role_id UUID; -- Declare a variable to store the fetched role ID +BEGIN + -- Fetch the role ID for 'viewer' from the roles table + SELECT id INTO role_id FROM public.roles WHERE name = 'viewer' LIMIT 1; + + -- Check if the role ID was found + IF role_id IS NULL THEN + RAISE EXCEPTION 'Role not found'; + END IF; + + -- Insert the new user into the public.users table with all available data + INSERT INTO public.users ( + id, + roles_id, + email, + phone, + encrypted_password, + invited_at, + confirmed_at, + email_confirmed_at, + recovery_sent_at, + last_sign_in_at, + app_metadata, + user_metadata, + created_at, + updated_at, + banned_until, + is_anonymous + ) VALUES ( + NEW.id, + role_id, -- Use the dynamically fetched role ID + NEW.email, + NEW.phone, + NEW.encrypted_password, + NEW.invited_at, + NEW.confirmed_at, + NEW.email_confirmed_at, + NEW.recovery_sent_at, + NEW.last_sign_in_at, + NEW.raw_app_meta_data, -- Ensure this matches your actual column name + NEW.raw_user_meta_data, -- Ensure this matches your actual column name + NEW.created_at, + NEW.updated_at, + NEW.banned_until, + NEW.is_anonymous + ); + + -- Create the associated profile record + INSERT INTO public.profiles ( + id, + user_id, + avatar, + username, + first_name, + last_name, + bio, + address, + birth_date + ) VALUES ( + gen_random_uuid(), + NEW.id, + NULL, + public.generate_username(NEW.email), + NULL, + NULL, + NULL, + NULL, + NULL + ); + + RETURN NEW; +END; +$$; + + +ALTER FUNCTION "public"."handle_new_user"() OWNER TO "postgres"; + + +CREATE OR REPLACE FUNCTION "public"."handle_user_delete"() RETURNS "trigger" + LANGUAGE "plpgsql" SECURITY DEFINER + AS $$BEGIN + -- Delete the profile record first due to foreign key constraints + DELETE FROM public.profiles + WHERE user_id = OLD.id; + + -- Delete the user record + DELETE FROM public.users + WHERE id = OLD.id; + + RETURN OLD; +END;$$; + + +ALTER FUNCTION "public"."handle_user_delete"() OWNER TO "postgres"; + + +CREATE OR REPLACE FUNCTION "public"."handle_user_update"() RETURNS "trigger" + LANGUAGE "plpgsql" SECURITY DEFINER + AS $$BEGIN + -- Update the public.users table with the latest data + UPDATE public.users + SET + email = COALESCE(NEW.email, email), + phone = COALESCE(NEW.phone, phone), + encrypted_password = COALESCE(NEW.encrypted_password, encrypted_password), + invited_at = COALESCE(NEW.invited_at, invited_at), + confirmed_at = COALESCE(NEW.confirmed_at, confirmed_at), + email_confirmed_at = COALESCE(NEW.email_confirmed_at, email_confirmed_at), + recovery_sent_at = COALESCE(NEW.recovery_sent_at, recovery_sent_at), + last_sign_in_at = COALESCE(NEW.last_sign_in_at, last_sign_in_at), + app_metadata = COALESCE(NEW.raw_app_meta_data, app_metadata), + user_metadata = COALESCE(NEW.raw_user_meta_data, user_metadata), + created_at = COALESCE(NEW.created_at, created_at), + updated_at = NOW(), + banned_until = CASE + WHEN NEW.banned_until IS NULL THEN NULL + ELSE COALESCE(NEW.banned_until, banned_until) + END, + is_anonymous = COALESCE(NEW.is_anonymous, is_anonymous) + WHERE id = NEW.id; + + -- Create profile record if it doesn't exist + INSERT INTO public.profiles (id, user_id) + SELECT gen_random_uuid(), NEW.id + WHERE NOT EXISTS ( + SELECT 1 FROM public.profiles WHERE user_id = NEW.id + ) + ON CONFLICT (user_id) DO NOTHING; + + RETURN NEW; +END;$$; + + +ALTER FUNCTION "public"."handle_user_update"() OWNER TO "postgres"; + + +CREATE OR REPLACE FUNCTION "public"."update_land_area"() RETURNS "trigger" + LANGUAGE "plpgsql" + AS $$ +BEGIN + NEW.land_area := ROUND(ST_Area(NEW.geometry::gis.geography) / 1000000.0); + RETURN NEW; +END; +$$; + + +ALTER FUNCTION "public"."update_land_area"() OWNER TO "postgres"; + + +CREATE OR REPLACE FUNCTION "public"."update_timestamp"() RETURNS "trigger" + LANGUAGE "plpgsql" + AS $$ +BEGIN + NEW.updated_at = CURRENT_TIMESTAMP; + RETURN NEW; +END; +$$; + + +ALTER FUNCTION "public"."update_timestamp"() OWNER TO "postgres"; + +SET default_tablespace = ''; + +SET default_table_access_method = "heap"; + + +CREATE TABLE IF NOT EXISTS "public"."cities" ( + "id" character varying(20) NOT NULL, + "name" character varying(100) NOT NULL, + "created_at" timestamp(6) with time zone DEFAULT CURRENT_TIMESTAMP, + "updated_at" timestamp(6) with time zone DEFAULT CURRENT_TIMESTAMP +); + + +-- ALTER TABLE "public"."cities" OWNER TO "prisma"; + + +CREATE TABLE IF NOT EXISTS "public"."contact_messages" ( + "id" "uuid" DEFAULT "gen_random_uuid"() NOT NULL, + "name" character varying(255), + "email" character varying(255), + "phone" character varying(20), + "message_type" character varying(50), + "message_type_label" character varying(50), + "message" "text", + "status" "public"."status_contact_messages" DEFAULT 'new'::"public"."status_contact_messages" NOT NULL, + "created_at" timestamp(6) with time zone DEFAULT CURRENT_TIMESTAMP NOT NULL, + "updated_at" timestamp(6) with time zone NOT NULL +); + + +-- ALTER TABLE "public"."contact_messages" OWNER TO "prisma"; + + +CREATE TABLE IF NOT EXISTS "public"."crime_categories" ( + "id" character varying(20) NOT NULL, + "name" character varying(255) NOT NULL, + "description" "text" NOT NULL, + "created_at" timestamp(6) with time zone DEFAULT CURRENT_TIMESTAMP, + "updated_at" timestamp(6) with time zone DEFAULT CURRENT_TIMESTAMP, + "type" character varying(100) +); + + +-- ALTER TABLE "public"."crime_categories" OWNER TO "prisma"; + + +CREATE TABLE IF NOT EXISTS "public"."crime_incidents" ( + "id" character varying(20) NOT NULL, + "crime_id" character varying(20) NOT NULL, + "crime_category_id" character varying(20) NOT NULL, + "location_id" "uuid" NOT NULL, + "description" "text" NOT NULL, + "victim_count" integer NOT NULL, + "status" "public"."crime_status" DEFAULT 'open'::"public"."crime_status", + "created_at" timestamp(6) with time zone DEFAULT CURRENT_TIMESTAMP, + "updated_at" timestamp(6) with time zone DEFAULT CURRENT_TIMESTAMP, + "timestamp" timestamp(6) with time zone NOT NULL +); + + +-- ALTER TABLE "public"."crime_incidents" OWNER TO "prisma"; + + +CREATE TABLE IF NOT EXISTS "public"."crimes" ( + "id" character varying(20) NOT NULL, + "district_id" character varying(20) NOT NULL, + "created_at" timestamp(6) with time zone DEFAULT CURRENT_TIMESTAMP, + "level" "public"."crime_rates" DEFAULT 'low'::"public"."crime_rates" NOT NULL, + "method" character varying(100), + "month" integer, + "number_of_crime" integer DEFAULT 0 NOT NULL, + "score" double precision DEFAULT 0 NOT NULL, + "updated_at" timestamp(6) with time zone DEFAULT CURRENT_TIMESTAMP, + "year" integer NOT NULL +); + + +-- ALTER TABLE "public"."crimes" OWNER TO "prisma"; + + +CREATE TABLE IF NOT EXISTS "public"."demographics" ( + "id" "uuid" DEFAULT "gen_random_uuid"() NOT NULL, + "district_id" character varying(20) NOT NULL, + "population" integer NOT NULL, + "number_of_unemployed" integer NOT NULL, + "population_density" double precision NOT NULL, + "year" integer NOT NULL, + "created_at" timestamp(6) with time zone DEFAULT CURRENT_TIMESTAMP, + "updated_at" timestamp(6) with time zone DEFAULT CURRENT_TIMESTAMP +); + + +-- ALTER TABLE "public"."demographics" OWNER TO "prisma"; + + +CREATE TABLE IF NOT EXISTS "public"."districts" ( + "id" character varying(20) NOT NULL, + "city_id" character varying(20) NOT NULL, + "name" character varying(100) NOT NULL, + "created_at" timestamp(6) with time zone DEFAULT CURRENT_TIMESTAMP, + "updated_at" timestamp(6) with time zone DEFAULT CURRENT_TIMESTAMP +); + + +-- ALTER TABLE "public"."districts" OWNER TO "prisma"; + + +CREATE TABLE IF NOT EXISTS "public"."events" ( + "id" "uuid" DEFAULT "gen_random_uuid"() NOT NULL, + "name" character varying(255) NOT NULL, + "description" character varying(255), + "code" "text" NOT NULL, + "created_at" timestamp(6) with time zone DEFAULT CURRENT_TIMESTAMP NOT NULL, + "user_id" "uuid" NOT NULL +); + + +-- ALTER TABLE "public"."events" OWNER TO "prisma"; + + +CREATE TABLE IF NOT EXISTS "public"."geographics" ( + "id" "uuid" DEFAULT "gen_random_uuid"() NOT NULL, + "district_id" character varying(20) NOT NULL, + "address" "text", + "longitude" double precision NOT NULL, + "latitude" double precision NOT NULL, + "land_area" double precision, + "polygon" "gis"."geometry", + "geometry" "gis"."geometry", + "created_at" timestamp(6) with time zone DEFAULT CURRENT_TIMESTAMP, + "updated_at" timestamp(6) with time zone DEFAULT CURRENT_TIMESTAMP, + "description" "text", + "type" character varying(100), + "location" "gis"."geography"(Point,4326) NOT NULL, + "year" integer +); + + +-- ALTER TABLE "public"."geographics" OWNER TO "prisma"; + + +CREATE TABLE IF NOT EXISTS "public"."incident_logs" ( + "id" "uuid" DEFAULT "gen_random_uuid"() NOT NULL, + "user_id" "uuid" NOT NULL, + "location_id" "uuid" NOT NULL, + "category_id" character varying(20) NOT NULL, + "description" "text", + "source" "text" DEFAULT 'manual'::"text", + "time" timestamp(6) with time zone NOT NULL, + "verified" boolean DEFAULT false, + "created_at" timestamp(6) with time zone DEFAULT CURRENT_TIMESTAMP, + "updated_at" timestamp(6) with time zone DEFAULT CURRENT_TIMESTAMP +); + + +-- ALTER TABLE "public"."incident_logs" OWNER TO "prisma"; + + +CREATE TABLE IF NOT EXISTS "public"."locations" ( + "id" "uuid" DEFAULT "gen_random_uuid"() NOT NULL, + "district_id" character varying(20) NOT NULL, + "event_id" "uuid" NOT NULL, + "address" character varying(255), + "type" character varying(100), + "latitude" double precision NOT NULL, + "longitude" double precision NOT NULL, + "land_area" double precision, + "polygon" "gis"."geometry", + "geometry" "gis"."geometry", + "created_at" timestamp(6) with time zone DEFAULT CURRENT_TIMESTAMP, + "updated_at" timestamp(6) with time zone DEFAULT CURRENT_TIMESTAMP, + "location" "gis"."geography"(Point,4326) NOT NULL +); + + +-- ALTER TABLE "public"."locations" OWNER TO "prisma"; + + +CREATE OR REPLACE VIEW "public"."location_paths" AS + SELECT "l"."event_id", + "e"."user_id", + "gis"."st_makeline"(("l"."location")::"gis"."geometry" ORDER BY "l"."created_at") AS "path" + FROM ("public"."locations" "l" + JOIN "public"."events" "e" ON (("l"."event_id" = "e"."id"))) + GROUP BY "l"."event_id", "e"."user_id"; + + +ALTER TABLE "public"."location_paths" OWNER TO "postgres"; + + +CREATE TABLE IF NOT EXISTS "public"."logs" ( + "id" "uuid" DEFAULT "gen_random_uuid"() NOT NULL, + "action" character varying(100) NOT NULL, + "entity" character varying(100) NOT NULL, + "entity_id" character varying(100), + "changes" "jsonb", + "user_id" character varying(100), + "ip_address" character varying(100), + "user_agent" character varying(255), + "created_at" timestamp(6) with time zone DEFAULT CURRENT_TIMESTAMP NOT NULL +); + + +-- ALTER TABLE "public"."logs" OWNER TO "prisma"; + + +CREATE TABLE IF NOT EXISTS "public"."permissions" ( + "id" "uuid" DEFAULT "gen_random_uuid"() NOT NULL, + "action" "text" NOT NULL, + "resource_id" "uuid" NOT NULL, + "role_id" "uuid" NOT NULL, + "created_at" timestamp(6) with time zone DEFAULT CURRENT_TIMESTAMP NOT NULL, + "updated_at" timestamp(6) with time zone NOT NULL +); + + +-- ALTER TABLE "public"."permissions" OWNER TO "prisma"; + + +CREATE TABLE IF NOT EXISTS "public"."profiles" ( + "id" "uuid" DEFAULT "gen_random_uuid"() NOT NULL, + "user_id" "uuid" NOT NULL, + "avatar" character varying(355), + "username" character varying(255), + "first_name" character varying(255), + "last_name" character varying(255), + "bio" character varying, + "address" "json", + "birth_date" timestamp(3) without time zone +); + + +-- ALTER TABLE "public"."profiles" OWNER TO "prisma"; + + +CREATE TABLE IF NOT EXISTS "public"."resources" ( + "id" "uuid" DEFAULT "gen_random_uuid"() NOT NULL, + "name" character varying(255) NOT NULL, + "type" "text", + "description" "text", + "instance_role" "text", + "relations" "text", + "attributes" "jsonb", + "created_at" timestamp(6) with time zone DEFAULT CURRENT_TIMESTAMP NOT NULL, + "updated_at" timestamp(6) with time zone DEFAULT CURRENT_TIMESTAMP NOT NULL +); + + +-- ALTER TABLE "public"."resources" OWNER TO "prisma"; + + +CREATE TABLE IF NOT EXISTS "public"."roles" ( + "id" "uuid" DEFAULT "gen_random_uuid"() NOT NULL, + "name" character varying(255) NOT NULL, + "description" "text", + "created_at" timestamp(6) with time zone DEFAULT CURRENT_TIMESTAMP NOT NULL, + "updated_at" timestamp(6) with time zone DEFAULT CURRENT_TIMESTAMP NOT NULL +); + + +-- ALTER TABLE "public"."roles" OWNER TO "prisma"; + + +CREATE TABLE IF NOT EXISTS "public"."sessions" ( + "id" "uuid" DEFAULT "gen_random_uuid"() NOT NULL, + "user_id" "uuid" NOT NULL, + "event_id" "uuid" NOT NULL, + "status" "public"."session_status" DEFAULT 'active'::"public"."session_status" NOT NULL, + "created_at" timestamp(6) with time zone DEFAULT CURRENT_TIMESTAMP NOT NULL +); + + +-- ALTER TABLE "public"."sessions" OWNER TO "prisma"; + + +CREATE TABLE IF NOT EXISTS "public"."unit_statistics" ( + "id" "uuid" DEFAULT "gen_random_uuid"() NOT NULL, + "unit_id" "uuid" NOT NULL, + "crime_total" integer NOT NULL, + "crime_cleared" integer NOT NULL, + "percentage" double precision, + "pending" integer, + "month" integer NOT NULL, + "year" integer NOT NULL, + "created_at" timestamp(6) with time zone DEFAULT CURRENT_TIMESTAMP, + "updated_at" timestamp(6) with time zone DEFAULT CURRENT_TIMESTAMP +); + + +-- ALTER TABLE "public"."unit_statistics" OWNER TO "prisma"; + + +CREATE TABLE IF NOT EXISTS "public"."units" ( + "id" "uuid" DEFAULT "gen_random_uuid"() NOT NULL, + "code_unit" character varying(20) NOT NULL, + "district_id" character varying(20) NOT NULL, + "name" character varying(100) NOT NULL, + "description" "text", + "type" "public"."unit_type" NOT NULL, + "created_at" timestamp(6) with time zone DEFAULT CURRENT_TIMESTAMP, + "updated_at" timestamp(6) with time zone DEFAULT CURRENT_TIMESTAMP, + "address" "text", + "land_area" double precision, + "latitude" double precision NOT NULL, + "longitude" double precision NOT NULL, + "location" "gis"."geography"(Point,4326) NOT NULL +); + + +-- ALTER TABLE "public"."units" OWNER TO "prisma"; + + +CREATE TABLE IF NOT EXISTS "public"."users" ( + "id" "uuid" DEFAULT "gen_random_uuid"() NOT NULL, + "roles_id" "uuid" NOT NULL, + "email" character varying(255) NOT NULL, + "phone" character varying(20), + "encrypted_password" character varying(255), + "invited_at" timestamp(6) with time zone, + "confirmed_at" timestamp(6) with time zone, + "email_confirmed_at" timestamp(6) with time zone, + "recovery_sent_at" timestamp(6) with time zone, + "last_sign_in_at" timestamp(6) with time zone, + "app_metadata" "jsonb", + "user_metadata" "jsonb", + "created_at" timestamp(6) with time zone DEFAULT CURRENT_TIMESTAMP NOT NULL, + "updated_at" timestamp(6) with time zone DEFAULT CURRENT_TIMESTAMP NOT NULL, + "banned_until" timestamp(6) with time zone, + "is_anonymous" boolean DEFAULT false NOT NULL +); + + +-- ALTER TABLE "public"."users" OWNER TO "prisma"; + + +ALTER TABLE ONLY "public"."cities" + ADD CONSTRAINT "cities_pkey" PRIMARY KEY ("id"); + + + +ALTER TABLE ONLY "public"."contact_messages" + ADD CONSTRAINT "contact_messages_pkey" PRIMARY KEY ("id"); + + + +ALTER TABLE ONLY "public"."crime_categories" + ADD CONSTRAINT "crime_categories_pkey" PRIMARY KEY ("id"); + + + +ALTER TABLE ONLY "public"."crime_incidents" + ADD CONSTRAINT "crime_incidents_pkey" PRIMARY KEY ("id"); + + + +ALTER TABLE ONLY "public"."crimes" + ADD CONSTRAINT "crimes_pkey" PRIMARY KEY ("id"); + + + +ALTER TABLE ONLY "public"."demographics" + ADD CONSTRAINT "demographics_pkey" PRIMARY KEY ("id"); + + + +ALTER TABLE ONLY "public"."districts" + ADD CONSTRAINT "districts_pkey" PRIMARY KEY ("id"); + + + +ALTER TABLE ONLY "public"."events" + ADD CONSTRAINT "events_pkey" PRIMARY KEY ("id"); + + + +ALTER TABLE ONLY "public"."geographics" + ADD CONSTRAINT "geographics_pkey" PRIMARY KEY ("id"); + + + +ALTER TABLE ONLY "public"."incident_logs" + ADD CONSTRAINT "incident_logs_pkey" PRIMARY KEY ("id"); + + + +ALTER TABLE ONLY "public"."locations" + ADD CONSTRAINT "locations_pkey" PRIMARY KEY ("id"); + + + +ALTER TABLE ONLY "public"."logs" + ADD CONSTRAINT "logs_pkey" PRIMARY KEY ("id"); + + + +ALTER TABLE ONLY "public"."permissions" + ADD CONSTRAINT "permissions_pkey" PRIMARY KEY ("id"); + + + +ALTER TABLE ONLY "public"."profiles" + ADD CONSTRAINT "profiles_pkey" PRIMARY KEY ("id"); + + + +ALTER TABLE ONLY "public"."resources" + ADD CONSTRAINT "resources_pkey" PRIMARY KEY ("id"); + + + +ALTER TABLE ONLY "public"."roles" + ADD CONSTRAINT "roles_pkey" PRIMARY KEY ("id"); + + + +ALTER TABLE ONLY "public"."sessions" + ADD CONSTRAINT "sessions_pkey" PRIMARY KEY ("id"); + + + +ALTER TABLE ONLY "public"."unit_statistics" + ADD CONSTRAINT "unit_statistics_pkey" PRIMARY KEY ("id"); + + + +ALTER TABLE ONLY "public"."units" + ADD CONSTRAINT "units_pkey" PRIMARY KEY ("id"); + + + +ALTER TABLE ONLY "public"."users" + ADD CONSTRAINT "users_pkey" PRIMARY KEY ("id"); + + + +CREATE UNIQUE INDEX "demographics_district_id_year_key" ON "public"."demographics" USING "btree" ("district_id", "year"); + + + +CREATE UNIQUE INDEX "events_code_key" ON "public"."events" USING "btree" ("code"); + + + +CREATE INDEX "idx_cities_name" ON "public"."cities" USING "btree" ("name"); + + + +CREATE INDEX "idx_crime_categories_name" ON "public"."crime_categories" USING "btree" ("name"); + + + +CREATE INDEX "idx_crime_incidents_crime_category_id" ON "public"."crime_incidents" USING "btree" ("crime_category_id"); + + + +CREATE INDEX "idx_crime_incidents_crime_id" ON "public"."crime_incidents" USING "btree" ("crime_id"); + + + +CREATE INDEX "idx_crime_incidents_date" ON "public"."crime_incidents" USING "btree" ("timestamp"); + + + +CREATE INDEX "idx_crime_incidents_location_id" ON "public"."crime_incidents" USING "btree" ("location_id"); + + + +CREATE INDEX "idx_crime_incidents_status" ON "public"."crime_incidents" USING "btree" ("status"); + + + +CREATE INDEX "idx_crimes_district_id_month" ON "public"."crimes" USING "btree" ("district_id", "month"); + + + +CREATE INDEX "idx_crimes_district_id_year_month" ON "public"."crimes" USING "btree" ("district_id", "year", "month"); + + + +CREATE INDEX "idx_crimes_month" ON "public"."crimes" USING "btree" ("month"); + + + +CREATE INDEX "idx_crimes_month_year" ON "public"."crimes" USING "btree" ("month", "year"); + + + +CREATE INDEX "idx_crimes_year" ON "public"."crimes" USING "btree" ("year"); + + + +CREATE INDEX "idx_demographics_year" ON "public"."demographics" USING "btree" ("year"); + + + +CREATE INDEX "idx_districts_city_id" ON "public"."districts" USING "btree" ("city_id"); + + + +CREATE INDEX "idx_districts_name" ON "public"."districts" USING "btree" ("name"); + + + +CREATE INDEX "idx_events_code" ON "public"."events" USING "btree" ("code"); + + + +CREATE INDEX "idx_events_id" ON "public"."events" USING "btree" ("id"); + + + +CREATE INDEX "idx_events_name" ON "public"."events" USING "btree" ("name"); + + + +CREATE INDEX "idx_geographics_district_id" ON "public"."geographics" USING "btree" ("district_id"); + + + +CREATE INDEX "idx_geographics_district_id_year" ON "public"."geographics" USING "btree" ("district_id", "year"); + + + +CREATE INDEX "idx_geographics_location" ON "public"."geographics" USING "gist" ("location"); + + + +CREATE INDEX "idx_geographics_type" ON "public"."geographics" USING "btree" ("type"); + + + +CREATE INDEX "idx_incident_logs_category_id" ON "public"."incident_logs" USING "btree" ("category_id"); + + + +CREATE INDEX "idx_incident_logs_time" ON "public"."incident_logs" USING "btree" ("time"); + + + +CREATE INDEX "idx_locations_district_id" ON "public"."locations" USING "btree" ("district_id"); + + + +CREATE INDEX "idx_locations_geography" ON "public"."locations" USING "gist" ("location"); + + + +CREATE INDEX "idx_locations_type" ON "public"."locations" USING "btree" ("type"); + + + +CREATE INDEX "idx_sessions_event_id" ON "public"."sessions" USING "btree" ("event_id"); + + + +CREATE INDEX "idx_sessions_status" ON "public"."sessions" USING "btree" ("status"); + + + +CREATE INDEX "idx_sessions_user_id" ON "public"."sessions" USING "btree" ("user_id"); + + + +CREATE INDEX "idx_unit_location" ON "public"."units" USING "gist" ("location"); + + + +CREATE INDEX "idx_unit_statistics_year_month" ON "public"."unit_statistics" USING "btree" ("year", "month"); + + + +CREATE INDEX "idx_units_code_unit" ON "public"."units" USING "btree" ("code_unit"); + + + +CREATE INDEX "idx_units_district_id" ON "public"."units" USING "btree" ("district_id"); + + + +CREATE INDEX "idx_units_name" ON "public"."units" USING "btree" ("name"); + + + +CREATE INDEX "idx_units_type" ON "public"."units" USING "btree" ("type"); + + + +CREATE INDEX "logs_entity_idx" ON "public"."logs" USING "btree" ("entity"); + + + +CREATE INDEX "logs_user_id_idx" ON "public"."logs" USING "btree" ("user_id"); + + + +CREATE INDEX "profiles_user_id_idx" ON "public"."profiles" USING "btree" ("user_id"); + + + +CREATE UNIQUE INDEX "profiles_user_id_key" ON "public"."profiles" USING "btree" ("user_id"); + + + +CREATE INDEX "profiles_username_idx" ON "public"."profiles" USING "btree" ("username"); + + + +CREATE UNIQUE INDEX "profiles_username_key" ON "public"."profiles" USING "btree" ("username"); + + + +CREATE UNIQUE INDEX "resources_name_key" ON "public"."resources" USING "btree" ("name"); + + + +CREATE UNIQUE INDEX "roles_name_key" ON "public"."roles" USING "btree" ("name"); + + + +CREATE UNIQUE INDEX "unit_statistics_unit_id_month_year_key" ON "public"."unit_statistics" USING "btree" ("unit_id", "month", "year"); + + + +CREATE UNIQUE INDEX "units_code_unit_key" ON "public"."units" USING "btree" ("code_unit"); + + + +CREATE UNIQUE INDEX "units_district_id_key" ON "public"."units" USING "btree" ("district_id"); + + + +CREATE INDEX "users_created_at_idx" ON "public"."users" USING "btree" ("created_at"); + + + +CREATE UNIQUE INDEX "users_email_key" ON "public"."users" USING "btree" ("email"); + + + +CREATE INDEX "users_is_anonymous_idx" ON "public"."users" USING "btree" ("is_anonymous"); + + + +CREATE UNIQUE INDEX "users_phone_key" ON "public"."users" USING "btree" ("phone"); + + + +CREATE INDEX "users_updated_at_idx" ON "public"."users" USING "btree" ("updated_at"); + + + +CREATE OR REPLACE TRIGGER "trg_update_land_area" BEFORE INSERT OR UPDATE ON "public"."geographics" FOR EACH ROW WHEN (("new"."geometry" IS NOT NULL)) EXECUTE FUNCTION "gis"."update_land_area"(); + +ALTER TABLE "public"."geographics" DISABLE TRIGGER "trg_update_land_area"; + + + +ALTER TABLE ONLY "public"."crime_incidents" + ADD CONSTRAINT "crime_incidents_crime_category_id_fkey" FOREIGN KEY ("crime_category_id") REFERENCES "public"."crime_categories"("id") ON DELETE RESTRICT; + + + +ALTER TABLE ONLY "public"."crime_incidents" + ADD CONSTRAINT "crime_incidents_crime_id_fkey" FOREIGN KEY ("crime_id") REFERENCES "public"."crimes"("id"); + + + +ALTER TABLE ONLY "public"."crime_incidents" + ADD CONSTRAINT "crime_incidents_location_id_fkey" FOREIGN KEY ("location_id") REFERENCES "public"."locations"("id") ON DELETE CASCADE; + + + +ALTER TABLE ONLY "public"."crimes" + ADD CONSTRAINT "crimes_district_id_fkey" FOREIGN KEY ("district_id") REFERENCES "public"."districts"("id") ON DELETE CASCADE; + + + +ALTER TABLE ONLY "public"."demographics" + ADD CONSTRAINT "demographics_district_id_fkey" FOREIGN KEY ("district_id") REFERENCES "public"."districts"("id") ON DELETE CASCADE; + + + +ALTER TABLE ONLY "public"."districts" + ADD CONSTRAINT "districts_city_id_fkey" FOREIGN KEY ("city_id") REFERENCES "public"."cities"("id") ON DELETE CASCADE; + + + +ALTER TABLE ONLY "public"."events" + ADD CONSTRAINT "events_user_id_fkey" FOREIGN KEY ("user_id") REFERENCES "public"."users"("id") ON UPDATE CASCADE ON DELETE RESTRICT; + + + +ALTER TABLE ONLY "public"."incident_logs" + ADD CONSTRAINT "fk_incident_category" FOREIGN KEY ("category_id") REFERENCES "public"."crime_categories"("id") ON UPDATE CASCADE ON DELETE RESTRICT; + + + +ALTER TABLE ONLY "public"."geographics" + ADD CONSTRAINT "geographics_district_id_fkey" FOREIGN KEY ("district_id") REFERENCES "public"."districts"("id") ON DELETE CASCADE; + + + +ALTER TABLE ONLY "public"."incident_logs" + ADD CONSTRAINT "incident_logs_location_id_fkey" FOREIGN KEY ("location_id") REFERENCES "public"."locations"("id") ON DELETE CASCADE; + + + +ALTER TABLE ONLY "public"."incident_logs" + ADD CONSTRAINT "incident_logs_user_id_fkey" FOREIGN KEY ("user_id") REFERENCES "public"."users"("id") ON DELETE CASCADE; + + + +ALTER TABLE ONLY "public"."locations" + ADD CONSTRAINT "locations_district_id_fkey" FOREIGN KEY ("district_id") REFERENCES "public"."districts"("id") ON DELETE CASCADE; + + + +ALTER TABLE ONLY "public"."locations" + ADD CONSTRAINT "locations_event_id_fkey" FOREIGN KEY ("event_id") REFERENCES "public"."events"("id") ON UPDATE CASCADE ON DELETE RESTRICT; + + + +ALTER TABLE ONLY "public"."permissions" + ADD CONSTRAINT "permissions_resource_id_fkey" FOREIGN KEY ("resource_id") REFERENCES "public"."resources"("id") ON UPDATE CASCADE ON DELETE RESTRICT; + + + +ALTER TABLE ONLY "public"."permissions" + ADD CONSTRAINT "permissions_role_id_fkey" FOREIGN KEY ("role_id") REFERENCES "public"."roles"("id") ON UPDATE CASCADE ON DELETE RESTRICT; + + + +ALTER TABLE ONLY "public"."profiles" + ADD CONSTRAINT "profiles_user_id_fkey" FOREIGN KEY ("user_id") REFERENCES "public"."users"("id") ON UPDATE CASCADE ON DELETE RESTRICT; + + + +ALTER TABLE ONLY "public"."sessions" + ADD CONSTRAINT "sessions_event_id_fkey" FOREIGN KEY ("event_id") REFERENCES "public"."events"("id") ON UPDATE CASCADE ON DELETE RESTRICT; + + + +ALTER TABLE ONLY "public"."sessions" + ADD CONSTRAINT "sessions_user_id_fkey" FOREIGN KEY ("user_id") REFERENCES "public"."users"("id") ON UPDATE CASCADE ON DELETE RESTRICT; + + + +ALTER TABLE ONLY "public"."unit_statistics" + ADD CONSTRAINT "unit_statistics_unit_id_fkey" FOREIGN KEY ("unit_id") REFERENCES "public"."units"("id") ON DELETE CASCADE; + + + +ALTER TABLE ONLY "public"."units" + ADD CONSTRAINT "units_district_id_fkey" FOREIGN KEY ("district_id") REFERENCES "public"."districts"("id") ON DELETE CASCADE; + + + +ALTER TABLE ONLY "public"."users" + ADD CONSTRAINT "users_roles_id_fkey" FOREIGN KEY ("roles_id") REFERENCES "public"."roles"("id") ON UPDATE CASCADE ON DELETE RESTRICT; + + + +CREATE POLICY "give all access to users" ON "public"."geographics" TO "authenticated", "anon", "postgres" USING (true); + + +ALTER PUBLICATION "supabase_realtime" OWNER TO "postgres"; + + +ALTER PUBLICATION "supabase_realtime" ADD TABLE ONLY "public"."locations"; + + + +GRANT USAGE ON SCHEMA "gis" TO "anon"; +GRANT USAGE ON SCHEMA "gis" TO "authenticated"; +-- GRANT USAGE ON SCHEMA "gis" TO "prisma"; + + + +GRANT USAGE ON SCHEMA "public" TO "postgres"; +GRANT USAGE ON SCHEMA "public" TO "anon"; +GRANT USAGE ON SCHEMA "public" TO "authenticated"; +GRANT USAGE ON SCHEMA "public" TO "service_role"; +-- GRANT ALL ON SCHEMA "public" TO "prisma"; + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +GRANT ALL ON FUNCTION "public"."generate_username"("email" "text") TO "anon"; +GRANT ALL ON FUNCTION "public"."generate_username"("email" "text") TO "authenticated"; +GRANT ALL ON FUNCTION "public"."generate_username"("email" "text") TO "service_role"; +-- GRANT ALL ON FUNCTION "public"."generate_username"("email" "text") TO "prisma"; + + + +GRANT ALL ON FUNCTION "public"."handle_new_user"() TO "anon"; +GRANT ALL ON FUNCTION "public"."handle_new_user"() TO "authenticated"; +GRANT ALL ON FUNCTION "public"."handle_new_user"() TO "service_role"; +-- GRANT ALL ON FUNCTION "public"."handle_new_user"() TO "prisma"; + + + +GRANT ALL ON FUNCTION "public"."handle_user_delete"() TO "anon"; +GRANT ALL ON FUNCTION "public"."handle_user_delete"() TO "authenticated"; +GRANT ALL ON FUNCTION "public"."handle_user_delete"() TO "service_role"; +-- GRANT ALL ON FUNCTION "public"."handle_user_delete"() TO "prisma"; + + + +GRANT ALL ON FUNCTION "public"."handle_user_update"() TO "anon"; +GRANT ALL ON FUNCTION "public"."handle_user_update"() TO "authenticated"; +GRANT ALL ON FUNCTION "public"."handle_user_update"() TO "service_role"; +-- GRANT ALL ON FUNCTION "public"."handle_user_update"() TO "prisma"; + + + +GRANT ALL ON FUNCTION "public"."update_land_area"() TO "anon"; +GRANT ALL ON FUNCTION "public"."update_land_area"() TO "authenticated"; +GRANT ALL ON FUNCTION "public"."update_land_area"() TO "service_role"; +-- GRANT ALL ON FUNCTION "public"."update_land_area"() TO "prisma"; + + + +GRANT ALL ON FUNCTION "public"."update_timestamp"() TO "anon"; +GRANT ALL ON FUNCTION "public"."update_timestamp"() TO "authenticated"; +GRANT ALL ON FUNCTION "public"."update_timestamp"() TO "service_role"; +-- GRANT ALL ON FUNCTION "public"."update_timestamp"() TO "prisma"; + + + + + + + + + + + + + + + + + + +GRANT SELECT,INSERT,UPDATE ON TABLE "public"."cities" TO "authenticated"; +GRANT SELECT,INSERT,UPDATE ON TABLE "public"."cities" TO "anon"; +GRANT SELECT,INSERT,UPDATE ON TABLE "public"."cities" TO "postgres"; + + + +GRANT SELECT,INSERT,UPDATE ON TABLE "public"."contact_messages" TO "authenticated"; +GRANT SELECT,INSERT,UPDATE ON TABLE "public"."contact_messages" TO "anon"; +GRANT SELECT,INSERT,UPDATE ON TABLE "public"."contact_messages" TO "postgres"; + + + +GRANT SELECT,INSERT,UPDATE ON TABLE "public"."crime_categories" TO "authenticated"; +GRANT SELECT,INSERT,UPDATE ON TABLE "public"."crime_categories" TO "anon"; +GRANT SELECT,INSERT,UPDATE ON TABLE "public"."crime_categories" TO "postgres"; + + + +GRANT SELECT,INSERT,UPDATE ON TABLE "public"."crime_incidents" TO "authenticated"; +GRANT SELECT,INSERT,UPDATE ON TABLE "public"."crime_incidents" TO "anon"; +GRANT SELECT,INSERT,UPDATE ON TABLE "public"."crime_incidents" TO "postgres"; + + + +GRANT SELECT,INSERT,UPDATE ON TABLE "public"."crimes" TO "authenticated"; +GRANT SELECT,INSERT,UPDATE ON TABLE "public"."crimes" TO "anon"; +GRANT SELECT,INSERT,UPDATE ON TABLE "public"."crimes" TO "postgres"; + + + +GRANT SELECT,INSERT,UPDATE ON TABLE "public"."demographics" TO "authenticated"; +GRANT SELECT,INSERT,UPDATE ON TABLE "public"."demographics" TO "anon"; +GRANT SELECT,INSERT,UPDATE ON TABLE "public"."demographics" TO "postgres"; + + + +GRANT SELECT,INSERT,UPDATE ON TABLE "public"."districts" TO "authenticated"; +GRANT SELECT,INSERT,UPDATE ON TABLE "public"."districts" TO "anon"; +GRANT SELECT,INSERT,UPDATE ON TABLE "public"."districts" TO "postgres"; + + + +GRANT SELECT,INSERT,UPDATE ON TABLE "public"."events" TO "authenticated"; +GRANT SELECT,INSERT,UPDATE ON TABLE "public"."events" TO "anon"; +GRANT SELECT,INSERT,UPDATE ON TABLE "public"."events" TO "postgres"; + + + +GRANT SELECT,INSERT,UPDATE ON TABLE "public"."geographics" TO "authenticated"; +GRANT SELECT,INSERT,UPDATE ON TABLE "public"."geographics" TO "anon"; +GRANT SELECT,INSERT,UPDATE ON TABLE "public"."geographics" TO "postgres"; + + + +GRANT SELECT,INSERT,UPDATE ON TABLE "public"."incident_logs" TO "authenticated"; +GRANT SELECT,INSERT,UPDATE ON TABLE "public"."incident_logs" TO "anon"; +GRANT SELECT,INSERT,UPDATE ON TABLE "public"."incident_logs" TO "postgres"; + + + +GRANT SELECT,INSERT,UPDATE ON TABLE "public"."locations" TO "authenticated"; +GRANT SELECT,INSERT,UPDATE ON TABLE "public"."locations" TO "anon"; +GRANT SELECT,INSERT,UPDATE ON TABLE "public"."locations" TO "postgres"; + + + +GRANT ALL ON TABLE "public"."location_paths" TO "anon"; +GRANT ALL ON TABLE "public"."location_paths" TO "authenticated"; +GRANT ALL ON TABLE "public"."location_paths" TO "service_role"; +-- GRANT ALL ON TABLE "public"."location_paths" TO "prisma"; + + + +GRANT SELECT,INSERT,UPDATE ON TABLE "public"."logs" TO "authenticated"; +GRANT SELECT,INSERT,UPDATE ON TABLE "public"."logs" TO "anon"; +GRANT SELECT,INSERT,UPDATE ON TABLE "public"."logs" TO "postgres"; + + + +GRANT SELECT,INSERT,UPDATE ON TABLE "public"."permissions" TO "authenticated"; +GRANT SELECT,INSERT,UPDATE ON TABLE "public"."permissions" TO "anon"; +GRANT SELECT,INSERT,UPDATE ON TABLE "public"."permissions" TO "postgres"; + + + +GRANT SELECT,INSERT,UPDATE ON TABLE "public"."profiles" TO "authenticated"; +GRANT SELECT,INSERT,UPDATE ON TABLE "public"."profiles" TO "anon"; +GRANT SELECT,INSERT,UPDATE ON TABLE "public"."profiles" TO "postgres"; + + + +GRANT SELECT,INSERT,UPDATE ON TABLE "public"."resources" TO "authenticated"; +GRANT SELECT,INSERT,UPDATE ON TABLE "public"."resources" TO "anon"; +GRANT SELECT,INSERT,UPDATE ON TABLE "public"."resources" TO "postgres"; + + + +GRANT SELECT,INSERT,UPDATE ON TABLE "public"."roles" TO "authenticated"; +GRANT SELECT,INSERT,UPDATE ON TABLE "public"."roles" TO "anon"; +GRANT SELECT,INSERT,UPDATE ON TABLE "public"."roles" TO "postgres"; + + + +GRANT SELECT,INSERT,UPDATE ON TABLE "public"."sessions" TO "authenticated"; +GRANT SELECT,INSERT,UPDATE ON TABLE "public"."sessions" TO "anon"; +GRANT SELECT,INSERT,UPDATE ON TABLE "public"."sessions" TO "postgres"; + + + +GRANT SELECT,INSERT,UPDATE ON TABLE "public"."unit_statistics" TO "authenticated"; +GRANT SELECT,INSERT,UPDATE ON TABLE "public"."unit_statistics" TO "anon"; +GRANT SELECT,INSERT,UPDATE ON TABLE "public"."unit_statistics" TO "postgres"; + + + +GRANT SELECT,INSERT,UPDATE ON TABLE "public"."units" TO "authenticated"; +GRANT SELECT,INSERT,UPDATE ON TABLE "public"."units" TO "anon"; +GRANT SELECT,INSERT,UPDATE ON TABLE "public"."units" TO "postgres"; + + + +GRANT SELECT,INSERT,UPDATE ON TABLE "public"."users" TO "authenticated"; +GRANT SELECT,INSERT,UPDATE ON TABLE "public"."users" TO "anon"; +GRANT SELECT,INSERT,UPDATE ON TABLE "public"."users" TO "postgres"; + + + +ALTER DEFAULT PRIVILEGES FOR ROLE "postgres" IN SCHEMA "public" GRANT ALL ON SEQUENCES TO "postgres"; +ALTER DEFAULT PRIVILEGES FOR ROLE "postgres" IN SCHEMA "public" GRANT ALL ON SEQUENCES TO "anon"; +ALTER DEFAULT PRIVILEGES FOR ROLE "postgres" IN SCHEMA "public" GRANT ALL ON SEQUENCES TO "authenticated"; +ALTER DEFAULT PRIVILEGES FOR ROLE "postgres" IN SCHEMA "public" GRANT ALL ON SEQUENCES TO "service_role"; +-- ALTER DEFAULT PRIVILEGES FOR ROLE "postgres" IN SCHEMA "public" GRANT ALL ON SEQUENCES TO "prisma"; + + + + + + +ALTER DEFAULT PRIVILEGES FOR ROLE "postgres" IN SCHEMA "public" GRANT ALL ON FUNCTIONS TO "postgres"; +ALTER DEFAULT PRIVILEGES FOR ROLE "postgres" IN SCHEMA "public" GRANT ALL ON FUNCTIONS TO "anon"; +ALTER DEFAULT PRIVILEGES FOR ROLE "postgres" IN SCHEMA "public" GRANT ALL ON FUNCTIONS TO "authenticated"; +ALTER DEFAULT PRIVILEGES FOR ROLE "postgres" IN SCHEMA "public" GRANT ALL ON FUNCTIONS TO "service_role"; +-- ALTER DEFAULT PRIVILEGES FOR ROLE "postgres" IN SCHEMA "public" GRANT ALL ON FUNCTIONS TO "prisma"; + + + + + + +ALTER DEFAULT PRIVILEGES FOR ROLE "postgres" IN SCHEMA "public" GRANT ALL ON TABLES TO "postgres"; +ALTER DEFAULT PRIVILEGES FOR ROLE "postgres" IN SCHEMA "public" GRANT ALL ON TABLES TO "anon"; +ALTER DEFAULT PRIVILEGES FOR ROLE "postgres" IN SCHEMA "public" GRANT ALL ON TABLES TO "authenticated"; +ALTER DEFAULT PRIVILEGES FOR ROLE "postgres" IN SCHEMA "public" GRANT ALL ON TABLES TO "service_role"; +-- ALTER DEFAULT PRIVILEGES FOR ROLE "postgres" IN SCHEMA "public" GRANT ALL ON TABLES TO "prisma"; + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +RESET ALL; diff --git a/sigap-website/prisma/backups/20250421114454_add_prisma_user.sql b/sigap-website/prisma/backups/20250421114454_add_prisma_user.sql new file mode 100644 index 0000000..749ea08 --- /dev/null +++ b/sigap-website/prisma/backups/20250421114454_add_prisma_user.sql @@ -0,0 +1,159 @@ +-- Migration to recreate the prisma user with proper privileges +-- First, drop the existing prisma role if it exists and recreate it + +-- Drop the role if it exists +DO $$ +BEGIN + IF EXISTS (SELECT FROM pg_catalog.pg_roles WHERE rolname = 'prisma') THEN + DROP ROLE prisma; + END IF; +END +$$; + +-- Create the prisma role with login capability +CREATE ROLE prisma WITH LOGIN PASSWORD 'prisma'; + +-- -- Grant usage on all necessary schemas +-- GRANT USAGE ON SCHEMA public TO prisma; +-- GRANT USAGE ON SCHEMA gis TO prisma; +-- GRANT USAGE ON SCHEMA auth TO prisma; +-- GRANT USAGE ON SCHEMA storage TO prisma; +-- GRANT USAGE ON SCHEMA graphql TO prisma; +-- GRANT USAGE ON SCHEMA extensions TO prisma; + +-- -- Explicitly grant permissions on auth and storage schemas +-- DO $$ +-- BEGIN +-- -- Explicitly grant on auth schema +-- EXECUTE 'GRANT USAGE ON SCHEMA auth TO prisma'; +-- -- Explicitly grant on storage schema +-- EXECUTE 'GRANT USAGE ON SCHEMA storage TO prisma'; +-- END +-- $$; + +-- -- Grant privileges on all tables in schemas +-- DO $$ +-- DECLARE +-- r RECORD; +-- BEGIN +-- -- Grant privileges on all tables in public schema +-- FOR r IN SELECT tablename FROM pg_tables WHERE schemaname = 'public' LOOP +-- EXECUTE 'GRANT ALL PRIVILEGES ON TABLE public.' || quote_ident(r.tablename) || ' TO prisma'; +-- END LOOP; + +-- -- Grant privileges on all tables in gis schema +-- FOR r IN SELECT tablename FROM pg_tables WHERE schemaname = 'gis' LOOP +-- EXECUTE 'GRANT ALL PRIVILEGES ON TABLE gis.' || quote_ident(r.tablename) || ' TO prisma'; +-- END LOOP; + +-- -- Grant privileges on all tables in auth schema +-- FOR r IN SELECT tablename FROM pg_tables WHERE schemaname = 'auth' LOOP +-- EXECUTE 'GRANT SELECT, DELETE ON TABLE auth.' || quote_ident(r.tablename) || ' TO prisma'; +-- END LOOP; + +-- -- Grant privileges on all tables in storage schema +-- FOR r IN SELECT tablename FROM pg_tables WHERE schemaname = 'storage' LOOP +-- EXECUTE 'GRANT SELECT, DELETE ON TABLE storage.' || quote_ident(r.tablename) || ' TO prisma'; +-- END LOOP; + +-- -- Grant privileges on all sequences in public schema +-- FOR r IN SELECT sequence_name FROM information_schema.sequences WHERE sequence_schema = 'public' LOOP +-- EXECUTE 'GRANT ALL PRIVILEGES ON SEQUENCE public.' || quote_ident(r.sequence_name) || ' TO prisma'; +-- END LOOP; + +-- -- Grant privileges on all sequences in gis schema +-- FOR r IN SELECT sequence_name FROM information_schema.sequences WHERE sequence_schema = 'gis' LOOP +-- EXECUTE 'GRANT ALL PRIVILEGES ON SEQUENCE gis.' || quote_ident(r.sequence_name) || ' TO prisma'; +-- END LOOP; + +-- -- Grant privileges on all sequences in auth schema +-- FOR r IN SELECT sequence_name FROM information_schema.sequences WHERE sequence_schema = 'auth' LOOP +-- EXECUTE 'GRANT USAGE ON SEQUENCE auth.' || quote_ident(r.sequence_name) || ' TO prisma'; +-- END LOOP; + +-- -- Grant privileges on all sequences in storage schema +-- FOR r IN SELECT sequence_name FROM information_schema.sequences WHERE sequence_schema = 'storage' LOOP +-- EXECUTE 'GRANT USAGE ON SEQUENCE storage.' || quote_ident(r.sequence_name) || ' TO prisma'; +-- END LOOP; + +-- -- Grant usage on all types in public schema +-- EXECUTE 'GRANT USAGE ON TYPE "public"."crime_rates" TO prisma'; +-- EXECUTE 'GRANT USAGE ON TYPE "public"."crime_status" TO prisma'; +-- EXECUTE 'GRANT USAGE ON TYPE "public"."session_status" TO prisma'; +-- EXECUTE 'GRANT USAGE ON TYPE "public"."status_contact_messages" TO prisma'; +-- EXECUTE 'GRANT USAGE ON TYPE "public"."unit_type" TO prisma'; +-- END +-- $$; + +-- -- Grant execute privileges on functions (separate DO block to avoid EXCEPTION issues) +-- DO $$ +-- DECLARE +-- r RECORD; +-- BEGIN +-- -- Grant execute privileges on all functions in public schema +-- FOR r IN SELECT routines.routine_name +-- FROM information_schema.routines +-- WHERE routines.specific_schema = 'public' +-- AND routines.routine_type = 'FUNCTION' LOOP +-- BEGIN +-- EXECUTE 'GRANT EXECUTE ON FUNCTION public.' || quote_ident(r.routine_name) || '() TO prisma'; +-- EXCEPTION WHEN OTHERS THEN +-- RAISE NOTICE 'Error granting execute on function public.%: %', r.routine_name, SQLERRM; +-- END; +-- END LOOP; +-- END +-- $$; + +-- -- Handle gis functions in a separate block +-- DO $$ +-- DECLARE +-- r RECORD; +-- BEGIN +-- -- Grant execute privileges on all functions in gis schema +-- FOR r IN SELECT routines.routine_name +-- FROM information_schema.routines +-- WHERE routines.specific_schema = 'gis' +-- AND routines.routine_type = 'FUNCTION' LOOP +-- BEGIN +-- EXECUTE 'GRANT EXECUTE ON FUNCTION gis.' || quote_ident(r.routine_name) || '() TO prisma'; +-- EXCEPTION WHEN OTHERS THEN +-- RAISE NOTICE 'Error granting execute on function gis.%: %', r.routine_name, SQLERRM; +-- END; +-- END LOOP; +-- END +-- $$; + +-- -- Set default privileges for future objects +-- ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT ALL ON TABLES TO prisma; +-- ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT ALL ON SEQUENCES TO prisma; +-- ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT ALL ON FUNCTIONS TO prisma; +-- ALTER DEFAULT PRIVILEGES IN SCHEMA gis GRANT ALL ON TABLES TO prisma; +-- ALTER DEFAULT PRIVILEGES IN SCHEMA gis GRANT ALL ON SEQUENCES TO prisma; +-- ALTER DEFAULT PRIVILEGES IN SCHEMA gis GRANT ALL ON FUNCTIONS TO prisma; +-- ALTER DEFAULT PRIVILEGES IN SCHEMA auth GRANT SELECT, DELETE ON TABLES TO prisma; +-- ALTER DEFAULT PRIVILEGES IN SCHEMA storage GRANT SELECT, DELETE ON TABLES TO prisma; + +-- -- Ensure the prisma role has the necessary permissions for the auth schema triggers +-- DO $$ +-- BEGIN +-- EXECUTE 'GRANT EXECUTE ON FUNCTION public.handle_new_user() TO prisma'; +-- EXCEPTION WHEN OTHERS THEN +-- RAISE NOTICE 'Error granting execute on function public.handle_new_user(): %', SQLERRM; +-- END $$; + +-- DO $$ +-- BEGIN +-- EXECUTE 'GRANT EXECUTE ON FUNCTION public.handle_user_delete() TO prisma'; +-- EXCEPTION WHEN OTHERS THEN +-- RAISE NOTICE 'Error granting execute on function public.handle_user_delete(): %', SQLERRM; +-- END $$; + +-- DO $$ +-- BEGIN +-- EXECUTE 'GRANT EXECUTE ON FUNCTION public.handle_user_update() TO prisma'; +-- EXCEPTION WHEN OTHERS THEN +-- RAISE NOTICE 'Error granting execute on function public.handle_user_update(): %', SQLERRM; +-- END $$; + +-- -- Grant postgres user the ability to manage prisma role +-- GRANT prisma TO postgres; diff --git a/sigap-website/prisma/backups/20250421115005_remote_schema.sql b/sigap-website/prisma/backups/20250421115005_remote_schema.sql new file mode 100644 index 0000000..b9b37f0 --- /dev/null +++ b/sigap-website/prisma/backups/20250421115005_remote_schema.sql @@ -0,0 +1,327 @@ +grant delete on table "auth"."audit_log_entries" to "prisma"; + +grant select on table "auth"."audit_log_entries" to "prisma"; + +grant delete on table "auth"."flow_state" to "prisma"; + +grant select on table "auth"."flow_state" to "prisma"; + +grant delete on table "auth"."identities" to "prisma"; + +grant select on table "auth"."identities" to "prisma"; + +grant delete on table "auth"."instances" to "prisma"; + +grant select on table "auth"."instances" to "prisma"; + +grant delete on table "auth"."mfa_amr_claims" to "prisma"; + +grant select on table "auth"."mfa_amr_claims" to "prisma"; + +grant delete on table "auth"."mfa_challenges" to "prisma"; + +grant select on table "auth"."mfa_challenges" to "prisma"; + +grant delete on table "auth"."mfa_factors" to "prisma"; + +grant select on table "auth"."mfa_factors" to "prisma"; + +grant delete on table "auth"."one_time_tokens" to "prisma"; + +grant select on table "auth"."one_time_tokens" to "prisma"; + +grant delete on table "auth"."refresh_tokens" to "prisma"; + +grant select on table "auth"."refresh_tokens" to "prisma"; + +grant delete on table "auth"."saml_providers" to "prisma"; + +grant select on table "auth"."saml_providers" to "prisma"; + +grant delete on table "auth"."saml_relay_states" to "prisma"; + +grant select on table "auth"."saml_relay_states" to "prisma"; + +grant delete on table "auth"."schema_migrations" to "prisma"; + +grant select on table "auth"."schema_migrations" to "prisma"; + +grant delete on table "auth"."sessions" to "prisma"; + +grant select on table "auth"."sessions" to "prisma"; + +grant delete on table "auth"."sso_domains" to "prisma"; + +grant select on table "auth"."sso_domains" to "prisma"; + +grant delete on table "auth"."sso_providers" to "prisma"; + +grant select on table "auth"."sso_providers" to "prisma"; + +grant delete on table "auth"."users" to "prisma"; + +grant select on table "auth"."users" to "prisma"; + +CREATE TRIGGER on_auth_user_created AFTER INSERT ON auth.users FOR EACH ROW EXECUTE FUNCTION handle_new_user(); + +CREATE TRIGGER on_auth_user_deleted BEFORE DELETE ON auth.users FOR EACH ROW EXECUTE FUNCTION handle_user_delete(); + +CREATE TRIGGER on_auth_user_updated AFTER UPDATE ON auth.users FOR EACH ROW EXECUTE FUNCTION handle_user_update(); + + +drop trigger if exists "objects_delete_delete_prefix" on "storage"."objects"; + +drop trigger if exists "objects_insert_create_prefix" on "storage"."objects"; + +drop trigger if exists "objects_update_create_prefix" on "storage"."objects"; + +drop trigger if exists "prefixes_create_hierarchy" on "storage"."prefixes"; + +drop trigger if exists "prefixes_delete_hierarchy" on "storage"."prefixes"; + +revoke delete on table "storage"."prefixes" from "anon"; + +revoke insert on table "storage"."prefixes" from "anon"; + +revoke references on table "storage"."prefixes" from "anon"; + +revoke select on table "storage"."prefixes" from "anon"; + +revoke trigger on table "storage"."prefixes" from "anon"; + +revoke truncate on table "storage"."prefixes" from "anon"; + +revoke update on table "storage"."prefixes" from "anon"; + +revoke delete on table "storage"."prefixes" from "authenticated"; + +revoke insert on table "storage"."prefixes" from "authenticated"; + +revoke references on table "storage"."prefixes" from "authenticated"; + +revoke select on table "storage"."prefixes" from "authenticated"; + +revoke trigger on table "storage"."prefixes" from "authenticated"; + +revoke truncate on table "storage"."prefixes" from "authenticated"; + +revoke update on table "storage"."prefixes" from "authenticated"; + +revoke delete on table "storage"."prefixes" from "service_role"; + +revoke insert on table "storage"."prefixes" from "service_role"; + +revoke references on table "storage"."prefixes" from "service_role"; + +revoke select on table "storage"."prefixes" from "service_role"; + +revoke trigger on table "storage"."prefixes" from "service_role"; + +revoke truncate on table "storage"."prefixes" from "service_role"; + +revoke update on table "storage"."prefixes" from "service_role"; + +alter table "storage"."prefixes" drop constraint "prefixes_bucketId_fkey"; + +drop function if exists "storage"."add_prefixes"(_bucket_id text, _name text); + +drop function if exists "storage"."delete_prefix"(_bucket_id text, _name text); + +drop function if exists "storage"."delete_prefix_hierarchy_trigger"(); + +drop function if exists "storage"."get_level"(name text); + +drop function if exists "storage"."get_prefix"(name text); + +drop function if exists "storage"."get_prefixes"(name text); + +drop function if exists "storage"."objects_insert_prefix_trigger"(); + +drop function if exists "storage"."objects_update_prefix_trigger"(); + +drop function if exists "storage"."prefixes_insert_trigger"(); + +drop function if exists "storage"."search_legacy_v1"(prefix text, bucketname text, limits integer, levels integer, offsets integer, search text, sortcolumn text, sortorder text); + +drop function if exists "storage"."search_v1_optimised"(prefix text, bucketname text, limits integer, levels integer, offsets integer, search text, sortcolumn text, sortorder text); + +drop function if exists "storage"."search_v2"(prefix text, bucket_name text, limits integer, levels integer, start_after text); + +alter table "storage"."prefixes" drop constraint "prefixes_pkey"; + +drop index if exists "storage"."idx_name_bucket_level_unique"; + +drop index if exists "storage"."idx_objects_lower_name"; + +drop index if exists "storage"."idx_prefixes_lower_name"; + +drop index if exists "storage"."objects_bucket_id_level_idx"; + +drop index if exists "storage"."prefixes_pkey"; + +drop table "storage"."prefixes"; + +alter table "storage"."objects" drop column "level"; + +set check_function_bodies = off; + +CREATE OR REPLACE FUNCTION storage.extension(name text) + RETURNS text + LANGUAGE plpgsql +AS $function$ +DECLARE +_parts text[]; +_filename text; +BEGIN + select string_to_array(name, '/') into _parts; + select _parts[array_length(_parts,1)] into _filename; + -- @todo return the last part instead of 2 + return reverse(split_part(reverse(_filename), '.', 1)); +END +$function$ +; + +CREATE OR REPLACE FUNCTION storage.foldername(name text) + RETURNS text[] + LANGUAGE plpgsql +AS $function$ +DECLARE +_parts text[]; +BEGIN + select string_to_array(name, '/') into _parts; + return _parts[1:array_length(_parts,1)-1]; +END +$function$ +; + +CREATE OR REPLACE FUNCTION storage.get_size_by_bucket() + RETURNS TABLE(size bigint, bucket_id text) + LANGUAGE plpgsql +AS $function$ +BEGIN + return query + select sum((metadata->>'size')::int) as size, obj.bucket_id + from "storage".objects as obj + group by obj.bucket_id; +END +$function$ +; + +CREATE OR REPLACE FUNCTION storage.search(prefix text, bucketname text, limits integer DEFAULT 100, levels integer DEFAULT 1, offsets integer DEFAULT 0, search text DEFAULT ''::text, sortcolumn text DEFAULT 'name'::text, sortorder text DEFAULT 'asc'::text) + RETURNS TABLE(name text, id uuid, updated_at timestamp with time zone, created_at timestamp with time zone, last_accessed_at timestamp with time zone, metadata jsonb) + LANGUAGE plpgsql + STABLE +AS $function$ +declare + v_order_by text; + v_sort_order text; +begin + case + when sortcolumn = 'name' then + v_order_by = 'name'; + when sortcolumn = 'updated_at' then + v_order_by = 'updated_at'; + when sortcolumn = 'created_at' then + v_order_by = 'created_at'; + when sortcolumn = 'last_accessed_at' then + v_order_by = 'last_accessed_at'; + else + v_order_by = 'name'; + end case; + + case + when sortorder = 'asc' then + v_sort_order = 'asc'; + when sortorder = 'desc' then + v_sort_order = 'desc'; + else + v_sort_order = 'asc'; + end case; + + v_order_by = v_order_by || ' ' || v_sort_order; + + return query execute + 'with folders as ( + select path_tokens[$1] as folder + from storage.objects + where objects.name ilike $2 || $3 || ''%'' + and bucket_id = $4 + and array_length(objects.path_tokens, 1) <> $1 + group by folder + order by folder ' || v_sort_order || ' + ) + (select folder as "name", + null as id, + null as updated_at, + null as created_at, + null as last_accessed_at, + null as metadata from folders) + union all + (select path_tokens[$1] as "name", + id, + updated_at, + created_at, + last_accessed_at, + metadata + from storage.objects + where objects.name ilike $2 || $3 || ''%'' + and bucket_id = $4 + and array_length(objects.path_tokens, 1) = $1 + order by ' || v_order_by || ') + limit $5 + offset $6' using levels, prefix, search, bucketname, limits, offsets; +end; +$function$ +; + +create policy "Anyone can update their own avatar." +on "storage"."objects" +as permissive +for update +to public +using ((( SELECT auth.uid() AS uid) = owner)) +with check ((bucket_id = 'avatars'::text)); + + +create policy "allow all 1oj01fe_0" +on "storage"."objects" +as permissive +for delete +to authenticated +using ((bucket_id = 'avatars'::text)); + + +create policy "allow all 1oj01fe_1" +on "storage"."objects" +as permissive +for update +to authenticated +using ((bucket_id = 'avatars'::text)); + + +create policy "allow all 1oj01fe_2" +on "storage"."objects" +as permissive +for insert +to authenticated +with check ((bucket_id = 'avatars'::text)); + + +create policy "allow all 1oj01fe_3" +on "storage"."objects" +as permissive +for select +to authenticated +using ((bucket_id = 'avatars'::text)); + + + +drop type "gis"."geometry_dump"; + +drop type "gis"."valid_detail"; + +create type "gis"."geometry_dump" as ("path" integer[], "geom" geometry); + +create type "gis"."valid_detail" as ("valid" boolean, "reason" character varying, "location" geometry); + + diff --git a/sigap-website/prisma/backups/20250421115206_grant_privileges_to_prisma.sql b/sigap-website/prisma/backups/20250421115206_grant_privileges_to_prisma.sql new file mode 100644 index 0000000..3322f9d --- /dev/null +++ b/sigap-website/prisma/backups/20250421115206_grant_privileges_to_prisma.sql @@ -0,0 +1,160 @@ +-- Grant usage on all necessary schemas +GRANT USAGE ON SCHEMA public TO prisma; +GRANT USAGE ON SCHEMA gis TO prisma; +GRANT USAGE ON SCHEMA auth TO prisma; +GRANT USAGE ON SCHEMA storage TO prisma; +GRANT USAGE ON SCHEMA graphql TO prisma; +GRANT USAGE ON SCHEMA extensions TO prisma; + +-- Explicitly grant permissions on auth and storage schemas +DO $$ +BEGIN + -- Explicitly grant on auth schema + EXECUTE 'GRANT USAGE ON SCHEMA auth TO prisma'; + -- Explicitly grant on storage schema + EXECUTE 'GRANT USAGE ON SCHEMA storage TO prisma'; +END +$$; + +-- Grant privileges on all tables in schemas +DO $$ +DECLARE + r RECORD; +BEGIN + -- Grant privileges on all tables in public schema + FOR r IN SELECT tablename FROM pg_tables WHERE schemaname = 'public' LOOP + EXECUTE 'GRANT ALL PRIVILEGES ON TABLE public.' || quote_ident(r.tablename) || ' TO prisma'; + END LOOP; + + -- Grant privileges on all tables in gis schema + FOR r IN SELECT tablename FROM pg_tables WHERE schemaname = 'gis' LOOP + EXECUTE 'GRANT ALL PRIVILEGES ON TABLE gis.' || quote_ident(r.tablename) || ' TO prisma'; + END LOOP; + + -- Grant privileges on all tables in auth schema + FOR r IN SELECT tablename FROM pg_tables WHERE schemaname = 'auth' LOOP + EXECUTE 'GRANT SELECT, DELETE ON TABLE auth.' || quote_ident(r.tablename) || ' TO prisma'; + END LOOP; + + -- Grant privileges on all tables in storage schema + FOR r IN SELECT tablename FROM pg_tables WHERE schemaname = 'storage' LOOP + EXECUTE 'GRANT SELECT, DELETE ON TABLE storage.' || quote_ident(r.tablename) || ' TO prisma'; + END LOOP; + + -- Grant privileges on all sequences in public schema + FOR r IN SELECT sequence_name FROM information_schema.sequences WHERE sequence_schema = 'public' LOOP + EXECUTE 'GRANT ALL PRIVILEGES ON SEQUENCE public.' || quote_ident(r.sequence_name) || ' TO prisma'; + END LOOP; + + -- Grant privileges on all sequences in gis schema + FOR r IN SELECT sequence_name FROM information_schema.sequences WHERE sequence_schema = 'gis' LOOP + EXECUTE 'GRANT ALL PRIVILEGES ON SEQUENCE gis.' || quote_ident(r.sequence_name) || ' TO prisma'; + END LOOP; + + -- Grant privileges on all sequences in auth schema + FOR r IN SELECT sequence_name FROM information_schema.sequences WHERE sequence_schema = 'auth' LOOP + EXECUTE 'GRANT USAGE ON SEQUENCE auth.' || quote_ident(r.sequence_name) || ' TO prisma'; + END LOOP; + + -- Grant privileges on all sequences in storage schema + FOR r IN SELECT sequence_name FROM information_schema.sequences WHERE sequence_schema = 'storage' LOOP + EXECUTE 'GRANT USAGE ON SEQUENCE storage.' || quote_ident(r.sequence_name) || ' TO prisma'; + END LOOP; + + -- Grant usage on all types in public schema + EXECUTE 'GRANT USAGE ON TYPE "public"."crime_rates" TO prisma'; + EXECUTE 'GRANT USAGE ON TYPE "public"."crime_status" TO prisma'; + EXECUTE 'GRANT USAGE ON TYPE "public"."session_status" TO prisma'; + EXECUTE 'GRANT USAGE ON TYPE "public"."status_contact_messages" TO prisma'; + EXECUTE 'GRANT USAGE ON TYPE "public"."unit_type" TO prisma'; +END +$$; + +-- Grant execute privileges on functions (separate DO block to avoid EXCEPTION issues) +DO $$ +DECLARE + r RECORD; +BEGIN + -- Grant execute privileges on all functions in public schema + FOR r IN SELECT routines.routine_name + FROM information_schema.routines + WHERE routines.specific_schema = 'public' + AND routines.routine_type = 'FUNCTION' LOOP + BEGIN + EXECUTE 'GRANT EXECUTE ON FUNCTION public.' || quote_ident(r.routine_name) || '() TO prisma'; + EXCEPTION WHEN OTHERS THEN + RAISE NOTICE 'Error granting execute on function public.%: %', r.routine_name, SQLERRM; + END; + END LOOP; +END +$$; + +-- Handle gis functions in a separate block - with enhanced function existence checking +DO $$ +DECLARE + r RECORD; + function_exists BOOLEAN; +BEGIN + -- Grant execute privileges on all functions in gis schema + FOR r IN SELECT routines.routine_name, routines.routine_schema, + array_to_string(array_agg(parameters.parameter_mode || ' ' || + parameters.data_type), ', ') AS params + FROM information_schema.routines + LEFT JOIN information_schema.parameters ON + routines.specific_schema = parameters.specific_schema AND + routines.specific_name = parameters.specific_name + WHERE routines.specific_schema = 'gis' + AND routines.routine_type = 'FUNCTION' + GROUP BY routines.routine_name, routines.routine_schema + LOOP + BEGIN + -- Check if function exists with proper arguments + EXECUTE format('SELECT EXISTS(SELECT 1 FROM pg_proc p JOIN pg_namespace n ON p.pronamespace = n.oid WHERE n.nspname = %L AND p.proname = %L)', + r.routine_schema, r.routine_name) + INTO function_exists; + + IF function_exists THEN + -- Use format to avoid '()' issue + EXECUTE format('GRANT EXECUTE ON FUNCTION %I.%I TO prisma', r.routine_schema, r.routine_name); + END IF; + EXCEPTION WHEN OTHERS THEN + RAISE NOTICE 'Error granting execute on function %.%: %', r.routine_schema, r.routine_name, SQLERRM; + END; + END LOOP; +END +$$; + +-- Set default privileges for future objects +ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT ALL ON TABLES TO prisma; +ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT ALL ON SEQUENCES TO prisma; +ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT ALL ON FUNCTIONS TO prisma; +ALTER DEFAULT PRIVILEGES IN SCHEMA gis GRANT ALL ON TABLES TO prisma; +ALTER DEFAULT PRIVILEGES IN SCHEMA gis GRANT ALL ON SEQUENCES TO prisma; +ALTER DEFAULT PRIVILEGES IN SCHEMA gis GRANT ALL ON FUNCTIONS TO prisma; +ALTER DEFAULT PRIVILEGES IN SCHEMA auth GRANT SELECT, DELETE ON TABLES TO prisma; +ALTER DEFAULT PRIVILEGES IN SCHEMA storage GRANT SELECT, DELETE ON TABLES TO prisma; + +-- Ensure the prisma role has the necessary permissions for the auth schema triggers +DO $$ +BEGIN + EXECUTE 'GRANT EXECUTE ON FUNCTION public.handle_new_user() TO prisma'; +EXCEPTION WHEN OTHERS THEN + RAISE NOTICE 'Error granting execute on function public.handle_new_user(): %', SQLERRM; +END $$; + +DO $$ +BEGIN + EXECUTE 'GRANT EXECUTE ON FUNCTION public.handle_user_delete() TO prisma'; +EXCEPTION WHEN OTHERS THEN + RAISE NOTICE 'Error granting execute on function public.handle_user_delete(): %', SQLERRM; +END $$; + +DO $$ +BEGIN + EXECUTE 'GRANT EXECUTE ON FUNCTION public.handle_user_update() TO prisma'; +EXCEPTION WHEN OTHERS THEN + RAISE NOTICE 'Error granting execute on function public.handle_user_update(): %', SQLERRM; +END $$; + +-- Grant postgres user the ability to manage prisma role +GRANT prisma TO postgres; diff --git a/sigap-website/prisma/backups/20250424033411_grant_deafult_previleges.sql b/sigap-website/prisma/backups/20250424033411_grant_deafult_previleges.sql new file mode 100644 index 0000000..a562cbf --- /dev/null +++ b/sigap-website/prisma/backups/20250424033411_grant_deafult_previleges.sql @@ -0,0 +1,11 @@ +grant all privileges on all tables in schema public to postgres, anon, authenticated, service_role, prisma; +grant all privileges on all functions in schema public to postgres, anon, authenticated, service_role, prisma; +grant all privileges on all sequences in schema public to postgres, anon, authenticated, service_role, prisma; + +alter default privileges in schema public grant all on tables to postgres, anon, authenticated, service_role, prisma; +alter default privileges in schema public grant all on functions to postgres, anon, authenticated, service_role, prisma; +alter default privileges in schema public grant all on sequences to postgres, anon, authenticated, service_role, prisma; + +grant usage on schema "public" to anon; +grant usage on schema "public" to authenticated; +grant usage on schema "public" to prisma; \ No newline at end of file diff --git a/sigap-website/prisma/backups/20250424180834_grant_deafult_previleges_again.sql b/sigap-website/prisma/backups/20250424180834_grant_deafult_previleges_again.sql new file mode 100644 index 0000000..a562cbf --- /dev/null +++ b/sigap-website/prisma/backups/20250424180834_grant_deafult_previleges_again.sql @@ -0,0 +1,11 @@ +grant all privileges on all tables in schema public to postgres, anon, authenticated, service_role, prisma; +grant all privileges on all functions in schema public to postgres, anon, authenticated, service_role, prisma; +grant all privileges on all sequences in schema public to postgres, anon, authenticated, service_role, prisma; + +alter default privileges in schema public grant all on tables to postgres, anon, authenticated, service_role, prisma; +alter default privileges in schema public grant all on functions to postgres, anon, authenticated, service_role, prisma; +alter default privileges in schema public grant all on sequences to postgres, anon, authenticated, service_role, prisma; + +grant usage on schema "public" to anon; +grant usage on schema "public" to authenticated; +grant usage on schema "public" to prisma; \ No newline at end of file diff --git a/sigap-website/prisma/seeds/crime-category.ts b/sigap-website/prisma/seeds/crime-category.ts index 29a391b..fcbc6d3 100644 --- a/sigap-website/prisma/seeds/crime-category.ts +++ b/sigap-website/prisma/seeds/crime-category.ts @@ -36,7 +36,7 @@ export class CrimeCategoriesSeeder { const data = XLSX.utils.sheet_to_json(sheet) as ICrimeCategory[]; for (const category of crimeCategoriesData) { - const newId = generateId({ + const newId = await generateId({ prefix: 'CC', segments: { sequentialDigits: 4, @@ -44,6 +44,8 @@ export class CrimeCategoriesSeeder { randomSequence: false, uniquenessStrategy: 'counter', separator: '-', + tableName: 'crime_categories', + storage: 'database', }); await this.prisma.crime_categories.create({ diff --git a/sigap-website/prisma/seeds/crime-incidents.ts b/sigap-website/prisma/seeds/crime-incidents.ts index 0ecda55..731b220 100644 --- a/sigap-website/prisma/seeds/crime-incidents.ts +++ b/sigap-website/prisma/seeds/crime-incidents.ts @@ -409,7 +409,7 @@ export class CrimeIncidentsSeeder { } // Generate a unique ID for the incident - const incidentId = generateId({ + const incidentId = await generateId({ prefix: 'CI', segments: { codes: [district.cities.id], @@ -420,6 +420,8 @@ export class CrimeIncidentsSeeder { separator: '-', randomSequence: false, uniquenessStrategy: 'counter', + storage: 'database', + tableName: 'crime_incidents', }); // Determine status based on crime_cleared diff --git a/sigap-website/prisma/seeds/crimes.ts b/sigap-website/prisma/seeds/crimes.ts index f30db3b..f099870 100644 --- a/sigap-website/prisma/seeds/crimes.ts +++ b/sigap-website/prisma/seeds/crimes.ts @@ -177,7 +177,7 @@ export class CrimesSeeder { const year = parseInt(record.year); // Create a unique ID for monthly crime data - const crimeId = generateId({ + const crimeId = await generateId({ prefix: 'CR', segments: { codes: [city.id], @@ -188,6 +188,8 @@ export class CrimesSeeder { separator: '-', randomSequence: false, uniquenessStrategy: 'counter', + storage: 'database', + tableName: 'crimes', }); await this.prisma.crimes.create({ @@ -257,7 +259,7 @@ export class CrimesSeeder { } // Create a unique ID for yearly crime data - const crimeId = generateId({ + const crimeId = await generateId({ prefix: 'CR', segments: { codes: [city.id], @@ -268,6 +270,8 @@ export class CrimesSeeder { separator: '-', randomSequence: false, uniquenessStrategy: 'counter', + storage: 'database', + tableName: 'crimes', }); await this.prisma.crimes.create({ @@ -333,7 +337,7 @@ export class CrimesSeeder { } // Create a unique ID for all-year summary data - const crimeId = generateId({ + const crimeId = await generateId({ prefix: 'CR', segments: { codes: [city.id], @@ -343,6 +347,8 @@ export class CrimesSeeder { separator: '-', randomSequence: false, uniquenessStrategy: 'counter', + storage: 'database', + tableName: 'crimes', }); await this.prisma.crimes.create({ diff --git a/sigap-website/prisma/seeds/units.ts b/sigap-website/prisma/seeds/units.ts index e3fe6f4..4deeeb7 100644 --- a/sigap-website/prisma/seeds/units.ts +++ b/sigap-website/prisma/seeds/units.ts @@ -89,7 +89,7 @@ export class UnitSeeder { const address = location.address; const phone = location.telepon?.replace(/-/g, ''); - const code_unit = generateId({ + const newId = await generateId({ prefix: 'UT', format: '{prefix}-{sequence}', separator: '-', @@ -98,11 +98,13 @@ export class UnitSeeder { segments: { sequentialDigits: 4, }, + storage: 'database', + tableName: 'units', }); let locationData: CreateLocationDto = { district_id: city.districts[0].id, // This will be replaced with Patrang's ID - code_unit: code_unit, + code_unit: newId, name: `Polres ${city.name}`, description: `Unit ${city.name} is categorized as POLRES and operates in the ${city.name} area.`, type: 'polres', @@ -152,7 +154,7 @@ export class UnitSeeder { const address = location.address; const phone = location.telepon?.replace(/-/g, ''); - const code_unit = generateId({ + const newId = await generateId({ prefix: 'UT', format: '{prefix}-{sequence}', separator: '-', @@ -161,11 +163,13 @@ export class UnitSeeder { segments: { sequentialDigits: 4, }, + storage: 'database', + tableName: 'units', }); const locationData: CreateLocationDto = { district_id: district.id, - code_unit, + code_unit: newId, name: `Polsek ${district.name}`, description: `Unit ${district.name} is categorized as POLSEK and operates in the ${district.name} area.`, type: 'polsek', @@ -187,7 +191,7 @@ export class UnitSeeder { } console.log( - `Inserted unit for district: ${district.name}, code_unit: ${code_unit} at ${lng}, ${lat}` + `Inserted unit for district: ${district.name}, newId: ${newId} at ${lng}, ${lat}` ); } } diff --git a/sigap-website/supabase/migrations/20250421115005_remote_schema.sql b/sigap-website/supabase/migrations/20250421115005_remote_schema.sql index b9b37f0..740011a 100644 --- a/sigap-website/supabase/migrations/20250421115005_remote_schema.sql +++ b/sigap-website/supabase/migrations/20250421115005_remote_schema.sql @@ -68,60 +68,51 @@ CREATE TRIGGER on_auth_user_deleted BEFORE DELETE ON auth.users FOR EACH ROW EXE CREATE TRIGGER on_auth_user_updated AFTER UPDATE ON auth.users FOR EACH ROW EXECUTE FUNCTION handle_user_update(); - drop trigger if exists "objects_delete_delete_prefix" on "storage"."objects"; drop trigger if exists "objects_insert_create_prefix" on "storage"."objects"; drop trigger if exists "objects_update_create_prefix" on "storage"."objects"; -drop trigger if exists "prefixes_create_hierarchy" on "storage"."prefixes"; - -drop trigger if exists "prefixes_delete_hierarchy" on "storage"."prefixes"; - -revoke delete on table "storage"."prefixes" from "anon"; - -revoke insert on table "storage"."prefixes" from "anon"; - -revoke references on table "storage"."prefixes" from "anon"; - -revoke select on table "storage"."prefixes" from "anon"; - -revoke trigger on table "storage"."prefixes" from "anon"; - -revoke truncate on table "storage"."prefixes" from "anon"; - -revoke update on table "storage"."prefixes" from "anon"; - -revoke delete on table "storage"."prefixes" from "authenticated"; - -revoke insert on table "storage"."prefixes" from "authenticated"; - -revoke references on table "storage"."prefixes" from "authenticated"; - -revoke select on table "storage"."prefixes" from "authenticated"; - -revoke trigger on table "storage"."prefixes" from "authenticated"; - -revoke truncate on table "storage"."prefixes" from "authenticated"; - -revoke update on table "storage"."prefixes" from "authenticated"; - -revoke delete on table "storage"."prefixes" from "service_role"; - -revoke insert on table "storage"."prefixes" from "service_role"; - -revoke references on table "storage"."prefixes" from "service_role"; - -revoke select on table "storage"."prefixes" from "service_role"; - -revoke trigger on table "storage"."prefixes" from "service_role"; - -revoke truncate on table "storage"."prefixes" from "service_role"; - -revoke update on table "storage"."prefixes" from "service_role"; - -alter table "storage"."prefixes" drop constraint "prefixes_bucketId_fkey"; +DO $$ +BEGIN + IF EXISTS (SELECT FROM pg_catalog.pg_tables + WHERE schemaname = 'storage' + AND tablename = 'prefixes') THEN + + EXECUTE 'drop trigger if exists "prefixes_create_hierarchy" on "storage"."prefixes"'; + EXECUTE 'drop trigger if exists "prefixes_delete_hierarchy" on "storage"."prefixes"'; + + EXECUTE 'revoke delete on table "storage"."prefixes" from "anon"'; + EXECUTE 'revoke insert on table "storage"."prefixes" from "anon"'; + EXECUTE 'revoke references on table "storage"."prefixes" from "anon"'; + EXECUTE 'revoke select on table "storage"."prefixes" from "anon"'; + EXECUTE 'revoke trigger on table "storage"."prefixes" from "anon"'; + EXECUTE 'revoke truncate on table "storage"."prefixes" from "anon"'; + EXECUTE 'revoke update on table "storage"."prefixes" from "anon"'; + + EXECUTE 'revoke delete on table "storage"."prefixes" from "authenticated"'; + EXECUTE 'revoke insert on table "storage"."prefixes" from "authenticated"'; + EXECUTE 'revoke references on table "storage"."prefixes" from "authenticated"'; + EXECUTE 'revoke select on table "storage"."prefixes" from "authenticated"'; + EXECUTE 'revoke trigger on table "storage"."prefixes" from "authenticated"'; + EXECUTE 'revoke truncate on table "storage"."prefixes" from "authenticated"'; + EXECUTE 'revoke update on table "storage"."prefixes" from "authenticated"'; + + EXECUTE 'revoke delete on table "storage"."prefixes" from "service_role"'; + EXECUTE 'revoke insert on table "storage"."prefixes" from "service_role"'; + EXECUTE 'revoke references on table "storage"."prefixes" from "service_role"'; + EXECUTE 'revoke select on table "storage"."prefixes" from "service_role"'; + EXECUTE 'revoke trigger on table "storage"."prefixes" from "service_role"'; + EXECUTE 'revoke truncate on table "storage"."prefixes" from "service_role"'; + EXECUTE 'revoke update on table "storage"."prefixes" from "service_role"'; + + EXECUTE 'alter table "storage"."prefixes" drop constraint if exists "prefixes_bucketId_fkey"'; + EXECUTE 'alter table "storage"."prefixes" drop constraint if exists "prefixes_pkey"'; + + EXECUTE 'drop table "storage"."prefixes"'; + END IF; +END $$; drop function if exists "storage"."add_prefixes"(_bucket_id text, _name text); @@ -147,21 +138,41 @@ drop function if exists "storage"."search_v1_optimised"(prefix text, bucketname drop function if exists "storage"."search_v2"(prefix text, bucket_name text, limits integer, levels integer, start_after text); -alter table "storage"."prefixes" drop constraint "prefixes_pkey"; +DO $$ +BEGIN + IF EXISTS (SELECT 1 FROM pg_indexes WHERE schemaname = 'storage' AND indexname = 'idx_name_bucket_level_unique') THEN + EXECUTE 'drop index "storage"."idx_name_bucket_level_unique"'; + END IF; + + IF EXISTS (SELECT 1 FROM pg_indexes WHERE schemaname = 'storage' AND indexname = 'idx_objects_lower_name') THEN + EXECUTE 'drop index "storage"."idx_objects_lower_name"'; + END IF; + + IF EXISTS (SELECT 1 FROM pg_indexes WHERE schemaname = 'storage' AND indexname = 'idx_prefixes_lower_name') THEN + EXECUTE 'drop index "storage"."idx_prefixes_lower_name"'; + END IF; + + IF EXISTS (SELECT 1 FROM pg_indexes WHERE schemaname = 'storage' AND indexname = 'objects_bucket_id_level_idx') THEN + EXECUTE 'drop index "storage"."objects_bucket_id_level_idx"'; + END IF; + + IF EXISTS (SELECT 1 FROM pg_indexes WHERE schemaname = 'storage' AND indexname = 'prefixes_pkey') THEN + EXECUTE 'drop index "storage"."prefixes_pkey"'; + END IF; +END $$; -drop index if exists "storage"."idx_name_bucket_level_unique"; - -drop index if exists "storage"."idx_objects_lower_name"; - -drop index if exists "storage"."idx_prefixes_lower_name"; - -drop index if exists "storage"."objects_bucket_id_level_idx"; - -drop index if exists "storage"."prefixes_pkey"; - -drop table "storage"."prefixes"; - -alter table "storage"."objects" drop column "level"; +DO $$ +BEGIN + IF EXISTS ( + SELECT 1 + FROM information_schema.columns + WHERE table_schema = 'storage' + AND table_name = 'objects' + AND column_name = 'level' + ) THEN + EXECUTE 'alter table "storage"."objects" drop column "level"'; + END IF; +END $$; set check_function_bodies = off; @@ -316,12 +327,12 @@ using ((bucket_id = 'avatars'::text)); -drop type "gis"."geometry_dump"; +-- drop type "gis"."geometry_dump"; -drop type "gis"."valid_detail"; +-- drop type "gis"."valid_detail"; -create type "gis"."geometry_dump" as ("path" integer[], "geom" geometry); +-- create type "gis"."geometry_dump" as ("path" integer[], "geom" geometry); -create type "gis"."valid_detail" as ("valid" boolean, "reason" character varying, "location" geometry); +-- create type "gis"."valid_detail" as ("valid" boolean, "reason" character varying, "location" geometry); diff --git a/sigap-website/supabase/migrations/20250505150200_remote_schema.sql b/sigap-website/supabase/migrations/20250505150200_remote_schema.sql new file mode 100644 index 0000000..df57f72 --- /dev/null +++ b/sigap-website/supabase/migrations/20250505150200_remote_schema.sql @@ -0,0 +1,204 @@ +grant delete on table "storage"."s3_multipart_uploads" to "postgres"; + +grant insert on table "storage"."s3_multipart_uploads" to "postgres"; + +grant references on table "storage"."s3_multipart_uploads" to "postgres"; + +grant select on table "storage"."s3_multipart_uploads" to "postgres"; + +grant trigger on table "storage"."s3_multipart_uploads" to "postgres"; + +grant truncate on table "storage"."s3_multipart_uploads" to "postgres"; + +grant update on table "storage"."s3_multipart_uploads" to "postgres"; + +grant delete on table "storage"."s3_multipart_uploads_parts" to "postgres"; + +grant insert on table "storage"."s3_multipart_uploads_parts" to "postgres"; + +grant references on table "storage"."s3_multipart_uploads_parts" to "postgres"; + +grant select on table "storage"."s3_multipart_uploads_parts" to "postgres"; + +grant trigger on table "storage"."s3_multipart_uploads_parts" to "postgres"; + +grant truncate on table "storage"."s3_multipart_uploads_parts" to "postgres"; + +grant update on table "storage"."s3_multipart_uploads_parts" to "postgres"; + + +-- drop type "gis"."geometry_dump"; + +-- drop type "gis"."valid_detail"; + +set check_function_bodies = off; + +CREATE OR REPLACE FUNCTION gis.calculate_unit_incident_distances(p_unit_id character varying, p_district_id character varying DEFAULT NULL::character varying) + RETURNS TABLE(unit_code character varying, unit_name character varying, unit_lat double precision, unit_lng double precision, incident_id character varying, incident_description text, incident_lat double precision, incident_lng double precision, category_name character varying, district_name character varying, distance_meters double precision) + LANGUAGE plpgsql + SECURITY DEFINER +AS $function$ +BEGIN + RETURN QUERY + WITH unit_locations AS ( + SELECT + u.code_unit, + u.name, + u.latitude, + u.longitude, + u.district_id, + ST_SetSRID(ST_MakePoint(u.longitude, u.latitude), 4326)::geography AS location + FROM + units u + WHERE + (p_unit_id IS NULL OR u.code_unit = p_unit_id) + AND (p_district_id IS NULL OR u.district_id = p_district_id) + AND u.latitude IS NOT NULL + AND u.longitude IS NOT NULL + ), + incident_locations AS ( + SELECT + ci.id, + ci.description, + ci.crime_id, + ci.crime_category_id, + l.latitude, + l.longitude, + ST_SetSRID(ST_MakePoint(l.longitude, l.latitude), 4326)::geography AS location + FROM + crime_incidents ci + JOIN + locations l ON ci.location_id = l.id + WHERE + l.latitude IS NOT NULL + AND l.longitude IS NOT NULL + ) + SELECT + ul.code_unit as unit_code, + ul.name as unit_name, + ul.latitude as unit_lat, + ul.longitude as unit_lng, + il.id as incident_id, + il.description as incident_description, + il.latitude as incident_lat, + il.longitude as incident_lng, + cc.name as category_name, + d.name as district_name, + ST_Distance(ul.location, il.location) as distance_meters + FROM + unit_locations ul + JOIN + districts d ON ul.district_id = d.id + JOIN + crimes c ON c.district_id = d.id + JOIN + incident_locations il ON il.crime_id = c.id + JOIN + crime_categories cc ON il.crime_category_id = cc.id + ORDER BY + ul.code_unit, + ul.location <-> il.location; -- Use KNN operator for efficient ordering +END; +$function$ +; + +CREATE OR REPLACE FUNCTION gis.find_nearest_unit_to_incident(p_incident_id integer) + RETURNS TABLE(unit_code text, unit_name text, distance_meters double precision) + LANGUAGE plpgsql + SECURITY DEFINER +AS $function$ +BEGIN + RETURN QUERY + WITH incident_location AS ( + SELECT + ci.id, + ST_SetSRID(ST_MakePoint( + (ci.locations->>'longitude')::float, + (ci.locations->>'latitude')::float + ), 4326)::geography AS location + FROM + crime_incidents ci + WHERE + ci.id = p_incident_id + AND (ci.locations->>'latitude') IS NOT NULL + AND (ci.locations->>'longitude') IS NOT NULL + ), + unit_locations AS ( + SELECT + u.code_unit, + u.name, + ST_SetSRID(ST_MakePoint(u.longitude, u.latitude), 4326)::geography AS location + FROM + units u + WHERE + u.latitude IS NOT NULL + AND u.longitude IS NOT NULL + ) + SELECT + ul.code_unit as unit_code, + ul.name as unit_name, + ST_Distance(ul.location, il.location) as distance_meters + FROM + unit_locations ul + CROSS JOIN + incident_location il + ORDER BY + ul.location <-> il.location + LIMIT 1; +END; +$function$ +; + +CREATE OR REPLACE FUNCTION gis.find_units_within_distance(p_incident_id integer, p_max_distance_meters double precision DEFAULT 5000) + RETURNS TABLE(unit_code text, unit_name text, distance_meters double precision) + LANGUAGE plpgsql + SECURITY DEFINER +AS $function$ +BEGIN + RETURN QUERY + WITH incident_location AS ( + SELECT + ci.id, + ST_SetSRID(ST_MakePoint( + (ci.locations->>'longitude')::float, + (ci.locations->>'latitude')::float + ), 4326)::geography AS location + FROM + crime_incidents ci + WHERE + ci.id = p_incident_id + AND (ci.locations->>'latitude') IS NOT NULL + AND (ci.locations->>'longitude') IS NOT NULL + ), + unit_locations AS ( + SELECT + u.code_unit, + u.name, + ST_SetSRID(ST_MakePoint(u.longitude, u.latitude), 4326)::geography AS location + FROM + units u + WHERE + u.latitude IS NOT NULL + AND u.longitude IS NOT NULL + ) + SELECT + ul.code_unit as unit_code, + ul.name as unit_name, + ST_Distance(ul.location, il.location) as distance_meters + FROM + unit_locations ul + CROSS JOIN + incident_location il + WHERE + ST_DWithin(ul.location, il.location, p_max_distance_meters) + ORDER BY + ST_Distance(ul.location, il.location); +END; +$function$ +; + +-- create type "gis"."geometry_dump" as ("path" integer[], "geom" geometry); + +-- create type "gis"."valid_detail" as ("valid" boolean, "reason" character varying, "location" geometry); + + diff --git a/sigap-website/supabase/migrations/20250505150857_remote_schema.sql b/sigap-website/supabase/migrations/20250505150857_remote_schema.sql new file mode 100644 index 0000000..bc459fe --- /dev/null +++ b/sigap-website/supabase/migrations/20250505150857_remote_schema.sql @@ -0,0 +1,94 @@ +-- drop type "gis"."geometry_dump"; + +-- drop type "gis"."valid_detail"; + +set check_function_bodies = off; + +CREATE OR REPLACE FUNCTION gis.find_nearest_unit(p_incident_id character varying) + RETURNS TABLE(unit_code character varying, unit_name character varying, distance_meters double precision) + LANGUAGE plpgsql + SECURITY DEFINER +AS $function$ +BEGIN + RETURN QUERY + WITH incident_location AS ( + SELECT + ci.id, + l.location AS location + FROM + crime_incidents ci + JOIN + locations l ON ci.location_id = l.id + WHERE + ci.id = p_incident_id + ), + unit_locations AS ( + SELECT + u.code_unit, + u.name, + u.location + FROM + units u + ) + SELECT + ul.code_unit as unit_code, + ul.name as unit_name, + ST_Distance(ul.location, il.location) as distance_meters + FROM + unit_locations ul + CROSS JOIN + incident_location il + ORDER BY + ul.location <-> il.location + LIMIT 1; +END; +$function$ +; + +CREATE OR REPLACE FUNCTION gis.find_units_within_distance(p_incident_id character varying, p_max_distance_meters double precision DEFAULT 5000) + RETURNS TABLE(unit_code character varying, unit_name character varying, distance_meters double precision) + LANGUAGE plpgsql + SECURITY DEFINER +AS $function$ +BEGIN + RETURN QUERY + WITH incident_location AS ( + SELECT + ci.id, + l.location AS location + FROM + crime_incidents ci + JOIN + locations l ON ci.location_id = l.id + WHERE + ci.id = p_incident_id + ), + unit_locations AS ( + SELECT + u.code_unit, + u.name, + u.location + FROM + units u + ) + SELECT + ul.code_unit as unit_code, + ul.name as unit_name, + ST_Distance(ul.location, il.location) as distance_meters + FROM + unit_locations ul + CROSS JOIN + incident_location il + WHERE + ST_DWithin(ul.location, il.location, p_max_distance_meters) + ORDER BY + ST_Distance(ul.location, il.location); +END; +$function$ +; + +-- create type "gis"."geometry_dump" as ("path" integer[], "geom" geometry); + +-- create type "gis"."valid_detail" as ("valid" boolean, "reason" character varying, "location" geometry); + + diff --git a/sigap-website/supabase/migrations/20250505161323_remote_schema.sql b/sigap-website/supabase/migrations/20250505161323_remote_schema.sql new file mode 100644 index 0000000..9b69d21 --- /dev/null +++ b/sigap-website/supabase/migrations/20250505161323_remote_schema.sql @@ -0,0 +1,9 @@ +drop type "gis"."geometry_dump"; + +drop type "gis"."valid_detail"; + +create type "gis"."geometry_dump" as ("path" integer[], "geom" geometry); + +create type "gis"."valid_detail" as ("valid" boolean, "reason" character varying, "location" geometry); + +