add seeds supaabse
This commit is contained in:
parent
2a8f249d0c
commit
db8e2a6321
|
@ -42,3 +42,6 @@ next-env.d.ts
|
||||||
|
|
||||||
# Sentry Config File
|
# Sentry Config File
|
||||||
.env.sentry-build-plugin
|
.env.sentry-build-plugin
|
||||||
|
|
||||||
|
# Snaplet
|
||||||
|
/.snaplet/
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"[typescript]": {
|
"[typescript]": {
|
||||||
"editor.defaultFormatter": "denoland.vscode-deno"
|
"editor.defaultFormatter": "vscode.typescript-language-features"
|
||||||
},
|
},
|
||||||
"deno.enablePaths": [
|
"deno.enablePaths": [
|
||||||
"supabase/functions"
|
"supabase/functions"
|
||||||
|
|
|
@ -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
|
|
@ -141,6 +141,7 @@ export async function getCrimes(): Promise<ICrimes[]> {
|
||||||
address: true,
|
address: true,
|
||||||
latitude: true,
|
latitude: true,
|
||||||
longitude: true,
|
longitude: true,
|
||||||
|
distance_to_unit: true
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
|
@ -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) {
|
} catch (err) {
|
||||||
if (err instanceof InputParseError) {
|
if (err instanceof InputParseError) {
|
||||||
// return {
|
// return {
|
||||||
|
|
|
@ -16,7 +16,7 @@ import SourceTypeSelector from "../source-type-selector"
|
||||||
// Define the additional tools and features
|
// Define the additional tools and features
|
||||||
const additionalTooltips = [
|
const additionalTooltips = [
|
||||||
{ id: "reports" as ITooltips, icon: <IconMessage size={20} />, label: "Police Report" },
|
{ 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 {
|
interface AdditionalTooltipsProps {
|
||||||
|
|
|
@ -2,16 +2,16 @@
|
||||||
|
|
||||||
import { Button } from "@/app/_components/ui/button"
|
import { Button } from "@/app/_components/ui/button"
|
||||||
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/app/_components/ui/tooltip"
|
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 type { ITooltips } from "./tooltips"
|
||||||
import { IconChartBubble, IconClock } from "@tabler/icons-react"
|
import { IconChartBubble, IconClock } from "@tabler/icons-react"
|
||||||
|
|
||||||
// Define the primary crime data controls
|
// Define the primary crime data controls
|
||||||
const crimeTooltips = [
|
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: "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: "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: "patrol" as ITooltips, icon: <Car size={20} />, label: "Patrol Areas" },
|
||||||
{ id: "timeline" as ITooltips, icon: <IconClock size={20} />, label: "Time Analysis" },
|
{ id: "timeline" as ITooltips, icon: <IconClock size={20} />, label: "Time Analysis" },
|
||||||
]
|
]
|
||||||
|
|
|
@ -11,12 +11,14 @@ import type { ReactNode } from "react"
|
||||||
export type ITooltips =
|
export type ITooltips =
|
||||||
// Crime data views
|
// Crime data views
|
||||||
| "incidents"
|
| "incidents"
|
||||||
|
| "historical"
|
||||||
| "heatmap"
|
| "heatmap"
|
||||||
| "units"
|
| "units"
|
||||||
| "patrol"
|
| "patrol"
|
||||||
| "reports"
|
| "reports"
|
||||||
| "clusters"
|
| "clusters"
|
||||||
| "timeline"
|
| "timeline"
|
||||||
|
| "recents"
|
||||||
|
|
||||||
// Tools and features
|
// Tools and features
|
||||||
| "refresh"
|
| "refresh"
|
||||||
|
|
|
@ -29,7 +29,7 @@ export default function CrimeMap() {
|
||||||
const [sidebarCollapsed, setSidebarCollapsed] = useState(true)
|
const [sidebarCollapsed, setSidebarCollapsed] = useState(true)
|
||||||
const [selectedDistrict, setSelectedDistrict] = useState<DistrictFeature | null>(null)
|
const [selectedDistrict, setSelectedDistrict] = useState<DistrictFeature | null>(null)
|
||||||
const [showLegend, setShowLegend] = useState<boolean>(true)
|
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 [selectedSourceType, setSelectedSourceType] = useState<string>("cbu")
|
||||||
const [selectedYear, setSelectedYear] = useState<number>(2024)
|
const [selectedYear, setSelectedYear] = useState<number>(2024)
|
||||||
const [selectedMonth, setSelectedMonth] = useState<number | "all">("all")
|
const [selectedMonth, setSelectedMonth] = useState<number | "all">("all")
|
||||||
|
@ -149,7 +149,7 @@ export default function CrimeMap() {
|
||||||
setShowClusters(true);
|
setShowClusters(true);
|
||||||
setShowUnclustered(false);
|
setShowUnclustered(false);
|
||||||
} else {
|
} else {
|
||||||
setActiveControl("incidents");
|
setActiveControl("clusters");
|
||||||
setShowUnclustered(true);
|
setShowUnclustered(true);
|
||||||
setShowClusters(false);
|
setShowClusters(false);
|
||||||
}
|
}
|
||||||
|
|
|
@ -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;
|
||||||
|
}
|
|
@ -29,7 +29,9 @@ import PanicButtonDemo from "../controls/panic-button-demo"
|
||||||
|
|
||||||
import { IIncidentLog } from "@/app/_utils/types/ews"
|
import { IIncidentLog } from "@/app/_utils/types/ews"
|
||||||
import { addMockIncident, getAllIncidents, resolveIncident } from "@/app/_utils/mock/ews-data"
|
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 for crime incident
|
||||||
interface ICrimeIncident {
|
interface ICrimeIncident {
|
||||||
|
@ -421,7 +423,9 @@ export default function Layers({
|
||||||
const showHeatmapLayer = activeControl === "heatmap" && sourceType !== "cbu"
|
const showHeatmapLayer = activeControl === "heatmap" && sourceType !== "cbu"
|
||||||
const showUnitsLayer = activeControl === "units"
|
const showUnitsLayer = activeControl === "units"
|
||||||
const showTimelineLayer = activeControl === "timeline"
|
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"
|
const showIncidentMarkers = activeControl !== "heatmap" && activeControl !== "timeline" && sourceType !== "cbu"
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -442,11 +446,11 @@ export default function Layers({
|
||||||
onDistrictClick={handleDistrictClick}
|
onDistrictClick={handleDistrictClick}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* Recent Crimes Layer for showing crime data from the last 24 hours */}
|
{/* Recent Incidents Layer (24 hours) */}
|
||||||
<RecentIncidentsLayer
|
<RecentIncidentsLayer
|
||||||
|
visible={showRecentIncidents}
|
||||||
map={mapboxMap}
|
map={mapboxMap}
|
||||||
incidents={recentIncidents}
|
incidents={recentIncidents}
|
||||||
visible={crimesVisible}
|
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<HeatmapLayer
|
<HeatmapLayer
|
||||||
|
@ -523,7 +527,7 @@ export default function Layers({
|
||||||
|
|
||||||
<FaultLinesLayer map={mapboxMap} />
|
<FaultLinesLayer map={mapboxMap} />
|
||||||
|
|
||||||
<CoastlineLayer map={mapboxMap} />
|
{/* <CoastlineLayer map={mapboxMap} /> */}
|
||||||
|
|
||||||
{showEWS && (
|
{showEWS && (
|
||||||
<EWSAlertLayer
|
<EWSAlertLayer
|
||||||
|
|
|
@ -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
|
|
||||||
}
|
|
|
@ -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;
|
||||||
|
}
|
|
@ -5,12 +5,10 @@ import { Layer, Source } from "react-map-gl/mapbox"
|
||||||
import type { ICrimes } from "@/app/_utils/types/crimes"
|
import type { ICrimes } from "@/app/_utils/types/crimes"
|
||||||
import type { IUnits } from "@/app/_utils/types/units"
|
import type { IUnits } from "@/app/_utils/types/units"
|
||||||
import type mapboxgl from "mapbox-gl"
|
import type mapboxgl from "mapbox-gl"
|
||||||
import { useQuery } from "@tanstack/react-query"
|
|
||||||
|
|
||||||
import { generateCategoryColorMap } from "@/app/_utils/colors"
|
import { generateCategoryColorMap } from "@/app/_utils/colors"
|
||||||
import UnitPopup from "../pop-up/unit-popup"
|
import UnitPopup from "../pop-up/unit-popup"
|
||||||
import IncidentPopup from "../pop-up/incident-popup"
|
import IncidentPopup from "../pop-up/incident-popup"
|
||||||
import { calculateDistances } from "@/app/(pages)/(admin)/dashboard/crime-management/crime-overview/action"
|
|
||||||
|
|
||||||
interface UnitsLayerProps {
|
interface UnitsLayerProps {
|
||||||
crimes: ICrimes[]
|
crimes: ICrimes[]
|
||||||
|
@ -20,23 +18,6 @@ interface UnitsLayerProps {
|
||||||
map?: mapboxgl.Map | null
|
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) {
|
export default function UnitsLayer({ crimes, units = [], filterCategory, visible = false, map }: UnitsLayerProps) {
|
||||||
const [loadedUnits, setLoadedUnits] = useState<IUnits[]>([])
|
const [loadedUnits, setLoadedUnits] = useState<IUnits[]>([])
|
||||||
const loadedUnitsRef = useRef<IUnits[]>([])
|
const loadedUnitsRef = useRef<IUnits[]>([])
|
||||||
|
@ -48,13 +29,6 @@ export default function UnitsLayer({ crimes, units = [], filterCategory, visible
|
||||||
const [isUnitSelected, setIsUnitSelected] = useState<boolean>(false)
|
const [isUnitSelected, setIsUnitSelected] = useState<boolean>(false)
|
||||||
const [selectedDistrictId, setSelectedDistrictId] = useState<string | undefined>()
|
const [selectedDistrictId, setSelectedDistrictId] = useState<string | undefined>()
|
||||||
|
|
||||||
// Use react-query for distance data
|
|
||||||
const { data: distances = [], isLoading: isLoadingDistances } = useDistanceData(
|
|
||||||
selectedEntityId,
|
|
||||||
isUnitSelected,
|
|
||||||
selectedDistrictId,
|
|
||||||
)
|
|
||||||
|
|
||||||
// Use either provided units or loaded units
|
// Use either provided units or loaded units
|
||||||
const unitsData = useMemo(() => {
|
const unitsData = useMemo(() => {
|
||||||
return units.length > 0 ? units : loadedUnits || []
|
return units.length > 0 ? units : loadedUnits || []
|
||||||
|
@ -66,10 +40,10 @@ export default function UnitsLayer({ crimes, units = [], filterCategory, visible
|
||||||
crimes.forEach((crime) => {
|
crimes.forEach((crime) => {
|
||||||
crime.crime_incidents.forEach((incident) => {
|
crime.crime_incidents.forEach((incident) => {
|
||||||
if (incident.crime_categories?.name) {
|
if (incident.crime_categories?.name) {
|
||||||
categories.add(incident.crime_categories.name)
|
categories.add(incident.crime_categories.name)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
return Array.from(categories)
|
return Array.from(categories)
|
||||||
}, [crimes])
|
}, [crimes])
|
||||||
|
|
||||||
|
@ -80,27 +54,52 @@ export default function UnitsLayer({ crimes, units = [], filterCategory, visible
|
||||||
|
|
||||||
// Process units data to GeoJSON format
|
// Process units data to GeoJSON format
|
||||||
const unitsGeoJSON = useMemo(() => {
|
const unitsGeoJSON = useMemo(() => {
|
||||||
|
console.log("Units data being processed:", unitsData); // Debug log
|
||||||
|
|
||||||
return {
|
return {
|
||||||
type: "FeatureCollection" as const,
|
type: "FeatureCollection" as const,
|
||||||
features: unitsData
|
features: unitsData
|
||||||
.map((unit) => ({
|
.map((unit) => {
|
||||||
type: "Feature" as const,
|
// Debug log for individual units
|
||||||
properties: {
|
console.log("Processing unit:", unit.code_unit, unit.name, {
|
||||||
id: unit.code_unit,
|
longitude: unit.longitude,
|
||||||
name: unit.name,
|
latitude: unit.latitude,
|
||||||
address: unit.address,
|
district: unit.district_name || unit.district_name
|
||||||
phone: unit.phone,
|
});
|
||||||
type: unit.type,
|
|
||||||
district: unit.districts?.name || "",
|
return {
|
||||||
district_id: unit.district_id,
|
type: "Feature" as const,
|
||||||
},
|
properties: {
|
||||||
geometry: {
|
id: unit.code_unit,
|
||||||
type: "Point" as const,
|
name: unit.name,
|
||||||
coordinates: [unit.longitude || 0, unit.latitude || 0],
|
address: unit.address,
|
||||||
},
|
phone: unit.phone,
|
||||||
}))
|
type: unit.type,
|
||||||
.filter((feature) => feature.geometry.coordinates[0] !== 0 && feature.geometry.coordinates[1] !== 0),
|
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])
|
}, [unitsData])
|
||||||
|
|
||||||
// Process incident data to GeoJSON format
|
// Process incident data to GeoJSON format
|
||||||
|
@ -114,27 +113,28 @@ export default function UnitsLayer({ crimes, units = [], filterCategory, visible
|
||||||
!incident.locations?.latitude ||
|
!incident.locations?.latitude ||
|
||||||
!incident.locations?.longitude ||
|
!incident.locations?.longitude ||
|
||||||
(filterCategory !== "all" && incident.crime_categories.name !== filterCategory)
|
(filterCategory !== "all" && incident.crime_categories.name !== filterCategory)
|
||||||
)
|
)
|
||||||
return
|
return
|
||||||
|
|
||||||
features.push({
|
features.push({
|
||||||
type: "Feature" as const,
|
type: "Feature" as const,
|
||||||
properties: {
|
properties: {
|
||||||
id: incident.id,
|
id: incident.id,
|
||||||
description: incident.description || "No description",
|
description: incident.description || "No description",
|
||||||
category: incident.crime_categories.name,
|
category: incident.crime_categories.name,
|
||||||
date: incident.timestamp,
|
date: incident.timestamp,
|
||||||
district: crime.districts?.name || "",
|
district: crime.districts?.name || "",
|
||||||
district_id: crime.district_id,
|
district_id: crime.district_id,
|
||||||
categoryColor: categoryColorMap[incident.crime_categories.name] || "#22c55e",
|
categoryColor: categoryColorMap[incident.crime_categories.name] || "#22c55e",
|
||||||
},
|
distance_to_unit: incident.locations.distance_to_unit || "Unknown",
|
||||||
geometry: {
|
},
|
||||||
type: "Point" as const,
|
geometry: {
|
||||||
coordinates: [incident.locations.longitude, incident.locations.latitude],
|
type: "Point" as const,
|
||||||
},
|
coordinates: [incident.locations.longitude, incident.locations.latitude],
|
||||||
})
|
},
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
})
|
||||||
|
|
||||||
return {
|
return {
|
||||||
type: "FeatureCollection" as const,
|
type: "FeatureCollection" as const,
|
||||||
|
@ -147,8 +147,8 @@ export default function UnitsLayer({ crimes, units = [], filterCategory, visible
|
||||||
if (!unitsData.length || !crimes.length)
|
if (!unitsData.length || !crimes.length)
|
||||||
return {
|
return {
|
||||||
type: "FeatureCollection" as const,
|
type: "FeatureCollection" as const,
|
||||||
features: [],
|
features: [],
|
||||||
}
|
}
|
||||||
|
|
||||||
// Map district IDs to their units
|
// Map district IDs to their units
|
||||||
const districtUnitsMap = new Map<string, IUnits[]>()
|
const districtUnitsMap = new Map<string, IUnits[]>()
|
||||||
|
@ -156,11 +156,11 @@ export default function UnitsLayer({ crimes, units = [], filterCategory, visible
|
||||||
unitsData.forEach((unit) => {
|
unitsData.forEach((unit) => {
|
||||||
if (!unit.district_id || !unit.longitude || !unit.latitude) return
|
if (!unit.district_id || !unit.longitude || !unit.latitude) return
|
||||||
|
|
||||||
if (!districtUnitsMap.has(unit.district_id)) {
|
if (!districtUnitsMap.has(unit.district_id)) {
|
||||||
districtUnitsMap.set(unit.district_id, [])
|
districtUnitsMap.set(unit.district_id, [])
|
||||||
}
|
}
|
||||||
districtUnitsMap.get(unit.district_id)!.push(unit)
|
districtUnitsMap.get(unit.district_id)!.push(unit)
|
||||||
})
|
})
|
||||||
|
|
||||||
// Create lines from units to incidents in their district
|
// Create lines from units to incidents in their district
|
||||||
const lineFeatures: any[] = []
|
const lineFeatures: any[] = []
|
||||||
|
@ -170,42 +170,42 @@ export default function UnitsLayer({ crimes, units = [], filterCategory, visible
|
||||||
const districtUnits = districtUnitsMap.get(crime.district_id) || []
|
const districtUnits = districtUnitsMap.get(crime.district_id) || []
|
||||||
if (!districtUnits.length) return
|
if (!districtUnits.length) return
|
||||||
|
|
||||||
// For each incident in this district
|
// For each incident in this district
|
||||||
crime.crime_incidents.forEach((incident) => {
|
crime.crime_incidents.forEach((incident) => {
|
||||||
// Skip incidents without location data or filtered by category
|
// Skip incidents without location data or filtered by category
|
||||||
if (
|
if (
|
||||||
!incident.locations?.latitude ||
|
!incident.locations?.latitude ||
|
||||||
!incident.locations?.longitude ||
|
!incident.locations?.longitude ||
|
||||||
(filterCategory !== "all" && incident.crime_categories.name !== filterCategory)
|
(filterCategory !== "all" && incident.crime_categories.name !== filterCategory)
|
||||||
)
|
)
|
||||||
return
|
return
|
||||||
|
|
||||||
// Create a line from each unit in this district to this incident
|
// Create a line from each unit in this district to this incident
|
||||||
districtUnits.forEach((unit) => {
|
districtUnits.forEach((unit) => {
|
||||||
if (!unit.longitude || !unit.latitude) return
|
if (!unit.longitude || !unit.latitude) return
|
||||||
|
|
||||||
lineFeatures.push({
|
lineFeatures.push({
|
||||||
type: "Feature" as const,
|
type: "Feature" as const,
|
||||||
properties: {
|
properties: {
|
||||||
unit_id: unit.code_unit,
|
unit_id: unit.code_unit,
|
||||||
unit_name: unit.name,
|
unit_name: unit.name,
|
||||||
incident_id: incident.id,
|
incident_id: incident.id,
|
||||||
district_id: crime.district_id,
|
district_id: crime.district_id,
|
||||||
district_name: crime.districts.name,
|
district_name: crime.districts.name,
|
||||||
category: incident.crime_categories.name,
|
category: incident.crime_categories.name,
|
||||||
lineColor: categoryColorMap[incident.crime_categories.name] || "#22c55e",
|
lineColor: categoryColorMap[incident.crime_categories.name] || "#22c55e",
|
||||||
},
|
},
|
||||||
geometry: {
|
geometry: {
|
||||||
type: "LineString" as const,
|
type: "LineString" as const,
|
||||||
coordinates: [
|
coordinates: [
|
||||||
[unit.longitude, unit.latitude],
|
[unit.longitude, unit.latitude],
|
||||||
[incident.locations.longitude, incident.locations.latitude],
|
[incident.locations.longitude, incident.locations.latitude],
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
})
|
||||||
|
})
|
||||||
})
|
})
|
||||||
})
|
|
||||||
})
|
})
|
||||||
})
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
type: "FeatureCollection" as const,
|
type: "FeatureCollection" as const,
|
||||||
|
@ -237,7 +237,10 @@ export default function UnitsLayer({ crimes, units = [], filterCategory, visible
|
||||||
|
|
||||||
// Find the unit in our data
|
// Find the unit in our data
|
||||||
const unit = unitsData.find((u) => u.code_unit === properties.id)
|
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
|
// Fly to the unit location
|
||||||
map.flyTo({
|
map.flyTo({
|
||||||
|
@ -258,7 +261,7 @@ export default function UnitsLayer({ crimes, units = [], filterCategory, visible
|
||||||
// Highlight the connected lines for this unit
|
// Highlight the connected lines for this unit
|
||||||
if (map.getLayer("units-connection-lines")) {
|
if (map.getLayer("units-connection-lines")) {
|
||||||
map.setFilter("units-connection-lines", ["==", ["get", "unit_id"], properties.id])
|
map.setFilter("units-connection-lines", ["==", ["get", "unit_id"], properties.id])
|
||||||
}
|
}
|
||||||
|
|
||||||
// Dispatch a custom event for other components to react to
|
// Dispatch a custom event for other components to react to
|
||||||
const customEvent = new CustomEvent("unit_click", {
|
const customEvent = new CustomEvent("unit_click", {
|
||||||
|
@ -320,9 +323,10 @@ export default function UnitsLayer({ crimes, units = [], filterCategory, visible
|
||||||
date: properties.date,
|
date: properties.date,
|
||||||
district: properties.district,
|
district: properties.district,
|
||||||
district_id: properties.district_id,
|
district_id: properties.district_id,
|
||||||
longitude,
|
distance_to_unit: properties.distance_to_unit,
|
||||||
latitude,
|
longitude,
|
||||||
}
|
latitude,
|
||||||
|
}
|
||||||
|
|
||||||
// Set the selected incident and query parameters
|
// Set the selected incident and query parameters
|
||||||
setSelectedIncident(incident)
|
setSelectedIncident(incident)
|
||||||
|
@ -394,50 +398,53 @@ export default function UnitsLayer({ crimes, units = [], filterCategory, visible
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!map || !visible) return
|
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
|
// Define event handlers that can be referenced for both adding and removing
|
||||||
const handleMouseEnter = () => {
|
const handleMouseEnter = () => {
|
||||||
map.getCanvas().style.cursor = "pointer"
|
map.getCanvas().style.cursor = "pointer"
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleMouseLeave = () => {
|
const handleMouseLeave = () => {
|
||||||
map.getCanvas().style.cursor = ""
|
map.getCanvas().style.cursor = ""
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add click event for units-points layer
|
// Add click event for units-points layer
|
||||||
if (map.getLayer("units-points")) {
|
if (map.getLayer("units-points")) {
|
||||||
map.off("click", "units-points", unitClickHandler)
|
map.off("click", "units-points", unitClickHandler)
|
||||||
map.on("click", "units-points", unitClickHandler)
|
map.on("click", "units-points", unitClickHandler)
|
||||||
|
|
||||||
// Change cursor on hover
|
// Change cursor on hover
|
||||||
map.on("mouseenter", "units-points", handleMouseEnter)
|
map.on("mouseenter", "units-points", handleMouseEnter)
|
||||||
map.on("mouseleave", "units-points", handleMouseLeave)
|
map.on("mouseleave", "units-points", handleMouseLeave)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add click event for incidents-points layer
|
// Add click event for incidents-points layer
|
||||||
if (map.getLayer("incidents-points")) {
|
if (map.getLayer("incidents-points")) {
|
||||||
map.off("click", "incidents-points", incidentClickHandler)
|
map.off("click", "incidents-points", incidentClickHandler)
|
||||||
map.on("click", "incidents-points", incidentClickHandler)
|
map.on("click", "incidents-points", incidentClickHandler)
|
||||||
|
|
||||||
// Change cursor on hover
|
// Change cursor on hover
|
||||||
map.on("mouseenter", "incidents-points", handleMouseEnter)
|
map.on("mouseenter", "incidents-points", handleMouseEnter)
|
||||||
map.on("mouseleave", "incidents-points", handleMouseLeave)
|
map.on("mouseleave", "incidents-points", handleMouseLeave)
|
||||||
}
|
}
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
if (map) {
|
if (map) {
|
||||||
if (map.getLayer("units-points")) {
|
if (map.getLayer("units-points")) {
|
||||||
map.off("click", "units-points", unitClickHandler)
|
map.off("click", "units-points", unitClickHandler)
|
||||||
map.off("mouseenter", "units-points", handleMouseEnter)
|
map.off("mouseenter", "units-points", handleMouseEnter)
|
||||||
map.off("mouseleave", "units-points", handleMouseLeave)
|
map.off("mouseleave", "units-points", handleMouseLeave)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (map.getLayer("incidents-points")) {
|
if (map.getLayer("incidents-points")) {
|
||||||
map.off("click", "incidents-points", incidentClickHandler)
|
map.off("click", "incidents-points", incidentClickHandler)
|
||||||
map.off("mouseenter", "incidents-points", handleMouseEnter)
|
map.off("mouseenter", "incidents-points", handleMouseEnter)
|
||||||
map.off("mouseleave", "incidents-points", handleMouseLeave)
|
map.off("mouseleave", "incidents-points", handleMouseLeave)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}, [map, visible, unitClickHandler, incidentClickHandler])
|
}, [map, visible, unitClickHandler, incidentClickHandler])
|
||||||
|
|
||||||
// Reset map filters when popup is closed
|
// Reset map filters when popup is closed
|
||||||
|
@ -504,8 +511,8 @@ export default function UnitsLayer({ crimes, units = [], filterCategory, visible
|
||||||
id="incidents-points"
|
id="incidents-points"
|
||||||
type="circle"
|
type="circle"
|
||||||
paint={{
|
paint={{
|
||||||
"circle-radius": 6,
|
"circle-radius": 6,
|
||||||
// Use the pre-computed color stored in the properties
|
// Use the pre-computed color stored in the properties
|
||||||
"circle-color": ["get", "categoryColor"],
|
"circle-color": ["get", "categoryColor"],
|
||||||
"circle-stroke-width": 1,
|
"circle-stroke-width": 1,
|
||||||
"circle-stroke-color": "#ffffff",
|
"circle-stroke-color": "#ffffff",
|
||||||
|
@ -520,7 +527,7 @@ export default function UnitsLayer({ crimes, units = [], filterCategory, visible
|
||||||
id="units-connection-lines"
|
id="units-connection-lines"
|
||||||
type="line"
|
type="line"
|
||||||
paint={{
|
paint={{
|
||||||
// Use the pre-computed color stored in the properties
|
// Use the pre-computed color stored in the properties
|
||||||
"line-color": ["get", "lineColor"],
|
"line-color": ["get", "lineColor"],
|
||||||
"line-width": 3,
|
"line-width": 3,
|
||||||
"line-opacity": 0.9,
|
"line-opacity": 0.9,
|
||||||
|
@ -542,11 +549,9 @@ export default function UnitsLayer({ crimes, units = [], filterCategory, visible
|
||||||
type: selectedUnit.type,
|
type: selectedUnit.type,
|
||||||
address: selectedUnit.address || "No address",
|
address: selectedUnit.address || "No address",
|
||||||
phone: selectedUnit.phone || "No phone",
|
phone: selectedUnit.phone || "No phone",
|
||||||
district: selectedUnit.districts?.name,
|
district: selectedUnit.district_name || "No district",
|
||||||
district_id: selectedUnit.district_id,
|
district_id: selectedUnit.district_id,
|
||||||
}}
|
}}
|
||||||
distances={distances}
|
|
||||||
isLoadingDistances={isLoadingDistances}
|
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
@ -557,8 +562,6 @@ export default function UnitsLayer({ crimes, units = [], filterCategory, visible
|
||||||
latitude={selectedIncident.latitude}
|
latitude={selectedIncident.latitude}
|
||||||
onClose={handleClosePopup}
|
onClose={handleClosePopup}
|
||||||
incident={selectedIncident}
|
incident={selectedIncident}
|
||||||
distances={distances}
|
|
||||||
isLoadingDistances={isLoadingDistances}
|
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
|
|
|
@ -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_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 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 = {
|
export const CRIME_RATE_COLORS = {
|
||||||
low: '#4ade80', // green
|
low: '#4ade80', // green
|
||||||
medium: '#facc15', // yellow
|
medium: '#facc15', // yellow
|
||||||
high: '#ef4444', // red
|
high: '#ef4444', // red
|
||||||
// critical: '#ef4444', // red
|
|
||||||
default: '#94a3b8', // gray
|
default: '#94a3b8', // gray
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -40,6 +40,7 @@ export interface ICrimes extends crimes {
|
||||||
address: string | null;
|
address: string | null;
|
||||||
longitude: number;
|
longitude: number;
|
||||||
latitude: number;
|
latitude: number;
|
||||||
|
distance_to_unit: number | null;
|
||||||
};
|
};
|
||||||
}[];
|
}[];
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,6 +2,7 @@ import { $Enums, units } from '@prisma/client';
|
||||||
|
|
||||||
export interface IUnits {
|
export interface IUnits {
|
||||||
district_id: string;
|
district_id: string;
|
||||||
|
district_name: string;
|
||||||
created_at: Date | null;
|
created_at: Date | null;
|
||||||
updated_at: Date | null;
|
updated_at: Date | null;
|
||||||
name: string;
|
name: string;
|
||||||
|
@ -13,9 +14,6 @@ export interface IUnits {
|
||||||
land_area: number | null;
|
land_area: number | null;
|
||||||
code_unit: string;
|
code_unit: string;
|
||||||
phone: string | null;
|
phone: string | null;
|
||||||
districts: {
|
|
||||||
name: string;
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// export interface IUnits {
|
// export interface IUnits {
|
||||||
|
|
Binary file not shown.
|
@ -6,6 +6,7 @@
|
||||||
"": {
|
"": {
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@evyweb/ioctopus": "^1.2.0",
|
"@evyweb/ioctopus": "^1.2.0",
|
||||||
|
"@faker-js/faker": "^9.7.0",
|
||||||
"@hookform/resolvers": "^4.1.2",
|
"@hookform/resolvers": "^4.1.2",
|
||||||
"@mapbox/mapbox-gl-geocoder": "^5.0.3",
|
"@mapbox/mapbox-gl-geocoder": "^5.0.3",
|
||||||
"@prisma/client": "^6.4.1",
|
"@prisma/client": "^6.4.1",
|
||||||
|
@ -78,6 +79,7 @@
|
||||||
"@types/three": "^0.176.0",
|
"@types/three": "^0.176.0",
|
||||||
"@types/uuid": "^10.0.0",
|
"@types/uuid": "^10.0.0",
|
||||||
"postcss": "8.4.49",
|
"postcss": "8.4.49",
|
||||||
|
"postgres": "^3.4.5",
|
||||||
"prisma": "^6.4.1",
|
"prisma": "^6.4.1",
|
||||||
"react-email": "3.0.7",
|
"react-email": "3.0.7",
|
||||||
"tailwind-merge": "^2.5.2",
|
"tailwind-merge": "^2.5.2",
|
||||||
|
@ -1081,6 +1083,22 @@
|
||||||
"integrity": "sha512-OIISYUx7WZDm6uxQkVsKmNF13tEiA3gbUeboTkr4LUTmJffhSVswiWAs8Ng5DoyvUlmgteTYcHP5XzOtrPTxLw==",
|
"integrity": "sha512-OIISYUx7WZDm6uxQkVsKmNF13tEiA3gbUeboTkr4LUTmJffhSVswiWAs8Ng5DoyvUlmgteTYcHP5XzOtrPTxLw==",
|
||||||
"license": "MIT"
|
"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": {
|
"node_modules/@floating-ui/core": {
|
||||||
"version": "1.6.9",
|
"version": "1.6.9",
|
||||||
"resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.6.9.tgz",
|
"resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.6.9.tgz",
|
||||||
|
@ -12350,6 +12368,20 @@
|
||||||
"integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==",
|
"integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==",
|
||||||
"license": "MIT"
|
"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": {
|
"node_modules/postgres-array": {
|
||||||
"version": "2.0.0",
|
"version": "2.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/postgres-array/-/postgres-array-2.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/postgres-array/-/postgres-array-2.0.0.tgz",
|
||||||
|
|
|
@ -12,6 +12,7 @@
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@evyweb/ioctopus": "^1.2.0",
|
"@evyweb/ioctopus": "^1.2.0",
|
||||||
|
"@faker-js/faker": "^9.7.0",
|
||||||
"@hookform/resolvers": "^4.1.2",
|
"@hookform/resolvers": "^4.1.2",
|
||||||
"@mapbox/mapbox-gl-geocoder": "^5.0.3",
|
"@mapbox/mapbox-gl-geocoder": "^5.0.3",
|
||||||
"@prisma/client": "^6.4.1",
|
"@prisma/client": "^6.4.1",
|
||||||
|
@ -75,6 +76,8 @@
|
||||||
"zustand": "^5.0.3"
|
"zustand": "^5.0.3"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@snaplet/copycat": "^6.0.0",
|
||||||
|
"@snaplet/seed": "0.98.0",
|
||||||
"@tanstack/eslint-plugin-query": "^5.67.2",
|
"@tanstack/eslint-plugin-query": "^5.67.2",
|
||||||
"@tanstack/react-query-devtools": "^5.67.2",
|
"@tanstack/react-query-devtools": "^5.67.2",
|
||||||
"@types/mapbox__mapbox-gl-geocoder": "^5.0.0",
|
"@types/mapbox__mapbox-gl-geocoder": "^5.0.0",
|
||||||
|
@ -84,6 +87,7 @@
|
||||||
"@types/three": "^0.176.0",
|
"@types/three": "^0.176.0",
|
||||||
"@types/uuid": "^10.0.0",
|
"@types/uuid": "^10.0.0",
|
||||||
"postcss": "8.4.49",
|
"postcss": "8.4.49",
|
||||||
|
"postgres": "^3.4.5",
|
||||||
"prisma": "^6.4.1",
|
"prisma": "^6.4.1",
|
||||||
"react-email": "3.0.7",
|
"react-email": "3.0.7",
|
||||||
"tailwind-merge": "^2.5.2",
|
"tailwind-merge": "^2.5.2",
|
||||||
|
|
|
@ -36,8 +36,8 @@ export const districtCenters = [
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
kecamatan: "Jenggawah",
|
kecamatan: "Jenggawah",
|
||||||
lat: -8.2409,
|
lat: -8.291111565708007,
|
||||||
lng: 113.6407,
|
lng: 113.6483542061137,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
kecamatan: "Jombang",
|
kecamatan: "Jombang",
|
||||||
|
@ -116,8 +116,8 @@ export const districtCenters = [
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
kecamatan: "Sukowono",
|
kecamatan: "Sukowono",
|
||||||
lat: -8.0547,
|
lat: -8.06492265726014,
|
||||||
lng: 113.8853,
|
lng: 113.83125008757588,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
kecamatan: "Sumberbaru",
|
kecamatan: "Sumberbaru",
|
||||||
|
|
|
@ -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;
|
|
@ -266,8 +266,7 @@ model incident_logs {
|
||||||
}
|
}
|
||||||
|
|
||||||
model units {
|
model units {
|
||||||
id String @id @default(dbgenerated("gen_random_uuid()")) @db.Uuid
|
code_unit String @id @unique @db.VarChar(20)
|
||||||
code_unit String @unique @db.VarChar(20)
|
|
||||||
district_id String? @unique @db.VarChar(20)
|
district_id String? @unique @db.VarChar(20)
|
||||||
city_id String @db.VarChar(20)
|
city_id String @db.VarChar(20)
|
||||||
name String @db.VarChar(100)
|
name String @db.VarChar(100)
|
||||||
|
@ -294,7 +293,7 @@ model units {
|
||||||
|
|
||||||
model unit_statistics {
|
model unit_statistics {
|
||||||
id String @id @default(dbgenerated("gen_random_uuid()")) @db.Uuid
|
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_total Int
|
||||||
crime_cleared Int
|
crime_cleared Int
|
||||||
percentage Float?
|
percentage Float?
|
||||||
|
@ -303,9 +302,9 @@ model unit_statistics {
|
||||||
year Int
|
year Int
|
||||||
created_at DateTime? @default(now()) @db.Timestamptz(6)
|
created_at DateTime? @default(now()) @db.Timestamptz(6)
|
||||||
updated_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")
|
@@index([year, month], map: "idx_unit_statistics_year_month")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -12,6 +12,7 @@ import { UnitSeeder } from './seeds/units';
|
||||||
import { CrimesSeeder } from './seeds/crimes';
|
import { CrimesSeeder } from './seeds/crimes';
|
||||||
import { CrimeIncidentsByUnitSeeder } from './seeds/crime-incidents';
|
import { CrimeIncidentsByUnitSeeder } from './seeds/crime-incidents';
|
||||||
import { CrimeIncidentsByTypeSeeder } from './seeds/crime-incidents-cbt';
|
import { CrimeIncidentsByTypeSeeder } from './seeds/crime-incidents-cbt';
|
||||||
|
import { IncidentLogSeeder } from './seeds/incident-logs';
|
||||||
|
|
||||||
const prisma = new PrismaClient();
|
const prisma = new PrismaClient();
|
||||||
|
|
||||||
|
@ -30,16 +31,17 @@ class DatabaseSeeder {
|
||||||
|
|
||||||
// Daftar semua seeders di sini
|
// Daftar semua seeders di sini
|
||||||
this.seeders = [
|
this.seeders = [
|
||||||
// new RoleSeeder(prisma),
|
new RoleSeeder(prisma),
|
||||||
// new ResourceSeeder(prisma),
|
new ResourceSeeder(prisma),
|
||||||
// new PermissionSeeder(prisma),
|
new PermissionSeeder(prisma),
|
||||||
// new CrimeCategoriesSeeder(prisma),
|
new CrimeCategoriesSeeder(prisma),
|
||||||
// new GeoJSONSeeder(prisma),
|
new GeoJSONSeeder(prisma),
|
||||||
// new UnitSeeder(prisma),
|
new UnitSeeder(prisma),
|
||||||
// new DemographicsSeeder(prisma),
|
new DemographicsSeeder(prisma),
|
||||||
new CrimesSeeder(prisma),
|
new CrimesSeeder(prisma),
|
||||||
// new CrimeIncidentsByUnitSeeder(prisma),
|
// new CrimeIncidentsByUnitSeeder(prisma),
|
||||||
new CrimeIncidentsByTypeSeeder(prisma),
|
new CrimeIncidentsByTypeSeeder(prisma),
|
||||||
|
new IncidentLogSeeder(prisma),
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -21,71 +21,88 @@ export class CrimeCategoriesSeeder {
|
||||||
async run(): Promise<void> {
|
async run(): Promise<void> {
|
||||||
console.log('Seeding crime categories...');
|
console.log('Seeding crime categories...');
|
||||||
|
|
||||||
// Hapus data yang ada untuk menghindari duplikasi
|
// Delete existing data to avoid duplicates
|
||||||
await this.prisma.crime_categories.deleteMany({});
|
await this.prisma.crime_categories.deleteMany({});
|
||||||
|
|
||||||
// Truncate table jika diperlukan
|
|
||||||
// await this.prisma.$executeRaw`TRUNCATE TABLE "crime_categories" CASCADE`;
|
|
||||||
|
|
||||||
const filePath = path.join(
|
const filePath = path.join(
|
||||||
__dirname,
|
__dirname,
|
||||||
'../data/excels/others/crime_categories.xlsx'
|
'../data/excels/others/crime_categories.xlsx'
|
||||||
);
|
);
|
||||||
const workbook = XLSX.readFile(filePath);
|
|
||||||
const sheet = workbook.Sheets[workbook.SheetNames[0]];
|
let categoriesToCreate = [];
|
||||||
const data = XLSX.utils.sheet_to_json(sheet) as ICrimeCategory[];
|
|
||||||
|
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
|
// Generate IDs and prepare data for batch insertion
|
||||||
const categoriesToCreate = [];
|
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
|
categoriesToCreate.push({
|
||||||
for (const category of crimeCategoriesData) {
|
id: newId.trim(),
|
||||||
const newId = await generateIdWithDbCounter('crime_categories', {
|
name: category.name,
|
||||||
prefix: 'CC',
|
description: category.description,
|
||||||
segments: {
|
});
|
||||||
sequentialDigits: 4,
|
}
|
||||||
},
|
|
||||||
format: '{prefix}-{sequence}',
|
|
||||||
separator: '-',
|
|
||||||
uniquenessStrategy: 'counter',
|
|
||||||
});
|
|
||||||
|
|
||||||
categoriesToCreate.push({
|
console.log(`Prepared ${categoriesToCreate.length} crime categories for creation`);
|
||||||
id: newId.trim(),
|
|
||||||
name: category.name,
|
// Create categories in smaller batches
|
||||||
description: category.description,
|
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`);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -769,4 +769,4 @@ if (require.main === module) {
|
||||||
};
|
};
|
||||||
|
|
||||||
testSeeder();
|
testSeeder();
|
||||||
}
|
}
|
|
@ -153,31 +153,105 @@ private generateDistributedPoints(
|
||||||
}
|
}
|
||||||
|
|
||||||
// Helper for chunked insertion
|
// 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) {
|
for (let i = 0; i < data.length; i += chunkSize) {
|
||||||
const chunk = data.slice(i, i + chunkSize);
|
const chunk = data.slice(i, i + chunkSize);
|
||||||
await this.prisma.crime_incidents.createMany({
|
try {
|
||||||
data: chunk,
|
await this.prisma.crime_incidents.createMany({
|
||||||
skipDuplicates: true,
|
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
|
// Helper for chunked Supabase insert
|
||||||
private async chunkedInsertLocations(
|
private async chunkedInsertLocations(
|
||||||
locations: any[],
|
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) {
|
for (let i = 0; i < locations.length; i += chunkSize) {
|
||||||
const chunk = locations.slice(i, i + chunkSize);
|
const chunk = locations.slice(i, i + chunkSize);
|
||||||
let { error } = await this.supabase
|
try {
|
||||||
.from('locations')
|
const { error } = await this.supabase
|
||||||
.insert(chunk)
|
.from('locations')
|
||||||
.select();
|
.insert(chunk)
|
||||||
if (error) {
|
.select();
|
||||||
throw error;
|
|
||||||
|
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> {
|
async run(): Promise<void> {
|
||||||
|
|
|
@ -40,8 +40,8 @@ export class CrimesSeeder {
|
||||||
// Create test user
|
// Create test user
|
||||||
const user = await this.createUsers();
|
const user = await this.createUsers();
|
||||||
|
|
||||||
await db.crime_incidents.deleteMany();
|
await db.crime_incidents.deleteMany({});
|
||||||
await db.crimes.deleteMany();
|
await db.crimes.deleteMany({});
|
||||||
|
|
||||||
if (!user) {
|
if (!user) {
|
||||||
throw new Error("Failed to create user");
|
throw new Error("Failed to create user");
|
||||||
|
@ -174,16 +174,16 @@ export class CrimesSeeder {
|
||||||
private async importMonthlyCrimeData() {
|
private async importMonthlyCrimeData() {
|
||||||
console.log("Importing monthly crime data...");
|
console.log("Importing monthly crime data...");
|
||||||
|
|
||||||
const existingCrimes = await this.prisma.crimes.findFirst({
|
// const existingCrimes = await this.prisma.crimes.findFirst({
|
||||||
where: {
|
// where: {
|
||||||
source_type: "cbu",
|
// source_type: "cbu",
|
||||||
},
|
// },
|
||||||
});
|
// });
|
||||||
|
|
||||||
if (existingCrimes) {
|
// if (existingCrimes) {
|
||||||
console.log("General crimes data already exists, skipping import.");
|
// console.log("General crimes data already exists, skipping import.");
|
||||||
return;
|
// return;
|
||||||
}
|
// }
|
||||||
|
|
||||||
const csvFilePath = path.resolve(
|
const csvFilePath = path.resolve(
|
||||||
__dirname,
|
__dirname,
|
||||||
|
@ -254,17 +254,17 @@ export class CrimesSeeder {
|
||||||
private async importYearlyCrimeData() {
|
private async importYearlyCrimeData() {
|
||||||
console.log("Importing yearly crime data...");
|
console.log("Importing yearly crime data...");
|
||||||
|
|
||||||
const existingYearlySummary = await this.prisma.crimes.findFirst({
|
// const existingYearlySummary = await this.prisma.crimes.findFirst({
|
||||||
where: {
|
// where: {
|
||||||
month: null,
|
// month: null,
|
||||||
source_type: "cbu",
|
// source_type: "cbu",
|
||||||
},
|
// },
|
||||||
});
|
// });
|
||||||
|
|
||||||
if (existingYearlySummary) {
|
// if (existingYearlySummary) {
|
||||||
console.log("Yearly crime data already exists, skipping import.");
|
// console.log("Yearly crime data already exists, skipping import.");
|
||||||
return;
|
// return;
|
||||||
}
|
// }
|
||||||
|
|
||||||
const csvFilePath = path.resolve(
|
const csvFilePath = path.resolve(
|
||||||
__dirname,
|
__dirname,
|
||||||
|
@ -337,23 +337,24 @@ export class CrimesSeeder {
|
||||||
private async importAllYearSummaries() {
|
private async importAllYearSummaries() {
|
||||||
console.log("Importing all-year (2020-2024) crime summaries...");
|
console.log("Importing all-year (2020-2024) crime summaries...");
|
||||||
|
|
||||||
const existingAllYearSummaries = await this.prisma.crimes.findFirst({
|
// const existingAllYearSummaries = await this.prisma.crimes.findFirst({
|
||||||
where: {
|
// where: {
|
||||||
month: null,
|
// month: null,
|
||||||
year: null,
|
// year: null,
|
||||||
source_type: "cbu",
|
// source_type: "cbu",
|
||||||
},
|
// },
|
||||||
});
|
// });
|
||||||
|
|
||||||
if (existingAllYearSummaries) {
|
// if (existingAllYearSummaries) {
|
||||||
console.log("All-year crime summaries already exist, skipping import.");
|
// console.log("All-year crime summaries already exist, skipping import.");
|
||||||
return;
|
// return;
|
||||||
}
|
// }
|
||||||
|
|
||||||
const csvFilePath = path.resolve(
|
const csvFilePath = path.resolve(
|
||||||
__dirname,
|
__dirname,
|
||||||
"../data/excels/crimes/crime_summary_by_unit.csv",
|
"../data/excels/crimes/crime_summary_by_unit.csv",
|
||||||
);
|
);
|
||||||
|
|
||||||
const fileContent = fs.readFileSync(csvFilePath, { encoding: "utf-8" });
|
const fileContent = fs.readFileSync(csvFilePath, { encoding: "utf-8" });
|
||||||
|
|
||||||
const records = parse(fileContent, {
|
const records = parse(fileContent, {
|
||||||
|
@ -420,16 +421,16 @@ export class CrimesSeeder {
|
||||||
private async importMonthlyCrimeDataByType() {
|
private async importMonthlyCrimeDataByType() {
|
||||||
console.log("Importing monthly crime data by type...");
|
console.log("Importing monthly crime data by type...");
|
||||||
|
|
||||||
const existingCrimeByType = await this.prisma.crimes.findFirst({
|
// const existingCrimeByType = await this.prisma.crimes.findFirst({
|
||||||
where: {
|
// where: {
|
||||||
source_type: "cbt",
|
// source_type: "cbt",
|
||||||
},
|
// },
|
||||||
});
|
// });
|
||||||
|
|
||||||
if (existingCrimeByType) {
|
// if (existingCrimeByType) {
|
||||||
console.log("Crime data by type already exists, skipping import.");
|
// console.log("Crime data by type already exists, skipping import.");
|
||||||
return;
|
// return;
|
||||||
}
|
// }
|
||||||
|
|
||||||
const csvFilePath = path.resolve(
|
const csvFilePath = path.resolve(
|
||||||
__dirname,
|
__dirname,
|
||||||
|
@ -497,17 +498,17 @@ export class CrimesSeeder {
|
||||||
private async importYearlyCrimeDataByType() {
|
private async importYearlyCrimeDataByType() {
|
||||||
console.log("Importing yearly crime data by type...");
|
console.log("Importing yearly crime data by type...");
|
||||||
|
|
||||||
const existingYearlySummary = await this.prisma.crimes.findFirst({
|
// const existingYearlySummary = await this.prisma.crimes.findFirst({
|
||||||
where: {
|
// where: {
|
||||||
month: null,
|
// month: null,
|
||||||
source_type: "cbt",
|
// source_type: "cbt",
|
||||||
},
|
// },
|
||||||
});
|
// });
|
||||||
|
|
||||||
if (existingYearlySummary) {
|
// if (existingYearlySummary) {
|
||||||
console.log("Yearly crime data by type already exists, skipping import.");
|
// console.log("Yearly crime data by type already exists, skipping import.");
|
||||||
return;
|
// return;
|
||||||
}
|
// }
|
||||||
|
|
||||||
const csvFilePath = path.resolve(
|
const csvFilePath = path.resolve(
|
||||||
__dirname,
|
__dirname,
|
||||||
|
@ -580,16 +581,16 @@ export class CrimesSeeder {
|
||||||
private async importSummaryByType() {
|
private async importSummaryByType() {
|
||||||
console.log("Importing crime summary by type...");
|
console.log("Importing crime summary by type...");
|
||||||
|
|
||||||
const existingSummary = await this.prisma.crimes.findFirst({
|
// const existingSummary = await this.prisma.crimes.findFirst({
|
||||||
where: {
|
// where: {
|
||||||
source_type: "cbt",
|
// source_type: "cbt",
|
||||||
},
|
// },
|
||||||
});
|
// });
|
||||||
|
|
||||||
if (existingSummary) {
|
// if (existingSummary) {
|
||||||
console.log("Crime summary by type already exists, skipping import.");
|
// console.log("Crime summary by type already exists, skipping import.");
|
||||||
return;
|
// return;
|
||||||
}
|
// }
|
||||||
|
|
||||||
const csvFilePath = path.resolve(
|
const csvFilePath = path.resolve(
|
||||||
__dirname,
|
__dirname,
|
||||||
|
|
|
@ -134,27 +134,47 @@ export class GeoJSONSeeder {
|
||||||
`Processing batch ${i + 1}/${batches.length} (${batch.length} records)`
|
`Processing batch ${i + 1}/${batches.length} (${batch.length} records)`
|
||||||
);
|
);
|
||||||
|
|
||||||
const { error } = await this.supabase
|
try {
|
||||||
.from('geographics')
|
const { error } = await this.supabase
|
||||||
.insert(batch)
|
.from('geographics')
|
||||||
.select();
|
.insert(batch)
|
||||||
|
.select();
|
||||||
|
|
||||||
if (error) {
|
if (error) {
|
||||||
console.error(`Error inserting batch ${i + 1}:`, error);
|
console.error(`Error inserting batch ${i + 1}:`, error);
|
||||||
// Optionally reduce batch size and retry for this specific batch
|
// Reduce batch size and retry for this specific batch
|
||||||
if (batch.length > 5) {
|
if (batch.length > 5) {
|
||||||
console.log(`Retrying batch ${i + 1} with smaller chunks...`);
|
console.log(`Retrying batch ${i + 1} with smaller chunks...`);
|
||||||
await this.insertInBatches(batch); // Recursive retry with automatic 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 {
|
} else {
|
||||||
console.error(
|
console.log(
|
||||||
`Failed to insert items even with small batch size:`,
|
`Successfully inserted batch ${i + 1} (${batch.length} records)`
|
||||||
batch
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
} else {
|
} catch (err) {
|
||||||
console.log(
|
console.error(`Exception when processing batch ${i + 1}:`, err);
|
||||||
`Successfully inserted batch ${i + 1} (${batch.length} records)`
|
// 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
|
// Add a small delay between batches to reduce database load
|
||||||
|
|
|
@ -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,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
|
@ -78,14 +78,24 @@ export class PermissionSeeder {
|
||||||
role_id: roleId,
|
role_id: roleId,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
// Create all permissions in a single batch operation
|
// Create permissions in smaller batches to avoid potential issues
|
||||||
const result = await this.prisma.permissions.createMany({
|
const batchSize = 50;
|
||||||
data: permissionsData,
|
for (let i = 0; i < permissionsData.length; i += batchSize) {
|
||||||
skipDuplicates: true, // Skip if the permission already exists
|
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(
|
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) {
|
} catch (error) {
|
||||||
console.error(
|
console.error(
|
||||||
|
|
|
@ -187,19 +187,28 @@ export class UnitSeeder {
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Insert all units in a single batch operation
|
// Insert units in smaller batches
|
||||||
if (unitsToInsert.length > 0) {
|
if (unitsToInsert.length > 0) {
|
||||||
const { error } = await this.supabase
|
const batchSize = 10;
|
||||||
.from('units')
|
for (let i = 0; i < unitsToInsert.length; i += batchSize) {
|
||||||
.insert(unitsToInsert)
|
const batch = unitsToInsert.slice(i, i + batchSize);
|
||||||
.select();
|
try {
|
||||||
|
const { error } = await this.supabase
|
||||||
|
.from('units')
|
||||||
|
.insert(batch)
|
||||||
|
.select();
|
||||||
|
|
||||||
if (error) {
|
if (error) {
|
||||||
console.error(`Error batch inserting units into Supabase:`, error);
|
console.error(`Error inserting units batch ${i / batchSize + 1}:`, error);
|
||||||
} else {
|
} else {
|
||||||
console.log(
|
console.log(`Successfully inserted batch ${i / batchSize + 1} (${batch.length} units)`);
|
||||||
`Successfully inserted ${unitsToInsert.length} units in batch`
|
}
|
||||||
);
|
|
||||||
|
// 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 {
|
} else {
|
||||||
console.warn('No unit data to insert');
|
console.warn('No unit data to insert');
|
||||||
|
|
|
@ -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",
|
||||||
|
],
|
||||||
|
});
|
|
@ -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();
|
|
@ -52,10 +52,10 @@ schema_paths = []
|
||||||
|
|
||||||
[db.seed]
|
[db.seed]
|
||||||
# If enabled, seeds the database after migrations during a db reset.
|
# 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.
|
# Specifies an ordered list of seed files to load during db reset.
|
||||||
# Supports glob patterns relative to supabase directory: "./seeds/*.sql"
|
# Supports glob patterns relative to supabase directory: "./seeds/*.sql"
|
||||||
sql_paths = ["./seed.sql"]
|
sql_paths = ['./seeds/*.sql']
|
||||||
|
|
||||||
[realtime]
|
[realtime]
|
||||||
enabled = true
|
enabled = true
|
||||||
|
|
|
@ -7,7 +7,7 @@ BEGIN
|
||||||
SELECT ST_Distance(
|
SELECT ST_Distance(
|
||||||
NEW.location::geography,
|
NEW.location::geography,
|
||||||
u.location::geography
|
u.location::geography
|
||||||
) / 1000 -- Convert to kilometers
|
)::gis.geography / 1000 -- Convert to kilometers
|
||||||
INTO NEW.distance_to_unit
|
INTO NEW.distance_to_unit
|
||||||
FROM units u
|
FROM units u
|
||||||
WHERE u.district_id = NEW.district_id;
|
WHERE u.district_id = NEW.district_id;
|
||||||
|
|
|
@ -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
|
@ -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');
|
|
@ -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
|
@ -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);
|
|
@ -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');
|
|
@ -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');
|
|
@ -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
|
@ -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');
|
Loading…
Reference in New Issue