Add crime data CSV files and update database schema
- Added crime summary by type CSV file with district-wise crime statistics. - Added yearly crime data CSV file with detailed crime statistics per district and year. - Modified the database schema to include a new column `source_type` in the `crimes` table and a new column `distance` in the `locations` table. - Created migration to add `location_logs` table for tracking user location data with relevant fields and indices. - Updated migration to drop the `distance` column and replace it with `distance_from_unit` in the `locations` table. - Added additional migrations to drop and recreate the `location_logs` table, modify existing columns, and enforce new constraints in the `unit_statistics` and `units` tables.
This commit is contained in:
parent
f4b1d9d633
commit
e422c59da9
|
@ -1,3 +1,5 @@
|
|||
{
|
||||
"recommendations": ["denoland.vscode-deno"]
|
||||
"recommendations": [
|
||||
"denoland.vscode-deno"
|
||||
]
|
||||
}
|
||||
|
|
|
@ -0,0 +1,43 @@
|
|||
"use client";
|
||||
|
||||
import { useState, useEffect } from 'react';
|
||||
import mapboxgl from 'mapbox-gl';
|
||||
import RecentCrimesLayer from '../layers/recent-crimes-layer';
|
||||
import EWSAlertLayer from '../layers/ews-alert-layer';
|
||||
|
||||
import { IIncidentLog } from '@/app/_utils/types/ews';
|
||||
|
||||
interface AlertLayerContainerProps {
|
||||
map: mapboxgl.Map | null;
|
||||
activeLayer: string;
|
||||
incidents: IIncidentLog[];
|
||||
onIncidentResolved?: (id: string) => void;
|
||||
}
|
||||
|
||||
export default function AlertLayerContainer({
|
||||
map,
|
||||
activeLayer,
|
||||
incidents,
|
||||
onIncidentResolved,
|
||||
}: AlertLayerContainerProps) {
|
||||
const [ewsVisible, setEwsVisible] = useState(false);
|
||||
|
||||
// Determine which layers to show based on activeLayer
|
||||
useEffect(() => {
|
||||
const isAlertLayer = activeLayer === 'alerts';
|
||||
setEwsVisible(isAlertLayer);
|
||||
}, [activeLayer]);
|
||||
|
||||
|
||||
return (
|
||||
<>
|
||||
{/* EWS Alert Layer for emergency notifications */}
|
||||
<EWSAlertLayer
|
||||
map={map}
|
||||
incidents={incidents}
|
||||
onIncidentResolved={onIncidentResolved}
|
||||
visible={ewsVisible}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
|
@ -1,6 +1,6 @@
|
|||
"use client"
|
||||
|
||||
import { useState } from 'react';
|
||||
import { useState, useEffect } from 'react';
|
||||
import { Button } from '@/app/_components/ui/button';
|
||||
import {
|
||||
AlertTriangle,
|
||||
|
@ -13,96 +13,144 @@ import {
|
|||
import { cn } from '@/app/_lib/utils';
|
||||
import { Badge } from '@/app/_components/ui/badge';
|
||||
import { IIncidentLog } from '@/app/_utils/types/ews';
|
||||
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/app/_components/ui/tooltip";
|
||||
|
||||
interface PanicButtonDemoProps {
|
||||
onTriggerAlert: (priority: 'high' | 'medium' | 'low') => void;
|
||||
onResolveAllAlerts: () => void;
|
||||
activeIncidents: IIncidentLog[];
|
||||
className?: string;
|
||||
onPanicTriggered?: () => void; // New callback for notifying parent about panic trigger
|
||||
}
|
||||
|
||||
export default function PanicButtonDemo({
|
||||
onTriggerAlert,
|
||||
onResolveAllAlerts,
|
||||
activeIncidents,
|
||||
className
|
||||
className,
|
||||
onPanicTriggered
|
||||
}: PanicButtonDemoProps) {
|
||||
const [isTriggering, setIsTriggering] = useState(false);
|
||||
const [activeButton, setActiveButton] = useState<string | null>(null);
|
||||
|
||||
const handleTriggerPanic = (priority: 'high' | 'medium' | 'low') => {
|
||||
setIsTriggering(true);
|
||||
setActiveButton(priority);
|
||||
onTriggerAlert(priority);
|
||||
|
||||
// Notify parent component that panic was triggered
|
||||
if (onPanicTriggered) {
|
||||
onPanicTriggered();
|
||||
}
|
||||
|
||||
// Reset animation
|
||||
setTimeout(() => {
|
||||
setIsTriggering(false);
|
||||
setActiveButton(null);
|
||||
}, 1000);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={cn("border border-muted bg-background p-3 rounded-lg shadow-xl", className)}>
|
||||
<div className="flex items-center justify-between mb-3">
|
||||
<h3 className="font-bold flex items-center gap-2">
|
||||
<Shield className="h-5 w-5 text-red-500" />
|
||||
<span>EWS Panic Button Demo</span>
|
||||
</h3>
|
||||
<div className={cn("border border-muted bg-background p-1 rounded-lg shadow-xl", className)}>
|
||||
<TooltipProvider>
|
||||
<div className="flex items-center space-x-1">
|
||||
{/* High Priority Alert Button */}
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
size="medium"
|
||||
className={cn(
|
||||
"h-8 w-8 rounded-md",
|
||||
activeButton === 'high'
|
||||
? "bg-red-600 text-white"
|
||||
: "bg-background text-red-600 hover:bg-red-600 hover:text-white",
|
||||
isTriggering && activeButton === 'high' && "animate-pulse"
|
||||
)}
|
||||
onClick={() => handleTriggerPanic('high')}
|
||||
>
|
||||
<AlertTriangle className="h-5 w-5" />
|
||||
<span className="sr-only">High Priority Alert</span>
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="bottom">
|
||||
<p>Send High Priority Alert</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
|
||||
{activeIncidents.length > 0 && (
|
||||
<Badge variant="destructive" className="ml-2 animate-pulse">
|
||||
{activeIncidents.length} Active
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
{/* Medium Priority Alert Button */}
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
size="medium"
|
||||
className={cn(
|
||||
"h-8 w-8 rounded-md",
|
||||
activeButton === 'medium'
|
||||
? "bg-amber-600 text-white"
|
||||
: "bg-background text-amber-600 hover:bg-amber-600 hover:text-white",
|
||||
isTriggering && activeButton === 'medium' && "animate-pulse"
|
||||
)}
|
||||
onClick={() => handleTriggerPanic('medium')}
|
||||
>
|
||||
<Bell className="h-5 w-5" />
|
||||
<span className="sr-only">Medium Priority Alert</span>
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="bottom">
|
||||
<p>Send Medium Priority Alert</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
|
||||
<div className="flex flex-col gap-2">
|
||||
<Button
|
||||
variant="destructive"
|
||||
size="lg"
|
||||
className={cn(
|
||||
"bg-red-600 hover:bg-red-700 flex items-center gap-2 transition-all",
|
||||
isTriggering && "animate-ping"
|
||||
{/* Low Priority Alert Button */}
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
size="medium"
|
||||
className={cn(
|
||||
"h-8 w-8 rounded-md",
|
||||
activeButton === 'low'
|
||||
? "bg-blue-600 text-white"
|
||||
: "bg-background text-blue-600 hover:bg-blue-600 hover:text-white",
|
||||
isTriggering && activeButton === 'low' && "animate-pulse"
|
||||
)}
|
||||
onClick={() => handleTriggerPanic('low')}
|
||||
>
|
||||
<RadioTower className="h-5 w-5" />
|
||||
<span className="sr-only">Low Priority Alert</span>
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="bottom">
|
||||
<p>Send Low Priority Alert</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
|
||||
{/* Resolve All Alerts Button (only shown when there are active incidents) */}
|
||||
{activeIncidents.length > 0 && (
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
size="medium"
|
||||
className="h-8 w-8 rounded-md bg-background text-green-600 hover:bg-green-600 hover:text-white relative"
|
||||
onClick={onResolveAllAlerts}
|
||||
>
|
||||
<Shield className="h-5 w-5" />
|
||||
<span className="sr-only">Resolve All Alerts</span>
|
||||
|
||||
{/* Badge showing active incident count */}
|
||||
<Badge
|
||||
variant="destructive"
|
||||
className="absolute -top-2 -right-2 h-4 w-4 flex items-center justify-center p-0 text-[10px]"
|
||||
>
|
||||
{activeIncidents.length}
|
||||
</Badge>
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="bottom">
|
||||
<p>Resolve All Active Alerts ({activeIncidents.length})</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
)}
|
||||
onClick={() => handleTriggerPanic('high')}
|
||||
>
|
||||
<AlertTriangle className="h-5 w-5" />
|
||||
<span>SEND HIGH PRIORITY ALERT</span>
|
||||
</Button>
|
||||
|
||||
<div className="grid grid-cols-2 gap-2">
|
||||
<Button
|
||||
variant="default"
|
||||
className="bg-amber-600 hover:bg-amber-700 flex items-center gap-2"
|
||||
onClick={() => handleTriggerPanic('medium')}
|
||||
>
|
||||
<Bell className="h-4 w-4" />
|
||||
<span>Medium Priority</span>
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
variant="outline"
|
||||
className="border-blue-600 text-blue-600 hover:bg-blue-100 flex items-center gap-2"
|
||||
onClick={() => handleTriggerPanic('low')}
|
||||
>
|
||||
<RadioTower className="h-4 w-4" />
|
||||
<span>Low Priority</span>
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{activeIncidents.length > 0 && (
|
||||
<Button
|
||||
variant="outline"
|
||||
className="mt-3 border-green-600 text-green-600 hover:bg-green-100"
|
||||
onClick={onResolveAllAlerts}
|
||||
>
|
||||
<Shield className="h-4 w-4 mr-2" />
|
||||
<span>Resolve All Alerts</span>
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<p className="text-xs text-muted-foreground mt-2">
|
||||
Simulates a mobile app panic button activation in the Jember area.
|
||||
</p>
|
||||
</TooltipProvider>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -6,7 +6,7 @@ import { Popover, PopoverContent, PopoverTrigger } from "@/app/_components/ui/po
|
|||
import { ChevronDown, Layers, Siren } from "lucide-react"
|
||||
import { IconMessage } from "@tabler/icons-react"
|
||||
|
||||
import { useRef, useState } from "react"
|
||||
import { useEffect, useRef, useState } from "react"
|
||||
import { ITooltips } from "./tooltips"
|
||||
import MonthSelector from "../month-selector"
|
||||
import YearSelector from "../year-selector"
|
||||
|
@ -30,7 +30,7 @@ interface AdditionalTooltipsProps {
|
|||
setSelectedCategory: (category: string | "all") => void
|
||||
availableYears?: (number | null)[]
|
||||
categories?: string[]
|
||||
|
||||
panicButtonTriggered?: boolean
|
||||
}
|
||||
|
||||
export default function AdditionalTooltips({
|
||||
|
@ -44,6 +44,7 @@ export default function AdditionalTooltips({
|
|||
setSelectedCategory,
|
||||
availableYears = [2022, 2023, 2024],
|
||||
categories = [],
|
||||
panicButtonTriggered = false,
|
||||
}: AdditionalTooltipsProps) {
|
||||
const [showSelectors, setShowSelectors] = useState(false)
|
||||
const containerRef = useRef<HTMLDivElement>(null)
|
||||
|
@ -51,6 +52,16 @@ export default function AdditionalTooltips({
|
|||
|
||||
const container = isClient ? document.getElementById("root") : null
|
||||
|
||||
useEffect(() => {
|
||||
if (panicButtonTriggered && activeControl !== "alerts" && onControlChange) {
|
||||
onControlChange("alerts");
|
||||
}
|
||||
}, [panicButtonTriggered, activeControl, onControlChange]);
|
||||
|
||||
useEffect(() => {
|
||||
setIsClient(true);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div ref={containerRef} className="z-10 bg-background rounded-md p-1 flex items-center space-x-1">
|
||||
|
@ -64,7 +75,7 @@ export default function AdditionalTooltips({
|
|||
className={`h-8 w-8 rounded-md ${activeControl === control.id
|
||||
? "bg-emerald-500 text-black hover:bg-emerald-500/90"
|
||||
: "text-white hover:bg-emerald-500/90 hover:text-background"
|
||||
}`}
|
||||
} ${control.id === "alerts" && panicButtonTriggered ? "animate-pulse ring-2 ring-red-500" : ""}`}
|
||||
onClick={() => onControlChange?.(control.id)}
|
||||
>
|
||||
{control.icon}
|
||||
|
|
|
@ -108,12 +108,12 @@ export default function EWSAlertLayer({
|
|||
setShowAlert(true);
|
||||
|
||||
// Play warning sound after a delay
|
||||
setTimeout(() => {
|
||||
if (warningAudioRef.current) {
|
||||
warningAudioRef.current.play()
|
||||
.catch(err => console.error("Error playing warning sound:", err));
|
||||
}
|
||||
}, 1000);
|
||||
// setTimeout(() => {
|
||||
// if (warningAudioRef.current) {
|
||||
// warningAudioRef.current.play()
|
||||
// .catch(err => console.error("Error playing warning sound:", err));
|
||||
// }
|
||||
// }, 1000);
|
||||
|
||||
// Auto-close the alert after 15 seconds
|
||||
setTimeout(() => {
|
||||
|
@ -159,7 +159,7 @@ export default function EWSAlertLayer({
|
|||
// Use React for the marker content with animated circles
|
||||
const markerRoot = createRoot(contentElement);
|
||||
markerRoot.render(
|
||||
<div className="circles flex justify-center items-center">
|
||||
<div className=" flex justify-center items-center">
|
||||
<div className="circle1"></div>
|
||||
<div className="circle2"></div>
|
||||
<div className="circle3"></div>
|
||||
|
@ -224,6 +224,7 @@ export default function EWSAlertLayer({
|
|||
<p className="font-bold">{incident.location.district}</p>
|
||||
<p className="text-xs">{incident.location.address}</p>
|
||||
<p className="text-xs mt-1">Reported by: {incident.reporter.name}</p>
|
||||
<p className="text-xs mt-1 font-mono text-red-300">Auto-closing in 10s</p>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-between mt-2">
|
||||
|
@ -315,30 +316,6 @@ export default function EWSAlertLayer({
|
|||
// Create status indicator element if it doesn't exist
|
||||
let statusContainer = document.getElementById('ews-status-indicator');
|
||||
|
||||
if (!statusContainer) {
|
||||
statusContainer = document.createElement('div');
|
||||
statusContainer.id = 'ews-status-indicator';
|
||||
statusContainer.className = 'absolute top-16 left-1/2 transform -translate-x-1/2 z-50';
|
||||
map.getContainer().appendChild(statusContainer);
|
||||
|
||||
// Use React for the status indicator
|
||||
const root = createRoot(statusContainer);
|
||||
root.render(
|
||||
<div className={`
|
||||
py-2 px-4 rounded-full shadow-lg flex items-center gap-2
|
||||
${ewsStatus === 'alert' ? 'bg-red-600 text-white' : 'bg-amber-500 text-white'}
|
||||
animate-pulse
|
||||
`}>
|
||||
<AlertTriangle className="h-4 w-4" />
|
||||
<span className="font-bold uppercase tracking-wider">
|
||||
{ewsStatus === 'alert'
|
||||
? `Alert: ${activeIncidents.length} Active Emergencies`
|
||||
: 'System Active'}
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// Cleanup function
|
||||
return () => {
|
||||
if (statusContainer && statusContainer.parentNode) {
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
"use client"
|
||||
|
||||
import { useState, useRef, useEffect, useCallback } from "react"
|
||||
import { useState, useRef, useEffect, useCallback, act } from "react"
|
||||
import { useMap } from "react-map-gl/mapbox"
|
||||
import { BASE_BEARING, BASE_PITCH, BASE_ZOOM, MAPBOX_TILESET_ID } from "@/app/_utils/const/map"
|
||||
import DistrictPopup from "../pop-up/district-popup"
|
||||
|
@ -9,7 +9,7 @@ import ClusterLayer from "./cluster-layer"
|
|||
import HeatmapLayer from "./heatmap-layer"
|
||||
import TimelineLayer from "./timeline-layer"
|
||||
|
||||
import type { ICrimes } from "@/app/_utils/types/crimes"
|
||||
import type { ICrimes, IIncidentLogs } from "@/app/_utils/types/crimes"
|
||||
import type { IDistrictFeature } from "@/app/_utils/types/map"
|
||||
import { createFillColorExpression, processCrimeDataByDistrict } from "@/app/_utils/map"
|
||||
import UnclusteredPointLayer from "./uncluster-layer"
|
||||
|
@ -29,6 +29,7 @@ import PanicButtonDemo from "../controls/panic-button-demo"
|
|||
|
||||
import { IIncidentLog } from "@/app/_utils/types/ews"
|
||||
import { addMockIncident, getAllIncidents, resolveIncident } from "@/app/_utils/mock/ews-data"
|
||||
import RecentIncidentsLayer from "./recent-crimes-layer"
|
||||
|
||||
// Interface for crime incident
|
||||
interface ICrimeIncident {
|
||||
|
@ -67,6 +68,7 @@ interface LayersProps {
|
|||
visible?: boolean
|
||||
crimes: ICrimes[]
|
||||
units?: IUnits[]
|
||||
recentIncidents: IIncidentLogs[]
|
||||
year: string
|
||||
month: string
|
||||
filterCategory: string | "all"
|
||||
|
@ -79,6 +81,7 @@ interface LayersProps {
|
|||
export default function Layers({
|
||||
visible = true,
|
||||
crimes,
|
||||
recentIncidents,
|
||||
units,
|
||||
year,
|
||||
month,
|
||||
|
@ -412,14 +415,11 @@ export default function Layers({
|
|||
|
||||
if (!visible) return null
|
||||
|
||||
const showDistrictLayer = activeControl === "incidents"
|
||||
const crimesVisible = activeControl === "incidents"
|
||||
const showHeatmapLayer = activeControl === "heatmap"
|
||||
const showClustersLayer = activeControl === "clusters"
|
||||
const showUnitsLayer = activeControl === "units"
|
||||
const showTimelineLayer = activeControl === "timeline"
|
||||
|
||||
const showDistrictFill = activeControl === "incidents" || activeControl === "clusters"
|
||||
|
||||
const showIncidentMarkers = activeControl !== "heatmap" && activeControl !== "timeline"
|
||||
|
||||
return (
|
||||
|
@ -440,6 +440,13 @@ export default function Layers({
|
|||
onDistrictClick={handleDistrictClick}
|
||||
/>
|
||||
|
||||
{/* Recent Crimes Layer for showing crime data from the last 24 hours */}
|
||||
<RecentIncidentsLayer
|
||||
map={mapboxMap}
|
||||
incidents={recentIncidents}
|
||||
visible={crimesVisible}
|
||||
/>
|
||||
|
||||
<HeatmapLayer
|
||||
crimes={crimes}
|
||||
year={year}
|
||||
|
@ -524,7 +531,7 @@ export default function Layers({
|
|||
)}
|
||||
|
||||
{showEWS && showPanicDemo && (
|
||||
<div className="absolute bottom-20 left-0 z-50 p-2">
|
||||
<div className="absolute top-0 right-20 z-50 p-2">
|
||||
<PanicButtonDemo
|
||||
onTriggerAlert={handleTriggerAlert}
|
||||
onResolveAllAlerts={handleResolveAllAlerts}
|
||||
|
|
|
@ -0,0 +1,164 @@
|
|||
"use client";
|
||||
|
||||
import { useEffect, useState, useRef } from 'react';
|
||||
import mapboxgl from 'mapbox-gl';
|
||||
import { createRoot } from 'react-dom/client';
|
||||
import { Clock, FileText, MapPin } from 'lucide-react';
|
||||
import { Badge } from '@/app/_components/ui/badge';
|
||||
import { formatDistanceToNow } from 'date-fns';
|
||||
import { CustomAnimatedPopup } from '@/app/_utils/map/custom-animated-popup';
|
||||
import { incident_logs } from '@prisma/client';
|
||||
import { IIncidentLogs } from '@/app/_utils/types/crimes';
|
||||
|
||||
// export interface ICrimeIncident {
|
||||
// id: string;
|
||||
// category: string;
|
||||
// location: {
|
||||
// latitude: number;
|
||||
// longitude: number;
|
||||
// address: string;
|
||||
// district: string;
|
||||
// };
|
||||
// timestamp: string;
|
||||
// description: string;
|
||||
// severity: 'high' | 'medium' | 'low';
|
||||
// reportedBy: string;
|
||||
// }
|
||||
|
||||
interface RecentCrimesLayerProps {
|
||||
map: mapboxgl.Map | null;
|
||||
incidents: IIncidentLogs[];
|
||||
visible: boolean;
|
||||
onIncidentClick?: (incident: IIncidentLogs) => void;
|
||||
}
|
||||
|
||||
export default function RecentCrimesLayer({
|
||||
map,
|
||||
incidents,
|
||||
visible,
|
||||
onIncidentClick,
|
||||
}: RecentCrimesLayerProps) {
|
||||
const markersRef = useRef<Map<string, mapboxgl.Marker>>(new Map());
|
||||
|
||||
// Filter incidents to only show those from the last 24 hours
|
||||
const recentIncidents = incidents.filter(incident => {
|
||||
const incidentDate = new Date(incident.timestamp);
|
||||
const now = new Date();
|
||||
const timeDiff = now.getTime() - incidentDate.getTime();
|
||||
const hoursDiff = timeDiff / (1000 * 60 * 60);
|
||||
return hoursDiff <= 24;
|
||||
});
|
||||
|
||||
// Create markers for each incident
|
||||
useEffect(() => {
|
||||
if (!map || !visible) {
|
||||
// Remove all markers if layer is not visible
|
||||
markersRef.current.forEach(marker => marker.remove());
|
||||
return;
|
||||
}
|
||||
|
||||
// Track existing incident IDs to avoid recreating markers
|
||||
const existingIds = new Set(Array.from(markersRef.current.keys()));
|
||||
|
||||
// Add markers for each recent incident
|
||||
recentIncidents.forEach(incident => {
|
||||
existingIds.delete(incident.id);
|
||||
|
||||
if (!markersRef.current.has(incident.id)) {
|
||||
// Create marker element
|
||||
const el = document.createElement('div');
|
||||
el.className = 'crime-marker';
|
||||
|
||||
// Style based on severity
|
||||
const colors = {
|
||||
high: 'bg-red-500',
|
||||
medium: 'bg-amber-500',
|
||||
low: 'bg-blue-500'
|
||||
};
|
||||
|
||||
const markerRoot = createRoot(el);
|
||||
markerRoot.render(
|
||||
<div className={`p-1 rounded-full ${colors[incident.severity]} shadow-lg pulse-animation`}>
|
||||
<MapPin className="h-4 w-4 text-white" />
|
||||
</div>
|
||||
);
|
||||
|
||||
// Create popup content
|
||||
const popupEl = document.createElement('div');
|
||||
const popupRoot = createRoot(popupEl);
|
||||
|
||||
popupRoot.render(
|
||||
<div className="p-2 max-w-[250px]">
|
||||
<div className="flex items-center gap-2 mb-2">
|
||||
<Badge className={`
|
||||
${incident.severity === 'high' ? 'bg-red-500' :
|
||||
incident.severity === 'medium' ? 'bg-amber-500' : 'bg-blue-500'}
|
||||
text-white`
|
||||
}>
|
||||
{incident.category}
|
||||
</Badge>
|
||||
<span className="text-xs flex items-center gap-1 opacity-75">
|
||||
<Clock className="h-3 w-3" />
|
||||
{formatDistanceToNow(new Date(incident.timestamp), { addSuffix: true })}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<h3 className="font-medium text-sm">{incident.district}</h3>
|
||||
<p className="text-xs text-muted-foreground">{incident.address}</p>
|
||||
|
||||
<p className="text-xs mt-2 line-clamp-3">{incident.description}</p>
|
||||
|
||||
<div className="flex items-center justify-between mt-2">
|
||||
<span className="text-xs opacity-75">ID: {incident.id.substring(0, 8)}...</span>
|
||||
<button
|
||||
className="text-xs flex items-center gap-1 text-blue-500 hover:underline"
|
||||
onClick={() => onIncidentClick?.(incident)}
|
||||
>
|
||||
<FileText className="h-3 w-3" /> Details
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
// Create popup
|
||||
const popup = new CustomAnimatedPopup({
|
||||
closeButton: false,
|
||||
maxWidth: '300px',
|
||||
offset: 15
|
||||
}).setDOMContent(popupEl);
|
||||
|
||||
// Create and add marker
|
||||
const marker = new mapboxgl.Marker(el)
|
||||
.setLngLat([incident.longitude, incident.latitude])
|
||||
.setPopup(popup)
|
||||
.addTo(map);
|
||||
|
||||
// Add click handler for the entire marker
|
||||
el.addEventListener('click', () => {
|
||||
if (onIncidentClick) {
|
||||
onIncidentClick(incident);
|
||||
}
|
||||
});
|
||||
|
||||
markersRef.current.set(incident.id, marker);
|
||||
}
|
||||
});
|
||||
|
||||
// Remove any markers that are no longer in the recent incidents list
|
||||
existingIds.forEach(id => {
|
||||
const marker = markersRef.current.get(id);
|
||||
if (marker) {
|
||||
marker.remove();
|
||||
markersRef.current.delete(id);
|
||||
}
|
||||
});
|
||||
|
||||
// Clean up on unmount
|
||||
return () => {
|
||||
markersRef.current.forEach(marker => marker.remove());
|
||||
markersRef.current.clear();
|
||||
};
|
||||
}, [map, recentIncidents, visible, onIncidentClick]);
|
||||
|
||||
return null; // This is a functional component with no visual rendering
|
||||
}
|
|
@ -1,145 +0,0 @@
|
|||
/* EWS Alert Marker */
|
||||
.ews-alert-marker {
|
||||
position: relative;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
.pulsing-dot {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
border-radius: 50%;
|
||||
background: rgb(220, 38, 38);
|
||||
box-shadow: 0 0 0 rgba(220, 38, 38, 0.4);
|
||||
transform-origin: center center;
|
||||
position: absolute;
|
||||
z-index: 3;
|
||||
}
|
||||
|
||||
.ews-alert-content {
|
||||
position: absolute;
|
||||
min-width: 200px;
|
||||
z-index: 1;
|
||||
top: -8px;
|
||||
left: 15px;
|
||||
}
|
||||
|
||||
/* Animation for alert transitions */
|
||||
@keyframes alert-pulse {
|
||||
0% {
|
||||
transform: scale(0.95);
|
||||
box-shadow: 0 0 0 0 rgba(255, 82, 82, 0.7);
|
||||
}
|
||||
|
||||
70% {
|
||||
transform: scale(1);
|
||||
box-shadow: 0 0 0 10px rgba(255, 82, 82, 0);
|
||||
}
|
||||
|
||||
100% {
|
||||
transform: scale(0.95);
|
||||
box-shadow: 0 0 0 0 rgba(255, 82, 82, 0);
|
||||
}
|
||||
}
|
||||
|
||||
/* EWS Status Indicator */
|
||||
#ews-status-indicator {
|
||||
transition: all 0.3s ease-in-out;
|
||||
}
|
||||
|
||||
/* Add to your global CSS file */
|
||||
.custom-animated-popup {
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
.marker-gempa {
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.circles {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.circle1, .circle2, .circle3 {
|
||||
position: absolute;
|
||||
border-radius: 50%;
|
||||
border: 2px solid red;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
opacity: 0;
|
||||
animation: pulse 2s infinite;
|
||||
}
|
||||
|
||||
.circle2 {
|
||||
animation-delay: 0.5s;
|
||||
}
|
||||
|
||||
.circle3 {
|
||||
animation-delay: 1s;
|
||||
}
|
||||
|
||||
@keyframes pulse {
|
||||
0% {
|
||||
transform: scale(0.5);
|
||||
opacity: 0;
|
||||
}
|
||||
50% {
|
||||
opacity: 0.8;
|
||||
}
|
||||
100% {
|
||||
transform: scale(1.5);
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.blink {
|
||||
animation: blink 1s infinite;
|
||||
color: red;
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
@keyframes blink {
|
||||
0% { opacity: 1; }
|
||||
50% { opacity: 0.3; }
|
||||
100% { opacity: 1; }
|
||||
}
|
||||
|
||||
/* For the warning styles */
|
||||
.overlay-bg {
|
||||
background: rgba(0, 0, 0, 0.7);
|
||||
z-index: 1000;
|
||||
}
|
||||
|
||||
.text-glow {
|
||||
text-shadow: 0 0 5px rgba(255, 255, 255, 0.7);
|
||||
}
|
||||
|
||||
.warning {
|
||||
z-index: 1001;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
/* Animation classes */
|
||||
.show-pop-up {
|
||||
animation: showPopUp 0.5s forwards;
|
||||
}
|
||||
|
||||
.close-pop-up {
|
||||
animation: closePopUp 0.5s forwards;
|
||||
}
|
||||
|
||||
@keyframes showPopUp {
|
||||
from { opacity: 0; transform: scale(0.8); }
|
||||
to { opacity: 1; transform: scale(1); }
|
||||
}
|
||||
|
||||
@keyframes closePopUp {
|
||||
from { opacity: 1; transform: scale(1); }
|
||||
to { opacity: 0; transform: scale(0.8); }
|
||||
}
|
|
@ -126,16 +126,17 @@ export class CustomAnimatedPopup extends mapboxgl.Popup {
|
|||
color?: string,
|
||||
maxRadius?: number,
|
||||
duration?: number,
|
||||
count?: number
|
||||
count?: number,
|
||||
showCenter?: boolean
|
||||
} = {}): void {
|
||||
const {
|
||||
color = 'red',
|
||||
maxRadius = 500,
|
||||
duration = 4000,
|
||||
count = 3
|
||||
maxRadius = 80, // Reduce max radius for less "over" effect
|
||||
duration = 2000, // Faster animation
|
||||
count = 2, // Fewer circles
|
||||
showCenter = true
|
||||
} = options;
|
||||
|
||||
// Create source for wave circles if it doesn't exist
|
||||
const sourceId = `wave-circles-${Math.random().toString(36).substring(2, 9)}`;
|
||||
|
||||
if (!map.getSource(sourceId)) {
|
||||
|
@ -156,7 +157,6 @@ export class CustomAnimatedPopup extends mapboxgl.Popup {
|
|||
}
|
||||
});
|
||||
|
||||
// Add layers for each circle (with different animation delays)
|
||||
for (let i = 0; i < count; i++) {
|
||||
const layerId = `${sourceId}-layer-${i}`;
|
||||
const delay = i * (duration / count);
|
||||
|
@ -168,13 +168,15 @@ export class CustomAnimatedPopup extends mapboxgl.Popup {
|
|||
paint: {
|
||||
'circle-radius': ['interpolate', ['linear'], ['get', 'radius'], 0, 0, 100, maxRadius],
|
||||
'circle-color': 'transparent',
|
||||
'circle-opacity': ['interpolate', ['linear'], ['get', 'radius'], 0, 0.6, 100, 0],
|
||||
'circle-stroke-width': 2,
|
||||
'circle-opacity': ['interpolate', ['linear'], ['get', 'radius'],
|
||||
0, showCenter ? 0.15 : 0, // Lower opacity
|
||||
100, 0
|
||||
],
|
||||
'circle-stroke-width': 1.5, // Thinner stroke
|
||||
'circle-stroke-color': color
|
||||
}
|
||||
});
|
||||
|
||||
// Animate the circles
|
||||
this.animateWaveCircle(map, sourceId, layerId, duration, delay);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -95,13 +95,17 @@ export interface IDistanceResult {
|
|||
|
||||
export interface IIncidentLogs {
|
||||
id: string;
|
||||
user_id: string;
|
||||
latitude: number;
|
||||
longitude: number;
|
||||
district: string;
|
||||
address: string;
|
||||
category: string;
|
||||
source: string;
|
||||
description: string;
|
||||
verified: boolean;
|
||||
severity: 'high' | 'medium' | 'low';
|
||||
timestamp: Date;
|
||||
created_at: Date;
|
||||
updated_at: Date;
|
||||
source: string;
|
||||
time: Date;
|
||||
description: string;
|
||||
user_id: string;
|
||||
location_id: string;
|
||||
category_id: string;
|
||||
verified: boolean;
|
||||
}
|
File diff suppressed because it is too large
Load Diff
|
@ -1,159 +0,0 @@
|
|||
-- 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;
|
|
@ -1,338 +0,0 @@
|
|||
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";
|
||||
|
||||
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);
|
||||
|
||||
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);
|
||||
|
||||
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 $$;
|
||||
|
||||
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;
|
||||
|
||||
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);
|
||||
|
||||
|
|
@ -1,160 +0,0 @@
|
|||
-- 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;
|
|
@ -1,11 +0,0 @@
|
|||
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;
|
|
@ -1,11 +0,0 @@
|
|||
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;
|
|
@ -1,204 +0,0 @@
|
|||
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);
|
||||
|
||||
|
|
@ -1,94 +0,0 @@
|
|||
-- 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);
|
||||
|
||||
|
|
@ -1,9 +0,0 @@
|
|||
-- 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);
|
||||
|
||||
|
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,32 @@
|
|||
district_id,district_name,crime_total,crime_cleared,avg_crime,avg_score,level
|
||||
350901,Jombang,41,41,8.2,99,low
|
||||
350902,Kencong,38,37,7.6,100,low
|
||||
350903,Sumberbaru,400,332,80.0,26,medium
|
||||
350904,Gumukmas,401,337,80.2,26,medium
|
||||
350905,Umbulsari,45,44,9.0,98,low
|
||||
350906,Tanggul,388,337,77.6,28,medium
|
||||
350907,Semboro,49,47,9.8,98,low
|
||||
350908,Puger,448,385,89.6,16,medium
|
||||
350909,Bangsalsari,473,403,94.6,11,medium
|
||||
350910,Balung,524,422,104.8,0,medium
|
||||
350911,Wuluhan,458,379,91.6,14,medium
|
||||
350912,Ambulu,442,366,88.4,17,medium
|
||||
350913,Rambipuji,426,373,85.2,21,medium
|
||||
350914,Panti,45,43,9.0,98,low
|
||||
350915,Sukorambi,45,44,9.0,98,low
|
||||
350916,Jenggawah,438,362,87.6,18,medium
|
||||
350917,Ajung,429,363,85.8,20,medium
|
||||
350918,Tempurejo,147,143,29.4,78,low
|
||||
350919,Kaliwates,476,400,95.2,10,high
|
||||
350920,Patrang,508,416,101.6,4,high
|
||||
350921,Sumbersari,422,359,84.4,21,high
|
||||
350922,Arjasa,57,56,11.4,96,low
|
||||
350923,Mumbulsari,35,35,7.0,100,low
|
||||
350924,Pakusari,52,52,10.4,97,low
|
||||
350925,Jelbuk,42,41,8.4,99,low
|
||||
350926,Mayang,51,51,10.2,97,low
|
||||
350927,Kalisat,45,45,9.0,98,low
|
||||
350928,Ledokombo,47,45,9.4,98,low
|
||||
350929,Sukowono,46,46,9.2,98,low
|
||||
350930,Silo,433,381,86.6,19,medium
|
||||
350931,Sumberjambe,43,43,8.6,99,low
|
|
|
@ -0,0 +1,156 @@
|
|||
district_id,district_name,number_of_crime,level,score,method,year
|
||||
350901,Jombang,10,low,93,kmeans,2020
|
||||
350901,Jombang,19,low,87,kmeans,2021
|
||||
350901,Jombang,4,low,98,kmeans,2022
|
||||
350901,Jombang,4,low,98,kmeans,2023
|
||||
350901,Jombang,4,low,98,kmeans,2024
|
||||
350902,Kencong,6,low,96,kmeans,2022
|
||||
350902,Kencong,10,low,93,kmeans,2021
|
||||
350902,Kencong,11,low,93,kmeans,2020
|
||||
350902,Kencong,7,low,95,kmeans,2023
|
||||
350902,Kencong,4,low,98,kmeans,2024
|
||||
350903,Sumberbaru,82,medium,42,kmeans,2024
|
||||
350903,Sumberbaru,75,medium,47,kmeans,2022
|
||||
350903,Sumberbaru,109,medium,23,kmeans,2021
|
||||
350903,Sumberbaru,49,medium,65,kmeans,2023
|
||||
350903,Sumberbaru,85,medium,40,kmeans,2020
|
||||
350904,Gumukmas,101,medium,28,kmeans,2020
|
||||
350904,Gumukmas,73,medium,48,kmeans,2022
|
||||
350904,Gumukmas,104,medium,26,kmeans,2021
|
||||
350904,Gumukmas,74,low,48,kmeans,2024
|
||||
350904,Gumukmas,49,low,65,kmeans,2023
|
||||
350905,Umbulsari,7,low,95,kmeans,2022
|
||||
350905,Umbulsari,5,low,97,kmeans,2023
|
||||
350905,Umbulsari,17,low,88,kmeans,2021
|
||||
350905,Umbulsari,14,low,90,kmeans,2020
|
||||
350905,Umbulsari,2,low,99,kmeans,2024
|
||||
350906,Tanggul,102,medium,28,kmeans,2020
|
||||
350906,Tanggul,69,medium,51,kmeans,2023
|
||||
350906,Tanggul,95,medium,33,kmeans,2021
|
||||
350906,Tanggul,29,low,80,kmeans,2024
|
||||
350906,Tanggul,93,medium,34,kmeans,2022
|
||||
350907,Semboro,6,low,96,kmeans,2022
|
||||
350907,Semboro,5,low,97,kmeans,2023
|
||||
350907,Semboro,21,low,85,kmeans,2021
|
||||
350907,Semboro,4,low,98,kmeans,2024
|
||||
350907,Semboro,13,low,91,kmeans,2020
|
||||
350908,Puger,102,medium,28,kmeans,2020
|
||||
350908,Puger,72,medium,49,kmeans,2024
|
||||
350908,Puger,98,medium,30,kmeans,2021
|
||||
350908,Puger,94,medium,33,kmeans,2023
|
||||
350908,Puger,82,medium,42,kmeans,2022
|
||||
350909,Bangsalsari,116,medium,18,kmeans,2022
|
||||
350909,Bangsalsari,75,medium,47,kmeans,2023
|
||||
350909,Bangsalsari,121,medium,14,kmeans,2021
|
||||
350909,Bangsalsari,64,medium,55,kmeans,2024
|
||||
350909,Bangsalsari,97,medium,31,kmeans,2020
|
||||
350910,Balung,122,medium,13,kmeans,2020
|
||||
350910,Balung,92,medium,35,kmeans,2023
|
||||
350910,Balung,127,medium,10,kmeans,2021
|
||||
350910,Balung,102,medium,28,kmeans,2024
|
||||
350910,Balung,81,medium,43,kmeans,2022
|
||||
350911,Wuluhan,72,medium,49,kmeans,2022
|
||||
350911,Wuluhan,74,medium,48,kmeans,2024
|
||||
350911,Wuluhan,132,medium,6,kmeans,2021
|
||||
350911,Wuluhan,84,medium,40,kmeans,2023
|
||||
350911,Wuluhan,96,medium,32,kmeans,2020
|
||||
350912,Ambulu,99,medium,30,kmeans,2020
|
||||
350912,Ambulu,70,medium,50,kmeans,2024
|
||||
350912,Ambulu,97,medium,31,kmeans,2021
|
||||
350912,Ambulu,99,medium,30,kmeans,2023
|
||||
350912,Ambulu,77,medium,45,kmeans,2022
|
||||
350913,Rambipuji,104,medium,26,kmeans,2022
|
||||
350913,Rambipuji,68,medium,52,kmeans,2023
|
||||
350913,Rambipuji,103,medium,27,kmeans,2021
|
||||
350913,Rambipuji,103,medium,27,kmeans,2020
|
||||
350913,Rambipuji,48,medium,66,kmeans,2024
|
||||
350914,Panti,11,low,93,kmeans,2020
|
||||
350914,Panti,5,low,97,kmeans,2023
|
||||
350914,Panti,19,low,87,kmeans,2021
|
||||
350914,Panti,3,low,98,kmeans,2024
|
||||
350914,Panti,7,low,95,kmeans,2022
|
||||
350915,Sukorambi,4,low,98,kmeans,2022
|
||||
350915,Sukorambi,5,low,97,kmeans,2024
|
||||
350915,Sukorambi,19,low,87,kmeans,2021
|
||||
350915,Sukorambi,6,low,96,kmeans,2023
|
||||
350915,Sukorambi,11,low,93,kmeans,2020
|
||||
350916,Jenggawah,59,medium,58,kmeans,2023
|
||||
350916,Jenggawah,66,medium,53,kmeans,2024
|
||||
350916,Jenggawah,96,medium,32,kmeans,2022
|
||||
350916,Jenggawah,106,medium,25,kmeans,2020
|
||||
350916,Jenggawah,111,medium,21,kmeans,2021
|
||||
350917,Ajung,107,medium,24,kmeans,2021
|
||||
350917,Ajung,82,medium,42,kmeans,2020
|
||||
350917,Ajung,95,medium,33,kmeans,2022
|
||||
350917,Ajung,82,medium,42,kmeans,2024
|
||||
350917,Ajung,63,medium,55,kmeans,2023
|
||||
350918,Tempurejo,15,low,90,kmeans,2023
|
||||
350918,Tempurejo,17,low,88,kmeans,2024
|
||||
350918,Tempurejo,27,low,81,kmeans,2022
|
||||
350918,Tempurejo,39,low,73,kmeans,2020
|
||||
350918,Tempurejo,49,low,65,kmeans,2021
|
||||
350919,Kaliwates,124,high,12,kmeans,2021
|
||||
350919,Kaliwates,100,high,29,kmeans,2024
|
||||
350919,Kaliwates,93,high,34,kmeans,2022
|
||||
350919,Kaliwates,89,high,37,kmeans,2020
|
||||
350919,Kaliwates,70,high,50,kmeans,2023
|
||||
350920,Patrang,52,high,63,kmeans,2023
|
||||
350920,Patrang,104,high,26,kmeans,2024
|
||||
350920,Patrang,88,high,38,kmeans,2022
|
||||
350920,Patrang,124,high,12,kmeans,2020
|
||||
350920,Patrang,140,high,0,kmeans,2021
|
||||
350921,Sumbersari,89,high,37,kmeans,2021
|
||||
350921,Sumbersari,94,medium,33,kmeans,2020
|
||||
350921,Sumbersari,107,high,24,kmeans,2022
|
||||
350921,Sumbersari,53,high,63,kmeans,2023
|
||||
350921,Sumbersari,79,high,44,kmeans,2024
|
||||
350922,Arjasa,6,low,96,kmeans,2023
|
||||
350922,Arjasa,8,low,95,kmeans,2020
|
||||
350922,Arjasa,14,low,90,kmeans,2022
|
||||
350922,Arjasa,3,low,98,kmeans,2024
|
||||
350922,Arjasa,26,low,82,kmeans,2021
|
||||
350923,Mumbulsari,17,low,88,kmeans,2021
|
||||
350923,Mumbulsari,4,low,98,kmeans,2024
|
||||
350923,Mumbulsari,2,low,99,kmeans,2022
|
||||
350923,Mumbulsari,10,low,93,kmeans,2020
|
||||
350923,Mumbulsari,2,low,99,kmeans,2023
|
||||
350924,Pakusari,7,low,95,kmeans,2023
|
||||
350924,Pakusari,3,low,98,kmeans,2024
|
||||
350924,Pakusari,10,low,93,kmeans,2022
|
||||
350924,Pakusari,11,low,93,kmeans,2020
|
||||
350924,Pakusari,21,low,85,kmeans,2021
|
||||
350925,Jelbuk,21,low,85,kmeans,2021
|
||||
350925,Jelbuk,12,low,92,kmeans,2020
|
||||
350925,Jelbuk,6,low,96,kmeans,2022
|
||||
350925,Jelbuk,0,low,100,kmeans,2024
|
||||
350925,Jelbuk,3,low,98,kmeans,2023
|
||||
350926,Mayang,7,low,95,kmeans,2023
|
||||
350926,Mayang,16,low,89,kmeans,2020
|
||||
350926,Mayang,6,low,96,kmeans,2022
|
||||
350926,Mayang,18,low,88,kmeans,2021
|
||||
350926,Mayang,4,low,98,kmeans,2024
|
||||
350927,Kalisat,16,low,89,kmeans,2020
|
||||
350927,Kalisat,15,low,90,kmeans,2021
|
||||
350927,Kalisat,4,low,98,kmeans,2023
|
||||
350927,Kalisat,5,low,97,kmeans,2022
|
||||
350927,Kalisat,5,low,97,kmeans,2024
|
||||
350928,Ledokombo,5,low,97,kmeans,2024
|
||||
350928,Ledokombo,18,low,88,kmeans,2021
|
||||
350928,Ledokombo,10,low,93,kmeans,2020
|
||||
350928,Ledokombo,10,low,93,kmeans,2022
|
||||
350928,Ledokombo,4,low,98,kmeans,2023
|
||||
350929,Sukowono,6,low,96,kmeans,2023
|
||||
350929,Sukowono,7,low,95,kmeans,2022
|
||||
350929,Sukowono,11,low,93,kmeans,2020
|
||||
350929,Sukowono,20,low,86,kmeans,2021
|
||||
350929,Sukowono,2,low,99,kmeans,2024
|
||||
350930,Silo,68,medium,52,kmeans,2024
|
||||
350930,Silo,86,medium,39,kmeans,2022
|
||||
350930,Silo,81,medium,43,kmeans,2023
|
||||
350930,Silo,89,medium,37,kmeans,2021
|
||||
350930,Silo,109,medium,23,kmeans,2020
|
||||
350931,Sumberjambe,7,low,95,kmeans,2020
|
||||
350931,Sumberjambe,19,low,87,kmeans,2021
|
||||
350931,Sumberjambe,8,low,95,kmeans,2023
|
||||
350931,Sumberjambe,5,low,97,kmeans,2022
|
||||
350931,Sumberjambe,4,low,98,kmeans,2024
|
|
|
@ -47,6 +47,8 @@ CREATE TYPE "crime_status" AS ENUM ('open', 'closed', 'under_investigation', 're
|
|||
-- CreateEnum
|
||||
CREATE TYPE "unit_type" AS ENUM ('polda', 'polsek', 'polres', 'other');
|
||||
|
||||
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "profiles" (
|
||||
"id" UUID NOT NULL DEFAULT gen_random_uuid(),
|
||||
|
|
|
@ -0,0 +1,5 @@
|
|||
-- AlterTable
|
||||
ALTER TABLE "crimes" ADD COLUMN "source_type" VARCHAR(100);
|
||||
|
||||
-- AlterTable
|
||||
ALTER TABLE "locations" ADD COLUMN "distance" DOUBLE PRECISION;
|
|
@ -0,0 +1,30 @@
|
|||
/*
|
||||
Warnings:
|
||||
|
||||
- You are about to drop the column `distance` on the `locations` table. All the data in the column will be lost.
|
||||
|
||||
*/
|
||||
-- AlterTable
|
||||
ALTER TABLE "locations" DROP COLUMN "distance",
|
||||
ADD COLUMN "distance_from_unit" DOUBLE PRECISION;
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "location_logs" (
|
||||
"id" UUID NOT NULL DEFAULT gen_random_uuid(),
|
||||
"user_id" UUID NOT NULL,
|
||||
"latitude" DOUBLE PRECISION NOT NULL,
|
||||
"longitude" DOUBLE PRECISION NOT NULL,
|
||||
"location" gis.geography(Point,4326) NOT NULL,
|
||||
"timestamp" TIMESTAMPTZ(6) NOT NULL,
|
||||
"description" VARCHAR(255),
|
||||
"created_at" TIMESTAMPTZ(6) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updated_at" TIMESTAMPTZ(6) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
|
||||
CONSTRAINT "location_logs_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "idx_location_logs_timestamp" ON "location_logs"("timestamp");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "idx_location_logs_user_id" ON "location_logs"("user_id");
|
|
@ -0,0 +1,8 @@
|
|||
/*
|
||||
Warnings:
|
||||
|
||||
- You are about to drop the `location_logs` table. If the table is not empty, all the data it contains will be lost.
|
||||
|
||||
*/
|
||||
-- DropTable
|
||||
DROP TABLE "location_logs";
|
|
@ -0,0 +1,73 @@
|
|||
/*
|
||||
Warnings:
|
||||
|
||||
- You are about to drop the column `distance_from_unit` on the `locations` table. All the data in the column will be lost.
|
||||
- You are about to drop the column `code_unit` on the `unit_statistics` table. All the data in the column will be lost.
|
||||
- The primary key for the `units` table will be changed. If it partially fails, the table could be left without primary key constraint.
|
||||
- You are about to drop the column `city_id` on the `units` table. All the data in the column will be lost.
|
||||
- You are about to drop the column `phone` on the `units` table. All the data in the column will be lost.
|
||||
- A unique constraint covering the columns `[unit_id,month,year]` on the table `unit_statistics` will be added. If there are existing duplicate values, this will fail.
|
||||
- A unique constraint covering the columns `[district_id]` on the table `units` will be added. If there are existing duplicate values, this will fail.
|
||||
- Made the column `year` on table `crimes` required. This step will fail if there are existing NULL values in that column.
|
||||
- Added the required column `unit_id` to the `unit_statistics` table without a default value. This is not possible if the table is not empty.
|
||||
|
||||
*/
|
||||
-- DropForeignKey
|
||||
ALTER TABLE "unit_statistics" DROP CONSTRAINT "unit_statistics_code_unit_fkey";
|
||||
|
||||
-- DropForeignKey
|
||||
ALTER TABLE "units" DROP CONSTRAINT "units_city_id_fkey";
|
||||
|
||||
-- DropIndex
|
||||
DROP INDEX "unit_statistics_code_unit_month_year_key";
|
||||
|
||||
-- AlterTable
|
||||
ALTER TABLE "crimes" ALTER COLUMN "year" SET NOT NULL;
|
||||
|
||||
-- AlterTable
|
||||
ALTER TABLE "locations" DROP COLUMN "distance_from_unit",
|
||||
ADD COLUMN "distance_to_unit" DOUBLE PRECISION;
|
||||
|
||||
-- AlterTable
|
||||
ALTER TABLE "unit_statistics" DROP COLUMN "code_unit",
|
||||
ADD COLUMN "unit_id" UUID NOT NULL;
|
||||
|
||||
-- AlterTable
|
||||
ALTER TABLE "units" DROP CONSTRAINT "units_pkey",
|
||||
DROP COLUMN "city_id",
|
||||
DROP COLUMN "phone",
|
||||
ADD COLUMN "id" UUID NOT NULL DEFAULT gen_random_uuid(),
|
||||
ADD CONSTRAINT "units_pkey" PRIMARY KEY ("id");
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "location_logs" (
|
||||
"id" UUID NOT NULL DEFAULT gen_random_uuid(),
|
||||
"user_id" UUID NOT NULL,
|
||||
"latitude" DOUBLE PRECISION NOT NULL,
|
||||
"longitude" DOUBLE PRECISION NOT NULL,
|
||||
"location" gis.geography NOT NULL,
|
||||
"timestamp" TIMESTAMPTZ(6) NOT NULL,
|
||||
"description" VARCHAR(255),
|
||||
"created_at" TIMESTAMPTZ(6) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updated_at" TIMESTAMPTZ(6) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
|
||||
CONSTRAINT "location_logs_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "idx_location_logs_timestamp" ON "location_logs"("timestamp");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "idx_location_logs_user_id" ON "location_logs"("user_id");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "unit_statistics_unit_id_month_year_key" ON "unit_statistics"("unit_id", "month", "year");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "units_district_id_key" ON "units"("district_id");
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "unit_statistics" ADD CONSTRAINT "unit_statistics_unit_id_fkey" FOREIGN KEY ("unit_id") REFERENCES "units"("id") ON DELETE CASCADE ON UPDATE NO ACTION;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "location_logs" ADD CONSTRAINT "location_logs_user_id_fkey" FOREIGN KEY ("user_id") REFERENCES "users"("id") ON DELETE CASCADE ON UPDATE NO ACTION;
|
|
@ -45,6 +45,7 @@ model users {
|
|||
is_anonymous Boolean @default(false)
|
||||
events events[]
|
||||
incident_logs incident_logs[]
|
||||
location_logs location_logs[]
|
||||
profile profiles?
|
||||
sessions sessions[]
|
||||
role roles @relation(fields: [roles_id], references: [id])
|
||||
|
@ -124,7 +125,6 @@ model cities {
|
|||
created_at DateTime? @default(now()) @db.Timestamptz(6)
|
||||
updated_at DateTime? @default(now()) @db.Timestamptz(6)
|
||||
districts districts[]
|
||||
units units[]
|
||||
|
||||
@@index([name], map: "idx_cities_name")
|
||||
}
|
||||
|
@ -174,7 +174,8 @@ model crimes {
|
|||
number_of_crime Int @default(0)
|
||||
score Float @default(0)
|
||||
updated_at DateTime? @default(now()) @db.Timestamptz(6)
|
||||
year Int?
|
||||
year Int
|
||||
source_type String? @db.VarChar(100)
|
||||
crime_incidents crime_incidents[]
|
||||
districts districts @relation(fields: [district_id], references: [id], onDelete: Cascade, onUpdate: NoAction)
|
||||
|
||||
|
@ -211,30 +212,31 @@ model districts {
|
|||
cities cities @relation(fields: [city_id], references: [id], onDelete: Cascade, onUpdate: NoAction)
|
||||
geographics geographics[]
|
||||
locations locations[]
|
||||
units units[]
|
||||
units units?
|
||||
|
||||
@@index([city_id], map: "idx_districts_city_id")
|
||||
@@index([name], map: "idx_districts_name")
|
||||
}
|
||||
|
||||
model locations {
|
||||
id String @id @default(dbgenerated("gen_random_uuid()")) @db.Uuid
|
||||
district_id String @db.VarChar(20)
|
||||
event_id String @db.Uuid
|
||||
address String? @db.VarChar(255)
|
||||
type String? @db.VarChar(100)
|
||||
latitude Float
|
||||
longitude Float
|
||||
land_area Float?
|
||||
polygon Unsupported("geometry")?
|
||||
geometry Unsupported("geometry")?
|
||||
created_at DateTime? @default(now()) @db.Timestamptz(6)
|
||||
updated_at DateTime? @default(now()) @db.Timestamptz(6)
|
||||
location Unsupported("geography")
|
||||
crime_incidents crime_incidents[]
|
||||
incident_logs incident_logs[]
|
||||
districts districts @relation(fields: [district_id], references: [id], onDelete: Cascade, onUpdate: NoAction)
|
||||
event events @relation(fields: [event_id], references: [id])
|
||||
id String @id @default(dbgenerated("gen_random_uuid()")) @db.Uuid
|
||||
district_id String @db.VarChar(20)
|
||||
event_id String @db.Uuid
|
||||
address String? @db.VarChar(255)
|
||||
type String? @db.VarChar(100)
|
||||
latitude Float
|
||||
longitude Float
|
||||
land_area Float?
|
||||
distance_to_unit Float?
|
||||
polygon Unsupported("geometry")?
|
||||
geometry Unsupported("geometry")?
|
||||
created_at DateTime? @default(now()) @db.Timestamptz(6)
|
||||
updated_at DateTime? @default(now()) @db.Timestamptz(6)
|
||||
location Unsupported("geography")
|
||||
crime_incidents crime_incidents[]
|
||||
incident_logs incident_logs[]
|
||||
districts districts @relation(fields: [district_id], references: [id], onDelete: Cascade, onUpdate: NoAction)
|
||||
event events @relation(fields: [event_id], references: [id])
|
||||
|
||||
@@index([district_id], map: "idx_locations_district_id")
|
||||
@@index([type], map: "idx_locations_type")
|
||||
|
@ -261,8 +263,9 @@ model incident_logs {
|
|||
}
|
||||
|
||||
model units {
|
||||
code_unit String @id @unique @db.VarChar(20)
|
||||
district_id String @db.VarChar(20)
|
||||
id String @id @default(dbgenerated("gen_random_uuid()")) @db.Uuid
|
||||
code_unit String @unique @db.VarChar(20)
|
||||
district_id String @unique @db.VarChar(20)
|
||||
name String @db.VarChar(100)
|
||||
description String?
|
||||
type unit_type
|
||||
|
@ -273,10 +276,7 @@ model units {
|
|||
latitude Float
|
||||
longitude Float
|
||||
location Unsupported("geography")
|
||||
phone String?
|
||||
city_id String @db.VarChar(20)
|
||||
unit_statistics unit_statistics[]
|
||||
cities cities @relation(fields: [city_id], references: [id], onDelete: Cascade, onUpdate: NoAction)
|
||||
districts districts @relation(fields: [district_id], references: [id], onDelete: Cascade, onUpdate: NoAction)
|
||||
|
||||
@@index([name], map: "idx_units_name")
|
||||
|
@ -288,6 +288,7 @@ model units {
|
|||
|
||||
model unit_statistics {
|
||||
id String @id @default(dbgenerated("gen_random_uuid()")) @db.Uuid
|
||||
unit_id String @db.Uuid
|
||||
crime_total Int
|
||||
crime_cleared Int
|
||||
percentage Float?
|
||||
|
@ -296,10 +297,9 @@ model unit_statistics {
|
|||
year Int
|
||||
created_at DateTime? @default(now()) @db.Timestamptz(6)
|
||||
updated_at DateTime? @default(now()) @db.Timestamptz(6)
|
||||
code_unit String @db.VarChar(20)
|
||||
units units @relation(fields: [code_unit], references: [code_unit], onDelete: Cascade, onUpdate: NoAction)
|
||||
units units @relation(fields: [unit_id], references: [id], onDelete: Cascade, onUpdate: NoAction)
|
||||
|
||||
@@unique([code_unit, month, year])
|
||||
@@unique([unit_id, month, year])
|
||||
@@index([year, month], map: "idx_unit_statistics_year_month")
|
||||
}
|
||||
|
||||
|
@ -354,6 +354,22 @@ model logs {
|
|||
@@index([user_id])
|
||||
}
|
||||
|
||||
model location_logs {
|
||||
id String @id @default(dbgenerated("gen_random_uuid()")) @db.Uuid
|
||||
user_id String @db.Uuid
|
||||
latitude Float
|
||||
longitude Float
|
||||
location Unsupported("geography")
|
||||
timestamp DateTime @db.Timestamptz(6)
|
||||
description String? @db.VarChar(255)
|
||||
created_at DateTime @default(now()) @db.Timestamptz(6)
|
||||
updated_at DateTime @default(now()) @db.Timestamptz(6)
|
||||
users users @relation(fields: [user_id], references: [id], onDelete: Cascade, onUpdate: NoAction)
|
||||
|
||||
@@index([timestamp], map: "idx_location_logs_timestamp")
|
||||
@@index([user_id], map: "idx_location_logs_user_id")
|
||||
}
|
||||
|
||||
enum session_status {
|
||||
active
|
||||
completed
|
||||
|
|
|
@ -1,9 +1,17 @@
|
|||
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);
|
||||
|
||||
DO $$
|
||||
BEGIN
|
||||
IF NOT EXISTS (
|
||||
SELECT 1 FROM pg_type WHERE typname = 'geometry_dump' AND typnamespace = 'gis'::regnamespace
|
||||
) THEN
|
||||
CREATE TYPE "gis"."geometry_dump" AS ("path" integer[], "geom" geometry);
|
||||
END IF;
|
||||
END$$;
|
||||
|
||||
DO $$
|
||||
BEGIN
|
||||
IF NOT EXISTS (
|
||||
SELECT 1 FROM pg_type WHERE typname = 'valid_detail' AND typnamespace = 'gis'::regnamespace
|
||||
) THEN
|
||||
CREATE TYPE "gis"."valid_detail" AS ("valid" boolean, "reason" character varying, "location" geometry);
|
||||
END IF;
|
||||
END$$;
|
||||
|
|
|
@ -0,0 +1,14 @@
|
|||
create table if not exists public.location_logs (
|
||||
id uuid primary key default gen_random_uuid(),
|
||||
user_id uuid not null references public.users(id) on delete cascade on update no action,
|
||||
latitude double precision not null,
|
||||
longitude double precision not null,
|
||||
location gis.geography(Point,4326) NOT NULL,
|
||||
timestamp timestamptz not null,
|
||||
description varchar(255),
|
||||
created_at timestamptz not null default now(),
|
||||
updated_at timestamptz not null default now()
|
||||
);
|
||||
|
||||
create index if not exists idx_location_logs_user_id on public.location_logs(user_id);
|
||||
create index if not exists idx_location_logs_timestamp on public.location_logs(timestamp);
|
Loading…
Reference in New Issue