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:
vergiLgood1 2025-05-11 14:44:14 +07:00
parent f4b1d9d633
commit e422c59da9
31 changed files with 4479 additions and 2845 deletions

View File

@ -1,3 +1,5 @@
{
"recommendations": ["denoland.vscode-deno"]
"recommendations": [
"denoland.vscode-deno"
]
}

View File

@ -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}
/>
</>
);
}

View File

@ -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>
);
}

View File

@ -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}

View File

@ -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) {

View File

@ -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}

View File

@ -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
}

View File

@ -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); }
}

View File

@ -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);
}
}

View File

@ -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

View File

@ -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;

View File

@ -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);

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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);

View File

@ -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);

View File

@ -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

View File

@ -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
1 district_id district_name crime_total crime_cleared avg_crime avg_score level
2 350901 Jombang 41 41 8.2 99 low
3 350902 Kencong 38 37 7.6 100 low
4 350903 Sumberbaru 400 332 80.0 26 medium
5 350904 Gumukmas 401 337 80.2 26 medium
6 350905 Umbulsari 45 44 9.0 98 low
7 350906 Tanggul 388 337 77.6 28 medium
8 350907 Semboro 49 47 9.8 98 low
9 350908 Puger 448 385 89.6 16 medium
10 350909 Bangsalsari 473 403 94.6 11 medium
11 350910 Balung 524 422 104.8 0 medium
12 350911 Wuluhan 458 379 91.6 14 medium
13 350912 Ambulu 442 366 88.4 17 medium
14 350913 Rambipuji 426 373 85.2 21 medium
15 350914 Panti 45 43 9.0 98 low
16 350915 Sukorambi 45 44 9.0 98 low
17 350916 Jenggawah 438 362 87.6 18 medium
18 350917 Ajung 429 363 85.8 20 medium
19 350918 Tempurejo 147 143 29.4 78 low
20 350919 Kaliwates 476 400 95.2 10 high
21 350920 Patrang 508 416 101.6 4 high
22 350921 Sumbersari 422 359 84.4 21 high
23 350922 Arjasa 57 56 11.4 96 low
24 350923 Mumbulsari 35 35 7.0 100 low
25 350924 Pakusari 52 52 10.4 97 low
26 350925 Jelbuk 42 41 8.4 99 low
27 350926 Mayang 51 51 10.2 97 low
28 350927 Kalisat 45 45 9.0 98 low
29 350928 Ledokombo 47 45 9.4 98 low
30 350929 Sukowono 46 46 9.2 98 low
31 350930 Silo 433 381 86.6 19 medium
32 350931 Sumberjambe 43 43 8.6 99 low

View File

@ -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
1 district_id district_name number_of_crime level score method year
2 350901 Jombang 10 low 93 kmeans 2020
3 350901 Jombang 19 low 87 kmeans 2021
4 350901 Jombang 4 low 98 kmeans 2022
5 350901 Jombang 4 low 98 kmeans 2023
6 350901 Jombang 4 low 98 kmeans 2024
7 350902 Kencong 6 low 96 kmeans 2022
8 350902 Kencong 10 low 93 kmeans 2021
9 350902 Kencong 11 low 93 kmeans 2020
10 350902 Kencong 7 low 95 kmeans 2023
11 350902 Kencong 4 low 98 kmeans 2024
12 350903 Sumberbaru 82 medium 42 kmeans 2024
13 350903 Sumberbaru 75 medium 47 kmeans 2022
14 350903 Sumberbaru 109 medium 23 kmeans 2021
15 350903 Sumberbaru 49 medium 65 kmeans 2023
16 350903 Sumberbaru 85 medium 40 kmeans 2020
17 350904 Gumukmas 101 medium 28 kmeans 2020
18 350904 Gumukmas 73 medium 48 kmeans 2022
19 350904 Gumukmas 104 medium 26 kmeans 2021
20 350904 Gumukmas 74 low 48 kmeans 2024
21 350904 Gumukmas 49 low 65 kmeans 2023
22 350905 Umbulsari 7 low 95 kmeans 2022
23 350905 Umbulsari 5 low 97 kmeans 2023
24 350905 Umbulsari 17 low 88 kmeans 2021
25 350905 Umbulsari 14 low 90 kmeans 2020
26 350905 Umbulsari 2 low 99 kmeans 2024
27 350906 Tanggul 102 medium 28 kmeans 2020
28 350906 Tanggul 69 medium 51 kmeans 2023
29 350906 Tanggul 95 medium 33 kmeans 2021
30 350906 Tanggul 29 low 80 kmeans 2024
31 350906 Tanggul 93 medium 34 kmeans 2022
32 350907 Semboro 6 low 96 kmeans 2022
33 350907 Semboro 5 low 97 kmeans 2023
34 350907 Semboro 21 low 85 kmeans 2021
35 350907 Semboro 4 low 98 kmeans 2024
36 350907 Semboro 13 low 91 kmeans 2020
37 350908 Puger 102 medium 28 kmeans 2020
38 350908 Puger 72 medium 49 kmeans 2024
39 350908 Puger 98 medium 30 kmeans 2021
40 350908 Puger 94 medium 33 kmeans 2023
41 350908 Puger 82 medium 42 kmeans 2022
42 350909 Bangsalsari 116 medium 18 kmeans 2022
43 350909 Bangsalsari 75 medium 47 kmeans 2023
44 350909 Bangsalsari 121 medium 14 kmeans 2021
45 350909 Bangsalsari 64 medium 55 kmeans 2024
46 350909 Bangsalsari 97 medium 31 kmeans 2020
47 350910 Balung 122 medium 13 kmeans 2020
48 350910 Balung 92 medium 35 kmeans 2023
49 350910 Balung 127 medium 10 kmeans 2021
50 350910 Balung 102 medium 28 kmeans 2024
51 350910 Balung 81 medium 43 kmeans 2022
52 350911 Wuluhan 72 medium 49 kmeans 2022
53 350911 Wuluhan 74 medium 48 kmeans 2024
54 350911 Wuluhan 132 medium 6 kmeans 2021
55 350911 Wuluhan 84 medium 40 kmeans 2023
56 350911 Wuluhan 96 medium 32 kmeans 2020
57 350912 Ambulu 99 medium 30 kmeans 2020
58 350912 Ambulu 70 medium 50 kmeans 2024
59 350912 Ambulu 97 medium 31 kmeans 2021
60 350912 Ambulu 99 medium 30 kmeans 2023
61 350912 Ambulu 77 medium 45 kmeans 2022
62 350913 Rambipuji 104 medium 26 kmeans 2022
63 350913 Rambipuji 68 medium 52 kmeans 2023
64 350913 Rambipuji 103 medium 27 kmeans 2021
65 350913 Rambipuji 103 medium 27 kmeans 2020
66 350913 Rambipuji 48 medium 66 kmeans 2024
67 350914 Panti 11 low 93 kmeans 2020
68 350914 Panti 5 low 97 kmeans 2023
69 350914 Panti 19 low 87 kmeans 2021
70 350914 Panti 3 low 98 kmeans 2024
71 350914 Panti 7 low 95 kmeans 2022
72 350915 Sukorambi 4 low 98 kmeans 2022
73 350915 Sukorambi 5 low 97 kmeans 2024
74 350915 Sukorambi 19 low 87 kmeans 2021
75 350915 Sukorambi 6 low 96 kmeans 2023
76 350915 Sukorambi 11 low 93 kmeans 2020
77 350916 Jenggawah 59 medium 58 kmeans 2023
78 350916 Jenggawah 66 medium 53 kmeans 2024
79 350916 Jenggawah 96 medium 32 kmeans 2022
80 350916 Jenggawah 106 medium 25 kmeans 2020
81 350916 Jenggawah 111 medium 21 kmeans 2021
82 350917 Ajung 107 medium 24 kmeans 2021
83 350917 Ajung 82 medium 42 kmeans 2020
84 350917 Ajung 95 medium 33 kmeans 2022
85 350917 Ajung 82 medium 42 kmeans 2024
86 350917 Ajung 63 medium 55 kmeans 2023
87 350918 Tempurejo 15 low 90 kmeans 2023
88 350918 Tempurejo 17 low 88 kmeans 2024
89 350918 Tempurejo 27 low 81 kmeans 2022
90 350918 Tempurejo 39 low 73 kmeans 2020
91 350918 Tempurejo 49 low 65 kmeans 2021
92 350919 Kaliwates 124 high 12 kmeans 2021
93 350919 Kaliwates 100 high 29 kmeans 2024
94 350919 Kaliwates 93 high 34 kmeans 2022
95 350919 Kaliwates 89 high 37 kmeans 2020
96 350919 Kaliwates 70 high 50 kmeans 2023
97 350920 Patrang 52 high 63 kmeans 2023
98 350920 Patrang 104 high 26 kmeans 2024
99 350920 Patrang 88 high 38 kmeans 2022
100 350920 Patrang 124 high 12 kmeans 2020
101 350920 Patrang 140 high 0 kmeans 2021
102 350921 Sumbersari 89 high 37 kmeans 2021
103 350921 Sumbersari 94 medium 33 kmeans 2020
104 350921 Sumbersari 107 high 24 kmeans 2022
105 350921 Sumbersari 53 high 63 kmeans 2023
106 350921 Sumbersari 79 high 44 kmeans 2024
107 350922 Arjasa 6 low 96 kmeans 2023
108 350922 Arjasa 8 low 95 kmeans 2020
109 350922 Arjasa 14 low 90 kmeans 2022
110 350922 Arjasa 3 low 98 kmeans 2024
111 350922 Arjasa 26 low 82 kmeans 2021
112 350923 Mumbulsari 17 low 88 kmeans 2021
113 350923 Mumbulsari 4 low 98 kmeans 2024
114 350923 Mumbulsari 2 low 99 kmeans 2022
115 350923 Mumbulsari 10 low 93 kmeans 2020
116 350923 Mumbulsari 2 low 99 kmeans 2023
117 350924 Pakusari 7 low 95 kmeans 2023
118 350924 Pakusari 3 low 98 kmeans 2024
119 350924 Pakusari 10 low 93 kmeans 2022
120 350924 Pakusari 11 low 93 kmeans 2020
121 350924 Pakusari 21 low 85 kmeans 2021
122 350925 Jelbuk 21 low 85 kmeans 2021
123 350925 Jelbuk 12 low 92 kmeans 2020
124 350925 Jelbuk 6 low 96 kmeans 2022
125 350925 Jelbuk 0 low 100 kmeans 2024
126 350925 Jelbuk 3 low 98 kmeans 2023
127 350926 Mayang 7 low 95 kmeans 2023
128 350926 Mayang 16 low 89 kmeans 2020
129 350926 Mayang 6 low 96 kmeans 2022
130 350926 Mayang 18 low 88 kmeans 2021
131 350926 Mayang 4 low 98 kmeans 2024
132 350927 Kalisat 16 low 89 kmeans 2020
133 350927 Kalisat 15 low 90 kmeans 2021
134 350927 Kalisat 4 low 98 kmeans 2023
135 350927 Kalisat 5 low 97 kmeans 2022
136 350927 Kalisat 5 low 97 kmeans 2024
137 350928 Ledokombo 5 low 97 kmeans 2024
138 350928 Ledokombo 18 low 88 kmeans 2021
139 350928 Ledokombo 10 low 93 kmeans 2020
140 350928 Ledokombo 10 low 93 kmeans 2022
141 350928 Ledokombo 4 low 98 kmeans 2023
142 350929 Sukowono 6 low 96 kmeans 2023
143 350929 Sukowono 7 low 95 kmeans 2022
144 350929 Sukowono 11 low 93 kmeans 2020
145 350929 Sukowono 20 low 86 kmeans 2021
146 350929 Sukowono 2 low 99 kmeans 2024
147 350930 Silo 68 medium 52 kmeans 2024
148 350930 Silo 86 medium 39 kmeans 2022
149 350930 Silo 81 medium 43 kmeans 2023
150 350930 Silo 89 medium 37 kmeans 2021
151 350930 Silo 109 medium 23 kmeans 2020
152 350931 Sumberjambe 7 low 95 kmeans 2020
153 350931 Sumberjambe 19 low 87 kmeans 2021
154 350931 Sumberjambe 8 low 95 kmeans 2023
155 350931 Sumberjambe 5 low 97 kmeans 2022
156 350931 Sumberjambe 4 low 98 kmeans 2024

View File

@ -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(),

View File

@ -0,0 +1,5 @@
-- AlterTable
ALTER TABLE "crimes" ADD COLUMN "source_type" VARCHAR(100);
-- AlterTable
ALTER TABLE "locations" ADD COLUMN "distance" DOUBLE PRECISION;

View File

@ -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");

View File

@ -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";

View File

@ -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;

View File

@ -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

View File

@ -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$$;

View File

@ -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);