add seeds supaabse

This commit is contained in:
vergiLgood1 2025-05-14 06:54:34 +07:00
parent 2a8f249d0c
commit db8e2a6321
53 changed files with 1484 additions and 506 deletions

View File

@ -42,3 +42,6 @@ next-env.d.ts
# Sentry Config File
.env.sentry-build-plugin
# Snaplet
/.snaplet/

View File

@ -1,6 +1,6 @@
{
"[typescript]": {
"editor.defaultFormatter": "denoland.vscode-deno"
"editor.defaultFormatter": "vscode.typescript-language-features"
},
"deno.enablePaths": [
"supabase/functions"

65
sigap-website/SEEDING.md Normal file
View File

@ -0,0 +1,65 @@
# Discalimer
seed.ts in root project is a seed for dummy datas
# Database Seeding Instructions
This document explains how to seed the SIGAP database with sample data.
## Prerequisites
- Node.js installed
- Required environment variables set (including database and Supabase credentials)
- Required data files in place (Excel files, GeoJSON, etc.)
## Running the Seed Script
The main seeding script is designed to use both Snaplet and native Prisma operations to populate the database:
```bash
# Run the seed script
npx tsx seed.ts
```
## What Gets Seeded
The seeding process runs the following operations in sequence:
1. Geographic data (cities, districts, geographic boundaries)
2. Police units (Polres and Polsek locations)
3. Permissions (Role-based access control)
4. Crime categories
5. Crime data (by unit and by type)
6. Crime incidents (by unit and by type)
## Seeding Control
By default, the script will not reset your database. To enable database reset before seeding, uncomment the reset lines in `seed.ts`:
```typescript
// Uncomment to reset database before seeding
// console.log("Resetting database...");
// await seed.$resetDatabase();
// console.log("Database reset complete.");
```
## Customizing Seeds
Each seeder is contained in its own file under `prisma/seeds/`. To customize the seeding behavior, edit the corresponding file:
- `geographic.ts` - Seeds geographic data
- `units.ts` - Seeds police units
- `permission.ts` - Seeds permissions
- `crime-category.ts` - Seeds crime categories
- `crimes.ts` - Seeds crime data
- `crime-incidents.ts` - Seeds crime incidents by unit
- `crime-incidents-cbt.ts` - Seeds crime incidents by type
## Performance Optimizations
The seeders include several optimizations to handle large datasets:
- Chunked batch operations
- Automatic retry with smaller batch sizes on failure
- Progress monitoring
- Throttling to prevent database overload

View File

@ -141,6 +141,7 @@ export async function getCrimes(): Promise<ICrimes[]> {
address: true,
latitude: true,
longitude: true,
distance_to_unit: true
},
},
},

View File

@ -22,7 +22,15 @@ export async function getUnits(): Promise<IUnits[]> {
},
},
});
return units;
return units
.filter((unit) => unit.district_id !== null)
.map((unit) => ({
...unit,
district_id: unit.district_id as string,
district_name: unit.districts?.name ?? '',
}));
} catch (err) {
if (err instanceof InputParseError) {
// return {

View File

@ -16,7 +16,7 @@ import SourceTypeSelector from "../source-type-selector"
// Define the additional tools and features
const additionalTooltips = [
{ id: "reports" as ITooltips, icon: <IconMessage size={20} />, label: "Police Report" },
{ id: "alerts" as ITooltips, icon: <Siren size={20} />, label: "Active Alerts" },
{ id: "recents" as ITooltips, icon: <Siren size={20} />, label: "Recent incidents" },
]
interface AdditionalTooltipsProps {

View File

@ -2,16 +2,16 @@
import { Button } from "@/app/_components/ui/button"
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/app/_components/ui/tooltip"
import { AlertTriangle, Building, Car, Thermometer } from "lucide-react"
import { AlertTriangle, Building, Car, Thermometer, History } from "lucide-react"
import type { ITooltips } from "./tooltips"
import { IconChartBubble, IconClock } from "@tabler/icons-react"
// Define the primary crime data controls
const crimeTooltips = [
{ id: "incidents" as ITooltips, icon: <AlertTriangle size={20} />, label: "All Incidents" },
// { id: "incidents" as ITooltips, icon: <AlertTriangle size={20} />, label: "All Incidents" },
{ id: "heatmap" as ITooltips, icon: <Thermometer size={20} />, label: "Density Heatmap" },
{ id: "clusters" as ITooltips, icon: <IconChartBubble size={20} />, label: "Clustered Incidents" },
{ id: "units" as ITooltips, icon: <Building size={20} />, label: "Police Units" },
{ id: "clusters" as ITooltips, icon: <IconChartBubble size={20} />, label: "Clustered Incidents" },
{ id: "patrol" as ITooltips, icon: <Car size={20} />, label: "Patrol Areas" },
{ id: "timeline" as ITooltips, icon: <IconClock size={20} />, label: "Time Analysis" },
]

View File

@ -11,12 +11,14 @@ import type { ReactNode } from "react"
export type ITooltips =
// Crime data views
| "incidents"
| "historical"
| "heatmap"
| "units"
| "patrol"
| "reports"
| "clusters"
| "timeline"
| "recents"
// Tools and features
| "refresh"

View File

@ -29,7 +29,7 @@ export default function CrimeMap() {
const [sidebarCollapsed, setSidebarCollapsed] = useState(true)
const [selectedDistrict, setSelectedDistrict] = useState<DistrictFeature | null>(null)
const [showLegend, setShowLegend] = useState<boolean>(true)
const [activeControl, setActiveControl] = useState<ITooltips>("incidents")
const [activeControl, setActiveControl] = useState<ITooltips>("clusters")
const [selectedSourceType, setSelectedSourceType] = useState<string>("cbu")
const [selectedYear, setSelectedYear] = useState<number>(2024)
const [selectedMonth, setSelectedMonth] = useState<number | "all">("all")
@ -149,7 +149,7 @@ export default function CrimeMap() {
setShowClusters(true);
setShowUnclustered(false);
} else {
setActiveControl("incidents");
setActiveControl("clusters");
setShowUnclustered(true);
setShowClusters(false);
}

View File

@ -0,0 +1,260 @@
"use client"
import { useEffect, useCallback, useRef, useState } from "react"
import type { ICrimes } from "@/app/_utils/types/crimes"
interface HistoricalIncidentsLayerProps {
visible?: boolean
map: any
crimes?: ICrimes[]
filterCategory?: string | "all"
focusedDistrictId?: string | null
}
export default function HistoricalIncidentsLayer({
visible = false,
map,
crimes = [],
filterCategory = "all",
focusedDistrictId,
}: HistoricalIncidentsLayerProps) {
const isInteractingWithMarker = useRef(false);
const currentYear = new Date().getFullYear();
const startYear = 2020;
const [yearColors, setYearColors] = useState<Record<number, string>>({});
// Generate colors for each year from 2020 to current year
useEffect(() => {
const colors: Record<number, string> = {};
const yearCount = currentYear - startYear + 1;
for (let i = 0; i < yearCount; i++) {
const year = startYear + i;
// Generate a color gradient from red (2020) to blue (current year)
const red = Math.floor(255 - (i * 255 / yearCount));
const blue = Math.floor(i * 255 / yearCount);
colors[year] = `rgb(${red}, 70, ${blue})`;
}
setYearColors(colors);
}, [currentYear]);
const handleIncidentClick = useCallback(
(e: any) => {
if (!map) return;
const features = map.queryRenderedFeatures(e.point, { layers: ["historical-incidents"] });
if (!features || features.length === 0) return;
isInteractingWithMarker.current = true;
const incident = features[0];
if (!incident.properties) return;
e.originalEvent.stopPropagation();
e.preventDefault();
const incidentDetails = {
id: incident.properties.id,
district: incident.properties.district,
category: incident.properties.category,
type: incident.properties.incidentType,
description: incident.properties.description,
status: incident.properties?.status || "Unknown",
longitude: (incident.geometry as any).coordinates[0],
latitude: (incident.geometry as any).coordinates[1],
timestamp: new Date(incident.properties.timestamp || Date.now()),
year: incident.properties.year,
};
console.log("Historical incident clicked:", incidentDetails);
// Ensure markers stay visible when clicking on them
if (map.getLayer("historical-incidents")) {
map.setLayoutProperty("historical-incidents", "visibility", "visible");
}
// First fly to the incident location
map.flyTo({
center: [incidentDetails.longitude, incidentDetails.latitude],
zoom: 15,
bearing: 0,
pitch: 45,
duration: 2000,
});
// Then dispatch the incident_click event to show the popup
const customEvent = new CustomEvent("incident_click", {
detail: incidentDetails,
bubbles: true,
});
map.getCanvas().dispatchEvent(customEvent);
document.dispatchEvent(customEvent);
// Reset the flag after a delay to allow the event to process
setTimeout(() => {
isInteractingWithMarker.current = false;
}, 5000);
},
[map]
);
useEffect(() => {
if (!map || !visible) return;
console.log("Setting up historical incidents layer");
// Filter incidents from 2020 to current year
const historicalData = {
type: "FeatureCollection" as const,
features: crimes.flatMap((crime) =>
crime.crime_incidents
.filter(
(incident) => {
const incidentYear = incident.timestamp ? new Date(incident.timestamp).getFullYear() : null;
return (
(filterCategory === "all" || incident.crime_categories.name === filterCategory) &&
incident.locations &&
typeof incident.locations.longitude === "number" &&
typeof incident.locations.latitude === "number" &&
incidentYear !== null &&
incidentYear >= startYear &&
incidentYear <= currentYear
);
}
)
.map((incident) => {
const incidentYear = incident.timestamp ? new Date(incident.timestamp).getFullYear() : currentYear;
return {
type: "Feature" as const,
geometry: {
type: "Point" as const,
coordinates: [incident.locations.longitude, incident.locations.latitude],
},
properties: {
id: incident.id,
district: crime.districts.name,
district_id: crime.district_id,
category: incident.crime_categories.name,
incidentType: incident.crime_categories.type || "",
description: incident.description,
status: incident.status || "",
timestamp: incident.timestamp ? incident.timestamp.toString() : "",
year: incidentYear,
},
};
})
),
};
console.log(`Found ${historicalData.features.length} historical incidents from 2020 to ${currentYear}`);
const setupLayerAndSource = () => {
try {
// Check if source exists and update it
if (map.getSource("historical-incidents-source")) {
(map.getSource("historical-incidents-source") as any).setData(historicalData);
} else {
// If not, add source
map.addSource("historical-incidents-source", {
type: "geojson",
data: historicalData,
// No clustering configuration
});
}
// Find first symbol layer for proper layering
const layers = map.getStyle().layers;
let firstSymbolId: string | undefined;
for (const layer of layers) {
if (layer.type === "symbol") {
firstSymbolId = layer.id;
break;
}
}
// Style for year-based coloring
const circleColorExpression: any[] = [
"match",
["get", "year"],
];
// Add entries for each year and its color
Object.entries(yearColors).forEach(([year, color]) => {
circleColorExpression.push(parseInt(year), color);
});
// Default color for unknown years
circleColorExpression.push("#888888");
// Check if layer exists already
if (!map.getLayer("historical-incidents")) {
map.addLayer({
id: "historical-incidents",
type: "circle",
source: "historical-incidents-source",
paint: {
"circle-color": circleColorExpression,
"circle-radius": [
"interpolate",
["linear"],
["zoom"],
7, 2, // Smaller circles at lower zoom levels
12, 4,
15, 6, // Smaller maximum size
],
"circle-stroke-width": 1,
"circle-stroke-color": "#ffffff",
"circle-opacity": 0.8,
},
layout: {
visibility: visible ? "visible" : "none",
}
}, firstSymbolId);
// Add mouse events
map.on("mouseenter", "historical-incidents", () => {
map.getCanvas().style.cursor = "pointer";
});
map.on("mouseleave", "historical-incidents", () => {
map.getCanvas().style.cursor = "";
});
} else {
// Update existing layer visibility
map.setLayoutProperty("historical-incidents", "visibility", visible ? "visible" : "none");
}
// Ensure click handler is properly registered
map.off("click", "historical-incidents", handleIncidentClick);
map.on("click", "historical-incidents", handleIncidentClick);
} catch (error) {
console.error("Error setting up historical incidents layer:", error);
}
};
// Check if style is loaded and set up layer accordingly
if (map.isStyleLoaded()) {
setupLayerAndSource();
} else {
map.once("style.load", setupLayerAndSource);
// Fallback
setTimeout(() => {
if (map.isStyleLoaded()) {
setupLayerAndSource();
}
}, 1000);
}
return () => {
if (map) {
map.off("click", "historical-incidents", handleIncidentClick);
}
};
}, [map, visible, crimes, filterCategory, handleIncidentClick, currentYear, yearColors]);
return null;
}

View File

@ -29,7 +29,9 @@ 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"
import HistoricalIncidentsLayer from "./historical-incidents-layer"
import RecentIncidentsLayer from "./recent-incidents-layer"
// Interface for crime incident
interface ICrimeIncident {
@ -421,7 +423,9 @@ export default function Layers({
const showHeatmapLayer = activeControl === "heatmap" && sourceType !== "cbu"
const showUnitsLayer = activeControl === "units"
const showTimelineLayer = activeControl === "timeline"
const showDistrictFill = activeControl === "incidents" || activeControl === "clusters"
const showHistoricalLayer = activeControl === "historical"
const showRecentIncidents = activeControl === "recents"
const showDistrictFill = activeControl === "incidents" || activeControl === "clusters" || activeControl === "historical" || activeControl === "recents"
const showIncidentMarkers = activeControl !== "heatmap" && activeControl !== "timeline" && sourceType !== "cbu"
return (
@ -442,11 +446,11 @@ export default function Layers({
onDistrictClick={handleDistrictClick}
/>
{/* Recent Crimes Layer for showing crime data from the last 24 hours */}
{/* Recent Incidents Layer (24 hours) */}
<RecentIncidentsLayer
visible={showRecentIncidents}
map={mapboxMap}
incidents={recentIncidents}
visible={crimesVisible}
/>
<HeatmapLayer
@ -523,7 +527,7 @@ export default function Layers({
<FaultLinesLayer map={mapboxMap} />
<CoastlineLayer map={mapboxMap} />
{/* <CoastlineLayer map={mapboxMap} /> */}
{showEWS && (
<EWSAlertLayer

View File

@ -1,164 +0,0 @@
"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

@ -0,0 +1,242 @@
"use client"
import { useEffect, useCallback, useRef } from "react"
import type { IIncidentLogs } from "@/app/_utils/types/crimes"
interface RecentIncidentsLayerProps {
visible?: boolean
map: any
incidents?: IIncidentLogs[]
}
export default function RecentIncidentsLayer({
visible = false,
map,
incidents = [],
}: RecentIncidentsLayerProps) {
const isInteractingWithMarker = useRef(false);
// Filter incidents from the last 24 hours
const recentIncidents = incidents.filter(incident => {
if (!incident.timestamp) return false;
const incidentDate = new Date(incident.timestamp);
const now = new Date();
const timeDiff = now.getTime() - incidentDate.getTime();
// 86400000 = 24 hours in milliseconds
return timeDiff <= 86400000;
});
const handleIncidentClick = useCallback(
(e: any) => {
if (!map) return;
const features = map.queryRenderedFeatures(e.point, { layers: ["recent-incidents"] });
if (!features || features.length === 0) return;
isInteractingWithMarker.current = true;
const incident = features[0];
if (!incident.properties) return;
e.originalEvent.stopPropagation();
e.preventDefault();
const incidentDetails = {
id: incident.properties.id,
description: incident.properties.description,
status: incident.properties?.status || "Active",
longitude: (incident.geometry as any).coordinates[0],
latitude: (incident.geometry as any).coordinates[1],
timestamp: new Date(incident.properties.timestamp || Date.now()),
category: incident.properties.category,
};
console.log("Recent incident clicked:", incidentDetails);
// Ensure markers stay visible
if (map.getLayer("recent-incidents")) {
map.setLayoutProperty("recent-incidents", "visibility", "visible");
}
// First fly to the incident location
map.flyTo({
center: [incidentDetails.longitude, incidentDetails.latitude],
zoom: 15,
bearing: 0,
pitch: 45,
duration: 2000,
});
// Dispatch the incident_click event to show the popup
const customEvent = new CustomEvent("incident_click", {
detail: incidentDetails,
bubbles: true,
});
map.getCanvas().dispatchEvent(customEvent);
document.dispatchEvent(customEvent);
// Reset the flag after a delay
setTimeout(() => {
isInteractingWithMarker.current = false;
}, 5000);
},
[map]
);
useEffect(() => {
if (!map || !visible) return;
console.log(`Setting up recent incidents layer with ${recentIncidents.length} incidents from the last 24 hours`);
// Convert incidents to GeoJSON
const recentData = {
type: "FeatureCollection" as const,
features: recentIncidents.map(incident => ({
type: "Feature" as const,
geometry: {
type: "Point" as const,
coordinates: [incident.longitude, incident.latitude],
},
properties: {
id: incident.id,
user_id: incident.user_id,
address: incident.address,
description: incident.description,
timestamp: incident.timestamp ? incident.timestamp.toString() : new Date().toString(),
category: incident.category,
district: incident.district,
severity: incident.severity,
status: incident.verified,
source: incident.source,
},
})),
};
const setupLayerAndSource = () => {
try {
// Check if source exists and update it
if (map.getSource("recent-incidents-source")) {
(map.getSource("recent-incidents-source") as any).setData(recentData);
} else {
// If not, add source
map.addSource("recent-incidents-source", {
type: "geojson",
data: recentData,
});
}
// Find first symbol layer for proper layering
const layers = map.getStyle().layers;
let firstSymbolId: string | undefined;
for (const layer of layers) {
if (layer.type === "symbol") {
firstSymbolId = layer.id;
break;
}
}
// Check if layer exists already
if (!map.getLayer("recent-incidents")) {
map.addLayer({
id: "recent-incidents",
type: "circle",
source: "recent-incidents-source",
paint: {
"circle-color": "#FF5252", // Red color for recent incidents
"circle-radius": [
"interpolate",
["linear"],
["zoom"],
7, 4, // Slightly larger at lower zooms for visibility
12, 8,
15, 12, // Larger maximum size
],
"circle-stroke-width": 2,
"circle-stroke-color": "#FFFFFF",
"circle-opacity": 0.8,
// Add a pulsing effect
"circle-stroke-opacity": [
"interpolate",
["linear"],
["zoom"],
7, 0.5,
15, 0.8
],
},
layout: {
visibility: visible ? "visible" : "none",
}
}, firstSymbolId);
// Add a glow effect with a larger circle behind
map.addLayer({
id: "recent-incidents-glow",
type: "circle",
source: "recent-incidents-source",
paint: {
"circle-color": "#FF5252",
"circle-radius": [
"interpolate",
["linear"],
["zoom"],
7, 6,
12, 12,
15, 18,
],
"circle-opacity": 0.2,
"circle-blur": 1,
},
layout: {
visibility: visible ? "visible" : "none",
}
}, "recent-incidents");
// Add mouse events
map.on("mouseenter", "recent-incidents", () => {
map.getCanvas().style.cursor = "pointer";
});
map.on("mouseleave", "recent-incidents", () => {
map.getCanvas().style.cursor = "";
});
} else {
// Update existing layer visibility
map.setLayoutProperty("recent-incidents", "visibility", visible ? "visible" : "none");
map.setLayoutProperty("recent-incidents-glow", "visibility", visible ? "visible" : "none");
}
// Ensure click handler is properly registered
map.off("click", "recent-incidents", handleIncidentClick);
map.on("click", "recent-incidents", handleIncidentClick);
} catch (error) {
console.error("Error setting up recent incidents layer:", error);
}
};
// Check if style is loaded and set up layer accordingly
if (map.isStyleLoaded()) {
setupLayerAndSource();
} else {
map.once("style.load", setupLayerAndSource);
// Fallback
setTimeout(() => {
if (map.isStyleLoaded()) {
setupLayerAndSource();
} else {
console.warn("Map style still not loaded after timeout");
}
}, 1000);
}
return () => {
if (map) {
map.off("click", "recent-incidents", handleIncidentClick);
}
};
}, [map, visible, recentIncidents, handleIncidentClick]);
return null;
}

View File

@ -5,12 +5,10 @@ import { Layer, Source } from "react-map-gl/mapbox"
import type { ICrimes } from "@/app/_utils/types/crimes"
import type { IUnits } from "@/app/_utils/types/units"
import type mapboxgl from "mapbox-gl"
import { useQuery } from "@tanstack/react-query"
import { generateCategoryColorMap } from "@/app/_utils/colors"
import UnitPopup from "../pop-up/unit-popup"
import IncidentPopup from "../pop-up/incident-popup"
import { calculateDistances } from "@/app/(pages)/(admin)/dashboard/crime-management/crime-overview/action"
interface UnitsLayerProps {
crimes: ICrimes[]
@ -20,23 +18,6 @@ interface UnitsLayerProps {
map?: mapboxgl.Map | null
}
// Custom hook for fetching distance data
const useDistanceData = (entityId?: string, isUnit = false, districtId?: string) => {
// Skip the query when no entity is selected
return useQuery({
queryKey: ["distance-incidents", entityId, isUnit, districtId],
queryFn: async () => {
if (!entityId) return []
const unitId = isUnit ? entityId : undefined
const result = await calculateDistances(unitId, districtId)
return result
},
enabled: !!entityId, // Only run query when there's an entityId
staleTime: 5 * 60 * 1000, // Consider data fresh for 5 minutes
gcTime: 10 * 60 * 1000, // Keep cache for 10 minutes
})
}
export default function UnitsLayer({ crimes, units = [], filterCategory, visible = false, map }: UnitsLayerProps) {
const [loadedUnits, setLoadedUnits] = useState<IUnits[]>([])
const loadedUnitsRef = useRef<IUnits[]>([])
@ -48,13 +29,6 @@ export default function UnitsLayer({ crimes, units = [], filterCategory, visible
const [isUnitSelected, setIsUnitSelected] = useState<boolean>(false)
const [selectedDistrictId, setSelectedDistrictId] = useState<string | undefined>()
// Use react-query for distance data
const { data: distances = [], isLoading: isLoadingDistances } = useDistanceData(
selectedEntityId,
isUnitSelected,
selectedDistrictId,
)
// Use either provided units or loaded units
const unitsData = useMemo(() => {
return units.length > 0 ? units : loadedUnits || []
@ -66,10 +40,10 @@ export default function UnitsLayer({ crimes, units = [], filterCategory, visible
crimes.forEach((crime) => {
crime.crime_incidents.forEach((incident) => {
if (incident.crime_categories?.name) {
categories.add(incident.crime_categories.name)
}
})
})
categories.add(incident.crime_categories.name)
}
})
})
return Array.from(categories)
}, [crimes])
@ -80,27 +54,52 @@ export default function UnitsLayer({ crimes, units = [], filterCategory, visible
// Process units data to GeoJSON format
const unitsGeoJSON = useMemo(() => {
console.log("Units data being processed:", unitsData); // Debug log
return {
type: "FeatureCollection" as const,
features: unitsData
.map((unit) => ({
type: "Feature" as const,
properties: {
id: unit.code_unit,
name: unit.name,
address: unit.address,
phone: unit.phone,
type: unit.type,
district: unit.districts?.name || "",
district_id: unit.district_id,
},
geometry: {
type: "Point" as const,
coordinates: [unit.longitude || 0, unit.latitude || 0],
},
}))
.filter((feature) => feature.geometry.coordinates[0] !== 0 && feature.geometry.coordinates[1] !== 0),
}
.map((unit) => {
// Debug log for individual units
console.log("Processing unit:", unit.code_unit, unit.name, {
longitude: unit.longitude,
latitude: unit.latitude,
district: unit.district_name || unit.district_name
});
return {
type: "Feature" as const,
properties: {
id: unit.code_unit,
name: unit.name,
address: unit.address,
phone: unit.phone,
type: unit.type,
district: unit.district_name || unit.district_name || "",
district_id: unit.district_id,
},
geometry: {
type: "Point" as const,
coordinates: [
parseFloat(String(unit.longitude)) || 0,
parseFloat(String(unit.latitude)) || 0
],
},
};
})
// Only filter out units with BOTH coordinates being exactly 0, allow any valid coordinate
.filter((feature) => {
const coords = feature.geometry.coordinates;
const isValid = coords[0] !== 0 || coords[1] !== 0;
// Debug which units are being filtered out
if (!isValid) {
console.log("Filtering out unit with invalid coordinates:", feature.properties.id, feature.properties.name);
}
return isValid;
}),
};
}, [unitsData])
// Process incident data to GeoJSON format
@ -114,27 +113,28 @@ export default function UnitsLayer({ crimes, units = [], filterCategory, visible
!incident.locations?.latitude ||
!incident.locations?.longitude ||
(filterCategory !== "all" && incident.crime_categories.name !== filterCategory)
)
return
)
return
features.push({
type: "Feature" as const,
properties: {
id: incident.id,
description: incident.description || "No description",
category: incident.crime_categories.name,
date: incident.timestamp,
district: crime.districts?.name || "",
district_id: crime.district_id,
categoryColor: categoryColorMap[incident.crime_categories.name] || "#22c55e",
},
geometry: {
type: "Point" as const,
coordinates: [incident.locations.longitude, incident.locations.latitude],
},
})
})
})
features.push({
type: "Feature" as const,
properties: {
id: incident.id,
description: incident.description || "No description",
category: incident.crime_categories.name,
date: incident.timestamp,
district: crime.districts?.name || "",
district_id: crime.district_id,
categoryColor: categoryColorMap[incident.crime_categories.name] || "#22c55e",
distance_to_unit: incident.locations.distance_to_unit || "Unknown",
},
geometry: {
type: "Point" as const,
coordinates: [incident.locations.longitude, incident.locations.latitude],
},
})
})
})
return {
type: "FeatureCollection" as const,
@ -147,8 +147,8 @@ export default function UnitsLayer({ crimes, units = [], filterCategory, visible
if (!unitsData.length || !crimes.length)
return {
type: "FeatureCollection" as const,
features: [],
}
features: [],
}
// Map district IDs to their units
const districtUnitsMap = new Map<string, IUnits[]>()
@ -156,11 +156,11 @@ export default function UnitsLayer({ crimes, units = [], filterCategory, visible
unitsData.forEach((unit) => {
if (!unit.district_id || !unit.longitude || !unit.latitude) return
if (!districtUnitsMap.has(unit.district_id)) {
districtUnitsMap.set(unit.district_id, [])
}
districtUnitsMap.get(unit.district_id)!.push(unit)
})
if (!districtUnitsMap.has(unit.district_id)) {
districtUnitsMap.set(unit.district_id, [])
}
districtUnitsMap.get(unit.district_id)!.push(unit)
})
// Create lines from units to incidents in their district
const lineFeatures: any[] = []
@ -170,42 +170,42 @@ export default function UnitsLayer({ crimes, units = [], filterCategory, visible
const districtUnits = districtUnitsMap.get(crime.district_id) || []
if (!districtUnits.length) return
// For each incident in this district
crime.crime_incidents.forEach((incident) => {
// Skip incidents without location data or filtered by category
if (
!incident.locations?.latitude ||
!incident.locations?.longitude ||
(filterCategory !== "all" && incident.crime_categories.name !== filterCategory)
)
return
// For each incident in this district
crime.crime_incidents.forEach((incident) => {
// Skip incidents without location data or filtered by category
if (
!incident.locations?.latitude ||
!incident.locations?.longitude ||
(filterCategory !== "all" && incident.crime_categories.name !== filterCategory)
)
return
// Create a line from each unit in this district to this incident
districtUnits.forEach((unit) => {
if (!unit.longitude || !unit.latitude) return
// Create a line from each unit in this district to this incident
districtUnits.forEach((unit) => {
if (!unit.longitude || !unit.latitude) return
lineFeatures.push({
type: "Feature" as const,
properties: {
unit_id: unit.code_unit,
unit_name: unit.name,
incident_id: incident.id,
district_id: crime.district_id,
district_name: crime.districts.name,
category: incident.crime_categories.name,
lineColor: categoryColorMap[incident.crime_categories.name] || "#22c55e",
},
geometry: {
type: "LineString" as const,
coordinates: [
[unit.longitude, unit.latitude],
[incident.locations.longitude, incident.locations.latitude],
],
},
lineFeatures.push({
type: "Feature" as const,
properties: {
unit_id: unit.code_unit,
unit_name: unit.name,
incident_id: incident.id,
district_id: crime.district_id,
district_name: crime.districts.name,
category: incident.crime_categories.name,
lineColor: categoryColorMap[incident.crime_categories.name] || "#22c55e",
},
geometry: {
type: "LineString" as const,
coordinates: [
[unit.longitude, unit.latitude],
[incident.locations.longitude, incident.locations.latitude],
],
},
})
})
})
})
})
})
return {
type: "FeatureCollection" as const,
@ -237,7 +237,10 @@ export default function UnitsLayer({ crimes, units = [], filterCategory, visible
// Find the unit in our data
const unit = unitsData.find((u) => u.code_unit === properties.id)
if (!unit) return
if (!unit) {
console.log("Unit not found in data:", properties.id);
return;
}
// Fly to the unit location
map.flyTo({
@ -258,7 +261,7 @@ export default function UnitsLayer({ crimes, units = [], filterCategory, visible
// Highlight the connected lines for this unit
if (map.getLayer("units-connection-lines")) {
map.setFilter("units-connection-lines", ["==", ["get", "unit_id"], properties.id])
}
}
// Dispatch a custom event for other components to react to
const customEvent = new CustomEvent("unit_click", {
@ -320,9 +323,10 @@ export default function UnitsLayer({ crimes, units = [], filterCategory, visible
date: properties.date,
district: properties.district,
district_id: properties.district_id,
longitude,
latitude,
}
distance_to_unit: properties.distance_to_unit,
longitude,
latitude,
}
// Set the selected incident and query parameters
setSelectedIncident(incident)
@ -394,50 +398,53 @@ export default function UnitsLayer({ crimes, units = [], filterCategory, visible
useEffect(() => {
if (!map || !visible) return
// Debug log to confirm map layers
console.log("Available map layers:", map.getStyle().layers?.map(l => l.id));
// Define event handlers that can be referenced for both adding and removing
const handleMouseEnter = () => {
map.getCanvas().style.cursor = "pointer"
}
map.getCanvas().style.cursor = "pointer"
}
const handleMouseLeave = () => {
map.getCanvas().style.cursor = ""
}
map.getCanvas().style.cursor = ""
}
// Add click event for units-points layer
if (map.getLayer("units-points")) {
map.off("click", "units-points", unitClickHandler)
map.on("click", "units-points", unitClickHandler)
// Change cursor on hover
map.on("mouseenter", "units-points", handleMouseEnter)
map.on("mouseleave", "units-points", handleMouseLeave)
}
// Change cursor on hover
map.on("mouseenter", "units-points", handleMouseEnter)
map.on("mouseleave", "units-points", handleMouseLeave)
}
// Add click event for incidents-points layer
if (map.getLayer("incidents-points")) {
map.off("click", "incidents-points", incidentClickHandler)
map.on("click", "incidents-points", incidentClickHandler)
// Change cursor on hover
map.on("mouseenter", "incidents-points", handleMouseEnter)
map.on("mouseleave", "incidents-points", handleMouseLeave)
}
// Change cursor on hover
map.on("mouseenter", "incidents-points", handleMouseEnter)
map.on("mouseleave", "incidents-points", handleMouseLeave)
}
return () => {
if (map) {
if (map.getLayer("units-points")) {
map.off("click", "units-points", unitClickHandler)
map.off("mouseenter", "units-points", handleMouseEnter)
map.off("mouseleave", "units-points", handleMouseLeave)
}
if (map) {
if (map.getLayer("units-points")) {
map.off("click", "units-points", unitClickHandler)
map.off("mouseenter", "units-points", handleMouseEnter)
map.off("mouseleave", "units-points", handleMouseLeave)
}
if (map.getLayer("incidents-points")) {
map.off("click", "incidents-points", incidentClickHandler)
map.off("mouseenter", "incidents-points", handleMouseEnter)
map.off("mouseleave", "incidents-points", handleMouseLeave)
if (map.getLayer("incidents-points")) {
map.off("click", "incidents-points", incidentClickHandler)
map.off("mouseenter", "incidents-points", handleMouseEnter)
map.off("mouseleave", "incidents-points", handleMouseLeave)
}
}
}
}
}, [map, visible, unitClickHandler, incidentClickHandler])
// Reset map filters when popup is closed
@ -504,8 +511,8 @@ export default function UnitsLayer({ crimes, units = [], filterCategory, visible
id="incidents-points"
type="circle"
paint={{
"circle-radius": 6,
// Use the pre-computed color stored in the properties
"circle-radius": 6,
// Use the pre-computed color stored in the properties
"circle-color": ["get", "categoryColor"],
"circle-stroke-width": 1,
"circle-stroke-color": "#ffffff",
@ -520,7 +527,7 @@ export default function UnitsLayer({ crimes, units = [], filterCategory, visible
id="units-connection-lines"
type="line"
paint={{
// Use the pre-computed color stored in the properties
// Use the pre-computed color stored in the properties
"line-color": ["get", "lineColor"],
"line-width": 3,
"line-opacity": 0.9,
@ -542,11 +549,9 @@ export default function UnitsLayer({ crimes, units = [], filterCategory, visible
type: selectedUnit.type,
address: selectedUnit.address || "No address",
phone: selectedUnit.phone || "No phone",
district: selectedUnit.districts?.name,
district: selectedUnit.district_name || "No district",
district_id: selectedUnit.district_id,
}}
distances={distances}
isLoadingDistances={isLoadingDistances}
/>
)}
@ -557,8 +562,6 @@ export default function UnitsLayer({ crimes, units = [], filterCategory, visible
latitude={selectedIncident.latitude}
onClose={handleClosePopup}
incident={selectedIncident}
distances={distances}
isLoadingDistances={isLoadingDistances}
/>
)}
</>

View File

@ -6,11 +6,18 @@ export const BASE_LONGITUDE = 113.65; // Default longitude for the map center (J
export const MAPBOX_ACCESS_TOKEN = process.env.NEXT_PUBLIC_MAPBOX_ACCESS_TOKEN;
export const MAPBOX_TILESET_ID = process.env.NEXT_PUBLIC_MAPBOX_TILESET_ID; // Replace with your tileset ID
// export const CRIME_RATE_COLORS = {
// low: '#FFB74D', // green
// medium: '#FC7216', // yellow
// high: '#BF360C', // red
// // critical: '#ef4444', // red
// default: '#94a3b8', // gray
// };
export const CRIME_RATE_COLORS = {
low: '#4ade80', // green
medium: '#facc15', // yellow
high: '#ef4444', // red
// critical: '#ef4444', // red
default: '#94a3b8', // gray
};

View File

@ -40,6 +40,7 @@ export interface ICrimes extends crimes {
address: string | null;
longitude: number;
latitude: number;
distance_to_unit: number | null;
};
}[];
}

View File

@ -2,6 +2,7 @@ import { $Enums, units } from '@prisma/client';
export interface IUnits {
district_id: string;
district_name: string;
created_at: Date | null;
updated_at: Date | null;
name: string;
@ -13,9 +14,6 @@ export interface IUnits {
land_area: number | null;
code_unit: string;
phone: string | null;
districts: {
name: string;
};
}
// export interface IUnits {

BIN
sigap-website/bun.lockb Normal file

Binary file not shown.

View File

@ -6,6 +6,7 @@
"": {
"dependencies": {
"@evyweb/ioctopus": "^1.2.0",
"@faker-js/faker": "^9.7.0",
"@hookform/resolvers": "^4.1.2",
"@mapbox/mapbox-gl-geocoder": "^5.0.3",
"@prisma/client": "^6.4.1",
@ -78,6 +79,7 @@
"@types/three": "^0.176.0",
"@types/uuid": "^10.0.0",
"postcss": "8.4.49",
"postgres": "^3.4.5",
"prisma": "^6.4.1",
"react-email": "3.0.7",
"tailwind-merge": "^2.5.2",
@ -1081,6 +1083,22 @@
"integrity": "sha512-OIISYUx7WZDm6uxQkVsKmNF13tEiA3gbUeboTkr4LUTmJffhSVswiWAs8Ng5DoyvUlmgteTYcHP5XzOtrPTxLw==",
"license": "MIT"
},
"node_modules/@faker-js/faker": {
"version": "9.7.0",
"resolved": "https://registry.npmjs.org/@faker-js/faker/-/faker-9.7.0.tgz",
"integrity": "sha512-aozo5vqjCmDoXLNUJarFZx2IN/GgGaogY4TMJ6so/WLZOWpSV7fvj2dmrV6sEAnUm1O7aCrhTibjpzeDFgNqbg==",
"funding": [
{
"type": "opencollective",
"url": "https://opencollective.com/fakerjs"
}
],
"license": "MIT",
"engines": {
"node": ">=18.0.0",
"npm": ">=9.0.0"
}
},
"node_modules/@floating-ui/core": {
"version": "1.6.9",
"resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.6.9.tgz",
@ -12350,6 +12368,20 @@
"integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==",
"license": "MIT"
},
"node_modules/postgres": {
"version": "3.4.5",
"resolved": "https://registry.npmjs.org/postgres/-/postgres-3.4.5.tgz",
"integrity": "sha512-cDWgoah1Gez9rN3H4165peY9qfpEo+SA61oQv65O3cRUE1pOEoJWwddwcqKE8XZYjbblOJlYDlLV4h67HrEVDg==",
"dev": true,
"license": "Unlicense",
"engines": {
"node": ">=12"
},
"funding": {
"type": "individual",
"url": "https://github.com/sponsors/porsager"
}
},
"node_modules/postgres-array": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/postgres-array/-/postgres-array-2.0.0.tgz",

View File

@ -12,6 +12,7 @@
},
"dependencies": {
"@evyweb/ioctopus": "^1.2.0",
"@faker-js/faker": "^9.7.0",
"@hookform/resolvers": "^4.1.2",
"@mapbox/mapbox-gl-geocoder": "^5.0.3",
"@prisma/client": "^6.4.1",
@ -75,6 +76,8 @@
"zustand": "^5.0.3"
},
"devDependencies": {
"@snaplet/copycat": "^6.0.0",
"@snaplet/seed": "0.98.0",
"@tanstack/eslint-plugin-query": "^5.67.2",
"@tanstack/react-query-devtools": "^5.67.2",
"@types/mapbox__mapbox-gl-geocoder": "^5.0.0",
@ -84,6 +87,7 @@
"@types/three": "^0.176.0",
"@types/uuid": "^10.0.0",
"postcss": "8.4.49",
"postgres": "^3.4.5",
"prisma": "^6.4.1",
"react-email": "3.0.7",
"tailwind-merge": "^2.5.2",

View File

@ -36,8 +36,8 @@ export const districtCenters = [
},
{
kecamatan: "Jenggawah",
lat: -8.2409,
lng: 113.6407,
lat: -8.291111565708007,
lng: 113.6483542061137,
},
{
kecamatan: "Jombang",
@ -116,8 +116,8 @@ export const districtCenters = [
},
{
kecamatan: "Sukowono",
lat: -8.0547,
lng: 113.8853,
lat: -8.06492265726014,
lng: 113.83125008757588,
},
{
kecamatan: "Sumberbaru",

View File

@ -0,0 +1,30 @@
/*
Warnings:
- You are about to drop the column `unit_id` 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 `id` on the `units` table. All the data in the column will be lost.
- A unique constraint covering the columns `[code_unit,month,year]` on the table `unit_statistics` will be added. If there are existing duplicate values, this will fail.
- Added the required column `code_unit` 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_unit_id_fkey";
-- DropIndex
DROP INDEX "unit_statistics_unit_id_month_year_key";
-- AlterTable
ALTER TABLE "unit_statistics" DROP COLUMN "unit_id",
ADD COLUMN "code_unit" VARCHAR(20) NOT NULL;
-- AlterTable
ALTER TABLE "units" DROP CONSTRAINT "units_pkey",
DROP COLUMN "id",
ADD CONSTRAINT "units_pkey" PRIMARY KEY ("code_unit");
-- CreateIndex
CREATE UNIQUE INDEX "unit_statistics_code_unit_month_year_key" ON "unit_statistics"("code_unit", "month", "year");
-- AddForeignKey
ALTER TABLE "unit_statistics" ADD CONSTRAINT "unit_statistics_code_unit_fkey" FOREIGN KEY ("code_unit") REFERENCES "units"("code_unit") ON DELETE CASCADE ON UPDATE NO ACTION;

View File

@ -266,8 +266,7 @@ model incident_logs {
}
model units {
id String @id @default(dbgenerated("gen_random_uuid()")) @db.Uuid
code_unit String @unique @db.VarChar(20)
code_unit String @id @unique @db.VarChar(20)
district_id String? @unique @db.VarChar(20)
city_id String @db.VarChar(20)
name String @db.VarChar(100)
@ -294,7 +293,7 @@ model units {
model unit_statistics {
id String @id @default(dbgenerated("gen_random_uuid()")) @db.Uuid
unit_id String @db.Uuid
code_unit String @db.VarChar(20)
crime_total Int
crime_cleared Int
percentage Float?
@ -303,9 +302,9 @@ model unit_statistics {
year Int
created_at DateTime? @default(now()) @db.Timestamptz(6)
updated_at DateTime? @default(now()) @db.Timestamptz(6)
units units @relation(fields: [unit_id], references: [id], onDelete: Cascade, onUpdate: NoAction)
units units @relation(fields: [code_unit], references: [code_unit], onDelete: Cascade, onUpdate: NoAction)
@@unique([unit_id, month, year])
@@unique([code_unit, month, year])
@@index([year, month], map: "idx_unit_statistics_year_month")
}

View File

@ -12,6 +12,7 @@ import { UnitSeeder } from './seeds/units';
import { CrimesSeeder } from './seeds/crimes';
import { CrimeIncidentsByUnitSeeder } from './seeds/crime-incidents';
import { CrimeIncidentsByTypeSeeder } from './seeds/crime-incidents-cbt';
import { IncidentLogSeeder } from './seeds/incident-logs';
const prisma = new PrismaClient();
@ -30,16 +31,17 @@ class DatabaseSeeder {
// Daftar semua seeders di sini
this.seeders = [
// new RoleSeeder(prisma),
// new ResourceSeeder(prisma),
// new PermissionSeeder(prisma),
// new CrimeCategoriesSeeder(prisma),
// new GeoJSONSeeder(prisma),
// new UnitSeeder(prisma),
// new DemographicsSeeder(prisma),
new RoleSeeder(prisma),
new ResourceSeeder(prisma),
new PermissionSeeder(prisma),
new CrimeCategoriesSeeder(prisma),
new GeoJSONSeeder(prisma),
new UnitSeeder(prisma),
new DemographicsSeeder(prisma),
new CrimesSeeder(prisma),
// new CrimeIncidentsByUnitSeeder(prisma),
new CrimeIncidentsByTypeSeeder(prisma),
new IncidentLogSeeder(prisma),
];
}

View File

@ -21,71 +21,88 @@ export class CrimeCategoriesSeeder {
async run(): Promise<void> {
console.log('Seeding crime categories...');
// Hapus data yang ada untuk menghindari duplikasi
// Delete existing data to avoid duplicates
await this.prisma.crime_categories.deleteMany({});
// Truncate table jika diperlukan
// await this.prisma.$executeRaw`TRUNCATE TABLE "crime_categories" CASCADE`;
const filePath = path.join(
__dirname,
'../data/excels/others/crime_categories.xlsx'
);
const workbook = XLSX.readFile(filePath);
const sheet = workbook.Sheets[workbook.SheetNames[0]];
const data = XLSX.utils.sheet_to_json(sheet) as ICrimeCategory[];
let categoriesToCreate = [];
try {
// Read from Excel file
const workbook = XLSX.readFile(filePath);
const sheet = workbook.Sheets[workbook.SheetNames[0]];
const data = XLSX.utils.sheet_to_json(sheet) as ICrimeCategory[];
// Prepare array for batch insertion
const categoriesToCreate = [];
// Generate IDs and prepare data for batch insertion
for (const category of crimeCategoriesData) {
const newId = await generateIdWithDbCounter('crime_categories', {
prefix: 'CC',
segments: {
sequentialDigits: 4,
},
format: '{prefix}-{sequence}',
separator: '-',
uniquenessStrategy: 'counter',
});
// Generate IDs and prepare data for batch insertion
for (const category of crimeCategoriesData) {
const newId = await generateIdWithDbCounter('crime_categories', {
prefix: 'CC',
segments: {
sequentialDigits: 4,
},
format: '{prefix}-{sequence}',
separator: '-',
uniquenessStrategy: 'counter',
});
categoriesToCreate.push({
id: newId.trim(),
name: category.name,
description: category.description,
});
}
categoriesToCreate.push({
id: newId.trim(),
name: category.name,
description: category.description,
});
console.log(`Prepared ${categoriesToCreate.length} crime categories for creation`);
// Create categories in smaller batches
const batchSize = 50;
for (let i = 0; i < categoriesToCreate.length; i += batchSize) {
const batch = categoriesToCreate.slice(i, i + batchSize);
await this.prisma.crime_categories.createMany({
data: batch,
skipDuplicates: true,
});
console.log(`Created batch ${Math.floor(i/batchSize) + 1} of categories (${batch.length} items)`);
}
console.log(`Batch created ${categoriesToCreate.length} crime categories.`);
// Update types in smaller batches
const categoriesToUpdate = data.map((row) => ({
id: row['id'].trim(),
type: row['type'].trim(),
name: row['name'].trim(),
}));
const updateBatchSize = 20;
for (let i = 0; i < categoriesToUpdate.length; i += updateBatchSize) {
const batch = categoriesToUpdate.slice(i, i + updateBatchSize);
await Promise.all(
batch.map((category) =>
this.prisma.crime_categories.updateMany({
where: { id: category.id },
data: { type: category.type },
})
)
);
console.log(`Updated types for batch ${Math.floor(i/updateBatchSize) + 1}`);
}
console.log(
`Updated types for ${categoriesToUpdate.length} crime categories.`
);
console.log(`${crimeCategoriesData.length} crime categories seeded`);
} catch (error) {
console.error('Error seeding crime categories:', error);
}
// Batch create categories
await this.prisma.crime_categories.createMany({
data: categoriesToCreate,
skipDuplicates: true,
});
console.log(`Batch created ${categoriesToCreate.length} crime categories.`);
// Prepare data for batch update
const categoriesToUpdate = data.map((row) => ({
id: row['id'].trim(),
type: row['type'].trim(),
name: row['name'].trim(),
}));
// Batch update is not directly supported by Prisma, so we'll use Promise.all with individual updates
await Promise.all(
categoriesToUpdate.map((category) =>
this.prisma.crime_categories.updateMany({
where: { id: category.id },
data: { type: category.type },
})
)
);
console.log(
`Updated types for ${categoriesToUpdate.length} crime categories.`
);
console.log(`${crimeCategoriesData.length} crime categories seeded`);
}
}

View File

@ -769,4 +769,4 @@ if (require.main === module) {
};
testSeeder();
}
}

View File

@ -153,31 +153,105 @@ private generateDistributedPoints(
}
// Helper for chunked insertion
private async chunkedInsertIncidents(data: any[], chunkSize: number = 200) {
private async chunkedInsertIncidents(data: any[], chunkSize: number = 100) {
console.log(`Inserting ${data.length} incidents in batches of ${chunkSize}...`);
const total = data.length;
let inserted = 0;
for (let i = 0; i < data.length; i += chunkSize) {
const chunk = data.slice(i, i + chunkSize);
await this.prisma.crime_incidents.createMany({
data: chunk,
skipDuplicates: true,
});
try {
await this.prisma.crime_incidents.createMany({
data: chunk,
skipDuplicates: true,
});
inserted += chunk.length;
if (inserted % 500 === 0 || inserted === total) {
console.log(`Progress: ${inserted}/${total} incidents inserted (${Math.round(inserted/total*100)}%)`);
}
// Add small delay to prevent database overload
await new Promise(resolve => setTimeout(resolve, 200));
} catch (error) {
console.error(`Error inserting chunk of incidents:`, error);
// If chunk is already small, try one by one
if (chunkSize <= 10) {
console.log("Attempting to insert incidents one by one...");
for (const incident of chunk) {
try {
await this.prisma.crime_incidents.create({
data: incident
});
inserted++;
} catch (err) {
console.error(`Failed to insert individual incident:`, err);
}
}
} else {
// Try with smaller chunk size
const smallerChunk = Math.max(10, Math.floor(chunkSize / 2));
console.log(`Retrying with smaller chunk size: ${smallerChunk}`);
await this.chunkedInsertIncidents(chunk, smallerChunk);
}
}
}
console.log(`Successfully inserted ${inserted} incidents`);
}
// Helper for chunked Supabase insert
private async chunkedInsertLocations(
locations: any[],
chunkSize: number = 200
chunkSize: number = 100
) {
console.log(`Inserting ${locations.length} locations in batches of ${chunkSize}...`);
const total = locations.length;
let inserted = 0;
for (let i = 0; i < locations.length; i += chunkSize) {
const chunk = locations.slice(i, i + chunkSize);
let { error } = await this.supabase
.from('locations')
.insert(chunk)
.select();
if (error) {
throw error;
try {
const { error } = await this.supabase
.from('locations')
.insert(chunk)
.select();
if (error) {
throw error;
}
inserted += chunk.length;
if (inserted % 500 === 0 || inserted === total) {
console.log(`Progress: ${inserted}/${total} locations inserted (${Math.round(inserted/total*100)}%)`);
}
// Add small delay
await new Promise(resolve => setTimeout(resolve, 200));
} catch (error) {
console.error(`Error inserting chunk of locations:`, error);
if (chunkSize <= 10) {
console.log("Attempting to insert locations one by one...");
for (const location of chunk) {
try {
await this.supabase.from('locations').insert(location).select();
inserted++;
} catch (err) {
console.error(`Failed to insert individual location:`, err);
}
}
} else {
// Try with smaller chunk size
const smallerChunk = Math.max(10, Math.floor(chunkSize / 2));
console.log(`Retrying with smaller chunk size: ${smallerChunk}`);
await this.chunkedInsertLocations(chunk, smallerChunk);
}
}
}
console.log(`Successfully inserted ${inserted} locations`);
}
async run(): Promise<void> {

View File

@ -40,8 +40,8 @@ export class CrimesSeeder {
// Create test user
const user = await this.createUsers();
await db.crime_incidents.deleteMany();
await db.crimes.deleteMany();
await db.crime_incidents.deleteMany({});
await db.crimes.deleteMany({});
if (!user) {
throw new Error("Failed to create user");
@ -174,16 +174,16 @@ export class CrimesSeeder {
private async importMonthlyCrimeData() {
console.log("Importing monthly crime data...");
const existingCrimes = await this.prisma.crimes.findFirst({
where: {
source_type: "cbu",
},
});
// const existingCrimes = await this.prisma.crimes.findFirst({
// where: {
// source_type: "cbu",
// },
// });
if (existingCrimes) {
console.log("General crimes data already exists, skipping import.");
return;
}
// if (existingCrimes) {
// console.log("General crimes data already exists, skipping import.");
// return;
// }
const csvFilePath = path.resolve(
__dirname,
@ -254,17 +254,17 @@ export class CrimesSeeder {
private async importYearlyCrimeData() {
console.log("Importing yearly crime data...");
const existingYearlySummary = await this.prisma.crimes.findFirst({
where: {
month: null,
source_type: "cbu",
},
});
// const existingYearlySummary = await this.prisma.crimes.findFirst({
// where: {
// month: null,
// source_type: "cbu",
// },
// });
if (existingYearlySummary) {
console.log("Yearly crime data already exists, skipping import.");
return;
}
// if (existingYearlySummary) {
// console.log("Yearly crime data already exists, skipping import.");
// return;
// }
const csvFilePath = path.resolve(
__dirname,
@ -337,23 +337,24 @@ export class CrimesSeeder {
private async importAllYearSummaries() {
console.log("Importing all-year (2020-2024) crime summaries...");
const existingAllYearSummaries = await this.prisma.crimes.findFirst({
where: {
month: null,
year: null,
source_type: "cbu",
},
});
// const existingAllYearSummaries = await this.prisma.crimes.findFirst({
// where: {
// month: null,
// year: null,
// source_type: "cbu",
// },
// });
if (existingAllYearSummaries) {
console.log("All-year crime summaries already exist, skipping import.");
return;
}
// if (existingAllYearSummaries) {
// console.log("All-year crime summaries already exist, skipping import.");
// return;
// }
const csvFilePath = path.resolve(
__dirname,
"../data/excels/crimes/crime_summary_by_unit.csv",
);
const fileContent = fs.readFileSync(csvFilePath, { encoding: "utf-8" });
const records = parse(fileContent, {
@ -420,16 +421,16 @@ export class CrimesSeeder {
private async importMonthlyCrimeDataByType() {
console.log("Importing monthly crime data by type...");
const existingCrimeByType = await this.prisma.crimes.findFirst({
where: {
source_type: "cbt",
},
});
// const existingCrimeByType = await this.prisma.crimes.findFirst({
// where: {
// source_type: "cbt",
// },
// });
if (existingCrimeByType) {
console.log("Crime data by type already exists, skipping import.");
return;
}
// if (existingCrimeByType) {
// console.log("Crime data by type already exists, skipping import.");
// return;
// }
const csvFilePath = path.resolve(
__dirname,
@ -497,17 +498,17 @@ export class CrimesSeeder {
private async importYearlyCrimeDataByType() {
console.log("Importing yearly crime data by type...");
const existingYearlySummary = await this.prisma.crimes.findFirst({
where: {
month: null,
source_type: "cbt",
},
});
// const existingYearlySummary = await this.prisma.crimes.findFirst({
// where: {
// month: null,
// source_type: "cbt",
// },
// });
if (existingYearlySummary) {
console.log("Yearly crime data by type already exists, skipping import.");
return;
}
// if (existingYearlySummary) {
// console.log("Yearly crime data by type already exists, skipping import.");
// return;
// }
const csvFilePath = path.resolve(
__dirname,
@ -580,16 +581,16 @@ export class CrimesSeeder {
private async importSummaryByType() {
console.log("Importing crime summary by type...");
const existingSummary = await this.prisma.crimes.findFirst({
where: {
source_type: "cbt",
},
});
// const existingSummary = await this.prisma.crimes.findFirst({
// where: {
// source_type: "cbt",
// },
// });
if (existingSummary) {
console.log("Crime summary by type already exists, skipping import.");
return;
}
// if (existingSummary) {
// console.log("Crime summary by type already exists, skipping import.");
// return;
// }
const csvFilePath = path.resolve(
__dirname,

View File

@ -134,27 +134,47 @@ export class GeoJSONSeeder {
`Processing batch ${i + 1}/${batches.length} (${batch.length} records)`
);
const { error } = await this.supabase
.from('geographics')
.insert(batch)
.select();
try {
const { error } = await this.supabase
.from('geographics')
.insert(batch)
.select();
if (error) {
console.error(`Error inserting batch ${i + 1}:`, error);
// Optionally reduce batch size and retry for this specific batch
if (batch.length > 5) {
console.log(`Retrying batch ${i + 1} with smaller chunks...`);
await this.insertInBatches(batch); // Recursive retry with automatic smaller chunks
if (error) {
console.error(`Error inserting batch ${i + 1}:`, error);
// Reduce batch size and retry for this specific batch
if (batch.length > 5) {
console.log(`Retrying batch ${i + 1} with smaller chunks...`);
await this.insertInBatches(batch); // Recursive retry with smaller chunks
} else {
console.error(
`Failed to insert items even with small batch size:`,
batch
);
}
} else {
console.error(
`Failed to insert items even with small batch size:`,
batch
console.log(
`Successfully inserted batch ${i + 1} (${batch.length} records)`
);
}
} else {
console.log(
`Successfully inserted batch ${i + 1} (${batch.length} records)`
);
} catch (err) {
console.error(`Exception when processing batch ${i + 1}:`, err);
// Try with even smaller chunks
if (batch.length > 2) {
const smallerBatchSize = Math.max(1, Math.floor(batch.length / 2));
console.log(`Retrying with much smaller chunks of size ${smallerBatchSize}...`);
await this.insertInBatches(this.chunkArray(batch, smallerBatchSize));
} else {
// Single item insertion as last resort
for (const item of batch) {
try {
await this.supabase.from('geographics').insert(item);
console.log(`Inserted single item successfully`);
} catch (e) {
console.error(`Failed to insert single item:`, e);
}
}
}
}
// Add a small delay between batches to reduce database load

View File

@ -0,0 +1,285 @@
import { PrismaClient } from "@prisma/client";
import { faker } from "@faker-js/faker";
import { districtCenters } from "../data/jsons/district-center";
import { createClient } from "../../app/_utils/supabase/client";
import db from "../db";
export class IncidentLogSeeder {
constructor(
private prisma: PrismaClient,
private supabase = createClient(),
) {}
// Add run method to satisfy the Seeder interface
async run(): Promise<void> {
await db.incident_logs.deleteMany({});
await this.seed();
}
async seed() {
// Step 1: Create a mock user if needed
const user = await this.getOrCreateUser();
// Step 2: Create an event
const event = await this.createEvent(user.id);
// Step 3: Create a session
const session = await this.createSession(user.id, event.id);
// Step 4: Create locations
const locations = await this.createLocations(event.id);
// Step 5: Create incident logs for the past 24 hours
await this.createIncidentLogs(user.id, locations);
console.log("Incident logs seeded successfully");
}
private async getOrCreateUser() {
// Check if we have an existing user
const existingUser = await this.prisma.users.findFirst({
where: { email: "incident-reporter@sigap.com" },
});
if (existingUser) return existingUser;
// Get a valid role ID
const role = await this.prisma.roles.findFirst();
if (!role) {
throw new Error("No roles found. Please seed roles first.");
}
// Create a new user if none exists
return await this.prisma.users.create({
data: {
email: "incident-reporter@sigap.com",
roles_id: role.id,
is_anonymous: false,
},
});
}
private async createEvent(userId: string) {
return await this.prisma.events.create({
data: {
name: "Incident Monitoring Event",
description: "24-hour incident monitoring",
user_id: userId,
},
});
}
private async createSession(userId: string, eventId: string) {
return await this.prisma.sessions.create({
data: {
user_id: userId,
event_id: eventId,
status: "active",
},
});
}
private async createLocations(eventId: string) {
const districts = await this.prisma.districts.findMany({});
if (!districts.length) {
throw new Error("No districts found. Please seed districts first.");
}
const locations = [];
// Create 10 random locations across the districts
for (let i = 0; i < 10; i++) {
const district =
districts[Math.floor(Math.random() * districts.length)];
// Find matching district center by name
const districtCenter = districtCenters.find(
(center) =>
center.kecamatan.toLowerCase() ===
district.name.toLowerCase(),
);
// If we have matching center coordinates, use them as base point
// Otherwise generate random coordinates
let latitude, longitude;
if (districtCenter) {
// Generate random coordinates within 3-5km of district center
const radius = this.getRandomInt(3000, 5000); // 3-5 km in meters
const randomPoint = this.getRandomPointInRadius(
districtCenter.lat,
districtCenter.lng,
radius,
);
latitude = randomPoint.latitude;
longitude = randomPoint.longitude;
}
const locationType = [
"residential",
"commercial",
"public",
"transportation",
][Math.floor(Math.random() * 4)];
const address = faker.location.streetAddress();
// Insert using Supabase with PostGIS
const { data, error } = await this.supabase
.from("locations")
.insert({
district_id: district.id,
event_id: eventId,
address: address,
type: locationType,
latitude: latitude,
longitude: longitude,
location: `POINT(${longitude} ${latitude})`, // PostGIS format
})
.select();
if (error) {
console.error("Error inserting location:", error);
continue;
}
if (data && data.length > 0) {
locations.push(data[0]);
}
}
return locations;
}
private async createIncidentLogs(userId: string, locations: any[]) {
// Get crime categories
const categories = await this.prisma.crime_categories.findMany({});
if (!categories.length) {
throw new Error(
"No crime categories found. Please seed crime categories first.",
);
}
const now = new Date();
// Create 24 incidents data array
const incidentData = [];
for (let i = 0; i < 24; i++) {
const hourOffset = this.getRandomInt(0, 24); // Random hour within last 24 hours
const timestamp = new Date(
now.getTime() - hourOffset * 60 * 60 * 1000,
);
const location =
locations[Math.floor(Math.random() * locations.length)];
const category =
categories[Math.floor(Math.random() * categories.length)];
incidentData.push({
user_id: userId,
location_id: location.id,
category_id: category.id,
description: this.getRandomIncidentDescription(),
time: timestamp,
source: Math.random() > 0.3 ? "resident" : "reporter",
verified: Math.random() > 0.5,
});
}
// Bulk insert all incidents at once
const createdIncidents = await this.prisma.incident_logs.createMany({
data: incidentData,
});
console.log(`Created ${createdIncidents.count} incident logs in bulk`);
// If you need the actual created records, query them after creation
const incidents = await this.prisma.incident_logs.findMany({
where: {
user_id: userId,
time: {
gte: new Date(now.getTime() - 24 * 60 * 60 * 1000),
},
},
orderBy: {
time: "asc",
},
});
return incidents;
}
// Helper methods
private getRandomInt(min: number, max: number): number {
return Math.floor(Math.random() * (max - min + 1)) + min;
}
private getRandomIncidentDescription(): string {
const descriptions = [
"Suspicious person loitering in the area",
"Vehicle break-in reported",
"Shoplifting incident at local store",
"Noise complaint from neighbors",
"Traffic accident with minor injuries",
"Vandalism to public property",
"Domestic dispute reported",
"Trespassing on private property",
"Armed robbery at convenience store",
"Drug-related activity observed",
"Assault reported outside nightclub",
"Missing person report filed",
"Public intoxication incident",
"Package theft from doorstep",
"Illegal dumping observed",
];
return descriptions[Math.floor(Math.random() * descriptions.length)];
}
/**
* Generates a random point within a specified radius from a center point
* @param centerLat Center latitude
* @param centerLng Center longitude
* @param radiusInMeters Radius in meters
* @returns Object containing latitude and longitude
*/
private getRandomPointInRadius(
centerLat: number,
centerLng: number,
radiusInMeters: number,
): { latitude: number; longitude: number } {
// Earth's radius in meters
const earthRadius = 6378137;
// Convert radius from meters to degrees
const radiusInDegrees = radiusInMeters / earthRadius * (180 / Math.PI);
// Generate random angle in radians
const randomAngle = Math.random() * Math.PI * 2;
// Generate random radius within the specified radius
const randomRadius = Math.sqrt(Math.random()) * radiusInDegrees;
// Calculate offset
const latOffset = randomRadius * Math.sin(randomAngle);
const lngOffset = randomRadius * Math.cos(randomAngle);
// Adjust for earth's curvature for longitude
const lngOffsetAdjusted = lngOffset /
Math.cos(centerLat * Math.PI / 180);
// Calculate final coordinates
const randomLat = centerLat + latOffset;
const randomLng = centerLng + lngOffsetAdjusted;
return {
latitude: randomLat,
longitude: randomLng,
};
}
}

View File

@ -78,14 +78,24 @@ export class PermissionSeeder {
role_id: roleId,
}));
// Create all permissions in a single batch operation
const result = await this.prisma.permissions.createMany({
data: permissionsData,
skipDuplicates: true, // Skip if the permission already exists
});
// Create permissions in smaller batches to avoid potential issues
const batchSize = 50;
for (let i = 0; i < permissionsData.length; i += batchSize) {
const batch = permissionsData.slice(i, i + batchSize);
// Create batch of permissions
const result = await this.prisma.permissions.createMany({
data: batch,
skipDuplicates: true, // Skip if the permission already exists
});
console.log(
`Created ${result.count} permissions for role ${roleId} on resource ${resourceId}`
);
}
console.log(
`Created ${result.count} permissions for role ${roleId} on resource ${resourceId}: ${actions.join(', ')}`
`Completed creating permissions for role ${roleId} on resource ${resourceId}: ${actions.join(', ')}`
);
} catch (error) {
console.error(

View File

@ -187,19 +187,28 @@ export class UnitSeeder {
);
});
// Insert all units in a single batch operation
// Insert units in smaller batches
if (unitsToInsert.length > 0) {
const { error } = await this.supabase
.from('units')
.insert(unitsToInsert)
.select();
const batchSize = 10;
for (let i = 0; i < unitsToInsert.length; i += batchSize) {
const batch = unitsToInsert.slice(i, i + batchSize);
try {
const { error } = await this.supabase
.from('units')
.insert(batch)
.select();
if (error) {
console.error(`Error batch inserting units into Supabase:`, error);
} else {
console.log(
`Successfully inserted ${unitsToInsert.length} units in batch`
);
if (error) {
console.error(`Error inserting units batch ${i / batchSize + 1}:`, error);
} else {
console.log(`Successfully inserted batch ${i / batchSize + 1} (${batch.length} units)`);
}
// Small delay between batches
await new Promise(resolve => setTimeout(resolve, 300));
} catch (err) {
console.error(`Exception when inserting units batch ${i / batchSize + 1}:`, err);
}
}
} else {
console.warn('No unit data to insert');

View File

@ -0,0 +1,23 @@
import { SeedPostgres } from "@snaplet/seed/adapter-postgres";
import { defineConfig } from "@snaplet/seed/config";
import postgres from "postgres";
export default defineConfig({
adapter: () => {
const client = postgres(
"postgresql://postgres:postgres@127.0.0.1:54322/postgres",
);
return new SeedPostgres(client);
},
select: [
// We don't alter any extensions tables that might be owned by extensions
"!*",
"!*_prisma_migrations",
// We want to alter all the tables under public schema
"public*",
// We also want to alter some of the tables under the auth schema
"auth.users",
"auth.identities",
"auth.sessions",
],
});

25
sigap-website/seed.ts Normal file
View File

@ -0,0 +1,25 @@
/**
* ! Executing this script will delete all data in your database and seed it with 10 roles.
* ! Make sure to adjust the script to your needs.
* Use any TypeScript runner to run this script, for example: `npx tsx seed.ts`
* Learn more about the Seed Client by following our guide: https://docs.snaplet.dev/seed/getting-started
*/
import { createSeedClient } from "@snaplet/seed";
const main = async () => {
const seed = await createSeedClient({ dryRun: true });
// Truncate all tables in the database
await seed.$resetDatabase();
// Seed the database with 10 roles
await seed.roles((x) => x(10));
// Type completion not working? You might want to reload your TypeScript Server to pick up the changes
console.log("Database seeded successfully!");
process.exit();
};
main();

View File

@ -52,10 +52,10 @@ schema_paths = []
[db.seed]
# If enabled, seeds the database after migrations during a db reset.
enabled = true
enabled = false
# Specifies an ordered list of seed files to load during db reset.
# Supports glob patterns relative to supabase directory: "./seeds/*.sql"
sql_paths = ["./seed.sql"]
sql_paths = ['./seeds/*.sql']
[realtime]
enabled = true

View File

@ -7,7 +7,7 @@ BEGIN
SELECT ST_Distance(
NEW.location::geography,
u.location::geography
) / 1000 -- Convert to kilometers
)::gis.geography / 1000 -- Convert to kilometers
INTO NEW.distance_to_unit
FROM units u
WHERE u.district_id = NEW.district_id;

View File

@ -0,0 +1 @@
INSERT INTO "public"."cities" ("id", "name", "created_at", "updated_at") VALUES ('3509', 'Jember', '2025-05-13 23:25:08.149+00', '2025-05-13 23:25:08.149+00');

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1 @@
INSERT INTO "public"."districts" ("id", "city_id", "name", "created_at", "updated_at") VALUES ('350901', '3509', 'Jombang', '2025-05-13 23:25:08.222+00', '2025-05-13 23:25:08.222+00'), ('350902', '3509', 'Kencong', '2025-05-13 23:25:08.222+00', '2025-05-13 23:25:08.222+00'), ('350903', '3509', 'Sumberbaru', '2025-05-13 23:25:08.222+00', '2025-05-13 23:25:08.222+00'), ('350904', '3509', 'Gumukmas', '2025-05-13 23:25:08.222+00', '2025-05-13 23:25:08.222+00'), ('350905', '3509', 'Umbulsari', '2025-05-13 23:25:08.222+00', '2025-05-13 23:25:08.222+00'), ('350906', '3509', 'Tanggul', '2025-05-13 23:25:08.222+00', '2025-05-13 23:25:08.222+00'), ('350907', '3509', 'Semboro', '2025-05-13 23:25:08.222+00', '2025-05-13 23:25:08.222+00'), ('350908', '3509', 'Puger', '2025-05-13 23:25:08.222+00', '2025-05-13 23:25:08.222+00'), ('350909', '3509', 'Bangsalsari', '2025-05-13 23:25:08.222+00', '2025-05-13 23:25:08.222+00'), ('350910', '3509', 'Balung', '2025-05-13 23:25:08.222+00', '2025-05-13 23:25:08.222+00'), ('350911', '3509', 'Wuluhan', '2025-05-13 23:25:08.222+00', '2025-05-13 23:25:08.222+00'), ('350912', '3509', 'Ambulu', '2025-05-13 23:25:08.222+00', '2025-05-13 23:25:08.222+00'), ('350913', '3509', 'Rambipuji', '2025-05-13 23:25:08.222+00', '2025-05-13 23:25:08.222+00'), ('350914', '3509', 'Panti', '2025-05-13 23:25:08.222+00', '2025-05-13 23:25:08.222+00'), ('350915', '3509', 'Sukorambi', '2025-05-13 23:25:08.222+00', '2025-05-13 23:25:08.222+00'), ('350916', '3509', 'Jenggawah', '2025-05-13 23:25:08.222+00', '2025-05-13 23:25:08.222+00'), ('350917', '3509', 'Ajung', '2025-05-13 23:25:08.222+00', '2025-05-13 23:25:08.222+00'), ('350918', '3509', 'Tempurejo', '2025-05-13 23:25:08.222+00', '2025-05-13 23:25:08.222+00'), ('350919', '3509', 'Kaliwates', '2025-05-13 23:25:08.222+00', '2025-05-13 23:25:08.222+00'), ('350920', '3509', 'Patrang', '2025-05-13 23:25:08.222+00', '2025-05-13 23:25:08.222+00'), ('350921', '3509', 'Sumbersari', '2025-05-13 23:25:08.222+00', '2025-05-13 23:25:08.222+00'), ('350922', '3509', 'Arjasa', '2025-05-13 23:25:08.222+00', '2025-05-13 23:25:08.222+00'), ('350923', '3509', 'Mumbulsari', '2025-05-13 23:25:08.222+00', '2025-05-13 23:25:08.222+00'), ('350924', '3509', 'Pakusari', '2025-05-13 23:25:08.222+00', '2025-05-13 23:25:08.222+00'), ('350925', '3509', 'Jelbuk', '2025-05-13 23:25:08.222+00', '2025-05-13 23:25:08.222+00'), ('350926', '3509', 'Mayang', '2025-05-13 23:25:08.222+00', '2025-05-13 23:25:08.222+00'), ('350927', '3509', 'Kalisat', '2025-05-13 23:25:08.222+00', '2025-05-13 23:25:08.222+00'), ('350928', '3509', 'Ledokombo', '2025-05-13 23:25:08.222+00', '2025-05-13 23:25:08.222+00'), ('350929', '3509', 'Sukowono', '2025-05-13 23:25:08.222+00', '2025-05-13 23:25:08.222+00'), ('350930', '3509', 'Silo', '2025-05-13 23:25:08.222+00', '2025-05-13 23:25:08.222+00'), ('350931', '3509', 'Sumberjambe', '2025-05-13 23:25:08.222+00', '2025-05-13 23:25:08.222+00');

View File

@ -0,0 +1 @@
INSERT INTO "public"."events" ("id", "name", "description", "code", "created_at", "user_id") VALUES ('51a3afba-6356-4320-8151-ea9b7d286bba', 'Crime Inserting Event For Crime 2020 - 2024', 'Event for inserting crimes in region Jember', 'wxZBS0VIgj', '2025-05-13 23:25:17.965+00', '37cd4c49-28ad-4049-83c0-a0794d67eb18'), ('a2369820-865b-4b58-bdbc-f7f8fcb1a110', 'Incident Monitoring Event', '24-hour incident monitoring', 'rl2DE5Pmt3', '2025-05-13 23:26:06.175+00', 'aaa7cf25-3064-4abe-b09e-b234d94277f0');

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1 @@
INSERT INTO "public"."profiles" ("id", "user_id", "avatar", "username", "first_name", "last_name", "bio", "address", "birth_date") VALUES ('0b9f8a85-030f-45f3-ae96-4840de13f4fc', '37cd4c49-28ad-4049-83c0-a0794d67eb18', null, 'adminsigap', 'Admin', 'Sigap', null, null, null);

View File

@ -0,0 +1 @@
INSERT INTO "public"."resources" ("id", "name", "type", "description", "instance_role", "relations", "attributes", "created_at", "updated_at") VALUES ('128ab8df-72db-419b-893e-535a12e27932', 'resources', null, 'Resource management', null, null, '{"fields": ["id", "name", "description", "instance_role", "relations", "attributes", "created_at", "updated_at"]}', '2025-05-13 23:25:07.518+00', '2025-05-13 23:25:07.518+00'), ('1e33b6d1-f86c-4fc9-8fe0-10fe015d23a0', 'crime_incidents', null, 'Crime case management', null, null, '{"fields": ["id", "crime_id", "crime_category_id", "date", "time", "location", "latitude", "longitude", "description", "victim_count", "status", "created_at", "updated_at"]}', '2025-05-13 23:25:07.518+00', '2025-05-13 23:25:07.518+00'), ('29ed609f-b7f4-4745-bf87-4ef668bbee3c', 'contact_messages', null, 'Contact message management', null, null, '{"fields": ["id", "name", "email", "phone", "message_type", "message_type_label", "message", "status", "created_at", "updated_at"]}', '2025-05-13 23:25:07.518+00', '2025-05-13 23:25:07.518+00'), ('32fa563c-c403-44b0-bd2e-6aab78249474', 'demographics', null, 'Demographic data management', null, null, '{"fields": ["id", "district_id", "city_id", "province_id", "year", "population", "population_density", "poverty_rate", "created_at", "updated_at"]}', '2025-05-13 23:25:07.518+00', '2025-05-13 23:25:07.518+00'), ('51e18f26-9a57-483a-bbf8-34c924d01d5a', 'districts', null, 'District data management', null, null, '{"fields": ["id", "city_id", "name", "code", "created_at", "updated_at"]}', '2025-05-13 23:25:07.518+00', '2025-05-13 23:25:07.518+00'), ('596e4b5b-7b32-41ed-824a-1f06ab445309', 'cities', null, 'City data management', null, null, '{"fields": ["id", "name", "code", "geographic_id", "created_at", "updated_at"]}', '2025-05-13 23:25:07.518+00', '2025-05-13 23:25:07.518+00'), ('5a72fb52-5738-4c97-93a1-abc827712a88', 'geographics', null, 'Geographic data management', null, null, '{"fields": ["id", "district_id", "latitude", "longitude", "land_area", "polygon", "created_at", "updated_at"]}', '2025-05-13 23:25:07.518+00', '2025-05-13 23:25:07.518+00'), ('7545614d-3665-404b-9e80-3a486d2fd940', 'crime_categories', null, 'Crime category management', null, null, '{"fields": ["id", "name", "description", "created_at", "updated_at"]}', '2025-05-13 23:25:07.518+00', '2025-05-13 23:25:07.518+00'), ('76878825-1ddd-4f35-8208-2a440791e918', 'crimes', null, 'Crime data management', null, null, '{"fields": ["id", "district_id", "city_id", "year", "number_of_crime", "rate", "heat_map", "created_at", "updated_at"]}', '2025-05-13 23:25:07.518+00', '2025-05-13 23:25:07.518+00'), ('7740ccf7-ee6f-4a11-8307-5db90f75b8d6', 'users', null, 'User account management', null, null, '{"fields": ["id", "roles_id", "email", "phone", "encrypted_password", "invited_at", "confirmed_at", "email_confirmed_at", "recovery_sent_at", "last_sign_in_at", "app_metadata", "user_metadata", "created_at", "updated_at", "banned_until", "is_anonymous"]}', '2025-05-13 23:25:07.518+00', '2025-05-13 23:25:07.518+00'), ('84c822f9-bd07-49fc-904f-1a75ff60cb55', 'roles', null, 'Role management', null, null, '{"fields": ["id", "name", "description", "created_at", "updated_at"]}', '2025-05-13 23:25:07.518+00', '2025-05-13 23:25:07.518+00'), ('ea9e5db8-4c5e-4a25-83ec-a2763b1a9850', 'permissions', null, 'Permission management', null, null, '{"fields": ["id", "action", "resource_id", "role_id", "created_at", "updated_at"]}', '2025-05-13 23:25:07.518+00', '2025-05-13 23:25:07.518+00'), ('feda98a0-cb77-40c5-82ca-f869da158517', 'profiles', null, 'User profile management', null, null, '{"fields": ["id", "user_id", "avatar", "username", "first_name", "last_name", "bio", "address", "birth_date"]}', '2025-05-13 23:25:07.518+00', '2025-05-13 23:25:07.518+00');

View File

@ -0,0 +1 @@
INSERT INTO "public"."roles" ("id", "name", "description", "created_at", "updated_at") VALUES ('918614c2-d121-4190-af50-b913596ba996', 'viewer', 'Read-only access to the data.', '2025-05-13 23:25:07.502+00', '2025-05-13 23:25:07.502+00'), ('c10c952a-74e5-4a23-83ac-9f401b98fa88', 'staff', 'Staff with limited administrative access.', '2025-05-13 23:25:07.502+00', '2025-05-13 23:25:07.502+00'), ('eba77d11-5281-42dc-963c-efddd785752b', 'admin', 'Administrator with full access to all features.', '2025-05-13 23:25:07.502+00', '2025-05-13 23:25:07.502+00');

View File

@ -0,0 +1 @@
INSERT INTO "public"."sessions" ("id", "user_id", "event_id", "status", "created_at") VALUES ('4d2d5249-787c-435d-95b2-7efb6e6c13ee', 'aaa7cf25-3064-4abe-b09e-b234d94277f0', 'a2369820-865b-4b58-bdbc-f7f8fcb1a110', 'active', '2025-05-13 23:26:06.178+00'), ('88ada883-dfee-4d22-80de-47f99e270ce9', '37cd4c49-28ad-4049-83c0-a0794d67eb18', '51a3afba-6356-4320-8151-ea9b7d286bba', 'completed', '2025-05-13 23:25:17.973+00');

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1 @@
INSERT INTO "public"."users" ("id", "roles_id", "email", "phone", "encrypted_password", "invited_at", "confirmed_at", "email_confirmed_at", "recovery_sent_at", "last_sign_in_at", "app_metadata", "user_metadata", "created_at", "updated_at", "banned_until", "is_anonymous") VALUES ('37cd4c49-28ad-4049-83c0-a0794d67eb18', 'eba77d11-5281-42dc-963c-efddd785752b', 'sigapcompany@gmail.com', null, null, null, '2025-05-13 23:25:17.942+00', '2025-05-13 23:25:17.942+00', null, null, '{}', '{}', '2025-05-13 23:25:17.942+00', '2025-05-13 23:25:17.942+00', null, 'false'), ('aaa7cf25-3064-4abe-b09e-b234d94277f0', 'eba77d11-5281-42dc-963c-efddd785752b', 'incident-reporter@sigap.com', null, null, null, null, null, null, null, null, null, '2025-05-13 23:26:06.17+00', '2025-05-13 23:26:06.17+00', null, 'false');