Refactor database privileges and schema management
- Created migration scripts to manage the 'prisma' role and its privileges across various schemas including 'public', 'gis', 'auth', and 'storage'. - Added explicit grants for all necessary tables, sequences, and functions to ensure the 'prisma' role has appropriate access. - Implemented triggers for user management in the 'auth' schema to handle new user creation, updates, and deletions. - Established default privileges for future objects in the specified schemas to streamline permission management. - Updated remote schema management scripts to include grants for 'postgres' on specific tables and functions. - Dropped and recreated types in the 'gis' schema to ensure proper structure and functionality.
This commit is contained in:
parent
fa7651619b
commit
0747897fc7
|
@ -1,6 +1,12 @@
|
||||||
'use server';
|
'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 { getInjection } from '@/di/container';
|
||||||
import db from '@/prisma/db';
|
import db from '@/prisma/db';
|
||||||
import {
|
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<IDistanceResult[]> {
|
||||||
|
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 [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -228,8 +228,9 @@ export default function Layers({
|
||||||
// District fill should only be visible for incidents and clusters
|
// District fill should only be visible for incidents and clusters
|
||||||
const showDistrictFill = activeControl === "incidents" || activeControl === "clusters";
|
const showDistrictFill = activeControl === "incidents" || activeControl === "clusters";
|
||||||
|
|
||||||
// Only show incident markers for incidents and clusters views - exclude timeline mode
|
// Show incident markers for incidents, clusters, AND units modes
|
||||||
const showIncidentMarkers = (activeControl === "incidents" || activeControl === "clusters")
|
// But hide for heatmap and timeline
|
||||||
|
const showIncidentMarkers = activeControl !== "heatmap" && activeControl !== "timeline";
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
@ -270,7 +271,7 @@ export default function Layers({
|
||||||
useAllData={useAllData}
|
useAllData={useAllData}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* Units Layer */}
|
{/* Units Layer - always show incidents when Units is active */}
|
||||||
<UnitsLayer
|
<UnitsLayer
|
||||||
crimes={crimes}
|
crimes={crimes}
|
||||||
units={units}
|
units={units}
|
||||||
|
@ -290,18 +291,18 @@ export default function Layers({
|
||||||
|
|
||||||
{/* Cluster Layer - only enable when the clusters control is active and NOT in timeline mode */}
|
{/* Cluster Layer - only enable when the clusters control is active and NOT in timeline mode */}
|
||||||
<ClusterLayer
|
<ClusterLayer
|
||||||
visible={visible && !showTimelineLayer}
|
visible={visible && activeControl === "clusters"}
|
||||||
map={mapboxMap}
|
map={mapboxMap}
|
||||||
crimes={crimes}
|
crimes={crimes}
|
||||||
filterCategory={filterCategory}
|
filterCategory={filterCategory}
|
||||||
focusedDistrictId={focusedDistrictId}
|
focusedDistrictId={focusedDistrictId}
|
||||||
clusteringEnabled={showClustersLayer}
|
clusteringEnabled={activeControl === "clusters"}
|
||||||
showClusters={showClustersLayer}
|
showClusters={activeControl === "clusters"}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* Unclustered Points Layer - explicitly hide in timeline mode */}
|
{/* Unclustered Points Layer - now show for both incidents and units modes */}
|
||||||
<UnclusteredPointLayer
|
<UnclusteredPointLayer
|
||||||
visible={visible && showIncidentMarkers && !focusedDistrictId && !showTimelineLayer}
|
visible={visible && showIncidentMarkers && !focusedDistrictId}
|
||||||
map={mapboxMap}
|
map={mapboxMap}
|
||||||
crimes={crimes}
|
crimes={crimes}
|
||||||
filterCategory={filterCategory}
|
filterCategory={filterCategory}
|
||||||
|
|
|
@ -2,10 +2,15 @@
|
||||||
|
|
||||||
import { useEffect, useMemo, useRef, useState } from 'react'
|
import { useEffect, useMemo, useRef, useState } from 'react'
|
||||||
import { Layer, Source } from "react-map-gl/mapbox"
|
import { Layer, Source } from "react-map-gl/mapbox"
|
||||||
import { ICrimes } from "@/app/_utils/types/crimes"
|
import { ICrimes, IDistanceResult } from "@/app/_utils/types/crimes"
|
||||||
import { IUnits } from "@/app/_utils/types/units"
|
import { IUnits } from "@/app/_utils/types/units"
|
||||||
import mapboxgl from 'mapbox-gl'
|
import mapboxgl from 'mapbox-gl'
|
||||||
|
import { useQuery } from '@tanstack/react-query'
|
||||||
|
|
||||||
import { generateCategoryColorMap, getCategoryColor } from '@/app/_utils/colors'
|
import { generateCategoryColorMap, getCategoryColor } from '@/app/_utils/colors'
|
||||||
|
import UnitPopup from '../pop-up/unit-popup'
|
||||||
|
import IncidentPopup from '../pop-up/incident-popup'
|
||||||
|
import { calculateDistances } from '@/app/(pages)/(admin)/dashboard/crime-management/crime-overview/action'
|
||||||
|
|
||||||
interface UnitsLayerProps {
|
interface UnitsLayerProps {
|
||||||
crimes: ICrimes[]
|
crimes: ICrimes[]
|
||||||
|
@ -15,6 +20,23 @@ interface UnitsLayerProps {
|
||||||
map?: mapboxgl.Map | null
|
map?: mapboxgl.Map | null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Custom hook for fetching distance data
|
||||||
|
const useDistanceData = (entityId?: string, isUnit: boolean = false, districtId?: string) => {
|
||||||
|
// 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({
|
export default function UnitsLayer({
|
||||||
crimes,
|
crimes,
|
||||||
units = [],
|
units = [],
|
||||||
|
@ -25,6 +47,20 @@ export default function UnitsLayer({
|
||||||
const [loadedUnits, setLoadedUnits] = useState<IUnits[]>([])
|
const [loadedUnits, setLoadedUnits] = useState<IUnits[]>([])
|
||||||
const loadedUnitsRef = useRef<IUnits[]>([])
|
const loadedUnitsRef = useRef<IUnits[]>([])
|
||||||
|
|
||||||
|
// For popups
|
||||||
|
const [selectedUnit, setSelectedUnit] = useState<IUnits | null>(null)
|
||||||
|
const [selectedIncident, setSelectedIncident] = useState<any | null>(null)
|
||||||
|
const [selectedEntityId, setSelectedEntityId] = useState<string | undefined>()
|
||||||
|
const [isUnitSelected, setIsUnitSelected] = useState<boolean>(false)
|
||||||
|
const [selectedDistrictId, setSelectedDistrictId] = useState<string | undefined>()
|
||||||
|
|
||||||
|
// Use react-query for distance data
|
||||||
|
const { data: distances = [], isLoading: isLoadingDistances } = useDistanceData(
|
||||||
|
selectedEntityId,
|
||||||
|
isUnitSelected,
|
||||||
|
selectedDistrictId
|
||||||
|
);
|
||||||
|
|
||||||
// Use either provided units or loaded units
|
// Use either provided units or loaded units
|
||||||
const unitsData = useMemo(() => {
|
const unitsData = useMemo(() => {
|
||||||
return units.length > 0 ? units : (loadedUnits || [])
|
return units.length > 0 ? units : (loadedUnits || [])
|
||||||
|
@ -74,6 +110,44 @@ export default function UnitsLayer({
|
||||||
}
|
}
|
||||||
}, [unitsData])
|
}, [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
|
// Create lines between units and incidents within their districts
|
||||||
const connectionLinesGeoJSON = useMemo(() => {
|
const connectionLinesGeoJSON = useMemo(() => {
|
||||||
if (!unitsData.length || !crimes.length) return {
|
if (!unitsData.length || !crimes.length) return {
|
||||||
|
@ -143,7 +217,6 @@ export default function UnitsLayer({
|
||||||
}
|
}
|
||||||
}, [unitsData, crimes, filterCategory, categoryColorMap])
|
}, [unitsData, crimes, filterCategory, categoryColorMap])
|
||||||
|
|
||||||
// Map click handler code and the rest remains the same...
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!map || !visible) return
|
if (!map || !visible) return
|
||||||
|
|
||||||
|
@ -155,22 +228,16 @@ export default function UnitsLayer({
|
||||||
|
|
||||||
if (!properties) return
|
if (!properties) return
|
||||||
|
|
||||||
// Create a popup for the unit
|
// Find the unit in our data
|
||||||
const popup = new mapboxgl.Popup()
|
const unit = unitsData.find(u => u.code_unit === properties.id);
|
||||||
.setLngLat(feature.geometry.type === 'Point' ?
|
if (!unit) return;
|
||||||
(feature.geometry as any).coordinates as [number, number] :
|
|
||||||
[0, 0]) // Fallback coordinates if not a Point geometry
|
// Set the selected unit and query parameters
|
||||||
.setHTML(`
|
setSelectedUnit(unit);
|
||||||
<div class="p-2">
|
setSelectedIncident(null); // Clear any selected incident
|
||||||
<h3 class="font-bold text-base">${properties.name}</h3>
|
setSelectedEntityId(properties.id);
|
||||||
<p class="text-sm">${properties.type}</p>
|
setIsUnitSelected(true);
|
||||||
<p class="text-sm">${properties.address || 'No address provided'}</p>
|
setSelectedDistrictId(properties.district_id);
|
||||||
<p class="text-xs mt-2">Staff: ${properties.staff_count || 'N/A'}</p>
|
|
||||||
<p class="text-xs">Phone: ${properties.phone || 'N/A'}</p>
|
|
||||||
<p class="text-xs">District: ${properties.district || 'N/A'}</p>
|
|
||||||
</div>
|
|
||||||
`)
|
|
||||||
.addTo(map)
|
|
||||||
|
|
||||||
// Highlight the connected lines for this unit
|
// Highlight the connected lines for this unit
|
||||||
if (map.getLayer('units-connection-lines')) {
|
if (map.getLayer('units-connection-lines')) {
|
||||||
|
@ -180,15 +247,49 @@ export default function UnitsLayer({
|
||||||
properties.id
|
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
|
// Define event handlers that can be referenced for both adding and removing
|
||||||
const handleMouseEnter = () => {
|
const handleMouseEnter = () => {
|
||||||
map.getCanvas().style.cursor = 'pointer'
|
map.getCanvas().style.cursor = 'pointer'
|
||||||
|
@ -207,14 +308,41 @@ export default function UnitsLayer({
|
||||||
map.on('mouseleave', 'units-points', handleMouseLeave)
|
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 () => {
|
return () => {
|
||||||
if (map.getLayer('units-points')) {
|
if (map.getLayer('units-points')) {
|
||||||
map.off('click', 'units-points', handleUnitClick)
|
map.off('click', 'units-points', handleUnitClick)
|
||||||
map.off('mouseenter', 'units-points', handleMouseEnter)
|
map.off('mouseenter', 'units-points', handleMouseEnter)
|
||||||
map.off('mouseleave', 'units-points', handleMouseLeave)
|
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
|
if (!visible) return null
|
||||||
|
|
||||||
|
@ -255,6 +383,22 @@ export default function UnitsLayer({
|
||||||
/>
|
/>
|
||||||
</Source>
|
</Source>
|
||||||
|
|
||||||
|
{/* Incidents Points */}
|
||||||
|
<Source id="incidents-source" type="geojson" data={incidentsGeoJSON}>
|
||||||
|
<Layer
|
||||||
|
id="incidents-points"
|
||||||
|
type="circle"
|
||||||
|
paint={{
|
||||||
|
'circle-radius': 6,
|
||||||
|
// Use the pre-computed color stored in the properties
|
||||||
|
'circle-color': ['get', 'categoryColor'],
|
||||||
|
'circle-stroke-width': 1,
|
||||||
|
'circle-stroke-color': '#ffffff',
|
||||||
|
'circle-opacity': 0.8
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Source>
|
||||||
|
|
||||||
{/* Connection Lines */}
|
{/* Connection Lines */}
|
||||||
<Source id="units-lines-source" type="geojson" data={connectionLinesGeoJSON}>
|
<Source id="units-lines-source" type="geojson" data={connectionLinesGeoJSON}>
|
||||||
<Layer
|
<Layer
|
||||||
|
@ -263,12 +407,45 @@ export default function UnitsLayer({
|
||||||
paint={{
|
paint={{
|
||||||
// Use the pre-computed color stored in the properties
|
// Use the pre-computed color stored in the properties
|
||||||
'line-color': ['get', 'lineColor'],
|
'line-color': ['get', 'lineColor'],
|
||||||
'line-width': 1.5,
|
'line-width': 3,
|
||||||
'line-opacity': 0.7,
|
'line-opacity': 0.9,
|
||||||
'line-dasharray': [1, 2] // Dashed line
|
'line-blur': 0.5,
|
||||||
|
'line-dasharray': [3, 1],
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</Source>
|
</Source>
|
||||||
|
|
||||||
|
{/* Custom Unit Popup */}
|
||||||
|
{selectedUnit && (
|
||||||
|
<UnitPopup
|
||||||
|
longitude={selectedUnit.longitude || 0}
|
||||||
|
latitude={selectedUnit.latitude || 0}
|
||||||
|
onClose={handleClosePopup}
|
||||||
|
unit={{
|
||||||
|
id: selectedUnit.code_unit,
|
||||||
|
name: selectedUnit.name,
|
||||||
|
type: selectedUnit.type,
|
||||||
|
address: selectedUnit.address || "No address",
|
||||||
|
phone: selectedUnit.phone || "No phone",
|
||||||
|
district: selectedUnit.districts?.name,
|
||||||
|
district_id: selectedUnit.district_id
|
||||||
|
}}
|
||||||
|
distances={distances}
|
||||||
|
isLoadingDistances={isLoadingDistances}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Custom Incident Popup */}
|
||||||
|
{selectedIncident && (
|
||||||
|
<IncidentPopup
|
||||||
|
longitude={selectedIncident.longitude}
|
||||||
|
latitude={selectedIncident.latitude}
|
||||||
|
onClose={handleClosePopup}
|
||||||
|
incident={selectedIncident}
|
||||||
|
distances={distances}
|
||||||
|
isLoadingDistances={isLoadingDistances}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -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 (
|
||||||
|
<div className={`p-4 rounded bg-white shadow-sm ${className}`}>
|
||||||
|
<h3 className="text-lg font-semibold mb-2">Distance Information</h3>
|
||||||
|
<Skeleton className="h-4 w-full mb-2" />
|
||||||
|
<Skeleton className="h-4 w-full mb-2" />
|
||||||
|
<Skeleton className="h-4 w-full" />
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!data) {
|
||||||
|
return (
|
||||||
|
<div className={`p-4 rounded bg-white shadow-sm ${className}`}>
|
||||||
|
<h3 className="text-lg font-semibold mb-2">Distance Information</h3>
|
||||||
|
<p className="text-sm text-gray-500">No distance data available</p>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!data.length) {
|
||||||
|
return (
|
||||||
|
<div className={`p-4 rounded bg-white shadow-sm ${className}`}>
|
||||||
|
<h3 className="text-lg font-semibold mb-2">Distance Information</h3>
|
||||||
|
<p className="text-sm text-gray-500">No distance data available</p>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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<string, { name: string, incidents: any[] }>) :
|
||||||
|
null
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={`p-4 rounded bg-white shadow-sm ${className}`}>
|
||||||
|
<h3 className="text-lg font-semibold mb-2">Distance Information</h3>
|
||||||
|
|
||||||
|
{unitId ? (
|
||||||
|
// Single unit view
|
||||||
|
<div>
|
||||||
|
<h4 className="font-medium text-sm">{data[0]?.unit_name || 'Selected Unit'}</h4>
|
||||||
|
<ul className="mt-2 space-y-2">
|
||||||
|
{data.map(item => (
|
||||||
|
<li key={item.incident_id} className="text-xs flex justify-between border-b pb-1">
|
||||||
|
<span className="font-medium">{item.category_name}</span>
|
||||||
|
<span className="text-gray-600">{formatDistance(item.distance_meters)}</span>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
// Multi-unit view (grouped)
|
||||||
|
<div className="space-y-4">
|
||||||
|
{unitGroups && Object.entries(unitGroups).map(([code, unit]) => (
|
||||||
|
<div key={code}>
|
||||||
|
<h4 className="font-medium text-sm">{unit.name}</h4>
|
||||||
|
<ul className="mt-1">
|
||||||
|
{unit.incidents.slice(0, 3).map(item => (
|
||||||
|
<li key={item.incident_id} className="text-xs flex justify-between border-b pb-1">
|
||||||
|
<span>{item.category_name}</span>
|
||||||
|
<span className="text-gray-600">{formatDistance(item.distance_meters)}</span>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
{unit.incidents.length > 3 && (
|
||||||
|
<li className="text-xs text-gray-500 mt-1">
|
||||||
|
+ {unit.incidents.length - 3} more incidents
|
||||||
|
</li>
|
||||||
|
)}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
|
@ -8,7 +8,7 @@ import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/app/_components/ui/t
|
||||||
import { Button } from "@/app/_components/ui/button"
|
import { Button } from "@/app/_components/ui/button"
|
||||||
import { getMonthName } from "@/app/_utils/common"
|
import { getMonthName } from "@/app/_utils/common"
|
||||||
import { BarChart, Users, Home, AlertTriangle, ChevronRight, Building, Calendar, X } from 'lucide-react'
|
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
|
// Helper function to format numbers
|
||||||
function formatNumber(num?: number): string {
|
function formatNumber(num?: number): string {
|
||||||
|
@ -29,7 +29,7 @@ interface DistrictPopupProps {
|
||||||
longitude: number
|
longitude: number
|
||||||
latitude: number
|
latitude: number
|
||||||
onClose: () => void
|
onClose: () => void
|
||||||
district: DistrictFeature
|
district: IDistrictFeature
|
||||||
year?: string
|
year?: string
|
||||||
month?: string
|
month?: string
|
||||||
filterCategory?: string | "all"
|
filterCategory?: string | "all"
|
||||||
|
|
|
@ -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 (
|
||||||
|
<Popup
|
||||||
|
longitude={longitude}
|
||||||
|
latitude={latitude}
|
||||||
|
closeButton={false}
|
||||||
|
closeOnClick={false}
|
||||||
|
onClose={onClose}
|
||||||
|
anchor="top"
|
||||||
|
maxWidth="320px"
|
||||||
|
className="incident-popup z-50"
|
||||||
|
>
|
||||||
|
<Card
|
||||||
|
className="bg-background p-0 w-full max-w-[320px] shadow-xl border-0 overflow-hidden border-l-4 border-l-red-600"
|
||||||
|
>
|
||||||
|
<div className="p-4 relative">
|
||||||
|
{/* Custom close button */}
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="icon"
|
||||||
|
className="absolute top-2 right-2 h-6 w-6 rounded-full bg-slate-100 hover:bg-slate-200 dark:bg-slate-800 dark:hover:bg-slate-700"
|
||||||
|
onClick={onClose}
|
||||||
|
>
|
||||||
|
<X className="h-4 w-4" />
|
||||||
|
<span className="sr-only">Close</span>
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<div className="flex items-center justify-between mb-3">
|
||||||
|
<h3 className="font-bold text-base flex items-center gap-1.5">
|
||||||
|
<AlertTriangle className="h-4 w-4 text-red-500" />
|
||||||
|
{incident.category || "Unknown Incident"}
|
||||||
|
</h3>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{incident.description && (
|
||||||
|
<div className="mb-3 bg-slate-50 dark:bg-slate-900/40 p-3 rounded-lg">
|
||||||
|
<p className="text-sm">
|
||||||
|
<FileText className="inline-block h-3.5 w-3.5 mr-1.5 align-text-top text-slate-500" />
|
||||||
|
{incident.description}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<Separator className="my-3" />
|
||||||
|
|
||||||
|
<div className="grid grid-cols-2 gap-2 text-sm">
|
||||||
|
{incident.district && (
|
||||||
|
<div className="col-span-2">
|
||||||
|
<p className="text-xs font-medium text-slate-500 dark:text-slate-400 mb-1">District</p>
|
||||||
|
<p className="flex items-center">
|
||||||
|
<Bookmark className="inline-block h-3.5 w-3.5 mr-1.5 shrink-0 text-purple-500" />
|
||||||
|
<span className="font-medium">{incident.district}</span>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{incident.date && (
|
||||||
|
<>
|
||||||
|
<div>
|
||||||
|
<p className="text-xs font-medium text-slate-500 dark:text-slate-400 mb-1">Date</p>
|
||||||
|
<p className="flex items-center">
|
||||||
|
<Calendar className="inline-block h-3.5 w-3.5 mr-1.5 shrink-0 text-blue-500" />
|
||||||
|
<span className="font-medium">{formatDate(incident.date)}</span>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p className="text-xs font-medium text-slate-500 dark:text-slate-400 mb-1">Time</p>
|
||||||
|
<p className="flex items-center">
|
||||||
|
<Clock className="inline-block h-3.5 w-3.5 mr-1.5 shrink-0 text-amber-500" />
|
||||||
|
<span className="font-medium">{formatTime(incident.date)}</span>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Distances to police units section */}
|
||||||
|
<Separator className="my-3" />
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<h4 className="text-sm font-medium mb-2">Nearby Police Units</h4>
|
||||||
|
|
||||||
|
{isLoadingDistances ? (
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Skeleton className="h-6 w-full" />
|
||||||
|
<Skeleton className="h-6 w-full" />
|
||||||
|
<Skeleton className="h-6 w-full" />
|
||||||
|
</div>
|
||||||
|
) : distances.length > 0 ? (
|
||||||
|
<ScrollArea className="h-[120px] rounded-md border p-2">
|
||||||
|
<div className="space-y-2">
|
||||||
|
{distances.map((item) => (
|
||||||
|
<div key={item.unit_code} className="flex justify-between items-center text-xs border-b pb-1">
|
||||||
|
<div>
|
||||||
|
<p className="font-medium">{item.unit_name || "Unknown Unit"}</p>
|
||||||
|
<p className="text-muted-foreground text-[10px]">
|
||||||
|
{item.unit_type || "Police Unit"}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<Badge variant="outline" className="ml-2">
|
||||||
|
{formatDistance(item.distance_meters)}
|
||||||
|
</Badge>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</ScrollArea>
|
||||||
|
) : (
|
||||||
|
<p className="text-xs text-muted-foreground text-center p-2">
|
||||||
|
No police units data available
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="mt-3 pt-3 border-t border-border">
|
||||||
|
<p className="text-xs text-muted-foreground flex items-center">
|
||||||
|
<Navigation className="inline-block h-3 w-3 mr-1 shrink-0" />
|
||||||
|
Coordinates: {latitude.toFixed(6)}, {longitude.toFixed(6)}
|
||||||
|
</p>
|
||||||
|
<p className="text-xs text-muted-foreground mt-1">ID: {incident.id}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
</Popup>
|
||||||
|
)
|
||||||
|
}
|
|
@ -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 (
|
||||||
|
<Popup
|
||||||
|
longitude={longitude}
|
||||||
|
latitude={latitude}
|
||||||
|
closeButton={false}
|
||||||
|
closeOnClick={false}
|
||||||
|
onClose={onClose}
|
||||||
|
anchor="top"
|
||||||
|
maxWidth="320px"
|
||||||
|
className="unit-popup z-50"
|
||||||
|
>
|
||||||
|
<Card
|
||||||
|
className="bg-background p-0 w-full max-w-[320px] shadow-xl border-0 overflow-hidden border-l-4 border-l-blue-700"
|
||||||
|
>
|
||||||
|
<div className="p-4 relative">
|
||||||
|
{/* Custom close button */}
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="icon"
|
||||||
|
className="absolute top-2 right-2 h-6 w-6 rounded-full bg-slate-100 hover:bg-slate-200 dark:bg-slate-800 dark:hover:bg-slate-700"
|
||||||
|
onClick={onClose}
|
||||||
|
>
|
||||||
|
<X className="h-4 w-4" />
|
||||||
|
<span className="sr-only">Close</span>
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<div className="flex items-center justify-between mb-3">
|
||||||
|
<h3 className="font-bold text-base flex items-center gap-1.5">
|
||||||
|
<Shield className="h-4 w-4 text-blue-700" />
|
||||||
|
{unit.name || "Police Unit"}
|
||||||
|
</h3>
|
||||||
|
<Badge variant="outline" className="bg-blue-100 text-blue-800 border-blue-200">
|
||||||
|
{unit.type || "Unit"}
|
||||||
|
</Badge>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-1 gap-2 text-sm">
|
||||||
|
{unit.address && (
|
||||||
|
<div>
|
||||||
|
<p className="text-xs font-medium text-slate-500 dark:text-slate-400 mb-1">Address</p>
|
||||||
|
<p className="flex items-center">
|
||||||
|
<MapPin className="inline-block h-3.5 w-3.5 mr-1.5 shrink-0 text-red-500" />
|
||||||
|
<span className="font-medium">{unit.address}</span>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{unit.phone && (
|
||||||
|
<div>
|
||||||
|
<p className="text-xs font-medium text-slate-500 dark:text-slate-400 mb-1">Contact</p>
|
||||||
|
<p className="flex items-center">
|
||||||
|
<Phone className="inline-block h-3.5 w-3.5 mr-1.5 shrink-0 text-green-500" />
|
||||||
|
<span className="font-medium">{unit.phone}</span>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{unit.district && (
|
||||||
|
<div>
|
||||||
|
<p className="text-xs font-medium text-slate-500 dark:text-slate-400 mb-1">District</p>
|
||||||
|
<p className="flex items-center">
|
||||||
|
<Building2 className="inline-block h-3.5 w-3.5 mr-1.5 shrink-0 text-purple-500" />
|
||||||
|
<span className="font-medium">{unit.district}</span>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Distances to incidents section */}
|
||||||
|
<Separator className="my-3" />
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<h4 className="text-sm font-medium mb-2 flex items-center">
|
||||||
|
<Compass className="h-4 w-4 mr-1.5 text-blue-600" />
|
||||||
|
Nearby Incidents
|
||||||
|
</h4>
|
||||||
|
|
||||||
|
{isLoadingDistances ? (
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Skeleton className="h-6 w-full" />
|
||||||
|
<Skeleton className="h-6 w-full" />
|
||||||
|
<Skeleton className="h-6 w-full" />
|
||||||
|
</div>
|
||||||
|
) : distances.length > 0 ? (
|
||||||
|
<ScrollArea className="h-[120px] rounded-md border p-2">
|
||||||
|
<div className="space-y-2">
|
||||||
|
{distances.map((item) => (
|
||||||
|
<div key={item.incident_id} className="flex justify-between items-center text-xs border-b pb-1">
|
||||||
|
<div>
|
||||||
|
<p className="font-medium">{item.category_name || "Unknown"}</p>
|
||||||
|
<p className="text-muted-foreground text-[10px] truncate" style={{ maxWidth: "160px" }}>
|
||||||
|
{item.incident_description || "No description"}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<Badge variant="outline" className="ml-2 whitespace-nowrap">
|
||||||
|
{formatDistance(item.distance_meters)}
|
||||||
|
</Badge>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</ScrollArea>
|
||||||
|
) : (
|
||||||
|
<p className="text-xs text-muted-foreground text-center p-2">
|
||||||
|
No incident data available
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="mt-3 pt-3 border-t border-border">
|
||||||
|
<p className="text-xs text-muted-foreground flex items-center">
|
||||||
|
<Navigation className="inline-block h-3 w-3 mr-1 shrink-0" />
|
||||||
|
Coordinates: {latitude.toFixed(6)}, {longitude.toFixed(6)}
|
||||||
|
</p>
|
||||||
|
<p className="text-xs text-muted-foreground mt-1">ID: {unit.id}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
</Popup>
|
||||||
|
)
|
||||||
|
}
|
File diff suppressed because one or more lines are too long
|
@ -15,9 +15,11 @@ import { districtsGeoJson } from '../../prisma/data/geojson/jember/districts';
|
||||||
// Used to track generated IDs
|
// Used to track generated IDs
|
||||||
const usedIdRegistry = new Set<string>();
|
const usedIdRegistry = new Set<string>();
|
||||||
|
|
||||||
// Add type definition for global counter
|
|
||||||
|
// Add type definition for global counter and registry
|
||||||
declare global {
|
declare global {
|
||||||
var __idCounter: number;
|
var __idCounter: number;
|
||||||
|
var __idRegistry: Record<string, string>;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -392,30 +394,12 @@ function formatDateV2(date: Date, formatStr: string): string {
|
||||||
* @param {boolean} options.upperCase - Convert result to uppercase
|
* @param {boolean} options.upperCase - Convert result to uppercase
|
||||||
* @returns {string} - Generated custom ID
|
* @returns {string} - Generated custom ID
|
||||||
*/
|
*/
|
||||||
/**
|
export async function generateId(
|
||||||
* 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(
|
|
||||||
options: {
|
options: {
|
||||||
prefix?: string;
|
prefix?: string;
|
||||||
segments?: {
|
segments?: {
|
||||||
codes?: string[];
|
codes?: string[];
|
||||||
year?: number | boolean; // Year diubah menjadi number | boolean
|
year?: number | boolean;
|
||||||
sequentialDigits?: number;
|
sequentialDigits?: number;
|
||||||
includeDate?: boolean;
|
includeDate?: boolean;
|
||||||
dateFormat?: string;
|
dateFormat?: string;
|
||||||
|
@ -429,10 +413,10 @@ export function generateId(
|
||||||
uniquenessStrategy?: 'uuid' | 'timestamp' | 'counter' | 'hash';
|
uniquenessStrategy?: 'uuid' | 'timestamp' | 'counter' | 'hash';
|
||||||
retryOnCollision?: boolean;
|
retryOnCollision?: boolean;
|
||||||
maxRetries?: number;
|
maxRetries?: number;
|
||||||
|
storage?: 'memory' | 'localStorage' | 'database';
|
||||||
|
tableName?: string; // Added table name for database interactions
|
||||||
} = {}
|
} = {}
|
||||||
): string {
|
): Promise<string> {
|
||||||
// Jika uniquenessStrategy tidak diatur dan randomSequence = false,
|
|
||||||
// gunakan counter sebagai strategi default
|
|
||||||
if (!options.uniquenessStrategy && options.randomSequence === false) {
|
if (!options.uniquenessStrategy && options.randomSequence === false) {
|
||||||
options.uniquenessStrategy = 'counter';
|
options.uniquenessStrategy = 'counter';
|
||||||
}
|
}
|
||||||
|
@ -441,7 +425,7 @@ export function generateId(
|
||||||
prefix: options.prefix || 'ID',
|
prefix: options.prefix || 'ID',
|
||||||
segments: {
|
segments: {
|
||||||
codes: options.segments?.codes || [],
|
codes: options.segments?.codes || [],
|
||||||
year: options.segments?.year, // Akan diproses secara kondisional nanti
|
year: options.segments?.year,
|
||||||
sequentialDigits: options.segments?.sequentialDigits || 6,
|
sequentialDigits: options.segments?.sequentialDigits || 6,
|
||||||
includeDate: options.segments?.includeDate ?? false,
|
includeDate: options.segments?.includeDate ?? false,
|
||||||
dateFormat: options.segments?.dateFormat || 'yyyyMMdd',
|
dateFormat: options.segments?.dateFormat || 'yyyyMMdd',
|
||||||
|
@ -455,22 +439,21 @@ export function generateId(
|
||||||
uniquenessStrategy: options.uniquenessStrategy || 'timestamp',
|
uniquenessStrategy: options.uniquenessStrategy || 'timestamp',
|
||||||
retryOnCollision: options.retryOnCollision ?? true,
|
retryOnCollision: options.retryOnCollision ?? true,
|
||||||
maxRetries: options.maxRetries || 10,
|
maxRetries: options.maxRetries || 10,
|
||||||
|
storage: options.storage || 'memory',
|
||||||
|
tableName: options.tableName
|
||||||
};
|
};
|
||||||
|
|
||||||
// Initialize global counter if not exists
|
|
||||||
if (typeof globalThis.__idCounter === 'undefined') {
|
if (typeof globalThis.__idCounter === 'undefined') {
|
||||||
globalThis.__idCounter = 0;
|
globalThis.__idCounter = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
const now = new Date();
|
const now = new Date();
|
||||||
|
|
||||||
// Generate date string if needed
|
|
||||||
let dateString = '';
|
let dateString = '';
|
||||||
if (config.segments.includeDate) {
|
if (config.segments.includeDate) {
|
||||||
dateString = format(now, config.segments.dateFormat);
|
dateString = format(now, config.segments.dateFormat);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Generate time string if needed
|
|
||||||
let timeString = '';
|
let timeString = '';
|
||||||
if (config.segments.includeTime) {
|
if (config.segments.includeTime) {
|
||||||
timeString = format(now, 'HHmmss');
|
timeString = format(now, 'HHmmss');
|
||||||
|
@ -479,7 +462,6 @@ export function generateId(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Generate sequential number based on uniqueness strategy
|
|
||||||
let sequentialNum: string;
|
let sequentialNum: string;
|
||||||
try {
|
try {
|
||||||
switch (config.uniquenessStrategy) {
|
switch (config.uniquenessStrategy) {
|
||||||
|
@ -491,9 +473,34 @@ export function generateId(
|
||||||
sequentialNum = sequentialNum.slice(-config.segments.sequentialDigits);
|
sequentialNum = sequentialNum.slice(-config.segments.sequentialDigits);
|
||||||
break;
|
break;
|
||||||
case 'counter':
|
case 'counter':
|
||||||
sequentialNum = (++globalThis.__idCounter)
|
const lastId = await getLastId(config.prefix, {
|
||||||
.toString()
|
separator: config.separator,
|
||||||
.padStart(config.segments.sequentialDigits, '0');
|
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;
|
break;
|
||||||
case 'hash':
|
case 'hash':
|
||||||
const hashSource = `${now.getTime()}-${JSON.stringify(options)}-${Math.random()}`;
|
const hashSource = `${now.getTime()}-${JSON.stringify(options)}-${Math.random()}`;
|
||||||
|
@ -513,39 +520,59 @@ export function generateId(
|
||||||
.toString()
|
.toString()
|
||||||
.padStart(config.segments.sequentialDigits, '0');
|
.padStart(config.segments.sequentialDigits, '0');
|
||||||
} else {
|
} else {
|
||||||
sequentialNum = (++globalThis.__idCounter)
|
const lastId = await getLastId(config.prefix, {
|
||||||
.toString()
|
separator: config.separator,
|
||||||
.padStart(config.segments.sequentialDigits, '0');
|
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) {
|
} catch (error) {
|
||||||
console.error('Error generating sequential number:', error);
|
console.error('Error generating sequential number:', error);
|
||||||
// Fallback to timestamp strategy if other methods fail
|
|
||||||
sequentialNum = `${now.getTime()}`.slice(-config.segments.sequentialDigits);
|
sequentialNum = `${now.getTime()}`.slice(-config.segments.sequentialDigits);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Determine if year should be included and what value to use
|
|
||||||
let yearValue = null;
|
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') {
|
if (typeof config.segments.year === 'number') {
|
||||||
yearValue = String(config.segments.year);
|
yearValue = String(config.segments.year);
|
||||||
} else if (config.segments.year === true) {
|
} else if (config.segments.year === true) {
|
||||||
yearValue = format(now, 'yyyy');
|
yearValue = format(now, 'yyyy');
|
||||||
}
|
}
|
||||||
// if year is false, yearValue remains null and won't be included
|
|
||||||
} else {
|
} else {
|
||||||
// Default behavior (backward compatibility)
|
|
||||||
yearValue = format(now, 'yyyy');
|
yearValue = format(now, 'yyyy');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Prepare components for ID assembly
|
|
||||||
const components = {
|
const components = {
|
||||||
prefix: config.prefix,
|
prefix: config.prefix,
|
||||||
codes:
|
codes:
|
||||||
config.segments.codes.length > 0
|
config.segments.codes.length > 0
|
||||||
? config.segments.codes.join(config.separator)
|
? config.segments.codes.join(config.separator)
|
||||||
: '',
|
: '',
|
||||||
year: yearValue, // Added the year value to components
|
year: yearValue,
|
||||||
sequence: sequentialNum,
|
sequence: sequentialNum,
|
||||||
date: dateString,
|
date: dateString,
|
||||||
time: timeString,
|
time: timeString,
|
||||||
|
@ -553,7 +580,6 @@ export function generateId(
|
||||||
|
|
||||||
let result: string;
|
let result: string;
|
||||||
|
|
||||||
// Use custom format if provided
|
|
||||||
if (config.format) {
|
if (config.format) {
|
||||||
let customID = config.format;
|
let customID = config.format;
|
||||||
for (const [key, value] of Object.entries(components)) {
|
for (const [key, value] of Object.entries(components)) {
|
||||||
|
@ -565,10 +591,8 @@ export function generateId(
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Remove unused placeholders
|
|
||||||
customID = customID.replace(/{[^}]+}/g, '');
|
customID = customID.replace(/{[^}]+}/g, '');
|
||||||
|
|
||||||
// Clean up separators
|
|
||||||
const escapedSeparator = config.separator.replace(
|
const escapedSeparator = config.separator.replace(
|
||||||
/[-\/\\^$*+?.()|[\]{}]/g,
|
/[-\/\\^$*+?.()|[\]{}]/g,
|
||||||
'\\$&'
|
'\\$&'
|
||||||
|
@ -582,7 +606,6 @@ export function generateId(
|
||||||
|
|
||||||
result = config.upperCase ? customID.toUpperCase() : customID;
|
result = config.upperCase ? customID.toUpperCase() : customID;
|
||||||
} else {
|
} else {
|
||||||
// Assemble ID from parts
|
|
||||||
const parts = [];
|
const parts = [];
|
||||||
if (components.prefix) parts.push(components.prefix);
|
if (components.prefix) parts.push(components.prefix);
|
||||||
if (components.codes) parts.push(components.codes);
|
if (components.codes) parts.push(components.codes);
|
||||||
|
@ -595,7 +618,6 @@ export function generateId(
|
||||||
if (config.upperCase) result = result.toUpperCase();
|
if (config.upperCase) result = result.toUpperCase();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle collisions if required
|
|
||||||
if (config.retryOnCollision) {
|
if (config.retryOnCollision) {
|
||||||
let retryCount = 0;
|
let retryCount = 0;
|
||||||
let originalResult = result;
|
let originalResult = result;
|
||||||
|
@ -607,7 +629,6 @@ export function generateId(
|
||||||
result = `${originalResult}${config.separator}${suffix}`;
|
result = `${originalResult}${config.separator}${suffix}`;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error generating collision suffix:', error);
|
console.error('Error generating collision suffix:', error);
|
||||||
// Simple fallback if crypto fails
|
|
||||||
result = `${originalResult}${config.separator}${Date.now().toString(36)}`;
|
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);
|
usedIdRegistry.add(result);
|
||||||
if (usedIdRegistry.size > 10000) {
|
if (usedIdRegistry.size > 10000) {
|
||||||
const entriesToKeep = Array.from(usedIdRegistry).slice(-5000);
|
const entriesToKeep = Array.from(usedIdRegistry).slice(-5000);
|
||||||
|
@ -627,31 +647,161 @@ export function generateId(
|
||||||
entriesToKeep.forEach((id) => usedIdRegistry.add(id));
|
entriesToKeep.forEach((id) => usedIdRegistry.add(id));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
await updateLastId(config.prefix, result, {
|
||||||
|
storage: config.storage,
|
||||||
|
tableName: config.tableName
|
||||||
|
});
|
||||||
|
|
||||||
return result.trim();
|
return result.trim();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Gets the last ID from a specified table and column.
|
* Retrieves the last generated ID for a specific prefix
|
||||||
* @param tableName - The name of the table to query.
|
* Used by generateId to determine the next sequential number
|
||||||
* @param columnName - The column containing the IDs.
|
*
|
||||||
* @returns The last ID as a string, or null if no records exist.
|
* @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(
|
export async function getLastId(
|
||||||
tableName: string,
|
prefix: string,
|
||||||
columnName: string
|
options: {
|
||||||
|
separator?: string;
|
||||||
|
extractFullSequence?: boolean;
|
||||||
|
storage?: 'memory' | 'localStorage' | 'database';
|
||||||
|
tableName?: string;
|
||||||
|
} = {}
|
||||||
): Promise<string | null> {
|
): Promise<string | null> {
|
||||||
try {
|
const config = {
|
||||||
const result = await db.$queryRawUnsafe(
|
separator: options.separator || '-',
|
||||||
`SELECT ${columnName} FROM ${tableName} ORDER BY ${columnName} DESC LIMIT 1`
|
extractFullSequence: options.extractFullSequence ?? false,
|
||||||
);
|
storage: options.storage || 'memory',
|
||||||
|
tableName: options.tableName
|
||||||
|
};
|
||||||
|
|
||||||
if (Array.isArray(result) && result.length > 0) {
|
if (typeof globalThis.__idRegistry === 'undefined') {
|
||||||
return result[0][columnName];
|
globalThis.__idRegistry = {};
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error fetching last ID:', error);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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<void> {
|
||||||
|
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<string, string> = {};
|
||||||
|
const storedIds = localStorage.getItem('customIdRegistry');
|
||||||
|
|
||||||
|
if (storedIds) {
|
||||||
|
registry = JSON.parse(storedIds) as Record<string, string>;
|
||||||
|
}
|
||||||
|
|
||||||
|
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 {
|
export function formatNumber(num?: number): string {
|
||||||
if (num === undefined || num === null) return "N/A";
|
if (num === undefined || num === null) return "N/A";
|
||||||
|
|
||||||
// If number is in the thousands, abbreviate
|
|
||||||
if (num >= 1_000_000) {
|
if (num >= 1_000_000) {
|
||||||
return (num / 1_000_000).toFixed(1) + 'M';
|
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';
|
return (num / 1_000).toFixed(1) + 'K';
|
||||||
}
|
}
|
||||||
|
|
||||||
// Otherwise, format with commas
|
|
||||||
return num.toLocaleString();
|
return num.toLocaleString();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -250,3 +250,16 @@ export const processDistrictFeature = (
|
||||||
isFocused: true,
|
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`;
|
||||||
|
}
|
||||||
|
}
|
|
@ -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;
|
||||||
|
}
|
|
@ -60,6 +60,8 @@
|
||||||
"recharts": "^2.15.2",
|
"recharts": "^2.15.2",
|
||||||
"resend": "^4.1.2",
|
"resend": "^4.1.2",
|
||||||
"sonner": "^2.0.1",
|
"sonner": "^2.0.1",
|
||||||
|
"three": "^0.176.0",
|
||||||
|
"threebox-plugin": "^2.2.7",
|
||||||
"uuid": "^11.1.0",
|
"uuid": "^11.1.0",
|
||||||
"vaul": "^1.1.2",
|
"vaul": "^1.1.2",
|
||||||
"zod": "^3.24.2",
|
"zod": "^3.24.2",
|
||||||
|
@ -72,6 +74,7 @@
|
||||||
"@types/node": "^22.10.2",
|
"@types/node": "^22.10.2",
|
||||||
"@types/react": "^19.0.2",
|
"@types/react": "^19.0.2",
|
||||||
"@types/react-dom": "19.0.2",
|
"@types/react-dom": "19.0.2",
|
||||||
|
"@types/three": "^0.176.0",
|
||||||
"@types/uuid": "^10.0.0",
|
"@types/uuid": "^10.0.0",
|
||||||
"postcss": "8.4.49",
|
"postcss": "8.4.49",
|
||||||
"prisma": "^6.4.1",
|
"prisma": "^6.4.1",
|
||||||
|
@ -429,6 +432,13 @@
|
||||||
"@jridgewell/sourcemap-codec": "^1.4.10"
|
"@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": {
|
"node_modules/@emnapi/runtime": {
|
||||||
"version": "1.3.1",
|
"version": "1.3.1",
|
||||||
"resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.3.1.tgz",
|
"resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.3.1.tgz",
|
||||||
|
@ -7207,6 +7217,13 @@
|
||||||
"url": "https://opencollective.com/turf"
|
"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": {
|
"node_modules/@types/cacheable-request": {
|
||||||
"version": "6.0.3",
|
"version": "6.0.3",
|
||||||
"resolved": "https://registry.npmjs.org/@types/cacheable-request/-/cacheable-request-6.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/@types/cacheable-request/-/cacheable-request-6.0.3.tgz",
|
||||||
|
@ -7506,6 +7523,13 @@
|
||||||
"integrity": "sha512-UE7oxhQLLd9gub6JKIAhDq06T0F6FnztwMNRvYgjeQSBeMc1ZG/tA47EwfduvkuQS8apbkM/lpLpWsaCeYsXVg==",
|
"integrity": "sha512-UE7oxhQLLd9gub6JKIAhDq06T0F6FnztwMNRvYgjeQSBeMc1ZG/tA47EwfduvkuQS8apbkM/lpLpWsaCeYsXVg==",
|
||||||
"license": "MIT"
|
"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": {
|
"node_modules/@types/supercluster": {
|
||||||
"version": "7.1.3",
|
"version": "7.1.3",
|
||||||
"resolved": "https://registry.npmjs.org/@types/supercluster/-/supercluster-7.1.3.tgz",
|
"resolved": "https://registry.npmjs.org/@types/supercluster/-/supercluster-7.1.3.tgz",
|
||||||
|
@ -7524,6 +7548,22 @@
|
||||||
"@types/node": "*"
|
"@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": {
|
"node_modules/@types/uuid": {
|
||||||
"version": "10.0.0",
|
"version": "10.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-10.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-10.0.0.tgz",
|
||||||
|
@ -7531,6 +7571,13 @@
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"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": {
|
"node_modules/@types/ws": {
|
||||||
"version": "8.5.14",
|
"version": "8.5.14",
|
||||||
"resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.5.14.tgz",
|
"resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.5.14.tgz",
|
||||||
|
@ -7850,6 +7897,13 @@
|
||||||
"@xtuc/long": "4.2.2"
|
"@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": {
|
"node_modules/@xtuc/ieee754": {
|
||||||
"version": "1.2.0",
|
"version": "1.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/@xtuc/ieee754/-/ieee754-1.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/@xtuc/ieee754/-/ieee754-1.2.0.tgz",
|
||||||
|
@ -9960,6 +10014,13 @@
|
||||||
"reusify": "^1.0.4"
|
"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": {
|
"node_modules/file-entry-cache": {
|
||||||
"version": "8.0.0",
|
"version": "8.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz",
|
||||||
|
@ -11270,6 +11331,13 @@
|
||||||
"node": ">= 8"
|
"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": {
|
"node_modules/micromatch": {
|
||||||
"version": "4.0.8",
|
"version": "4.0.8",
|
||||||
"resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz",
|
"resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz",
|
||||||
|
@ -14824,6 +14892,18 @@
|
||||||
"node": ">=0.8"
|
"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": {
|
"node_modules/tiny-invariant": {
|
||||||
"version": "1.3.3",
|
"version": "1.3.3",
|
||||||
"resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.3.3.tgz",
|
"resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.3.3.tgz",
|
||||||
|
|
|
@ -66,6 +66,8 @@
|
||||||
"recharts": "^2.15.2",
|
"recharts": "^2.15.2",
|
||||||
"resend": "^4.1.2",
|
"resend": "^4.1.2",
|
||||||
"sonner": "^2.0.1",
|
"sonner": "^2.0.1",
|
||||||
|
"three": "^0.176.0",
|
||||||
|
"threebox-plugin": "^2.2.7",
|
||||||
"uuid": "^11.1.0",
|
"uuid": "^11.1.0",
|
||||||
"vaul": "^1.1.2",
|
"vaul": "^1.1.2",
|
||||||
"zod": "^3.24.2",
|
"zod": "^3.24.2",
|
||||||
|
@ -78,6 +80,7 @@
|
||||||
"@types/node": "^22.10.2",
|
"@types/node": "^22.10.2",
|
||||||
"@types/react": "^19.0.2",
|
"@types/react": "^19.0.2",
|
||||||
"@types/react-dom": "19.0.2",
|
"@types/react-dom": "19.0.2",
|
||||||
|
"@types/three": "^0.176.0",
|
||||||
"@types/uuid": "^10.0.0",
|
"@types/uuid": "^10.0.0",
|
||||||
"postcss": "8.4.49",
|
"postcss": "8.4.49",
|
||||||
"prisma": "^6.4.1",
|
"prisma": "^6.4.1",
|
||||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -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;
|
|
@ -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);
|
||||||
|
|
||||||
|
|
|
@ -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;
|
|
@ -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;
|
|
@ -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;
|
|
@ -36,7 +36,7 @@ export class CrimeCategoriesSeeder {
|
||||||
const data = XLSX.utils.sheet_to_json(sheet) as ICrimeCategory[];
|
const data = XLSX.utils.sheet_to_json(sheet) as ICrimeCategory[];
|
||||||
|
|
||||||
for (const category of crimeCategoriesData) {
|
for (const category of crimeCategoriesData) {
|
||||||
const newId = generateId({
|
const newId = await generateId({
|
||||||
prefix: 'CC',
|
prefix: 'CC',
|
||||||
segments: {
|
segments: {
|
||||||
sequentialDigits: 4,
|
sequentialDigits: 4,
|
||||||
|
@ -44,6 +44,8 @@ export class CrimeCategoriesSeeder {
|
||||||
randomSequence: false,
|
randomSequence: false,
|
||||||
uniquenessStrategy: 'counter',
|
uniquenessStrategy: 'counter',
|
||||||
separator: '-',
|
separator: '-',
|
||||||
|
tableName: 'crime_categories',
|
||||||
|
storage: 'database',
|
||||||
});
|
});
|
||||||
|
|
||||||
await this.prisma.crime_categories.create({
|
await this.prisma.crime_categories.create({
|
||||||
|
|
|
@ -409,7 +409,7 @@ export class CrimeIncidentsSeeder {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Generate a unique ID for the incident
|
// Generate a unique ID for the incident
|
||||||
const incidentId = generateId({
|
const incidentId = await generateId({
|
||||||
prefix: 'CI',
|
prefix: 'CI',
|
||||||
segments: {
|
segments: {
|
||||||
codes: [district.cities.id],
|
codes: [district.cities.id],
|
||||||
|
@ -420,6 +420,8 @@ export class CrimeIncidentsSeeder {
|
||||||
separator: '-',
|
separator: '-',
|
||||||
randomSequence: false,
|
randomSequence: false,
|
||||||
uniquenessStrategy: 'counter',
|
uniquenessStrategy: 'counter',
|
||||||
|
storage: 'database',
|
||||||
|
tableName: 'crime_incidents',
|
||||||
});
|
});
|
||||||
|
|
||||||
// Determine status based on crime_cleared
|
// Determine status based on crime_cleared
|
||||||
|
|
|
@ -177,7 +177,7 @@ export class CrimesSeeder {
|
||||||
|
|
||||||
const year = parseInt(record.year);
|
const year = parseInt(record.year);
|
||||||
// Create a unique ID for monthly crime data
|
// Create a unique ID for monthly crime data
|
||||||
const crimeId = generateId({
|
const crimeId = await generateId({
|
||||||
prefix: 'CR',
|
prefix: 'CR',
|
||||||
segments: {
|
segments: {
|
||||||
codes: [city.id],
|
codes: [city.id],
|
||||||
|
@ -188,6 +188,8 @@ export class CrimesSeeder {
|
||||||
separator: '-',
|
separator: '-',
|
||||||
randomSequence: false,
|
randomSequence: false,
|
||||||
uniquenessStrategy: 'counter',
|
uniquenessStrategy: 'counter',
|
||||||
|
storage: 'database',
|
||||||
|
tableName: 'crimes',
|
||||||
});
|
});
|
||||||
|
|
||||||
await this.prisma.crimes.create({
|
await this.prisma.crimes.create({
|
||||||
|
@ -257,7 +259,7 @@ export class CrimesSeeder {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create a unique ID for yearly crime data
|
// Create a unique ID for yearly crime data
|
||||||
const crimeId = generateId({
|
const crimeId = await generateId({
|
||||||
prefix: 'CR',
|
prefix: 'CR',
|
||||||
segments: {
|
segments: {
|
||||||
codes: [city.id],
|
codes: [city.id],
|
||||||
|
@ -268,6 +270,8 @@ export class CrimesSeeder {
|
||||||
separator: '-',
|
separator: '-',
|
||||||
randomSequence: false,
|
randomSequence: false,
|
||||||
uniquenessStrategy: 'counter',
|
uniquenessStrategy: 'counter',
|
||||||
|
storage: 'database',
|
||||||
|
tableName: 'crimes',
|
||||||
});
|
});
|
||||||
|
|
||||||
await this.prisma.crimes.create({
|
await this.prisma.crimes.create({
|
||||||
|
@ -333,7 +337,7 @@ export class CrimesSeeder {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create a unique ID for all-year summary data
|
// Create a unique ID for all-year summary data
|
||||||
const crimeId = generateId({
|
const crimeId = await generateId({
|
||||||
prefix: 'CR',
|
prefix: 'CR',
|
||||||
segments: {
|
segments: {
|
||||||
codes: [city.id],
|
codes: [city.id],
|
||||||
|
@ -343,6 +347,8 @@ export class CrimesSeeder {
|
||||||
separator: '-',
|
separator: '-',
|
||||||
randomSequence: false,
|
randomSequence: false,
|
||||||
uniquenessStrategy: 'counter',
|
uniquenessStrategy: 'counter',
|
||||||
|
storage: 'database',
|
||||||
|
tableName: 'crimes',
|
||||||
});
|
});
|
||||||
|
|
||||||
await this.prisma.crimes.create({
|
await this.prisma.crimes.create({
|
||||||
|
|
|
@ -89,7 +89,7 @@ export class UnitSeeder {
|
||||||
const address = location.address;
|
const address = location.address;
|
||||||
const phone = location.telepon?.replace(/-/g, '');
|
const phone = location.telepon?.replace(/-/g, '');
|
||||||
|
|
||||||
const code_unit = generateId({
|
const newId = await generateId({
|
||||||
prefix: 'UT',
|
prefix: 'UT',
|
||||||
format: '{prefix}-{sequence}',
|
format: '{prefix}-{sequence}',
|
||||||
separator: '-',
|
separator: '-',
|
||||||
|
@ -98,11 +98,13 @@ export class UnitSeeder {
|
||||||
segments: {
|
segments: {
|
||||||
sequentialDigits: 4,
|
sequentialDigits: 4,
|
||||||
},
|
},
|
||||||
|
storage: 'database',
|
||||||
|
tableName: 'units',
|
||||||
});
|
});
|
||||||
|
|
||||||
let locationData: CreateLocationDto = {
|
let locationData: CreateLocationDto = {
|
||||||
district_id: city.districts[0].id, // This will be replaced with Patrang's ID
|
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}`,
|
name: `Polres ${city.name}`,
|
||||||
description: `Unit ${city.name} is categorized as POLRES and operates in the ${city.name} area.`,
|
description: `Unit ${city.name} is categorized as POLRES and operates in the ${city.name} area.`,
|
||||||
type: 'polres',
|
type: 'polres',
|
||||||
|
@ -152,7 +154,7 @@ export class UnitSeeder {
|
||||||
const address = location.address;
|
const address = location.address;
|
||||||
const phone = location.telepon?.replace(/-/g, '');
|
const phone = location.telepon?.replace(/-/g, '');
|
||||||
|
|
||||||
const code_unit = generateId({
|
const newId = await generateId({
|
||||||
prefix: 'UT',
|
prefix: 'UT',
|
||||||
format: '{prefix}-{sequence}',
|
format: '{prefix}-{sequence}',
|
||||||
separator: '-',
|
separator: '-',
|
||||||
|
@ -161,11 +163,13 @@ export class UnitSeeder {
|
||||||
segments: {
|
segments: {
|
||||||
sequentialDigits: 4,
|
sequentialDigits: 4,
|
||||||
},
|
},
|
||||||
|
storage: 'database',
|
||||||
|
tableName: 'units',
|
||||||
});
|
});
|
||||||
|
|
||||||
const locationData: CreateLocationDto = {
|
const locationData: CreateLocationDto = {
|
||||||
district_id: district.id,
|
district_id: district.id,
|
||||||
code_unit,
|
code_unit: newId,
|
||||||
name: `Polsek ${district.name}`,
|
name: `Polsek ${district.name}`,
|
||||||
description: `Unit ${district.name} is categorized as POLSEK and operates in the ${district.name} area.`,
|
description: `Unit ${district.name} is categorized as POLSEK and operates in the ${district.name} area.`,
|
||||||
type: 'polsek',
|
type: 'polsek',
|
||||||
|
@ -187,7 +191,7 @@ export class UnitSeeder {
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log(
|
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}`
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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();
|
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_delete_delete_prefix" on "storage"."objects";
|
||||||
|
|
||||||
drop trigger if exists "objects_insert_create_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 "objects_update_create_prefix" on "storage"."objects";
|
||||||
|
|
||||||
drop trigger if exists "prefixes_create_hierarchy" on "storage"."prefixes";
|
DO $$
|
||||||
|
BEGIN
|
||||||
|
IF EXISTS (SELECT FROM pg_catalog.pg_tables
|
||||||
|
WHERE schemaname = 'storage'
|
||||||
|
AND tablename = 'prefixes') THEN
|
||||||
|
|
||||||
drop trigger if exists "prefixes_delete_hierarchy" on "storage"."prefixes";
|
EXECUTE 'drop trigger if exists "prefixes_create_hierarchy" on "storage"."prefixes"';
|
||||||
|
EXECUTE 'drop trigger if exists "prefixes_delete_hierarchy" on "storage"."prefixes"';
|
||||||
|
|
||||||
revoke delete on table "storage"."prefixes" from "anon";
|
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"';
|
||||||
|
|
||||||
revoke insert 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"';
|
||||||
|
|
||||||
revoke references on table "storage"."prefixes" from "anon";
|
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"';
|
||||||
|
|
||||||
revoke select on table "storage"."prefixes" from "anon";
|
EXECUTE 'alter table "storage"."prefixes" drop constraint if exists "prefixes_bucketId_fkey"';
|
||||||
|
EXECUTE 'alter table "storage"."prefixes" drop constraint if exists "prefixes_pkey"';
|
||||||
|
|
||||||
revoke trigger on table "storage"."prefixes" from "anon";
|
EXECUTE 'drop table "storage"."prefixes"';
|
||||||
|
END IF;
|
||||||
revoke truncate on table "storage"."prefixes" from "anon";
|
END $$;
|
||||||
|
|
||||||
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"."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);
|
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;
|
||||||
|
|
||||||
drop index if exists "storage"."idx_name_bucket_level_unique";
|
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;
|
||||||
|
|
||||||
drop index if exists "storage"."idx_objects_lower_name";
|
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;
|
||||||
|
|
||||||
drop index if exists "storage"."idx_prefixes_lower_name";
|
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;
|
||||||
|
|
||||||
drop index if exists "storage"."objects_bucket_id_level_idx";
|
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"."prefixes_pkey";
|
DO $$
|
||||||
|
BEGIN
|
||||||
drop table "storage"."prefixes";
|
IF EXISTS (
|
||||||
|
SELECT 1
|
||||||
alter table "storage"."objects" drop column "level";
|
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;
|
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);
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -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);
|
||||||
|
|
||||||
|
|
|
@ -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);
|
||||||
|
|
||||||
|
|
|
@ -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);
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue