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';
|
||||
|
||||
import { ICrimes, ICrimesByYearAndMonth } from '@/app/_utils/types/crimes';
|
||||
import { createClient } from '@/app/_utils/supabase/client';
|
||||
|
||||
import {
|
||||
ICrimes,
|
||||
ICrimesByYearAndMonth,
|
||||
IDistanceResult,
|
||||
} from '@/app/_utils/types/crimes';
|
||||
import { getInjection } from '@/di/container';
|
||||
import db from '@/prisma/db';
|
||||
import {
|
||||
|
@ -304,3 +310,38 @@ export async function getCrimeByYearAndMonth(
|
|||
}
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate distances between units and incidents using PostGIS
|
||||
* @param unitId Optional unit code to filter by specific unit
|
||||
* @param districtId Optional district ID to filter by specific district
|
||||
* @returns Array of distance calculations between units and incidents
|
||||
*/
|
||||
export async function calculateDistances(
|
||||
unitId?: string,
|
||||
districtId?: string
|
||||
): Promise<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
|
||||
const showDistrictFill = activeControl === "incidents" || activeControl === "clusters";
|
||||
|
||||
// Only show incident markers for incidents and clusters views - exclude timeline mode
|
||||
const showIncidentMarkers = (activeControl === "incidents" || activeControl === "clusters")
|
||||
// Show incident markers for incidents, clusters, AND units modes
|
||||
// But hide for heatmap and timeline
|
||||
const showIncidentMarkers = activeControl !== "heatmap" && activeControl !== "timeline";
|
||||
|
||||
return (
|
||||
<>
|
||||
|
@ -270,7 +271,7 @@ export default function Layers({
|
|||
useAllData={useAllData}
|
||||
/>
|
||||
|
||||
{/* Units Layer */}
|
||||
{/* Units Layer - always show incidents when Units is active */}
|
||||
<UnitsLayer
|
||||
crimes={crimes}
|
||||
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 */}
|
||||
<ClusterLayer
|
||||
visible={visible && !showTimelineLayer}
|
||||
visible={visible && activeControl === "clusters"}
|
||||
map={mapboxMap}
|
||||
crimes={crimes}
|
||||
filterCategory={filterCategory}
|
||||
focusedDistrictId={focusedDistrictId}
|
||||
clusteringEnabled={showClustersLayer}
|
||||
showClusters={showClustersLayer}
|
||||
clusteringEnabled={activeControl === "clusters"}
|
||||
showClusters={activeControl === "clusters"}
|
||||
/>
|
||||
|
||||
{/* Unclustered Points Layer - explicitly hide in timeline mode */}
|
||||
{/* Unclustered Points Layer - now show for both incidents and units modes */}
|
||||
<UnclusteredPointLayer
|
||||
visible={visible && showIncidentMarkers && !focusedDistrictId && !showTimelineLayer}
|
||||
visible={visible && showIncidentMarkers && !focusedDistrictId}
|
||||
map={mapboxMap}
|
||||
crimes={crimes}
|
||||
filterCategory={filterCategory}
|
||||
|
|
|
@ -2,10 +2,15 @@
|
|||
|
||||
import { useEffect, useMemo, useRef, useState } from 'react'
|
||||
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 mapboxgl from 'mapbox-gl'
|
||||
import { useQuery } from '@tanstack/react-query'
|
||||
|
||||
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 {
|
||||
crimes: ICrimes[]
|
||||
|
@ -15,6 +20,23 @@ interface UnitsLayerProps {
|
|||
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({
|
||||
crimes,
|
||||
units = [],
|
||||
|
@ -25,6 +47,20 @@ export default function UnitsLayer({
|
|||
const [loadedUnits, setLoadedUnits] = useState<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
|
||||
const unitsData = useMemo(() => {
|
||||
return units.length > 0 ? units : (loadedUnits || [])
|
||||
|
@ -74,6 +110,44 @@ export default function UnitsLayer({
|
|||
}
|
||||
}, [unitsData])
|
||||
|
||||
// Process incident data to GeoJSON format
|
||||
const incidentsGeoJSON = useMemo(() => {
|
||||
const features: any[] = [];
|
||||
|
||||
crimes.forEach(crime => {
|
||||
crime.crime_incidents.forEach(incident => {
|
||||
// Skip incidents without location data or filtered by category
|
||||
if (
|
||||
!incident.locations?.latitude ||
|
||||
!incident.locations?.longitude ||
|
||||
(filterCategory !== "all" && incident.crime_categories.name !== filterCategory)
|
||||
) return;
|
||||
|
||||
features.push({
|
||||
type: "Feature" as const,
|
||||
properties: {
|
||||
id: incident.id,
|
||||
description: incident.description || "No description",
|
||||
category: incident.crime_categories.name,
|
||||
date: incident.timestamp,
|
||||
district: crime.districts?.name || "",
|
||||
district_id: crime.district_id,
|
||||
categoryColor: categoryColorMap[incident.crime_categories.name] || '#22c55e',
|
||||
},
|
||||
geometry: {
|
||||
type: "Point" as const,
|
||||
coordinates: [incident.locations.longitude, incident.locations.latitude]
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
return {
|
||||
type: "FeatureCollection" as const,
|
||||
features
|
||||
};
|
||||
}, [crimes, filterCategory, categoryColorMap]);
|
||||
|
||||
// Create lines between units and incidents within their districts
|
||||
const connectionLinesGeoJSON = useMemo(() => {
|
||||
if (!unitsData.length || !crimes.length) return {
|
||||
|
@ -143,7 +217,6 @@ export default function UnitsLayer({
|
|||
}
|
||||
}, [unitsData, crimes, filterCategory, categoryColorMap])
|
||||
|
||||
// Map click handler code and the rest remains the same...
|
||||
useEffect(() => {
|
||||
if (!map || !visible) return
|
||||
|
||||
|
@ -155,22 +228,16 @@ export default function UnitsLayer({
|
|||
|
||||
if (!properties) return
|
||||
|
||||
// Create a popup for the unit
|
||||
const popup = new mapboxgl.Popup()
|
||||
.setLngLat(feature.geometry.type === 'Point' ?
|
||||
(feature.geometry as any).coordinates as [number, number] :
|
||||
[0, 0]) // Fallback coordinates if not a Point geometry
|
||||
.setHTML(`
|
||||
<div class="p-2">
|
||||
<h3 class="font-bold text-base">${properties.name}</h3>
|
||||
<p class="text-sm">${properties.type}</p>
|
||||
<p class="text-sm">${properties.address || 'No address provided'}</p>
|
||||
<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)
|
||||
// Find the unit in our data
|
||||
const unit = unitsData.find(u => u.code_unit === properties.id);
|
||||
if (!unit) return;
|
||||
|
||||
// Set the selected unit and query parameters
|
||||
setSelectedUnit(unit);
|
||||
setSelectedIncident(null); // Clear any selected incident
|
||||
setSelectedEntityId(properties.id);
|
||||
setIsUnitSelected(true);
|
||||
setSelectedDistrictId(properties.district_id);
|
||||
|
||||
// Highlight the connected lines for this unit
|
||||
if (map.getLayer('units-connection-lines')) {
|
||||
|
@ -180,15 +247,49 @@ export default function UnitsLayer({
|
|||
properties.id
|
||||
])
|
||||
}
|
||||
|
||||
// When popup closes, reset the lines filter
|
||||
popup.on('close', () => {
|
||||
if (map.getLayer('units-connection-lines')) {
|
||||
map.setFilter('units-connection-lines', ['has', 'unit_id'])
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const handleIncidentClick = (e: mapboxgl.MapMouseEvent & { features?: mapboxgl.MapboxGeoJSONFeature[] }) => {
|
||||
if (!e.features || e.features.length === 0) return;
|
||||
|
||||
const feature = e.features[0];
|
||||
const properties = feature.properties;
|
||||
|
||||
if (!properties) return;
|
||||
|
||||
// Create incident object from properties
|
||||
const incident = {
|
||||
id: properties.id,
|
||||
category: properties.category,
|
||||
description: properties.description,
|
||||
date: properties.date,
|
||||
district: properties.district,
|
||||
district_id: properties.district_id,
|
||||
};
|
||||
|
||||
// Set the selected incident and query parameters
|
||||
setSelectedIncident({
|
||||
...incident,
|
||||
latitude: feature.geometry.type === 'Point' ?
|
||||
(feature.geometry as any).coordinates[1] : 0,
|
||||
longitude: feature.geometry.type === 'Point' ?
|
||||
(feature.geometry as any).coordinates[0] : 0
|
||||
});
|
||||
setSelectedUnit(null);
|
||||
setSelectedEntityId(properties.id);
|
||||
setIsUnitSelected(false);
|
||||
setSelectedDistrictId(properties.district_id);
|
||||
|
||||
// Highlight the connected lines for this incident
|
||||
if (map.getLayer('units-connection-lines')) {
|
||||
map.setFilter('units-connection-lines', [
|
||||
'==',
|
||||
['get', 'incident_id'],
|
||||
properties.id
|
||||
]);
|
||||
}
|
||||
};
|
||||
|
||||
// Define event handlers that can be referenced for both adding and removing
|
||||
const handleMouseEnter = () => {
|
||||
map.getCanvas().style.cursor = 'pointer'
|
||||
|
@ -207,14 +308,41 @@ export default function UnitsLayer({
|
|||
map.on('mouseleave', 'units-points', handleMouseLeave)
|
||||
}
|
||||
|
||||
// Add click event for incidents-points layer
|
||||
if (map.getLayer('incidents-points')) {
|
||||
map.on('click', 'incidents-points', handleIncidentClick)
|
||||
|
||||
// Change cursor on hover
|
||||
map.on('mouseenter', 'incidents-points', handleMouseEnter)
|
||||
map.on('mouseleave', 'incidents-points', handleMouseLeave)
|
||||
}
|
||||
|
||||
return () => {
|
||||
if (map.getLayer('units-points')) {
|
||||
map.off('click', 'units-points', handleUnitClick)
|
||||
map.off('mouseenter', 'units-points', handleMouseEnter)
|
||||
map.off('mouseleave', 'units-points', handleMouseLeave)
|
||||
}
|
||||
|
||||
if (map.getLayer('incidents-points')) {
|
||||
map.off('click', 'incidents-points', handleIncidentClick)
|
||||
map.off('mouseenter', 'incidents-points', handleMouseEnter)
|
||||
map.off('mouseleave', 'incidents-points', handleMouseLeave)
|
||||
}
|
||||
}
|
||||
}, [map, visible])
|
||||
}, [map, visible, unitsData])
|
||||
|
||||
// Reset map filters when popup is closed
|
||||
const handleClosePopup = () => {
|
||||
setSelectedUnit(null);
|
||||
setSelectedIncident(null);
|
||||
setSelectedEntityId(undefined);
|
||||
setSelectedDistrictId(undefined);
|
||||
|
||||
if (map && map.getLayer('units-connection-lines')) {
|
||||
map.setFilter('units-connection-lines', ['has', 'unit_id']);
|
||||
}
|
||||
};
|
||||
|
||||
if (!visible) return null
|
||||
|
||||
|
@ -255,6 +383,22 @@ export default function UnitsLayer({
|
|||
/>
|
||||
</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 */}
|
||||
<Source id="units-lines-source" type="geojson" data={connectionLinesGeoJSON}>
|
||||
<Layer
|
||||
|
@ -263,12 +407,45 @@ export default function UnitsLayer({
|
|||
paint={{
|
||||
// Use the pre-computed color stored in the properties
|
||||
'line-color': ['get', 'lineColor'],
|
||||
'line-width': 1.5,
|
||||
'line-opacity': 0.7,
|
||||
'line-dasharray': [1, 2] // Dashed line
|
||||
'line-width': 3,
|
||||
'line-opacity': 0.9,
|
||||
'line-blur': 0.5,
|
||||
'line-dasharray': [3, 1],
|
||||
}}
|
||||
/>
|
||||
</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 { getMonthName } from "@/app/_utils/common"
|
||||
import { BarChart, Users, Home, AlertTriangle, ChevronRight, Building, Calendar, X } from 'lucide-react'
|
||||
import type { DistrictFeature } from "../layers/district-layer"
|
||||
import { IDistrictFeature } from "@/app/_utils/types/map"
|
||||
|
||||
// Helper function to format numbers
|
||||
function formatNumber(num?: number): string {
|
||||
|
@ -29,7 +29,7 @@ interface DistrictPopupProps {
|
|||
longitude: number
|
||||
latitude: number
|
||||
onClose: () => void
|
||||
district: DistrictFeature
|
||||
district: IDistrictFeature
|
||||
year?: string
|
||||
month?: string
|
||||
filterCategory?: string | "all"
|
||||
|
|
|
@ -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
|
||||
const usedIdRegistry = new Set<string>();
|
||||
|
||||
// Add type definition for global counter
|
||||
|
||||
// Add type definition for global counter and registry
|
||||
declare global {
|
||||
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
|
||||
* @returns {string} - Generated custom ID
|
||||
*/
|
||||
/**
|
||||
* Universal Custom ID Generator
|
||||
* Creates structured, readable IDs for any system or entity
|
||||
*
|
||||
* @param {Object} options - Configuration options
|
||||
* @param {string} options.prefix - Primary identifier prefix (e.g., "CRIME", "USER", "INVOICE")
|
||||
* @param {Object} options.segments - Collection of ID segments to include
|
||||
* @param {string[]} options.segments.codes - Array of short codes (e.g., region codes, department codes)
|
||||
* @param {number} options.segments.year - Year to include in the ID
|
||||
* @param {number} options.segments.sequentialDigits - Number of digits for sequential number
|
||||
* @param {boolean} options.segments.includeDate - Whether to include current date
|
||||
* @param {string} options.segments.dateFormat - Format for date (e.g., "yyyy-MM-dd", "dd/MM/yyyy")
|
||||
* @param {boolean} options.segments.includeTime - Whether to include timestamp
|
||||
* @param {string} options.format - Custom format string for ID structure
|
||||
* @param {string} options.separator - Character to separate ID components
|
||||
* @param {boolean} options.upperCase - Convert result to uppercase
|
||||
* @returns {string} - Generated custom ID
|
||||
*/
|
||||
export function generateId(
|
||||
export async function generateId(
|
||||
options: {
|
||||
prefix?: string;
|
||||
segments?: {
|
||||
codes?: string[];
|
||||
year?: number | boolean; // Year diubah menjadi number | boolean
|
||||
year?: number | boolean;
|
||||
sequentialDigits?: number;
|
||||
includeDate?: boolean;
|
||||
dateFormat?: string;
|
||||
|
@ -429,10 +413,10 @@ export function generateId(
|
|||
uniquenessStrategy?: 'uuid' | 'timestamp' | 'counter' | 'hash';
|
||||
retryOnCollision?: boolean;
|
||||
maxRetries?: number;
|
||||
storage?: 'memory' | 'localStorage' | 'database';
|
||||
tableName?: string; // Added table name for database interactions
|
||||
} = {}
|
||||
): string {
|
||||
// Jika uniquenessStrategy tidak diatur dan randomSequence = false,
|
||||
// gunakan counter sebagai strategi default
|
||||
): Promise<string> {
|
||||
if (!options.uniquenessStrategy && options.randomSequence === false) {
|
||||
options.uniquenessStrategy = 'counter';
|
||||
}
|
||||
|
@ -441,7 +425,7 @@ export function generateId(
|
|||
prefix: options.prefix || 'ID',
|
||||
segments: {
|
||||
codes: options.segments?.codes || [],
|
||||
year: options.segments?.year, // Akan diproses secara kondisional nanti
|
||||
year: options.segments?.year,
|
||||
sequentialDigits: options.segments?.sequentialDigits || 6,
|
||||
includeDate: options.segments?.includeDate ?? false,
|
||||
dateFormat: options.segments?.dateFormat || 'yyyyMMdd',
|
||||
|
@ -455,22 +439,21 @@ export function generateId(
|
|||
uniquenessStrategy: options.uniquenessStrategy || 'timestamp',
|
||||
retryOnCollision: options.retryOnCollision ?? true,
|
||||
maxRetries: options.maxRetries || 10,
|
||||
storage: options.storage || 'memory',
|
||||
tableName: options.tableName
|
||||
};
|
||||
|
||||
// Initialize global counter if not exists
|
||||
if (typeof globalThis.__idCounter === 'undefined') {
|
||||
globalThis.__idCounter = 0;
|
||||
}
|
||||
|
||||
const now = new Date();
|
||||
|
||||
// Generate date string if needed
|
||||
let dateString = '';
|
||||
if (config.segments.includeDate) {
|
||||
dateString = format(now, config.segments.dateFormat);
|
||||
}
|
||||
|
||||
// Generate time string if needed
|
||||
let timeString = '';
|
||||
if (config.segments.includeTime) {
|
||||
timeString = format(now, 'HHmmss');
|
||||
|
@ -479,7 +462,6 @@ export function generateId(
|
|||
}
|
||||
}
|
||||
|
||||
// Generate sequential number based on uniqueness strategy
|
||||
let sequentialNum: string;
|
||||
try {
|
||||
switch (config.uniquenessStrategy) {
|
||||
|
@ -491,9 +473,34 @@ export function generateId(
|
|||
sequentialNum = sequentialNum.slice(-config.segments.sequentialDigits);
|
||||
break;
|
||||
case 'counter':
|
||||
sequentialNum = (++globalThis.__idCounter)
|
||||
.toString()
|
||||
.padStart(config.segments.sequentialDigits, '0');
|
||||
const lastId = await getLastId(config.prefix, {
|
||||
separator: config.separator,
|
||||
storage: config.storage,
|
||||
tableName: config.tableName
|
||||
});
|
||||
|
||||
let counterStart = 0;
|
||||
|
||||
if (lastId !== null) {
|
||||
const parts = lastId.split(config.separator);
|
||||
const lastPart = parts[parts.length - 1];
|
||||
|
||||
if (/^\d+$/.test(lastPart)) {
|
||||
counterStart = parseInt(lastPart, 10);
|
||||
} else {
|
||||
for (let i = parts.length - 1; i >= 0; i--) {
|
||||
if (/^\d+$/.test(parts[i])) {
|
||||
counterStart = parseInt(parts[i], 10);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const nextCounter = counterStart + 1;
|
||||
globalThis.__idCounter = nextCounter;
|
||||
|
||||
sequentialNum = nextCounter.toString().padStart(config.segments.sequentialDigits, '0');
|
||||
break;
|
||||
case 'hash':
|
||||
const hashSource = `${now.getTime()}-${JSON.stringify(options)}-${Math.random()}`;
|
||||
|
@ -513,39 +520,59 @@ export function generateId(
|
|||
.toString()
|
||||
.padStart(config.segments.sequentialDigits, '0');
|
||||
} else {
|
||||
sequentialNum = (++globalThis.__idCounter)
|
||||
.toString()
|
||||
.padStart(config.segments.sequentialDigits, '0');
|
||||
const lastId = await getLastId(config.prefix, {
|
||||
separator: config.separator,
|
||||
storage: config.storage,
|
||||
tableName: config.tableName
|
||||
});
|
||||
|
||||
let counterStart = 0;
|
||||
|
||||
if (lastId !== null) {
|
||||
const parts = lastId.split(config.separator);
|
||||
const lastPart = parts[parts.length - 1];
|
||||
|
||||
if (/^\d+$/.test(lastPart)) {
|
||||
counterStart = parseInt(lastPart, 10);
|
||||
} else {
|
||||
for (let i = parts.length - 1; i >= 0; i--) {
|
||||
if (/^\d+$/.test(parts[i])) {
|
||||
counterStart = parseInt(parts[i], 10);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const nextCounter = counterStart + 1;
|
||||
globalThis.__idCounter = nextCounter;
|
||||
|
||||
sequentialNum = nextCounter.toString().padStart(config.segments.sequentialDigits, '0');
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error generating sequential number:', error);
|
||||
// Fallback to timestamp strategy if other methods fail
|
||||
sequentialNum = `${now.getTime()}`.slice(-config.segments.sequentialDigits);
|
||||
}
|
||||
|
||||
// Determine if year should be included and what value to use
|
||||
let yearValue = null;
|
||||
if (config.segments.year !== undefined || config.segments.year != false) {
|
||||
if (config.segments.year !== undefined && config.segments.year !== false) {
|
||||
if (typeof config.segments.year === 'number') {
|
||||
yearValue = String(config.segments.year);
|
||||
} else if (config.segments.year === true) {
|
||||
yearValue = format(now, 'yyyy');
|
||||
}
|
||||
// if year is false, yearValue remains null and won't be included
|
||||
} else {
|
||||
// Default behavior (backward compatibility)
|
||||
yearValue = format(now, 'yyyy');
|
||||
}
|
||||
|
||||
// Prepare components for ID assembly
|
||||
const components = {
|
||||
prefix: config.prefix,
|
||||
codes:
|
||||
config.segments.codes.length > 0
|
||||
? config.segments.codes.join(config.separator)
|
||||
: '',
|
||||
year: yearValue, // Added the year value to components
|
||||
year: yearValue,
|
||||
sequence: sequentialNum,
|
||||
date: dateString,
|
||||
time: timeString,
|
||||
|
@ -553,7 +580,6 @@ export function generateId(
|
|||
|
||||
let result: string;
|
||||
|
||||
// Use custom format if provided
|
||||
if (config.format) {
|
||||
let customID = config.format;
|
||||
for (const [key, value] of Object.entries(components)) {
|
||||
|
@ -565,10 +591,8 @@ export function generateId(
|
|||
);
|
||||
}
|
||||
}
|
||||
// Remove unused placeholders
|
||||
customID = customID.replace(/{[^}]+}/g, '');
|
||||
|
||||
// Clean up separators
|
||||
const escapedSeparator = config.separator.replace(
|
||||
/[-\/\\^$*+?.()|[\]{}]/g,
|
||||
'\\$&'
|
||||
|
@ -582,7 +606,6 @@ export function generateId(
|
|||
|
||||
result = config.upperCase ? customID.toUpperCase() : customID;
|
||||
} else {
|
||||
// Assemble ID from parts
|
||||
const parts = [];
|
||||
if (components.prefix) parts.push(components.prefix);
|
||||
if (components.codes) parts.push(components.codes);
|
||||
|
@ -595,7 +618,6 @@ export function generateId(
|
|||
if (config.upperCase) result = result.toUpperCase();
|
||||
}
|
||||
|
||||
// Handle collisions if required
|
||||
if (config.retryOnCollision) {
|
||||
let retryCount = 0;
|
||||
let originalResult = result;
|
||||
|
@ -607,7 +629,6 @@ export function generateId(
|
|||
result = `${originalResult}${config.separator}${suffix}`;
|
||||
} catch (error) {
|
||||
console.error('Error generating collision suffix:', error);
|
||||
// Simple fallback if crypto fails
|
||||
result = `${originalResult}${config.separator}${Date.now().toString(36)}`;
|
||||
}
|
||||
}
|
||||
|
@ -619,7 +640,6 @@ export function generateId(
|
|||
}
|
||||
}
|
||||
|
||||
// Register the ID and maintain registry size
|
||||
usedIdRegistry.add(result);
|
||||
if (usedIdRegistry.size > 10000) {
|
||||
const entriesToKeep = Array.from(usedIdRegistry).slice(-5000);
|
||||
|
@ -627,31 +647,161 @@ export function generateId(
|
|||
entriesToKeep.forEach((id) => usedIdRegistry.add(id));
|
||||
}
|
||||
|
||||
await updateLastId(config.prefix, result, {
|
||||
storage: config.storage,
|
||||
tableName: config.tableName
|
||||
});
|
||||
|
||||
return result.trim();
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the last ID from a specified table and column.
|
||||
* @param tableName - The name of the table to query.
|
||||
* @param columnName - The column containing the IDs.
|
||||
* @returns The last ID as a string, or null if no records exist.
|
||||
* Retrieves the last generated ID for a specific prefix
|
||||
* Used by generateId to determine the next sequential number
|
||||
*
|
||||
* @param {string} prefix - The prefix to look up (e.g., "INVOICE", "USER")
|
||||
* @param {Object} options - Additional options
|
||||
* @param {string} options.separator - The separator used in IDs (must match generateId)
|
||||
* @param {boolean} options.extractFullSequence - Whether to extract the full sequence or just the numeric part
|
||||
* @param {string} options.storage - Storage method ('memory', 'localStorage', 'database')
|
||||
* @returns {string|null} - Returns the last ID or null if none exists
|
||||
*/
|
||||
export async function getLastId(
|
||||
tableName: string,
|
||||
columnName: string
|
||||
prefix: string,
|
||||
options: {
|
||||
separator?: string;
|
||||
extractFullSequence?: boolean;
|
||||
storage?: 'memory' | 'localStorage' | 'database';
|
||||
tableName?: string;
|
||||
} = {}
|
||||
): Promise<string | null> {
|
||||
try {
|
||||
const result = await db.$queryRawUnsafe(
|
||||
`SELECT ${columnName} FROM ${tableName} ORDER BY ${columnName} DESC LIMIT 1`
|
||||
);
|
||||
const config = {
|
||||
separator: options.separator || '-',
|
||||
extractFullSequence: options.extractFullSequence ?? false,
|
||||
storage: options.storage || 'memory',
|
||||
tableName: options.tableName
|
||||
};
|
||||
|
||||
if (Array.isArray(result) && result.length > 0) {
|
||||
return result[0][columnName];
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error fetching last ID:', error);
|
||||
if (typeof globalThis.__idRegistry === 'undefined') {
|
||||
globalThis.__idRegistry = {};
|
||||
}
|
||||
|
||||
return null;
|
||||
const normalizedPrefix = prefix.toUpperCase();
|
||||
|
||||
let lastId: string | null = null;
|
||||
|
||||
switch (config.storage) {
|
||||
case 'localStorage':
|
||||
try {
|
||||
const storedIds = localStorage.getItem('customIdRegistry');
|
||||
if (storedIds) {
|
||||
const registry = JSON.parse(storedIds);
|
||||
lastId = registry[normalizedPrefix] || null;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error accessing localStorage:', error);
|
||||
lastId = globalThis.__idRegistry[normalizedPrefix] || null;
|
||||
}
|
||||
break;
|
||||
|
||||
case 'database':
|
||||
if (!config.tableName) {
|
||||
console.warn('Table name not provided for database storage. Falling back to memory storage.');
|
||||
lastId = globalThis.__idRegistry[normalizedPrefix] || null;
|
||||
break;
|
||||
}
|
||||
|
||||
try {
|
||||
const result = await db.$queryRawUnsafe<{id: string}[]>(
|
||||
`SELECT id FROM ${config.tableName}
|
||||
WHERE id LIKE $1
|
||||
ORDER BY id DESC
|
||||
LIMIT 1`,
|
||||
`${normalizedPrefix}%`
|
||||
);
|
||||
|
||||
if (result && result.length > 0) {
|
||||
lastId = result[0].id;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`Error querying database for last ID in ${config.tableName}:`, error);
|
||||
lastId = globalThis.__idRegistry[normalizedPrefix] || null;
|
||||
}
|
||||
break;
|
||||
|
||||
case 'memory':
|
||||
default:
|
||||
lastId = globalThis.__idRegistry[normalizedPrefix] || null;
|
||||
break;
|
||||
}
|
||||
|
||||
return lastId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the last ID record for a specific prefix
|
||||
* Should be called by generateId after creating a new ID
|
||||
*
|
||||
* @param {string} prefix - The prefix to update
|
||||
* @param {string} id - The newly generated ID to store
|
||||
* @param {Object} options - Additional options (matching getLastId options)
|
||||
*/
|
||||
export async function updateLastId(
|
||||
prefix: string,
|
||||
id: string,
|
||||
options: {
|
||||
storage?: 'memory' | 'localStorage' | 'database';
|
||||
tableName?: string;
|
||||
} = {}
|
||||
): Promise<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 {
|
||||
if (num === undefined || num === null) return "N/A";
|
||||
|
||||
// If number is in the thousands, abbreviate
|
||||
if (num >= 1_000_000) {
|
||||
return (num / 1_000_000).toFixed(1) + 'M';
|
||||
}
|
||||
|
@ -786,7 +935,6 @@ export function formatNumber(num?: number): string {
|
|||
return (num / 1_000).toFixed(1) + 'K';
|
||||
}
|
||||
|
||||
// Otherwise, format with commas
|
||||
return num.toLocaleString();
|
||||
}
|
||||
|
||||
|
|
|
@ -250,3 +250,16 @@ export const processDistrictFeature = (
|
|||
isFocused: true,
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Format distance in a human-readable way
|
||||
* @param meters Distance in meters
|
||||
* @returns Formatted distance string
|
||||
*/
|
||||
export function formatDistance(meters: number): string {
|
||||
if (meters < 1000) {
|
||||
return `${Math.round(meters)} m`;
|
||||
} else {
|
||||
return `${(meters / 1000).toFixed(1)} km`;
|
||||
}
|
||||
}
|
|
@ -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",
|
||||
"resend": "^4.1.2",
|
||||
"sonner": "^2.0.1",
|
||||
"three": "^0.176.0",
|
||||
"threebox-plugin": "^2.2.7",
|
||||
"uuid": "^11.1.0",
|
||||
"vaul": "^1.1.2",
|
||||
"zod": "^3.24.2",
|
||||
|
@ -72,6 +74,7 @@
|
|||
"@types/node": "^22.10.2",
|
||||
"@types/react": "^19.0.2",
|
||||
"@types/react-dom": "19.0.2",
|
||||
"@types/three": "^0.176.0",
|
||||
"@types/uuid": "^10.0.0",
|
||||
"postcss": "8.4.49",
|
||||
"prisma": "^6.4.1",
|
||||
|
@ -429,6 +432,13 @@
|
|||
"@jridgewell/sourcemap-codec": "^1.4.10"
|
||||
}
|
||||
},
|
||||
"node_modules/@dimforge/rapier3d-compat": {
|
||||
"version": "0.12.0",
|
||||
"resolved": "https://registry.npmjs.org/@dimforge/rapier3d-compat/-/rapier3d-compat-0.12.0.tgz",
|
||||
"integrity": "sha512-uekIGetywIgopfD97oDL5PfeezkFpNhwlzlaEYNOA0N6ghdsOvh/HYjSMek5Q2O1PYvRSDFcqFVJl4r4ZBwOow==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0"
|
||||
},
|
||||
"node_modules/@emnapi/runtime": {
|
||||
"version": "1.3.1",
|
||||
"resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.3.1.tgz",
|
||||
|
@ -7207,6 +7217,13 @@
|
|||
"url": "https://opencollective.com/turf"
|
||||
}
|
||||
},
|
||||
"node_modules/@tweenjs/tween.js": {
|
||||
"version": "23.1.3",
|
||||
"resolved": "https://registry.npmjs.org/@tweenjs/tween.js/-/tween.js-23.1.3.tgz",
|
||||
"integrity": "sha512-vJmvvwFxYuGnF2axRtPYocag6Clbb5YS7kLL+SO/TeVFzHqDIWrNKYtcsPMibjDx9O+bu+psAy9NKfWklassUA==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/cacheable-request": {
|
||||
"version": "6.0.3",
|
||||
"resolved": "https://registry.npmjs.org/@types/cacheable-request/-/cacheable-request-6.0.3.tgz",
|
||||
|
@ -7506,6 +7523,13 @@
|
|||
"integrity": "sha512-UE7oxhQLLd9gub6JKIAhDq06T0F6FnztwMNRvYgjeQSBeMc1ZG/tA47EwfduvkuQS8apbkM/lpLpWsaCeYsXVg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/stats.js": {
|
||||
"version": "0.17.3",
|
||||
"resolved": "https://registry.npmjs.org/@types/stats.js/-/stats.js-0.17.3.tgz",
|
||||
"integrity": "sha512-pXNfAD3KHOdif9EQXZ9deK82HVNaXP5ZIF5RP2QG6OQFNTaY2YIetfrE9t528vEreGQvEPRDDc8muaoYeK0SxQ==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/supercluster": {
|
||||
"version": "7.1.3",
|
||||
"resolved": "https://registry.npmjs.org/@types/supercluster/-/supercluster-7.1.3.tgz",
|
||||
|
@ -7524,6 +7548,22 @@
|
|||
"@types/node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/three": {
|
||||
"version": "0.176.0",
|
||||
"resolved": "https://registry.npmjs.org/@types/three/-/three-0.176.0.tgz",
|
||||
"integrity": "sha512-FwfPXxCqOtP7EdYMagCFePNKoG1AGBDUEVKtluv2BTVRpSt7b+X27xNsirPCTCqY1pGYsPUzaM3jgWP7dXSxlw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@dimforge/rapier3d-compat": "^0.12.0",
|
||||
"@tweenjs/tween.js": "~23.1.3",
|
||||
"@types/stats.js": "*",
|
||||
"@types/webxr": "*",
|
||||
"@webgpu/types": "*",
|
||||
"fflate": "~0.8.2",
|
||||
"meshoptimizer": "~0.18.1"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/uuid": {
|
||||
"version": "10.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-10.0.0.tgz",
|
||||
|
@ -7531,6 +7571,13 @@
|
|||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/webxr": {
|
||||
"version": "0.5.22",
|
||||
"resolved": "https://registry.npmjs.org/@types/webxr/-/webxr-0.5.22.tgz",
|
||||
"integrity": "sha512-Vr6Stjv5jPRqH690f5I5GLjVk8GSsoQSYJ2FVd/3jJF7KaqfwPi3ehfBS96mlQ2kPCwZaX6U0rG2+NGHBKkA/A==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/ws": {
|
||||
"version": "8.5.14",
|
||||
"resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.5.14.tgz",
|
||||
|
@ -7850,6 +7897,13 @@
|
|||
"@xtuc/long": "4.2.2"
|
||||
}
|
||||
},
|
||||
"node_modules/@webgpu/types": {
|
||||
"version": "0.1.60",
|
||||
"resolved": "https://registry.npmjs.org/@webgpu/types/-/types-0.1.60.tgz",
|
||||
"integrity": "sha512-8B/tdfRFKdrnejqmvq95ogp8tf52oZ51p3f4QD5m5Paey/qlX4Rhhy5Y8tgFMi7Ms70HzcMMw3EQjH/jdhTwlA==",
|
||||
"dev": true,
|
||||
"license": "BSD-3-Clause"
|
||||
},
|
||||
"node_modules/@xtuc/ieee754": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/@xtuc/ieee754/-/ieee754-1.2.0.tgz",
|
||||
|
@ -9960,6 +10014,13 @@
|
|||
"reusify": "^1.0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/fflate": {
|
||||
"version": "0.8.2",
|
||||
"resolved": "https://registry.npmjs.org/fflate/-/fflate-0.8.2.tgz",
|
||||
"integrity": "sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/file-entry-cache": {
|
||||
"version": "8.0.0",
|
||||
"resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz",
|
||||
|
@ -11270,6 +11331,13 @@
|
|||
"node": ">= 8"
|
||||
}
|
||||
},
|
||||
"node_modules/meshoptimizer": {
|
||||
"version": "0.18.1",
|
||||
"resolved": "https://registry.npmjs.org/meshoptimizer/-/meshoptimizer-0.18.1.tgz",
|
||||
"integrity": "sha512-ZhoIoL7TNV4s5B6+rx5mC//fw8/POGyNxS/DZyCJeiZ12ScLfVwRE/GfsxwiTkMYYD5DmK2/JXnEVXqL4rF+Sw==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/micromatch": {
|
||||
"version": "4.0.8",
|
||||
"resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz",
|
||||
|
@ -14824,6 +14892,18 @@
|
|||
"node": ">=0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/three": {
|
||||
"version": "0.176.0",
|
||||
"resolved": "https://registry.npmjs.org/three/-/three-0.176.0.tgz",
|
||||
"integrity": "sha512-PWRKYWQo23ojf9oZSlRGH8K09q7nRSWx6LY/HF/UUrMdYgN9i1e2OwJYHoQjwc6HF/4lvvYLC5YC1X8UJL2ZpA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/threebox-plugin": {
|
||||
"version": "2.2.7",
|
||||
"resolved": "https://registry.npmjs.org/threebox-plugin/-/threebox-plugin-2.2.7.tgz",
|
||||
"integrity": "sha512-H87Nm4w1PfisHPHzavTGXlwIoJpx2+QU57GooQYIhF51lsg+U5A0KGf3Jrv/HWsLCGOwV2BTnv7UTLfpO1EccQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/tiny-invariant": {
|
||||
"version": "1.3.3",
|
||||
"resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.3.3.tgz",
|
||||
|
|
|
@ -66,6 +66,8 @@
|
|||
"recharts": "^2.15.2",
|
||||
"resend": "^4.1.2",
|
||||
"sonner": "^2.0.1",
|
||||
"three": "^0.176.0",
|
||||
"threebox-plugin": "^2.2.7",
|
||||
"uuid": "^11.1.0",
|
||||
"vaul": "^1.1.2",
|
||||
"zod": "^3.24.2",
|
||||
|
@ -78,6 +80,7 @@
|
|||
"@types/node": "^22.10.2",
|
||||
"@types/react": "^19.0.2",
|
||||
"@types/react-dom": "19.0.2",
|
||||
"@types/three": "^0.176.0",
|
||||
"@types/uuid": "^10.0.0",
|
||||
"postcss": "8.4.49",
|
||||
"prisma": "^6.4.1",
|
||||
|
|
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[];
|
||||
|
||||
for (const category of crimeCategoriesData) {
|
||||
const newId = generateId({
|
||||
const newId = await generateId({
|
||||
prefix: 'CC',
|
||||
segments: {
|
||||
sequentialDigits: 4,
|
||||
|
@ -44,6 +44,8 @@ export class CrimeCategoriesSeeder {
|
|||
randomSequence: false,
|
||||
uniquenessStrategy: 'counter',
|
||||
separator: '-',
|
||||
tableName: 'crime_categories',
|
||||
storage: 'database',
|
||||
});
|
||||
|
||||
await this.prisma.crime_categories.create({
|
||||
|
|
|
@ -409,7 +409,7 @@ export class CrimeIncidentsSeeder {
|
|||
}
|
||||
|
||||
// Generate a unique ID for the incident
|
||||
const incidentId = generateId({
|
||||
const incidentId = await generateId({
|
||||
prefix: 'CI',
|
||||
segments: {
|
||||
codes: [district.cities.id],
|
||||
|
@ -420,6 +420,8 @@ export class CrimeIncidentsSeeder {
|
|||
separator: '-',
|
||||
randomSequence: false,
|
||||
uniquenessStrategy: 'counter',
|
||||
storage: 'database',
|
||||
tableName: 'crime_incidents',
|
||||
});
|
||||
|
||||
// Determine status based on crime_cleared
|
||||
|
|
|
@ -177,7 +177,7 @@ export class CrimesSeeder {
|
|||
|
||||
const year = parseInt(record.year);
|
||||
// Create a unique ID for monthly crime data
|
||||
const crimeId = generateId({
|
||||
const crimeId = await generateId({
|
||||
prefix: 'CR',
|
||||
segments: {
|
||||
codes: [city.id],
|
||||
|
@ -188,6 +188,8 @@ export class CrimesSeeder {
|
|||
separator: '-',
|
||||
randomSequence: false,
|
||||
uniquenessStrategy: 'counter',
|
||||
storage: 'database',
|
||||
tableName: 'crimes',
|
||||
});
|
||||
|
||||
await this.prisma.crimes.create({
|
||||
|
@ -257,7 +259,7 @@ export class CrimesSeeder {
|
|||
}
|
||||
|
||||
// Create a unique ID for yearly crime data
|
||||
const crimeId = generateId({
|
||||
const crimeId = await generateId({
|
||||
prefix: 'CR',
|
||||
segments: {
|
||||
codes: [city.id],
|
||||
|
@ -268,6 +270,8 @@ export class CrimesSeeder {
|
|||
separator: '-',
|
||||
randomSequence: false,
|
||||
uniquenessStrategy: 'counter',
|
||||
storage: 'database',
|
||||
tableName: 'crimes',
|
||||
});
|
||||
|
||||
await this.prisma.crimes.create({
|
||||
|
@ -333,7 +337,7 @@ export class CrimesSeeder {
|
|||
}
|
||||
|
||||
// Create a unique ID for all-year summary data
|
||||
const crimeId = generateId({
|
||||
const crimeId = await generateId({
|
||||
prefix: 'CR',
|
||||
segments: {
|
||||
codes: [city.id],
|
||||
|
@ -343,6 +347,8 @@ export class CrimesSeeder {
|
|||
separator: '-',
|
||||
randomSequence: false,
|
||||
uniquenessStrategy: 'counter',
|
||||
storage: 'database',
|
||||
tableName: 'crimes',
|
||||
});
|
||||
|
||||
await this.prisma.crimes.create({
|
||||
|
|
|
@ -89,7 +89,7 @@ export class UnitSeeder {
|
|||
const address = location.address;
|
||||
const phone = location.telepon?.replace(/-/g, '');
|
||||
|
||||
const code_unit = generateId({
|
||||
const newId = await generateId({
|
||||
prefix: 'UT',
|
||||
format: '{prefix}-{sequence}',
|
||||
separator: '-',
|
||||
|
@ -98,11 +98,13 @@ export class UnitSeeder {
|
|||
segments: {
|
||||
sequentialDigits: 4,
|
||||
},
|
||||
storage: 'database',
|
||||
tableName: 'units',
|
||||
});
|
||||
|
||||
let locationData: CreateLocationDto = {
|
||||
district_id: city.districts[0].id, // This will be replaced with Patrang's ID
|
||||
code_unit: code_unit,
|
||||
code_unit: newId,
|
||||
name: `Polres ${city.name}`,
|
||||
description: `Unit ${city.name} is categorized as POLRES and operates in the ${city.name} area.`,
|
||||
type: 'polres',
|
||||
|
@ -152,7 +154,7 @@ export class UnitSeeder {
|
|||
const address = location.address;
|
||||
const phone = location.telepon?.replace(/-/g, '');
|
||||
|
||||
const code_unit = generateId({
|
||||
const newId = await generateId({
|
||||
prefix: 'UT',
|
||||
format: '{prefix}-{sequence}',
|
||||
separator: '-',
|
||||
|
@ -161,11 +163,13 @@ export class UnitSeeder {
|
|||
segments: {
|
||||
sequentialDigits: 4,
|
||||
},
|
||||
storage: 'database',
|
||||
tableName: 'units',
|
||||
});
|
||||
|
||||
const locationData: CreateLocationDto = {
|
||||
district_id: district.id,
|
||||
code_unit,
|
||||
code_unit: newId,
|
||||
name: `Polsek ${district.name}`,
|
||||
description: `Unit ${district.name} is categorized as POLSEK and operates in the ${district.name} area.`,
|
||||
type: 'polsek',
|
||||
|
@ -187,7 +191,7 @@ export class UnitSeeder {
|
|||
}
|
||||
|
||||
console.log(
|
||||
`Inserted unit for district: ${district.name}, code_unit: ${code_unit} at ${lng}, ${lat}`
|
||||
`Inserted unit for district: ${district.name}, newId: ${newId} at ${lng}, ${lat}`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -68,60 +68,51 @@ CREATE TRIGGER on_auth_user_deleted BEFORE DELETE ON auth.users FOR EACH ROW EXE
|
|||
|
||||
CREATE TRIGGER on_auth_user_updated AFTER UPDATE ON auth.users FOR EACH ROW EXECUTE FUNCTION handle_user_update();
|
||||
|
||||
|
||||
drop trigger if exists "objects_delete_delete_prefix" on "storage"."objects";
|
||||
|
||||
drop trigger if exists "objects_insert_create_prefix" on "storage"."objects";
|
||||
|
||||
drop trigger if exists "objects_update_create_prefix" on "storage"."objects";
|
||||
|
||||
drop trigger if exists "prefixes_create_hierarchy" on "storage"."prefixes";
|
||||
|
||||
drop trigger if exists "prefixes_delete_hierarchy" on "storage"."prefixes";
|
||||
|
||||
revoke delete on table "storage"."prefixes" from "anon";
|
||||
|
||||
revoke insert on table "storage"."prefixes" from "anon";
|
||||
|
||||
revoke references on table "storage"."prefixes" from "anon";
|
||||
|
||||
revoke select on table "storage"."prefixes" from "anon";
|
||||
|
||||
revoke trigger on table "storage"."prefixes" from "anon";
|
||||
|
||||
revoke truncate on table "storage"."prefixes" from "anon";
|
||||
|
||||
revoke update on table "storage"."prefixes" from "anon";
|
||||
|
||||
revoke delete on table "storage"."prefixes" from "authenticated";
|
||||
|
||||
revoke insert on table "storage"."prefixes" from "authenticated";
|
||||
|
||||
revoke references on table "storage"."prefixes" from "authenticated";
|
||||
|
||||
revoke select on table "storage"."prefixes" from "authenticated";
|
||||
|
||||
revoke trigger on table "storage"."prefixes" from "authenticated";
|
||||
|
||||
revoke truncate on table "storage"."prefixes" from "authenticated";
|
||||
|
||||
revoke update on table "storage"."prefixes" from "authenticated";
|
||||
|
||||
revoke delete on table "storage"."prefixes" from "service_role";
|
||||
|
||||
revoke insert on table "storage"."prefixes" from "service_role";
|
||||
|
||||
revoke references on table "storage"."prefixes" from "service_role";
|
||||
|
||||
revoke select on table "storage"."prefixes" from "service_role";
|
||||
|
||||
revoke trigger on table "storage"."prefixes" from "service_role";
|
||||
|
||||
revoke truncate on table "storage"."prefixes" from "service_role";
|
||||
|
||||
revoke update on table "storage"."prefixes" from "service_role";
|
||||
|
||||
alter table "storage"."prefixes" drop constraint "prefixes_bucketId_fkey";
|
||||
DO $$
|
||||
BEGIN
|
||||
IF EXISTS (SELECT FROM pg_catalog.pg_tables
|
||||
WHERE schemaname = 'storage'
|
||||
AND tablename = 'prefixes') THEN
|
||||
|
||||
EXECUTE 'drop trigger if exists "prefixes_create_hierarchy" on "storage"."prefixes"';
|
||||
EXECUTE 'drop trigger if exists "prefixes_delete_hierarchy" on "storage"."prefixes"';
|
||||
|
||||
EXECUTE 'revoke delete on table "storage"."prefixes" from "anon"';
|
||||
EXECUTE 'revoke insert on table "storage"."prefixes" from "anon"';
|
||||
EXECUTE 'revoke references on table "storage"."prefixes" from "anon"';
|
||||
EXECUTE 'revoke select on table "storage"."prefixes" from "anon"';
|
||||
EXECUTE 'revoke trigger on table "storage"."prefixes" from "anon"';
|
||||
EXECUTE 'revoke truncate on table "storage"."prefixes" from "anon"';
|
||||
EXECUTE 'revoke update on table "storage"."prefixes" from "anon"';
|
||||
|
||||
EXECUTE 'revoke delete on table "storage"."prefixes" from "authenticated"';
|
||||
EXECUTE 'revoke insert on table "storage"."prefixes" from "authenticated"';
|
||||
EXECUTE 'revoke references on table "storage"."prefixes" from "authenticated"';
|
||||
EXECUTE 'revoke select on table "storage"."prefixes" from "authenticated"';
|
||||
EXECUTE 'revoke trigger on table "storage"."prefixes" from "authenticated"';
|
||||
EXECUTE 'revoke truncate on table "storage"."prefixes" from "authenticated"';
|
||||
EXECUTE 'revoke update on table "storage"."prefixes" from "authenticated"';
|
||||
|
||||
EXECUTE 'revoke delete on table "storage"."prefixes" from "service_role"';
|
||||
EXECUTE 'revoke insert on table "storage"."prefixes" from "service_role"';
|
||||
EXECUTE 'revoke references on table "storage"."prefixes" from "service_role"';
|
||||
EXECUTE 'revoke select on table "storage"."prefixes" from "service_role"';
|
||||
EXECUTE 'revoke trigger on table "storage"."prefixes" from "service_role"';
|
||||
EXECUTE 'revoke truncate on table "storage"."prefixes" from "service_role"';
|
||||
EXECUTE 'revoke update on table "storage"."prefixes" from "service_role"';
|
||||
|
||||
EXECUTE 'alter table "storage"."prefixes" drop constraint if exists "prefixes_bucketId_fkey"';
|
||||
EXECUTE 'alter table "storage"."prefixes" drop constraint if exists "prefixes_pkey"';
|
||||
|
||||
EXECUTE 'drop table "storage"."prefixes"';
|
||||
END IF;
|
||||
END $$;
|
||||
|
||||
drop function if exists "storage"."add_prefixes"(_bucket_id text, _name text);
|
||||
|
||||
|
@ -147,21 +138,41 @@ drop function if exists "storage"."search_v1_optimised"(prefix text, bucketname
|
|||
|
||||
drop function if exists "storage"."search_v2"(prefix text, bucket_name text, limits integer, levels integer, start_after text);
|
||||
|
||||
alter table "storage"."prefixes" drop constraint "prefixes_pkey";
|
||||
DO $$
|
||||
BEGIN
|
||||
IF EXISTS (SELECT 1 FROM pg_indexes WHERE schemaname = 'storage' AND indexname = 'idx_name_bucket_level_unique') THEN
|
||||
EXECUTE 'drop index "storage"."idx_name_bucket_level_unique"';
|
||||
END IF;
|
||||
|
||||
IF EXISTS (SELECT 1 FROM pg_indexes WHERE schemaname = 'storage' AND indexname = 'idx_objects_lower_name') THEN
|
||||
EXECUTE 'drop index "storage"."idx_objects_lower_name"';
|
||||
END IF;
|
||||
|
||||
IF EXISTS (SELECT 1 FROM pg_indexes WHERE schemaname = 'storage' AND indexname = 'idx_prefixes_lower_name') THEN
|
||||
EXECUTE 'drop index "storage"."idx_prefixes_lower_name"';
|
||||
END IF;
|
||||
|
||||
IF EXISTS (SELECT 1 FROM pg_indexes WHERE schemaname = 'storage' AND indexname = 'objects_bucket_id_level_idx') THEN
|
||||
EXECUTE 'drop index "storage"."objects_bucket_id_level_idx"';
|
||||
END IF;
|
||||
|
||||
IF EXISTS (SELECT 1 FROM pg_indexes WHERE schemaname = 'storage' AND indexname = 'prefixes_pkey') THEN
|
||||
EXECUTE 'drop index "storage"."prefixes_pkey"';
|
||||
END IF;
|
||||
END $$;
|
||||
|
||||
drop index if exists "storage"."idx_name_bucket_level_unique";
|
||||
|
||||
drop index if exists "storage"."idx_objects_lower_name";
|
||||
|
||||
drop index if exists "storage"."idx_prefixes_lower_name";
|
||||
|
||||
drop index if exists "storage"."objects_bucket_id_level_idx";
|
||||
|
||||
drop index if exists "storage"."prefixes_pkey";
|
||||
|
||||
drop table "storage"."prefixes";
|
||||
|
||||
alter table "storage"."objects" drop column "level";
|
||||
DO $$
|
||||
BEGIN
|
||||
IF EXISTS (
|
||||
SELECT 1
|
||||
FROM information_schema.columns
|
||||
WHERE table_schema = 'storage'
|
||||
AND table_name = 'objects'
|
||||
AND column_name = 'level'
|
||||
) THEN
|
||||
EXECUTE 'alter table "storage"."objects" drop column "level"';
|
||||
END IF;
|
||||
END $$;
|
||||
|
||||
set check_function_bodies = off;
|
||||
|
||||
|
@ -316,12 +327,12 @@ using ((bucket_id = 'avatars'::text));
|
|||
|
||||
|
||||
|
||||
drop type "gis"."geometry_dump";
|
||||
-- drop type "gis"."geometry_dump";
|
||||
|
||||
drop type "gis"."valid_detail";
|
||||
-- drop type "gis"."valid_detail";
|
||||
|
||||
create type "gis"."geometry_dump" as ("path" integer[], "geom" geometry);
|
||||
-- create type "gis"."geometry_dump" as ("path" integer[], "geom" geometry);
|
||||
|
||||
create type "gis"."valid_detail" as ("valid" boolean, "reason" character varying, "location" geometry);
|
||||
-- create type "gis"."valid_detail" as ("valid" boolean, "reason" character varying, "location" geometry);
|
||||
|
||||
|
||||
|
|
|
@ -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