feat: Add crime data JSON and district center data for crime incidents
feat: Update database schema to allow nullable year in crimes table feat: Create test table for future use in migrations feat: Add crime_cleared column to crimes table and drop test table feat: Add city_id column to units table and adjust district_id constraints feat: Add phone column to units table feat: Add avg_crime column to crimes table feat: Implement seeder for crime incidents with detailed mock data generation feat: Add trigger and function for calculating distance to district's police unit
This commit is contained in:
parent
e422c59da9
commit
77c865958a
|
@ -1,4 +1,6 @@
|
|||
export class CRegex {
|
||||
static readonly PHONE_REGEX = /^(\+62|62|0)[8][1-9][0-9]{6,11}$/;
|
||||
static readonly BAN_DURATION_REGEX = /^(\d+(ns|us|µs|ms|s|m|h)|none)$/;
|
||||
static readonly CR_YEAR_SEQUENCE = /(\d{4})(?=-\d{4}$)/;
|
||||
static readonly CR_SEQUENCE_END = /(\d{4})$/;
|
||||
}
|
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
|
@ -1,32 +1,32 @@
|
|||
district_id,district_name,crime_total,crime_cleared,avg_crime,avg_score,level
|
||||
350901,Jombang,41,41,8.2,99,low
|
||||
350902,Kencong,38,37,7.6,100,low
|
||||
350903,Sumberbaru,400,332,80.0,26,medium
|
||||
350904,Gumukmas,401,337,80.2,26,medium
|
||||
350905,Umbulsari,45,44,9.0,98,low
|
||||
350906,Tanggul,388,337,77.6,28,medium
|
||||
350907,Semboro,49,47,9.8,98,low
|
||||
350908,Puger,448,385,89.6,16,medium
|
||||
350909,Bangsalsari,473,403,94.6,11,medium
|
||||
350910,Balung,524,422,104.8,0,medium
|
||||
350911,Wuluhan,458,379,91.6,14,medium
|
||||
350912,Ambulu,442,366,88.4,17,medium
|
||||
350913,Rambipuji,426,373,85.2,21,medium
|
||||
350914,Panti,45,43,9.0,98,low
|
||||
350915,Sukorambi,45,44,9.0,98,low
|
||||
350916,Jenggawah,438,362,87.6,18,medium
|
||||
350917,Ajung,429,363,85.8,20,medium
|
||||
350918,Tempurejo,147,143,29.4,78,low
|
||||
350919,Kaliwates,476,400,95.2,10,high
|
||||
350920,Patrang,508,416,101.6,4,high
|
||||
350921,Sumbersari,422,359,84.4,21,high
|
||||
350922,Arjasa,57,56,11.4,96,low
|
||||
350923,Mumbulsari,35,35,7.0,100,low
|
||||
350924,Pakusari,52,52,10.4,97,low
|
||||
350925,Jelbuk,42,41,8.4,99,low
|
||||
350926,Mayang,51,51,10.2,97,low
|
||||
350927,Kalisat,45,45,9.0,98,low
|
||||
350928,Ledokombo,47,45,9.4,98,low
|
||||
350929,Sukowono,46,46,9.2,98,low
|
||||
350930,Silo,433,381,86.6,19,medium
|
||||
350931,Sumberjambe,43,43,8.6,99,low
|
||||
district_id,district_name,crime_total,crime_cleared,avg_crime,score,level
|
||||
350901,Jombang,112,41,10.25,99,low
|
||||
350902,Kencong,95,37,9.5,100,low
|
||||
350903,Sumberbaru,458,332,100,26,medium
|
||||
350904,Gumukmas,456,337,100.25,26,medium
|
||||
350905,Umbulsari,124,44,11.25,98,low
|
||||
350906,Tanggul,501,337,97,28,medium
|
||||
350907,Semboro,127,47,12.25,98,low
|
||||
350908,Puger,553,385,112,16,medium
|
||||
350909,Bangsalsari,568,403,118.25,11,medium
|
||||
350910,Balung,582,422,131,0,medium
|
||||
350911,Wuluhan,520,379,114.5,14,medium
|
||||
350912,Ambulu,534,366,110.5,17,medium
|
||||
350913,Rambipuji,500,373,106.5,21,medium
|
||||
350914,Panti,135,43,11.25,98,low
|
||||
350915,Sukorambi,134,44,11.25,98,low
|
||||
350916,Jenggawah,515,362,109.5,18,medium
|
||||
350917,Ajung,516,363,107.25,20,medium
|
||||
350918,Tempurejo,208,143,36.75,78,low
|
||||
350919,Kaliwates,547,400,119,10,high
|
||||
350920,Patrang,574,416,127,4,high
|
||||
350921,Sumbersari,529,359,105.5,21,high
|
||||
350922,Arjasa,156,56,14.25,96,low
|
||||
350923,Mumbulsari,103,35,8.75,100,low
|
||||
350924,Pakusari,113,52,13,97,low
|
||||
350925,Jelbuk,144,41,10.5,99,low
|
||||
350926,Mayang,141,51,12.75,97,low
|
||||
350927,Kalisat,112,45,11.25,98,low
|
||||
350928,Ledokombo,118,45,11.75,98,low
|
||||
350929,Sukowono,112,46,11.5,98,low
|
||||
350930,Silo,522,381,108.25,19,medium
|
||||
350931,Sumberjambe,141,43,10.75,99,low
|
||||
|
|
|
|
@ -0,0 +1,32 @@
|
|||
district_id,district_name,crime_total,crime_cleared,avg_crime,score,level
|
||||
350901,Jombang,118,110,29.5,73,low
|
||||
350902,Kencong,91,74,22.75,80,low
|
||||
350903,Sumberbaru,157,130,39.25,64,high
|
||||
350904,Gumukmas,91,78,22.75,80,low
|
||||
350905,Umbulsari,115,88,28.75,74,low
|
||||
350906,Tanggul,266,213,66.5,39,high
|
||||
350907,Semboro,94,89,23.5,79,low
|
||||
350908,Puger,180,160,45,59,high
|
||||
350909,Bangsalsari,154,132,38.5,65,high
|
||||
350910,Balung,278,223,69.5,37,high
|
||||
350911,Wuluhan,216,176,54,51,high
|
||||
350912,Ambulu,157,124,39.25,64,high
|
||||
350913,Rambipuji,278,170,69.5,37,low
|
||||
350914,Panti,139,109,34.75,69,low
|
||||
350915,Sukorambi,77,55,19.25,83,low
|
||||
350916,Jenggawah,235,224,58.75,47,high
|
||||
350917,Ajung,88,77,22,80,low
|
||||
350918,Tempurejo,74,48,18.5,84,low
|
||||
350919,Kaliwates,194,139,48.5,56,medium
|
||||
350920,Patrang,202,145,50.5,54,medium
|
||||
350921,Sumbersari,217,138,54.25,51,medium
|
||||
350922,Arjasa,116,81,29,74,low
|
||||
350923,Mumbulsari,99,81,24.75,78,low
|
||||
350924,Pakusari,152,129,38,66,low
|
||||
350925,Jelbuk,132,90,33,70,low
|
||||
350926,Mayang,89,60,22.25,80,low
|
||||
350927,Kalisat,270,163,67.5,39,high
|
||||
350928,Ledokombo,103,76,25.75,77,low
|
||||
350929,Sukowono,171,125,42.75,61,low
|
||||
350930,Silo,143,85,35.75,68,high
|
||||
350931,Sumberjambe,109,94,27.25,75,low
|
|
|
@ -1,156 +1,187 @@
|
|||
district_id,district_name,number_of_crime,level,score,method,year
|
||||
350901,Jombang,10,low,93,kmeans,2020
|
||||
350901,Jombang,19,low,87,kmeans,2021
|
||||
350901,Jombang,4,low,98,kmeans,2022
|
||||
350901,Jombang,4,low,98,kmeans,2023
|
||||
350901,Jombang,4,low,98,kmeans,2024
|
||||
350902,Kencong,6,low,96,kmeans,2022
|
||||
350902,Kencong,10,low,93,kmeans,2021
|
||||
350902,Kencong,11,low,93,kmeans,2020
|
||||
350902,Kencong,7,low,95,kmeans,2023
|
||||
350902,Kencong,4,low,98,kmeans,2024
|
||||
350903,Sumberbaru,82,medium,42,kmeans,2024
|
||||
350903,Sumberbaru,75,medium,47,kmeans,2022
|
||||
350903,Sumberbaru,109,medium,23,kmeans,2021
|
||||
350903,Sumberbaru,49,medium,65,kmeans,2023
|
||||
350903,Sumberbaru,85,medium,40,kmeans,2020
|
||||
350904,Gumukmas,101,medium,28,kmeans,2020
|
||||
350904,Gumukmas,73,medium,48,kmeans,2022
|
||||
350904,Gumukmas,104,medium,26,kmeans,2021
|
||||
350904,Gumukmas,74,low,48,kmeans,2024
|
||||
350904,Gumukmas,49,low,65,kmeans,2023
|
||||
350905,Umbulsari,7,low,95,kmeans,2022
|
||||
350905,Umbulsari,5,low,97,kmeans,2023
|
||||
350905,Umbulsari,17,low,88,kmeans,2021
|
||||
350905,Umbulsari,14,low,90,kmeans,2020
|
||||
350905,Umbulsari,2,low,99,kmeans,2024
|
||||
350906,Tanggul,102,medium,28,kmeans,2020
|
||||
350906,Tanggul,69,medium,51,kmeans,2023
|
||||
350906,Tanggul,95,medium,33,kmeans,2021
|
||||
350906,Tanggul,29,low,80,kmeans,2024
|
||||
350906,Tanggul,93,medium,34,kmeans,2022
|
||||
350907,Semboro,6,low,96,kmeans,2022
|
||||
350907,Semboro,5,low,97,kmeans,2023
|
||||
350907,Semboro,21,low,85,kmeans,2021
|
||||
350907,Semboro,4,low,98,kmeans,2024
|
||||
350907,Semboro,13,low,91,kmeans,2020
|
||||
350908,Puger,102,medium,28,kmeans,2020
|
||||
350908,Puger,72,medium,49,kmeans,2024
|
||||
350908,Puger,98,medium,30,kmeans,2021
|
||||
350908,Puger,94,medium,33,kmeans,2023
|
||||
350908,Puger,82,medium,42,kmeans,2022
|
||||
350909,Bangsalsari,116,medium,18,kmeans,2022
|
||||
350909,Bangsalsari,75,medium,47,kmeans,2023
|
||||
350909,Bangsalsari,121,medium,14,kmeans,2021
|
||||
350909,Bangsalsari,64,medium,55,kmeans,2024
|
||||
350909,Bangsalsari,97,medium,31,kmeans,2020
|
||||
350910,Balung,122,medium,13,kmeans,2020
|
||||
350910,Balung,92,medium,35,kmeans,2023
|
||||
350910,Balung,127,medium,10,kmeans,2021
|
||||
350910,Balung,102,medium,28,kmeans,2024
|
||||
350910,Balung,81,medium,43,kmeans,2022
|
||||
350911,Wuluhan,72,medium,49,kmeans,2022
|
||||
350911,Wuluhan,74,medium,48,kmeans,2024
|
||||
350911,Wuluhan,132,medium,6,kmeans,2021
|
||||
350911,Wuluhan,84,medium,40,kmeans,2023
|
||||
350911,Wuluhan,96,medium,32,kmeans,2020
|
||||
350912,Ambulu,99,medium,30,kmeans,2020
|
||||
350912,Ambulu,70,medium,50,kmeans,2024
|
||||
350912,Ambulu,97,medium,31,kmeans,2021
|
||||
350912,Ambulu,99,medium,30,kmeans,2023
|
||||
350912,Ambulu,77,medium,45,kmeans,2022
|
||||
350913,Rambipuji,104,medium,26,kmeans,2022
|
||||
350913,Rambipuji,68,medium,52,kmeans,2023
|
||||
350913,Rambipuji,103,medium,27,kmeans,2021
|
||||
350913,Rambipuji,103,medium,27,kmeans,2020
|
||||
350913,Rambipuji,48,medium,66,kmeans,2024
|
||||
350914,Panti,11,low,93,kmeans,2020
|
||||
350914,Panti,5,low,97,kmeans,2023
|
||||
350914,Panti,19,low,87,kmeans,2021
|
||||
350914,Panti,3,low,98,kmeans,2024
|
||||
350914,Panti,7,low,95,kmeans,2022
|
||||
350915,Sukorambi,4,low,98,kmeans,2022
|
||||
350915,Sukorambi,5,low,97,kmeans,2024
|
||||
350915,Sukorambi,19,low,87,kmeans,2021
|
||||
350915,Sukorambi,6,low,96,kmeans,2023
|
||||
350915,Sukorambi,11,low,93,kmeans,2020
|
||||
350916,Jenggawah,59,medium,58,kmeans,2023
|
||||
350916,Jenggawah,66,medium,53,kmeans,2024
|
||||
350916,Jenggawah,96,medium,32,kmeans,2022
|
||||
350916,Jenggawah,106,medium,25,kmeans,2020
|
||||
350916,Jenggawah,111,medium,21,kmeans,2021
|
||||
350917,Ajung,107,medium,24,kmeans,2021
|
||||
350917,Ajung,82,medium,42,kmeans,2020
|
||||
350917,Ajung,95,medium,33,kmeans,2022
|
||||
350917,Ajung,82,medium,42,kmeans,2024
|
||||
350917,Ajung,63,medium,55,kmeans,2023
|
||||
350918,Tempurejo,15,low,90,kmeans,2023
|
||||
350918,Tempurejo,17,low,88,kmeans,2024
|
||||
350918,Tempurejo,27,low,81,kmeans,2022
|
||||
350918,Tempurejo,39,low,73,kmeans,2020
|
||||
350918,Tempurejo,49,low,65,kmeans,2021
|
||||
350919,Kaliwates,124,high,12,kmeans,2021
|
||||
350919,Kaliwates,100,high,29,kmeans,2024
|
||||
350919,Kaliwates,93,high,34,kmeans,2022
|
||||
350919,Kaliwates,89,high,37,kmeans,2020
|
||||
350919,Kaliwates,70,high,50,kmeans,2023
|
||||
350920,Patrang,52,high,63,kmeans,2023
|
||||
350920,Patrang,104,high,26,kmeans,2024
|
||||
350920,Patrang,88,high,38,kmeans,2022
|
||||
350920,Patrang,124,high,12,kmeans,2020
|
||||
350920,Patrang,140,high,0,kmeans,2021
|
||||
350921,Sumbersari,89,high,37,kmeans,2021
|
||||
350921,Sumbersari,94,medium,33,kmeans,2020
|
||||
350921,Sumbersari,107,high,24,kmeans,2022
|
||||
350921,Sumbersari,53,high,63,kmeans,2023
|
||||
350921,Sumbersari,79,high,44,kmeans,2024
|
||||
350922,Arjasa,6,low,96,kmeans,2023
|
||||
350922,Arjasa,8,low,95,kmeans,2020
|
||||
350922,Arjasa,14,low,90,kmeans,2022
|
||||
350922,Arjasa,3,low,98,kmeans,2024
|
||||
350922,Arjasa,26,low,82,kmeans,2021
|
||||
350923,Mumbulsari,17,low,88,kmeans,2021
|
||||
350923,Mumbulsari,4,low,98,kmeans,2024
|
||||
350923,Mumbulsari,2,low,99,kmeans,2022
|
||||
350923,Mumbulsari,10,low,93,kmeans,2020
|
||||
350923,Mumbulsari,2,low,99,kmeans,2023
|
||||
350924,Pakusari,7,low,95,kmeans,2023
|
||||
350924,Pakusari,3,low,98,kmeans,2024
|
||||
350924,Pakusari,10,low,93,kmeans,2022
|
||||
350924,Pakusari,11,low,93,kmeans,2020
|
||||
350924,Pakusari,21,low,85,kmeans,2021
|
||||
350925,Jelbuk,21,low,85,kmeans,2021
|
||||
350925,Jelbuk,12,low,92,kmeans,2020
|
||||
350925,Jelbuk,6,low,96,kmeans,2022
|
||||
350925,Jelbuk,0,low,100,kmeans,2024
|
||||
350925,Jelbuk,3,low,98,kmeans,2023
|
||||
350926,Mayang,7,low,95,kmeans,2023
|
||||
350926,Mayang,16,low,89,kmeans,2020
|
||||
350926,Mayang,6,low,96,kmeans,2022
|
||||
350926,Mayang,18,low,88,kmeans,2021
|
||||
350926,Mayang,4,low,98,kmeans,2024
|
||||
350927,Kalisat,16,low,89,kmeans,2020
|
||||
350927,Kalisat,15,low,90,kmeans,2021
|
||||
350927,Kalisat,4,low,98,kmeans,2023
|
||||
350927,Kalisat,5,low,97,kmeans,2022
|
||||
350927,Kalisat,5,low,97,kmeans,2024
|
||||
350928,Ledokombo,5,low,97,kmeans,2024
|
||||
350928,Ledokombo,18,low,88,kmeans,2021
|
||||
350928,Ledokombo,10,low,93,kmeans,2020
|
||||
350928,Ledokombo,10,low,93,kmeans,2022
|
||||
350928,Ledokombo,4,low,98,kmeans,2023
|
||||
350929,Sukowono,6,low,96,kmeans,2023
|
||||
350929,Sukowono,7,low,95,kmeans,2022
|
||||
350929,Sukowono,11,low,93,kmeans,2020
|
||||
350929,Sukowono,20,low,86,kmeans,2021
|
||||
350929,Sukowono,2,low,99,kmeans,2024
|
||||
350930,Silo,68,medium,52,kmeans,2024
|
||||
350930,Silo,86,medium,39,kmeans,2022
|
||||
350930,Silo,81,medium,43,kmeans,2023
|
||||
350930,Silo,89,medium,37,kmeans,2021
|
||||
350930,Silo,109,medium,23,kmeans,2020
|
||||
350931,Sumberjambe,7,low,95,kmeans,2020
|
||||
350931,Sumberjambe,19,low,87,kmeans,2021
|
||||
350931,Sumberjambe,8,low,95,kmeans,2023
|
||||
350931,Sumberjambe,5,low,97,kmeans,2022
|
||||
350931,Sumberjambe,4,low,98,kmeans,2024
|
||||
district_id,district_name,number_of_crime,avg_crime,crime_cleared,level,score,method,year
|
||||
350901,Jombang,10,0.83,10,low,93,kmeans,2020
|
||||
350901,Jombang,19,1.58,19,low,87,kmeans,2021
|
||||
350901,Jombang,4,0.33,4,low,98,kmeans,2022
|
||||
350901,Jombang,4,0.33,4,low,98,kmeans,2023
|
||||
350901,Jombang,4,0.33,4,low,98,kmeans,2024
|
||||
350901,Jombang,71,5.92,40,medium,50,kmeans,2025
|
||||
350902,Kencong,11,0.92,9,low,93,kmeans,2020
|
||||
350902,Kencong,10,0.83,10,low,93,kmeans,2021
|
||||
350902,Kencong,6,0.5,6,low,96,kmeans,2022
|
||||
350902,Kencong,7,0.58,7,low,95,kmeans,2023
|
||||
350902,Kencong,4,0.33,5,low,98,kmeans,2024
|
||||
350902,Kencong,57,4.75,7,medium,60,kmeans,2025
|
||||
350903,Sumberbaru,85,7.08,73,medium,40,kmeans,2020
|
||||
350903,Sumberbaru,109,9.08,87,medium,23,kmeans,2021
|
||||
350903,Sumberbaru,75,6.25,63,medium,47,kmeans,2022
|
||||
350903,Sumberbaru,49,4.08,39,medium,65,kmeans,2023
|
||||
350903,Sumberbaru,82,6.83,70,medium,42,kmeans,2024
|
||||
350903,Sumberbaru,58,4.83,38,medium,59,kmeans,2025
|
||||
350904,Gumukmas,101,8.42,80,medium,28,kmeans,2020
|
||||
350904,Gumukmas,104,8.67,88,medium,26,kmeans,2021
|
||||
350904,Gumukmas,73,6.08,69,medium,48,kmeans,2022
|
||||
350904,Gumukmas,49,4.08,40,medium,65,kmeans,2023
|
||||
350904,Gumukmas,74,6.17,60,low,48,kmeans,2024
|
||||
350904,Gumukmas,55,4.58,32,medium,61,kmeans,2025
|
||||
350905,Umbulsari,14,1.17,13,low,90,kmeans,2020
|
||||
350905,Umbulsari,17,1.42,17,low,88,kmeans,2021
|
||||
350905,Umbulsari,7,0.58,7,low,95,kmeans,2022
|
||||
350905,Umbulsari,5,0.42,5,low,97,kmeans,2023
|
||||
350905,Umbulsari,2,0.17,2,low,99,kmeans,2024
|
||||
350905,Umbulsari,79,6.58,33,medium,44,kmeans,2025
|
||||
350906,Tanggul,102,8.5,78,medium,28,kmeans,2020
|
||||
350906,Tanggul,95,7.92,86,medium,33,kmeans,2021
|
||||
350906,Tanggul,93,7.75,83,medium,34,kmeans,2022
|
||||
350906,Tanggul,69,5.75,63,medium,51,kmeans,2023
|
||||
350906,Tanggul,29,2.42,27,low,80,kmeans,2024
|
||||
350906,Tanggul,113,9.42,20,medium,20,kmeans,2025
|
||||
350907,Semboro,13,1.08,11,low,91,kmeans,2020
|
||||
350907,Semboro,21,1.75,21,low,85,kmeans,2021
|
||||
350907,Semboro,6,0.5,6,low,96,kmeans,2022
|
||||
350907,Semboro,5,0.42,5,low,97,kmeans,2023
|
||||
350907,Semboro,4,0.33,4,low,98,kmeans,2024
|
||||
350907,Semboro,78,6.5,32,medium,45,kmeans,2025
|
||||
350908,Puger,102,8.5,85,medium,28,kmeans,2020
|
||||
350908,Puger,98,8.17,84,medium,30,kmeans,2021
|
||||
350908,Puger,82,6.83,74,medium,42,kmeans,2022
|
||||
350908,Puger,94,7.83,76,medium,33,kmeans,2023
|
||||
350908,Puger,72,6,66,medium,49,kmeans,2024
|
||||
350908,Puger,105,8.75,64,medium,25,kmeans,2025
|
||||
350909,Bangsalsari,97,8.08,80,medium,31,kmeans,2020
|
||||
350909,Bangsalsari,121,10.08,103,medium,14,kmeans,2021
|
||||
350909,Bangsalsari,116,9.67,102,medium,18,kmeans,2022
|
||||
350909,Bangsalsari,75,6.25,65,medium,47,kmeans,2023
|
||||
350909,Bangsalsari,64,5.33,53,medium,55,kmeans,2024
|
||||
350909,Bangsalsari,95,7.92,55,medium,33,kmeans,2025
|
||||
350910,Balung,122,10.17,94,medium,13,kmeans,2020
|
||||
350910,Balung,127,10.58,104,medium,10,kmeans,2021
|
||||
350910,Balung,81,6.75,71,medium,43,kmeans,2022
|
||||
350910,Balung,92,7.67,74,medium,35,kmeans,2023
|
||||
350910,Balung,102,8.5,79,medium,28,kmeans,2024
|
||||
350910,Balung,58,4.83,16,medium,59,kmeans,2025
|
||||
350911,Wuluhan,96,8,77,medium,32,kmeans,2020
|
||||
350911,Wuluhan,132,11,103,medium,6,kmeans,2021
|
||||
350911,Wuluhan,72,6,67,medium,49,kmeans,2022
|
||||
350911,Wuluhan,84,7,70,medium,40,kmeans,2023
|
||||
350911,Wuluhan,74,6.17,62,medium,48,kmeans,2024
|
||||
350911,Wuluhan,62,5.17,31,medium,56,kmeans,2025
|
||||
350912,Ambulu,99,8.25,79,medium,30,kmeans,2020
|
||||
350912,Ambulu,97,8.08,83,medium,31,kmeans,2021
|
||||
350912,Ambulu,77,6.42,73,medium,45,kmeans,2022
|
||||
350912,Ambulu,99,8.25,75,medium,30,kmeans,2023
|
||||
350912,Ambulu,70,5.83,56,medium,50,kmeans,2024
|
||||
350912,Ambulu,92,7.67,55,medium,35,kmeans,2025
|
||||
350913,Rambipuji,103,8.58,86,medium,27,kmeans,2020
|
||||
350913,Rambipuji,103,8.58,91,medium,27,kmeans,2021
|
||||
350913,Rambipuji,104,8.67,96,medium,26,kmeans,2022
|
||||
350913,Rambipuji,68,5.67,58,medium,52,kmeans,2023
|
||||
350913,Rambipuji,48,4,42,medium,66,kmeans,2024
|
||||
350913,Rambipuji,74,6.17,47,medium,48,kmeans,2025
|
||||
350914,Panti,11,0.92,9,low,93,kmeans,2020
|
||||
350914,Panti,19,1.58,19,low,87,kmeans,2021
|
||||
350914,Panti,7,0.58,7,low,95,kmeans,2022
|
||||
350914,Panti,5,0.42,5,low,97,kmeans,2023
|
||||
350914,Panti,3,0.25,3,low,98,kmeans,2024
|
||||
350914,Panti,90,7.5,54,medium,36,kmeans,2025
|
||||
350915,Sukorambi,11,0.92,10,low,93,kmeans,2020
|
||||
350915,Sukorambi,19,1.58,19,low,87,kmeans,2021
|
||||
350915,Sukorambi,4,0.33,4,low,98,kmeans,2022
|
||||
350915,Sukorambi,6,0.5,6,low,96,kmeans,2023
|
||||
350915,Sukorambi,5,0.42,5,low,97,kmeans,2024
|
||||
350915,Sukorambi,89,7.42,21,medium,37,kmeans,2025
|
||||
350916,Jenggawah,106,8.83,83,medium,25,kmeans,2020
|
||||
350916,Jenggawah,111,9.25,92,medium,21,kmeans,2021
|
||||
350916,Jenggawah,96,8,80,medium,32,kmeans,2022
|
||||
350916,Jenggawah,59,4.92,54,medium,58,kmeans,2023
|
||||
350916,Jenggawah,66,5.5,53,medium,53,kmeans,2024
|
||||
350916,Jenggawah,77,6.42,27,medium,45,kmeans,2025
|
||||
350917,Ajung,82,6.83,71,medium,42,kmeans,2020
|
||||
350917,Ajung,107,8.92,92,medium,24,kmeans,2021
|
||||
350917,Ajung,95,7.92,87,medium,33,kmeans,2022
|
||||
350917,Ajung,63,5.25,56,medium,55,kmeans,2023
|
||||
350917,Ajung,82,6.83,57,medium,42,kmeans,2024
|
||||
350917,Ajung,87,7.25,66,medium,38,kmeans,2025
|
||||
350918,Tempurejo,39,3.25,35,low,73,kmeans,2020
|
||||
350918,Tempurejo,49,4.08,48,low,65,kmeans,2021
|
||||
350918,Tempurejo,27,2.25,27,low,81,kmeans,2022
|
||||
350918,Tempurejo,15,1.25,16,low,90,kmeans,2023
|
||||
350918,Tempurejo,17,1.42,17,low,88,kmeans,2024
|
||||
350918,Tempurejo,61,5.08,26,medium,57,kmeans,2025
|
||||
350919,Kaliwates,89,7.42,73,high,37,kmeans,2020
|
||||
350919,Kaliwates,124,10.33,105,high,12,kmeans,2021
|
||||
350919,Kaliwates,93,7.75,80,high,34,kmeans,2022
|
||||
350919,Kaliwates,70,5.83,60,high,50,kmeans,2023
|
||||
350919,Kaliwates,100,8.33,82,high,29,kmeans,2024
|
||||
350919,Kaliwates,71,5.92,46,medium,50,kmeans,2025
|
||||
350920,Patrang,124,10.33,98,high,12,kmeans,2020
|
||||
350920,Patrang,140,11.67,110,high,0,kmeans,2021
|
||||
350920,Patrang,88,7.33,83,high,38,kmeans,2022
|
||||
350920,Patrang,52,4.33,48,high,63,kmeans,2023
|
||||
350920,Patrang,104,8.67,77,high,26,kmeans,2024
|
||||
350920,Patrang,66,5.5,27,medium,53,kmeans,2025
|
||||
350921,Sumbersari,94,7.83,77,medium,33,kmeans,2020
|
||||
350921,Sumbersari,89,7.42,79,high,37,kmeans,2021
|
||||
350921,Sumbersari,107,8.92,89,high,24,kmeans,2022
|
||||
350921,Sumbersari,53,4.42,51,high,63,kmeans,2023
|
||||
350921,Sumbersari,79,6.58,63,high,44,kmeans,2024
|
||||
350921,Sumbersari,107,8.92,42,medium,24,kmeans,2025
|
||||
350922,Arjasa,8,0.67,6,low,95,kmeans,2020
|
||||
350922,Arjasa,26,2.17,26,low,82,kmeans,2021
|
||||
350922,Arjasa,14,1.17,14,low,90,kmeans,2022
|
||||
350922,Arjasa,6,0.5,6,low,96,kmeans,2023
|
||||
350922,Arjasa,3,0.25,4,low,98,kmeans,2024
|
||||
350922,Arjasa,99,8.25,56,medium,30,kmeans,2025
|
||||
350923,Mumbulsari,10,0.83,9,low,93,kmeans,2020
|
||||
350923,Mumbulsari,17,1.42,18,low,88,kmeans,2021
|
||||
350923,Mumbulsari,2,0.17,2,low,99,kmeans,2022
|
||||
350923,Mumbulsari,2,0.17,2,low,99,kmeans,2023
|
||||
350923,Mumbulsari,4,0.33,4,low,98,kmeans,2024
|
||||
350923,Mumbulsari,68,5.67,35,medium,52,kmeans,2025
|
||||
350924,Pakusari,11,0.92,12,low,93,kmeans,2020
|
||||
350924,Pakusari,21,1.75,21,low,85,kmeans,2021
|
||||
350924,Pakusari,10,0.83,10,low,93,kmeans,2022
|
||||
350924,Pakusari,7,0.58,7,low,95,kmeans,2023
|
||||
350924,Pakusari,3,0.25,3,low,98,kmeans,2024
|
||||
350924,Pakusari,61,5.08,43,medium,57,kmeans,2025
|
||||
350925,Jelbuk,12,1,11,low,92,kmeans,2020
|
||||
350925,Jelbuk,21,1.75,21,low,85,kmeans,2021
|
||||
350925,Jelbuk,6,0.5,6,low,96,kmeans,2022
|
||||
350925,Jelbuk,3,0.25,3,low,98,kmeans,2023
|
||||
350925,Jelbuk,0,0,0,low,100,kmeans,2024
|
||||
350925,Jelbuk,102,8.5,76,medium,28,kmeans,2025
|
||||
350926,Mayang,16,1.33,15,low,89,kmeans,2020
|
||||
350926,Mayang,18,1.5,19,low,88,kmeans,2021
|
||||
350926,Mayang,6,0.5,6,low,96,kmeans,2022
|
||||
350926,Mayang,7,0.58,7,low,95,kmeans,2023
|
||||
350926,Mayang,4,0.33,4,low,98,kmeans,2024
|
||||
350926,Mayang,90,7.5,46,medium,36,kmeans,2025
|
||||
350927,Kalisat,16,1.33,15,low,89,kmeans,2020
|
||||
350927,Kalisat,15,1.25,16,low,90,kmeans,2021
|
||||
350927,Kalisat,5,0.42,5,low,97,kmeans,2022
|
||||
350927,Kalisat,4,0.33,4,low,98,kmeans,2023
|
||||
350927,Kalisat,5,0.42,5,low,97,kmeans,2024
|
||||
350927,Kalisat,67,5.58,46,medium,53,kmeans,2025
|
||||
350928,Ledokombo,10,0.83,8,low,93,kmeans,2020
|
||||
350928,Ledokombo,18,1.5,18,low,88,kmeans,2021
|
||||
350928,Ledokombo,10,0.83,10,low,93,kmeans,2022
|
||||
350928,Ledokombo,4,0.33,4,low,98,kmeans,2023
|
||||
350928,Ledokombo,5,0.42,5,low,97,kmeans,2024
|
||||
350928,Ledokombo,71,5.92,46,medium,50,kmeans,2025
|
||||
350929,Sukowono,11,0.92,11,low,93,kmeans,2020
|
||||
350929,Sukowono,20,1.67,20,low,86,kmeans,2021
|
||||
350929,Sukowono,7,0.58,7,low,95,kmeans,2022
|
||||
350929,Sukowono,6,0.5,6,low,96,kmeans,2023
|
||||
350929,Sukowono,2,0.17,3,low,99,kmeans,2024
|
||||
350929,Sukowono,66,5.5,30,medium,53,kmeans,2025
|
||||
350930,Silo,109,9.08,92,medium,23,kmeans,2020
|
||||
350930,Silo,89,7.42,79,medium,37,kmeans,2021
|
||||
350930,Silo,86,7.17,80,medium,39,kmeans,2022
|
||||
350930,Silo,81,6.75,68,medium,43,kmeans,2023
|
||||
350930,Silo,68,5.67,62,medium,52,kmeans,2024
|
||||
350930,Silo,89,7.42,27,medium,37,kmeans,2025
|
||||
350931,Sumberjambe,7,0.58,8,low,95,kmeans,2020
|
||||
350931,Sumberjambe,19,1.58,19,low,87,kmeans,2021
|
||||
350931,Sumberjambe,5,0.42,5,low,97,kmeans,2022
|
||||
350931,Sumberjambe,8,0.67,8,low,95,kmeans,2023
|
||||
350931,Sumberjambe,4,0.33,4,low,98,kmeans,2024
|
||||
350931,Sumberjambe,98,8.17,56,medium,30,kmeans,2025
|
||||
|
|
|
|
@ -0,0 +1,156 @@
|
|||
district_id,district_name,number_of_crime,crime_cleared,avg_crime,level,score,method,year
|
||||
350901,Jombang,16,14,1.33,low,86,kmeans,2020
|
||||
350901,Jombang,32,28,2.67,low,71,kmeans,2021
|
||||
350901,Jombang,23,21,1.92,low,79,kmeans,2022
|
||||
350901,Jombang,26,26,2.17,low,77,kmeans,2023
|
||||
350901,Jombang,21,21,1.75,low,81,kmeans,2024
|
||||
350902,Kencong,11,9,0.92,low,90,kmeans,2020
|
||||
350902,Kencong,18,14,1.50,low,84,kmeans,2021
|
||||
350902,Kencong,17,15,1.42,low,85,kmeans,2022
|
||||
350902,Kencong,21,17,1.75,low,81,kmeans,2023
|
||||
350902,Kencong,24,19,2.00,low,78,kmeans,2024
|
||||
350903,Sumberbaru,51,35,4.25,high,54,kmeans,2020
|
||||
350903,Sumberbaru,37,30,3.08,high,67,kmeans,2021
|
||||
350903,Sumberbaru,23,23,1.92,high,79,kmeans,2022
|
||||
350903,Sumberbaru,23,21,1.92,high,79,kmeans,2023
|
||||
350903,Sumberbaru,23,21,1.92,high,79,kmeans,2024
|
||||
350904,Gumukmas,16,15,1.33,low,86,kmeans,2020
|
||||
350904,Gumukmas,27,20,2.25,low,76,kmeans,2021
|
||||
350904,Gumukmas,18,13,1.50,low,84,kmeans,2022
|
||||
350904,Gumukmas,10,10,0.83,low,91,kmeans,2023
|
||||
350904,Gumukmas,20,20,1.67,low,82,kmeans,2024
|
||||
350905,Umbulsari,28,23,2.33,high,75,kmeans,2020
|
||||
350905,Umbulsari,26,17,2.17,low,77,kmeans,2021
|
||||
350905,Umbulsari,24,12,2.00,low,78,kmeans,2022
|
||||
350905,Umbulsari,21,20,1.75,low,81,kmeans,2023
|
||||
350905,Umbulsari,16,16,1.33,low,86,kmeans,2024
|
||||
350906,Tanggul,61,43,5.08,high,45,kmeans,2020
|
||||
350906,Tanggul,58,42,4.83,high,47,kmeans,2021
|
||||
350906,Tanggul,61,45,5.08,high,45,kmeans,2022
|
||||
350906,Tanggul,55,55,4.58,high,50,kmeans,2023
|
||||
350906,Tanggul,31,28,2.58,high,72,kmeans,2024
|
||||
350907,Semboro,34,32,2.83,low,69,kmeans,2020
|
||||
350907,Semboro,23,21,1.92,low,79,kmeans,2021
|
||||
350907,Semboro,19,19,1.58,low,83,kmeans,2022
|
||||
350907,Semboro,9,9,0.75,low,92,kmeans,2023
|
||||
350907,Semboro,9,8,0.75,low,92,kmeans,2024
|
||||
350908,Puger,43,43,3.58,high,61,kmeans,2020
|
||||
350908,Puger,57,49,4.75,high,48,kmeans,2021
|
||||
350908,Puger,33,27,2.75,high,70,kmeans,2022
|
||||
350908,Puger,26,21,2.17,high,77,kmeans,2023
|
||||
350908,Puger,21,20,1.75,high,81,kmeans,2024
|
||||
350909,Bangsalsari,41,38,3.42,high,63,kmeans,2020
|
||||
350909,Bangsalsari,41,24,3.42,high,63,kmeans,2021
|
||||
350909,Bangsalsari,34,32,2.83,high,69,kmeans,2022
|
||||
350909,Bangsalsari,13,13,1.08,low,89,kmeans,2023
|
||||
350909,Bangsalsari,25,25,2.08,high,78,kmeans,2024
|
||||
350910,Balung,56,47,4.67,high,49,kmeans,2020
|
||||
350910,Balung,74,62,6.17,high,33,kmeans,2021
|
||||
350910,Balung,54,43,4.50,high,51,kmeans,2022
|
||||
350910,Balung,55,43,4.58,high,50,kmeans,2023
|
||||
350910,Balung,39,28,3.25,high,65,kmeans,2024
|
||||
350911,Wuluhan,48,41,4.00,high,56,kmeans,2020
|
||||
350911,Wuluhan,70,49,5.83,high,36,kmeans,2021
|
||||
350911,Wuluhan,45,37,3.75,high,59,kmeans,2022
|
||||
350911,Wuluhan,26,25,2.17,high,77,kmeans,2023
|
||||
350911,Wuluhan,27,24,2.25,high,76,kmeans,2024
|
||||
350912,Ambulu,31,22,2.58,high,72,kmeans,2020
|
||||
350912,Ambulu,39,27,3.25,high,65,kmeans,2021
|
||||
350912,Ambulu,26,25,2.17,high,77,kmeans,2022
|
||||
350912,Ambulu,31,28,2.58,high,72,kmeans,2023
|
||||
350912,Ambulu,30,22,2.50,high,73,kmeans,2024
|
||||
350913,Rambipuji,96,57,8.00,high,12,kmeans,2020
|
||||
350913,Rambipuji,109,60,9.08,high,0,kmeans,2021
|
||||
350913,Rambipuji,32,23,2.67,low,71,kmeans,2022
|
||||
350913,Rambipuji,20,12,1.67,low,82,kmeans,2023
|
||||
350913,Rambipuji,21,18,1.75,low,81,kmeans,2024
|
||||
350914,Panti,56,30,4.67,high,49,kmeans,2020
|
||||
350914,Panti,28,24,2.33,low,75,kmeans,2021
|
||||
350914,Panti,22,25,1.83,low,80,kmeans,2022
|
||||
350914,Panti,24,23,2.00,low,78,kmeans,2023
|
||||
350914,Panti,9,7,0.75,low,92,kmeans,2024
|
||||
350915,Sukorambi,14,13,1.17,low,88,kmeans,2020
|
||||
350915,Sukorambi,27,19,2.25,low,76,kmeans,2021
|
||||
350915,Sukorambi,14,7,1.17,low,88,kmeans,2022
|
||||
350915,Sukorambi,18,12,1.50,low,84,kmeans,2023
|
||||
350915,Sukorambi,4,4,0.33,low,97,kmeans,2024
|
||||
350916,Jenggawah,55,51,4.58,high,50,kmeans,2020
|
||||
350916,Jenggawah,51,49,4.25,high,54,kmeans,2021
|
||||
350916,Jenggawah,34,31,2.83,low,69,kmeans,2022
|
||||
350916,Jenggawah,57,57,4.75,high,48,kmeans,2023
|
||||
350916,Jenggawah,38,36,3.17,high,66,kmeans,2024
|
||||
350917,Ajung,0,0,0.00,low,100,kmeans,2020
|
||||
350917,Ajung,16,10,1.33,low,86,kmeans,2021
|
||||
350917,Ajung,23,22,1.92,low,79,kmeans,2022
|
||||
350917,Ajung,24,20,2.00,low,78,kmeans,2023
|
||||
350917,Ajung,25,25,2.08,low,78,kmeans,2024
|
||||
350918,Tempurejo,19,14,1.58,low,83,kmeans,2020
|
||||
350918,Tempurejo,14,12,1.17,low,88,kmeans,2021
|
||||
350918,Tempurejo,18,11,1.50,low,84,kmeans,2022
|
||||
350918,Tempurejo,10,5,0.83,low,91,kmeans,2023
|
||||
350918,Tempurejo,13,6,1.08,low,89,kmeans,2024
|
||||
350919,Kaliwates,56,37,4.67,medium,49,kmeans,2020
|
||||
350919,Kaliwates,63,43,5.25,medium,43,kmeans,2021
|
||||
350919,Kaliwates,36,28,3.00,medium,67,kmeans,2022
|
||||
350919,Kaliwates,23,15,1.92,medium,79,kmeans,2023
|
||||
350919,Kaliwates,16,16,1.33,medium,86,kmeans,2024
|
||||
350920,Patrang,46,33,3.83,medium,58,kmeans,2020
|
||||
350920,Patrang,88,54,7.33,medium,20,kmeans,2021
|
||||
350920,Patrang,42,37,3.50,medium,62,kmeans,2022
|
||||
350920,Patrang,16,13,1.33,medium,86,kmeans,2023
|
||||
350920,Patrang,10,8,0.83,medium,91,kmeans,2024
|
||||
350921,Sumbersari,38,28,3.17,high,66,kmeans,2020
|
||||
350921,Sumbersari,52,37,4.33,medium,53,kmeans,2021
|
||||
350921,Sumbersari,59,43,4.92,medium,46,kmeans,2022
|
||||
350921,Sumbersari,35,18,2.92,medium,68,kmeans,2023
|
||||
350921,Sumbersari,33,12,2.75,medium,70,kmeans,2024
|
||||
350922,Arjasa,29,20,2.42,low,74,kmeans,2020
|
||||
350922,Arjasa,47,30,3.92,low,57,kmeans,2021
|
||||
350922,Arjasa,19,14,1.58,low,83,kmeans,2022
|
||||
350922,Arjasa,10,9,0.83,low,91,kmeans,2023
|
||||
350922,Arjasa,11,8,0.92,low,90,kmeans,2024
|
||||
350923,Mumbulsari,28,27,2.33,high,75,kmeans,2020
|
||||
350923,Mumbulsari,27,19,2.25,low,76,kmeans,2021
|
||||
350923,Mumbulsari,23,17,1.92,low,79,kmeans,2022
|
||||
350923,Mumbulsari,11,11,0.92,low,90,kmeans,2023
|
||||
350923,Mumbulsari,10,7,0.83,low,91,kmeans,2024
|
||||
350924,Pakusari,39,29,3.25,low,65,kmeans,2020
|
||||
350924,Pakusari,52,44,4.33,low,53,kmeans,2021
|
||||
350924,Pakusari,34,33,2.83,low,69,kmeans,2022
|
||||
350924,Pakusari,12,12,1.00,low,89,kmeans,2023
|
||||
350924,Pakusari,15,11,1.25,low,87,kmeans,2024
|
||||
350925,Jelbuk,32,29,2.67,low,71,kmeans,2020
|
||||
350925,Jelbuk,55,30,4.58,low,50,kmeans,2021
|
||||
350925,Jelbuk,16,10,1.33,low,86,kmeans,2022
|
||||
350925,Jelbuk,16,15,1.33,low,86,kmeans,2023
|
||||
350925,Jelbuk,13,6,1.08,low,89,kmeans,2024
|
||||
350926,Mayang,11,6,0.92,low,90,kmeans,2020
|
||||
350926,Mayang,35,19,2.92,low,68,kmeans,2021
|
||||
350926,Mayang,20,19,1.67,low,82,kmeans,2022
|
||||
350926,Mayang,13,7,1.08,low,89,kmeans,2023
|
||||
350926,Mayang,10,9,0.83,low,91,kmeans,2024
|
||||
350927,Kalisat,84,47,7.00,high,23,kmeans,2020
|
||||
350927,Kalisat,95,58,7.92,high,13,kmeans,2021
|
||||
350927,Kalisat,68,36,5.67,high,38,kmeans,2022
|
||||
350927,Kalisat,13,12,1.08,low,89,kmeans,2023
|
||||
350927,Kalisat,10,10,0.83,low,91,kmeans,2024
|
||||
350928,Ledokombo,22,12,1.83,low,80,kmeans,2020
|
||||
350928,Ledokombo,45,34,3.75,low,59,kmeans,2021
|
||||
350928,Ledokombo,17,12,1.42,low,85,kmeans,2022
|
||||
350928,Ledokombo,13,13,1.08,low,89,kmeans,2023
|
||||
350928,Ledokombo,6,5,0.50,low,95,kmeans,2024
|
||||
350929,Sukowono,38,27,3.17,high,66,kmeans,2020
|
||||
350929,Sukowono,46,32,3.83,low,58,kmeans,2021
|
||||
350929,Sukowono,34,29,2.83,low,69,kmeans,2022
|
||||
350929,Sukowono,25,18,2.08,low,78,kmeans,2023
|
||||
350929,Sukowono,28,19,2.33,low,75,kmeans,2024
|
||||
350930,Silo,26,15,2.17,high,77,kmeans,2020
|
||||
350930,Silo,39,20,3.25,high,65,kmeans,2021
|
||||
350930,Silo,28,18,2.33,high,75,kmeans,2022
|
||||
350930,Silo,29,16,2.42,high,74,kmeans,2023
|
||||
350930,Silo,21,16,1.75,low,81,kmeans,2024
|
||||
350931,Sumberjambe,55,46,4.58,high,50,kmeans,2020
|
||||
350931,Sumberjambe,20,19,1.67,low,82,kmeans,2021
|
||||
350931,Sumberjambe,12,12,1.00,low,89,kmeans,2022
|
||||
350931,Sumberjambe,16,11,1.33,low,86,kmeans,2023
|
||||
350931,Sumberjambe,6,6,0.50,low,95,kmeans,2024
|
|
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,33 @@
|
|||
export const districtCenters = [
|
||||
{ kecamatan: "Sumbersari", lat: -8.170662, lng: 113.727582 },
|
||||
{ kecamatan: "Wuluhan", lat: -8.365478, lng: 113.537137 },
|
||||
{ kecamatan: "Bangsalsari", lat: -8.2013, lng: 113.5323 },
|
||||
{ kecamatan: "Kaliwates", lat: -8.1725, lng: 113.7000 },
|
||||
{ kecamatan: "Puger", lat: -8.477942, lng: 113.370447 },
|
||||
{ kecamatan: "Ambulu", lat: -8.381355, lng: 113.608561 },
|
||||
{ kecamatan: "Silo", lat: -8.22917, lng: 113.86500 },
|
||||
{ kecamatan: "Sumberbaru", lat: -8.119229, lng: 113.392973 },
|
||||
{ kecamatan: "Patrang", lat: -8.14611, lng: 113.71250 },
|
||||
{ kecamatan: "Tanggul", lat: -8.161572, lng: 113.451239 },
|
||||
{ kecamatan: "Jenggawah", lat: -8.296967, lng: 113.656173 },
|
||||
{ kecamatan: "Gumukmas", lat: -8.32306, lng: 113.40667 },
|
||||
{ kecamatan: "Rambipuji", lat: -8.210581, lng: 113.603610 },
|
||||
{ kecamatan: "Ajung", lat: -8.245360, lng: 113.653218 },
|
||||
{ kecamatan: "Balung", lat: -8.26861, lng: 113.52667 },
|
||||
{ kecamatan: "Tempurejo", lat: -8.4000, lng: 113.8000 },
|
||||
{ kecamatan: "Kalisat", lat: -8.1500, lng: 113.8000 },
|
||||
{ kecamatan: "Umbulsari", lat: -8.2500, lng: 113.5000 },
|
||||
{ kecamatan: "Kencong", lat: -8.3500, lng: 113.4000 },
|
||||
{ kecamatan: "Ledokombo", lat: -8.2000, lng: 113.8000 },
|
||||
{ kecamatan: "Mumbulsari", lat: -8.3000, lng: 113.6000 },
|
||||
{ kecamatan: "Panti", lat: -8.1500, lng: 113.6000 },
|
||||
{ kecamatan: "Sumberjambe", lat: -8.1000, lng: 113.8000 },
|
||||
{ kecamatan: "Sukowono", lat: -8.2000, lng: 113.7000 },
|
||||
{ kecamatan: "Mayang", lat: -8.2500, lng: 113.7000 },
|
||||
{ kecamatan: "Semboro", lat: -8.3000, lng: 113.5000 },
|
||||
{ kecamatan: "Jombang", lat: -8.3500, lng: 113.5000 },
|
||||
{ kecamatan: "Pakusari", lat: -8.2000, lng: 113.7000 },
|
||||
{ kecamatan: "Arjasa", lat: -8.1500, lng: 113.7000 },
|
||||
{ kecamatan: "Sukorambi", lat: -8.2000, lng: 113.6000 },
|
||||
{ kecamatan: "Jelbuk", lat: -8.1000, lng: 113.7000 },
|
||||
];
|
|
@ -0,0 +1,2 @@
|
|||
-- AlterTable
|
||||
ALTER TABLE "crimes" ALTER COLUMN "year" DROP NOT NULL;
|
|
@ -0,0 +1,12 @@
|
|||
-- CreateTable
|
||||
CREATE TABLE "test" (
|
||||
"id" UUID NOT NULL DEFAULT gen_random_uuid(),
|
||||
"name" VARCHAR(100) NOT NULL,
|
||||
"created_at" TIMESTAMPTZ(6) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updated_at" TIMESTAMPTZ(6) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
|
||||
CONSTRAINT "test_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "idx_test_name" ON "test"("name");
|
|
@ -0,0 +1,11 @@
|
|||
/*
|
||||
Warnings:
|
||||
|
||||
- You are about to drop the `test` table. If the table is not empty, all the data it contains will be lost.
|
||||
|
||||
*/
|
||||
-- AlterTable
|
||||
ALTER TABLE "crimes" ADD COLUMN "crime_cleared" INTEGER NOT NULL DEFAULT 0;
|
||||
|
||||
-- DropTable
|
||||
DROP TABLE "test";
|
|
@ -0,0 +1,12 @@
|
|||
/*
|
||||
Warnings:
|
||||
|
||||
- Added the required column `city_id` to the `units` table without a default value. This is not possible if the table is not empty.
|
||||
|
||||
*/
|
||||
-- AlterTable
|
||||
ALTER TABLE "units" ADD COLUMN "city_id" VARCHAR(20) NOT NULL,
|
||||
ALTER COLUMN "district_id" DROP NOT NULL;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "units" ADD CONSTRAINT "units_city_id_fkey" FOREIGN KEY ("city_id") REFERENCES "cities"("id") ON DELETE CASCADE ON UPDATE NO ACTION;
|
|
@ -0,0 +1,2 @@
|
|||
-- AlterTable
|
||||
ALTER TABLE "units" ADD COLUMN "phone" TEXT;
|
|
@ -0,0 +1,2 @@
|
|||
-- AlterTable
|
||||
ALTER TABLE "crimes" ADD COLUMN "avg_crime" DOUBLE PRECISION NOT NULL DEFAULT 0;
|
|
@ -125,6 +125,7 @@ model cities {
|
|||
created_at DateTime? @default(now()) @db.Timestamptz(6)
|
||||
updated_at DateTime? @default(now()) @db.Timestamptz(6)
|
||||
districts districts[]
|
||||
units units[]
|
||||
|
||||
@@index([name], map: "idx_cities_name")
|
||||
}
|
||||
|
@ -172,9 +173,11 @@ model crimes {
|
|||
method String? @db.VarChar(100)
|
||||
month Int?
|
||||
number_of_crime Int @default(0)
|
||||
crime_cleared Int @default(0)
|
||||
avg_crime Float @default(0)
|
||||
score Float @default(0)
|
||||
updated_at DateTime? @default(now()) @db.Timestamptz(6)
|
||||
year Int
|
||||
year Int?
|
||||
source_type String? @db.VarChar(100)
|
||||
crime_incidents crime_incidents[]
|
||||
districts districts @relation(fields: [district_id], references: [id], onDelete: Cascade, onUpdate: NoAction)
|
||||
|
@ -265,19 +268,22 @@ model incident_logs {
|
|||
model units {
|
||||
id String @id @default(dbgenerated("gen_random_uuid()")) @db.Uuid
|
||||
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)
|
||||
name String @db.VarChar(100)
|
||||
description String?
|
||||
type unit_type
|
||||
created_at DateTime? @default(now()) @db.Timestamptz(6)
|
||||
updated_at DateTime? @default(now()) @db.Timestamptz(6)
|
||||
address String?
|
||||
phone String?
|
||||
land_area Float?
|
||||
latitude Float
|
||||
longitude Float
|
||||
location Unsupported("geography")
|
||||
unit_statistics unit_statistics[]
|
||||
districts districts @relation(fields: [district_id], references: [id], onDelete: Cascade, onUpdate: NoAction)
|
||||
districts districts? @relation(fields: [district_id], references: [id], onDelete: Cascade, onUpdate: NoAction)
|
||||
cities cities @relation(fields: [city_id], references: [id], onDelete: Cascade, onUpdate: NoAction)
|
||||
|
||||
@@index([name], map: "idx_units_name")
|
||||
@@index([type], map: "idx_units_type")
|
||||
|
|
|
@ -10,7 +10,8 @@ import { CrimeCategoriesSeeder } from './seeds/crime-category';
|
|||
|
||||
import { UnitSeeder } from './seeds/units';
|
||||
import { CrimesSeeder } from './seeds/crimes';
|
||||
import { CrimeIncidentsSeeder as DetailedCrimeIncidentsSeeder } from './seeds/crime-incidents';
|
||||
import { CrimeIncidentsByUnitSeeder } from './seeds/crime-incidents';
|
||||
import { CrimeIncidentsByTypeSeeder } from './seeds/crime-incidents-cbt';
|
||||
|
||||
const prisma = new PrismaClient();
|
||||
|
||||
|
@ -37,8 +38,8 @@ class DatabaseSeeder {
|
|||
new UnitSeeder(prisma),
|
||||
new DemographicsSeeder(prisma),
|
||||
new CrimesSeeder(prisma),
|
||||
new DetailedCrimeIncidentsSeeder(prisma), // Add the new crime incidents seeder
|
||||
// new CrimeIncidentsSeederV2(prisma),
|
||||
// new CrimeIncidentsByUnitSeeder(prisma),
|
||||
new CrimeIncidentsByTypeSeeder(prisma),
|
||||
];
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,779 @@
|
|||
import {
|
||||
crime_incidents,
|
||||
crime_rates,
|
||||
crime_status,
|
||||
crimes,
|
||||
locations,
|
||||
PrismaClient,
|
||||
} from "@prisma/client";
|
||||
import { generateId, generateIdWithDbCounter } from "../../app/_utils/common";
|
||||
import { createClient } from "../../app/_utils/supabase/client";
|
||||
import * as fs from "fs";
|
||||
import * as path from "path";
|
||||
import { CRegex } from "../../app/_utils/const/regex";
|
||||
|
||||
import { districtCenters } from "../data/jsons/district-center";
|
||||
|
||||
type ICreateLocations = {
|
||||
id: string;
|
||||
created_at: Date;
|
||||
updated_at: Date;
|
||||
district_id: string;
|
||||
event_id: string;
|
||||
address: string;
|
||||
type: string;
|
||||
latitude: number;
|
||||
longitude: number;
|
||||
land_area: number;
|
||||
location: string;
|
||||
};
|
||||
|
||||
// New type for crime data structure from JSON
|
||||
interface CrimeData {
|
||||
district_id: string;
|
||||
district_name: string;
|
||||
month: string;
|
||||
year: number;
|
||||
[key: string]: string | number; // For dynamic crime category fields ending with "_crimes"
|
||||
crime_cleared: number;
|
||||
number_of_crime: number;
|
||||
}
|
||||
|
||||
export class CrimeIncidentsByTypeSeeder {
|
||||
private crimeMonthlyData: Map<
|
||||
string,
|
||||
{
|
||||
number_of_crime: number;
|
||||
crime_cleared: number;
|
||||
categories: { [category: string]: number };
|
||||
}
|
||||
> = new Map();
|
||||
|
||||
private monthNameMap: { [key: string]: number } = {
|
||||
"JAN": 1,
|
||||
"FEB": 2,
|
||||
"MAR": 3,
|
||||
"APR": 4,
|
||||
"MAY": 5,
|
||||
"JUN": 6,
|
||||
"JUL": 7,
|
||||
"AUG": 8,
|
||||
"SEP": 9,
|
||||
"OCT": 10,
|
||||
"NOV": 11,
|
||||
"DEC": 12,
|
||||
};
|
||||
|
||||
constructor(
|
||||
private prisma: PrismaClient,
|
||||
private supabase = createClient(),
|
||||
) {}
|
||||
|
||||
private async loadCrimeMonthlyData(): Promise<void> {
|
||||
const jsonFilePath = path.resolve(
|
||||
__dirname,
|
||||
"../data/jsons/crimes/cbt.json",
|
||||
);
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
fs.readFile(jsonFilePath, "utf8", (err, data) => {
|
||||
if (err) {
|
||||
console.error(
|
||||
"Error reading crime monthly JSON data:",
|
||||
err,
|
||||
);
|
||||
reject(err);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const crimeData: CrimeData[] = JSON.parse(data);
|
||||
|
||||
for (const row of crimeData) {
|
||||
const monthNum = this.monthNameToNumber(row.month);
|
||||
const key =
|
||||
`${row.district_id}-${monthNum}-${row.year}`;
|
||||
|
||||
// Extract crime categories (fields ending with "_crimes")
|
||||
const categories: { [category: string]: number } = {};
|
||||
for (const [field, value] of Object.entries(row)) {
|
||||
if (
|
||||
field.endsWith("_crimes") &&
|
||||
typeof value === "number"
|
||||
) {
|
||||
// Extract category name from field name (remove "_crimes" suffix)
|
||||
const categoryName = field.slice(0, -7);
|
||||
categories[categoryName] = value;
|
||||
}
|
||||
}
|
||||
|
||||
this.crimeMonthlyData.set(key, {
|
||||
number_of_crime: row.number_of_crime as number,
|
||||
crime_cleared: row.crime_cleared as number,
|
||||
categories: categories,
|
||||
});
|
||||
}
|
||||
|
||||
resolve();
|
||||
} catch (error) {
|
||||
console.error(
|
||||
"Error parsing crime monthly JSON data:",
|
||||
error,
|
||||
);
|
||||
reject(error);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
private monthNameToNumber(monthName: string): number {
|
||||
const month = this.monthNameMap[monthName.toUpperCase()];
|
||||
if (!month) {
|
||||
console.warn(
|
||||
`Unknown month name: ${monthName}, defaulting to January`,
|
||||
);
|
||||
return 1;
|
||||
}
|
||||
return month;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates mock incidents for 2025 data
|
||||
*/
|
||||
private async generateMock2025Incidents(): Promise<void> {
|
||||
const crimes2025 = await this.prisma.crimes.findMany({
|
||||
where: {
|
||||
year: 2025,
|
||||
month: { not: null },
|
||||
source_type: "cbt",
|
||||
},
|
||||
orderBy: [{ district_id: "asc" }, { month: "asc" }],
|
||||
});
|
||||
|
||||
if (crimes2025.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const crimeCategories = await this.prisma.crime_categories.findMany();
|
||||
|
||||
let totalIncidentsCreated = 0;
|
||||
let totalMatched = 0;
|
||||
let totalMismatched = 0;
|
||||
let totalResolved = 0;
|
||||
let districtsProcessed = new Set();
|
||||
|
||||
for (const crimeRecord of crimes2025) {
|
||||
if (
|
||||
!crimeRecord.number_of_crime || crimeRecord.number_of_crime <= 0
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const incidents = await this.createMockIncidentsForCrime(
|
||||
crimeRecord,
|
||||
crimeCategories,
|
||||
);
|
||||
|
||||
totalIncidentsCreated += incidents.length;
|
||||
districtsProcessed.add(crimeRecord.district_id);
|
||||
totalResolved += incidents.filter((inc) =>
|
||||
inc.status === "resolved"
|
||||
).length;
|
||||
totalMatched += Math.min(3, Math.floor(Math.random() * 5));
|
||||
}
|
||||
|
||||
console.log("\n📊 2025 Mock Data Summary:");
|
||||
console.log(`├─ Total incidents created: ${totalIncidentsCreated}`);
|
||||
console.log(`├─ Districts processed: ${districtsProcessed.size}`);
|
||||
console.log(
|
||||
`├─ Categories: ${totalMatched} matched, ${totalMismatched} mismatched`,
|
||||
);
|
||||
console.log(`└─ Total resolved cases: ${totalResolved}`);
|
||||
}
|
||||
|
||||
private async createMockIncidentsForCrime(
|
||||
crime: crimes,
|
||||
allCategories: any[],
|
||||
): Promise<any[]> {
|
||||
const incidentsCreated = [];
|
||||
|
||||
const district = await this.prisma.districts.findUnique({
|
||||
where: { id: crime.district_id },
|
||||
include: { cities: true },
|
||||
});
|
||||
|
||||
if (!district) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const geo = await this.prisma.geographics.findFirst({
|
||||
where: {
|
||||
district_id: district.id,
|
||||
year: crime.year ?? 2025,
|
||||
},
|
||||
orderBy: {
|
||||
year: "desc",
|
||||
},
|
||||
select: {
|
||||
latitude: true,
|
||||
longitude: true,
|
||||
land_area: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!geo) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const numberOfCrimes = crime.number_of_crime || 10;
|
||||
const crimesCleared = Math.floor(
|
||||
numberOfCrimes * (0.3 + Math.random() * 0.4),
|
||||
);
|
||||
|
||||
const locationPool = this.generateDistributedPoints(
|
||||
geo.land_area!,
|
||||
numberOfCrimes,
|
||||
district.id,
|
||||
district.name,
|
||||
);
|
||||
|
||||
const jemberStreets = [
|
||||
"Jalan Pahlawan",
|
||||
"Jalan Merdeka",
|
||||
"Jalan Cendrawasih",
|
||||
"Jalan Srikandi",
|
||||
"Jalan Sumbermujur",
|
||||
"Jalan Taman Siswa",
|
||||
"Jalan Pantai",
|
||||
"Jalan Raya Sumberbaru",
|
||||
"Jalan Abdurrahman Saleh",
|
||||
"Jalan Mastrip",
|
||||
"Jalan PB Sudirman",
|
||||
"Jalan Kalimantan",
|
||||
"Jalan Sumatra",
|
||||
"Jalan Jawa",
|
||||
"Jalan Gajah Mada",
|
||||
"Jalan Letjen Suprapto",
|
||||
"Jalan Hayam Wuruk",
|
||||
"Jalan Trunojoyo",
|
||||
"Jalan Imam Bonjol",
|
||||
"Jalan Diponegoro",
|
||||
"Jalan Ahmad Yani",
|
||||
"Jalan Kartini",
|
||||
"Jalan Gatot Subroto",
|
||||
];
|
||||
|
||||
const placeTypes = [
|
||||
"Perumahan",
|
||||
"Apartemen",
|
||||
"Komplek",
|
||||
"Pasar",
|
||||
"Toko",
|
||||
"Terminal",
|
||||
"Stasiun",
|
||||
"Kampus",
|
||||
"Sekolah",
|
||||
"Perkantoran",
|
||||
"Pertokoan",
|
||||
"Pusat Perbelanjaan",
|
||||
"Taman",
|
||||
"Alun-alun",
|
||||
"Simpang",
|
||||
"Pertigaan",
|
||||
"Perempatan",
|
||||
];
|
||||
|
||||
const user = await this.prisma.users.findFirst({
|
||||
where: {
|
||||
email: "sigapcompany@gmail.com",
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!user) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const event = await this.prisma.events.findFirst({
|
||||
where: {
|
||||
user_id: user.id,
|
||||
},
|
||||
});
|
||||
|
||||
if (!event) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const incidentsToCreate: Partial<crime_incidents>[] = [];
|
||||
const locationsToCreate: Partial<locations>[] = [];
|
||||
|
||||
const categoryDistribution: { categoryId: string; count: number }[] =
|
||||
[];
|
||||
|
||||
let remainingCrimes = numberOfCrimes;
|
||||
const shuffledCategories = [...allCategories].sort(() =>
|
||||
Math.random() - 0.5
|
||||
);
|
||||
|
||||
const categoriesToUse = Math.max(
|
||||
1,
|
||||
Math.min(3, Math.floor(Math.random() * 5)),
|
||||
);
|
||||
|
||||
for (
|
||||
let i = 0;
|
||||
i < categoriesToUse && i < shuffledCategories.length &&
|
||||
remainingCrimes > 0;
|
||||
i++
|
||||
) {
|
||||
const category = shuffledCategories[i];
|
||||
|
||||
if (
|
||||
i === categoriesToUse - 1 || i === shuffledCategories.length - 1
|
||||
) {
|
||||
categoryDistribution.push({
|
||||
categoryId: category.id,
|
||||
count: remainingCrimes,
|
||||
});
|
||||
remainingCrimes = 0;
|
||||
} else {
|
||||
const count = Math.max(
|
||||
1,
|
||||
Math.floor(remainingCrimes * (0.2 + Math.random() * 0.6)),
|
||||
);
|
||||
categoryDistribution.push({
|
||||
categoryId: category.id,
|
||||
count: count,
|
||||
});
|
||||
remainingCrimes -= count;
|
||||
}
|
||||
}
|
||||
|
||||
let resolvedCount = 0;
|
||||
|
||||
for (const category of categoryDistribution) {
|
||||
for (let i = 0; i < category.count; i++) {
|
||||
const year = 2025;
|
||||
const month = (crime.month as number) - 1;
|
||||
|
||||
const maxDay = new Date(year, month + 1, 0).getDate();
|
||||
const day = Math.floor(Math.random() * maxDay) + 1;
|
||||
const hour = Math.floor(Math.random() * 24);
|
||||
const minute = Math.floor(Math.random() * 60);
|
||||
|
||||
const timestamp = new Date(year, month, day, hour, minute);
|
||||
|
||||
const randomLocationIndex = Math.floor(
|
||||
Math.random() * locationPool.length,
|
||||
);
|
||||
const selectedLocation = locationPool[randomLocationIndex];
|
||||
|
||||
const streetName = jemberStreets[
|
||||
Math.floor(Math.random() * jemberStreets.length)
|
||||
];
|
||||
const buildingNumber = Math.floor(Math.random() * 200) + 1;
|
||||
const placeType =
|
||||
placeTypes[Math.floor(Math.random() * placeTypes.length)];
|
||||
|
||||
let randomAddress;
|
||||
const addressType = Math.floor(Math.random() * 3);
|
||||
switch (addressType) {
|
||||
case 0:
|
||||
randomAddress =
|
||||
`${streetName} No. ${buildingNumber}, ${district.name}, Jember`;
|
||||
break;
|
||||
case 1:
|
||||
randomAddress =
|
||||
`${placeType} ${district.name}, ${streetName}, Jember`;
|
||||
break;
|
||||
case 2:
|
||||
randomAddress = `${streetName} Blok ${
|
||||
String.fromCharCode(
|
||||
65 + Math.floor(Math.random() * 26),
|
||||
)
|
||||
}-${
|
||||
Math.floor(Math.random() * 20) + 1
|
||||
}, ${district.name}, Jember`;
|
||||
break;
|
||||
}
|
||||
|
||||
const locationData: Partial<ICreateLocations> = {
|
||||
district_id: district.id,
|
||||
event_id: event.id,
|
||||
address: randomAddress,
|
||||
type: "crime incident",
|
||||
latitude: selectedLocation.latitude,
|
||||
longitude: selectedLocation.longitude,
|
||||
location:
|
||||
`POINT(${selectedLocation.longitude} ${selectedLocation.latitude})`,
|
||||
};
|
||||
|
||||
locationsToCreate.push(locationData);
|
||||
|
||||
const incidentId = await generateIdWithDbCounter(
|
||||
"crime_incidents",
|
||||
{
|
||||
prefix: "CI",
|
||||
segments: {
|
||||
codes: [district.city_id],
|
||||
sequentialDigits: 4,
|
||||
year,
|
||||
},
|
||||
format: "{prefix}-{codes}-{sequence}-{year}",
|
||||
separator: "-",
|
||||
uniquenessStrategy: "counter",
|
||||
},
|
||||
CRegex.CR_YEAR_SEQUENCE,
|
||||
);
|
||||
|
||||
const status = resolvedCount < crimesCleared
|
||||
? ("resolved" as crime_status)
|
||||
: ("unresolved" as crime_status);
|
||||
|
||||
if (status === "resolved") {
|
||||
resolvedCount++;
|
||||
}
|
||||
|
||||
const categoryDetails = allCategories.find((c) =>
|
||||
c.id === category.categoryId
|
||||
);
|
||||
|
||||
const categoryName = categoryDetails.name;
|
||||
|
||||
const locs = [
|
||||
`di daerah ${district.name}`,
|
||||
`di sekitar ${district.name}`,
|
||||
`di wilayah ${district.name}`,
|
||||
`di jalan utama ${district.name}`,
|
||||
`di perumahan ${district.name}`,
|
||||
`di pasar ${district.name}`,
|
||||
`di perbatasan ${district.name}`,
|
||||
`di kawasan ${placeType.toLowerCase()} ${district.name}`,
|
||||
`di persimpangan jalan ${streetName}`,
|
||||
`di dekat ${placeType.toLowerCase()} ${district.name}`,
|
||||
`di belakang ${placeType.toLowerCase()} ${district.name}`,
|
||||
`di area ${streetName}`,
|
||||
`di sekitar ${streetName} ${district.name}`,
|
||||
`tidak jauh dari pusat ${district.name}`,
|
||||
`di pinggiran ${district.name}`,
|
||||
];
|
||||
|
||||
const randomLocation =
|
||||
locs[Math.floor(Math.random() * locs.length)];
|
||||
|
||||
const descriptions = [
|
||||
`Kasus ${categoryName.toLowerCase()} ${randomAddress}`,
|
||||
`Laporan ${categoryName.toLowerCase()} terjadi pada ${timestamp} ${randomLocation}`,
|
||||
`${categoryName} dilaporkan ${randomLocation}`,
|
||||
`Insiden ${categoryName.toLowerCase()} terjadi ${randomLocation}`,
|
||||
`Kejadian ${categoryName.toLowerCase()} ${randomLocation}`,
|
||||
`${categoryName} terdeteksi ${randomLocation} pada ${timestamp.toLocaleTimeString()}`,
|
||||
`Pelaporan ${categoryName.toLowerCase()} di ${randomAddress}`,
|
||||
`Kasus ${categoryName.toLowerCase()} terjadi di ${streetName}`,
|
||||
`${categoryName} terjadi di dekat ${placeType.toLowerCase()} ${district.name}`,
|
||||
`Insiden ${categoryName.toLowerCase()} dilaporkan warga setempat ${randomLocation}`,
|
||||
];
|
||||
|
||||
const randomDescription = descriptions[
|
||||
Math.floor(Math.random() * descriptions.length)
|
||||
];
|
||||
|
||||
incidentsToCreate.push({
|
||||
id: incidentId,
|
||||
crime_id: crime.id,
|
||||
crime_category_id: category.categoryId,
|
||||
location_id: undefined as string | undefined,
|
||||
description: randomDescription,
|
||||
victim_count: 0,
|
||||
status: status,
|
||||
timestamp: timestamp,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
await this.chunkedInsertLocations(locationsToCreate);
|
||||
} catch (error) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const createdLocations = await this.prisma.locations.findMany({
|
||||
where: {
|
||||
event_id: event.id,
|
||||
district_id: district.id,
|
||||
address: {
|
||||
in: locationsToCreate
|
||||
.map((loc) => loc.address)
|
||||
.filter((address): address is string =>
|
||||
address !== undefined
|
||||
),
|
||||
},
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
address: true,
|
||||
},
|
||||
});
|
||||
|
||||
const addressToId = new Map<string, string>();
|
||||
for (const loc of createdLocations) {
|
||||
if (loc.address !== null) {
|
||||
addressToId.set(loc.address, loc.id);
|
||||
}
|
||||
}
|
||||
|
||||
for (let i = 0; i < incidentsToCreate.length; i++) {
|
||||
const address = locationsToCreate[i].address;
|
||||
if (typeof address === "string") {
|
||||
incidentsToCreate[i].location_id = addressToId.get(address);
|
||||
}
|
||||
}
|
||||
|
||||
await this.chunkedInsertIncidents(incidentsToCreate);
|
||||
|
||||
incidentsCreated.push(...incidentsToCreate);
|
||||
|
||||
return incidentsCreated;
|
||||
}
|
||||
|
||||
private async chunkedInsertIncidents(data: any[], chunkSize: number = 200) {
|
||||
for (let i = 0; i < data.length; i += chunkSize) {
|
||||
const chunk = data.slice(i, i + chunkSize);
|
||||
await this.prisma.crime_incidents.createMany({
|
||||
data: chunk,
|
||||
skipDuplicates: true,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private async chunkedInsertLocations(
|
||||
locations: any[],
|
||||
chunkSize: number = 200,
|
||||
) {
|
||||
for (let i = 0; i < locations.length; i += chunkSize) {
|
||||
const chunk = locations.slice(i, i + chunkSize);
|
||||
let { error } = await this.supabase
|
||||
.from("locations")
|
||||
.insert(chunk)
|
||||
.select();
|
||||
if (error) {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async run(): Promise<void> {
|
||||
console.log("🌱 Seeding crime incidents data...");
|
||||
|
||||
try {
|
||||
await this.loadCrimeMonthlyData();
|
||||
|
||||
const existingIncidents = await this.prisma.crime_incidents
|
||||
.findFirst({
|
||||
where: {
|
||||
crimes: {
|
||||
source_type: "cbt",
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (existingIncidents) {
|
||||
const existing2025Incidents = await this.prisma.crime_incidents
|
||||
.findFirst({
|
||||
where: {
|
||||
timestamp: {
|
||||
gte: new Date("2025-01-01"),
|
||||
lt: new Date("2026-01-01"),
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (!existing2025Incidents) {
|
||||
await this.generateMock2025Incidents();
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const crimeRecords = await this.prisma.crimes.findMany({
|
||||
where: {
|
||||
month: { not: null },
|
||||
number_of_crime: { gt: 0 },
|
||||
source_type: "cbt",
|
||||
year: { lt: 2025 },
|
||||
},
|
||||
orderBy: [{ district_id: "asc" }, { year: "asc" }, {
|
||||
month: "asc",
|
||||
}],
|
||||
});
|
||||
|
||||
const crimeCategories = await this.prisma.crime_categories
|
||||
.findMany();
|
||||
if (crimeCategories.length === 0) {
|
||||
console.error(
|
||||
"No crime categories found, please seed crime categories first",
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
let totalIncidentsCreated = 0;
|
||||
let skippedMonths = 0;
|
||||
let processingStats = {
|
||||
districts: new Set(),
|
||||
years: new Set(),
|
||||
totalMatched: 0,
|
||||
totalMismatched: 0,
|
||||
totalResolved: 0,
|
||||
totalUnresolved: 0,
|
||||
};
|
||||
|
||||
let yearlyStats = new Map<number, {
|
||||
incidents: number;
|
||||
resolved: number;
|
||||
matched: number;
|
||||
mismatched: number;
|
||||
districts: Set<string>;
|
||||
}>();
|
||||
|
||||
for (const crimeRecord of crimeRecords) {
|
||||
if (crimeRecord.number_of_crime === 0) {
|
||||
skippedMonths++;
|
||||
continue;
|
||||
}
|
||||
|
||||
const key =
|
||||
`${crimeRecord.district_id}-${crimeRecord.month}-${crimeRecord.year}`;
|
||||
const crimeMonthlyInfo = this.crimeMonthlyData.get(key);
|
||||
|
||||
if (
|
||||
!crimeMonthlyInfo || crimeMonthlyInfo.number_of_crime <= 0
|
||||
) {
|
||||
skippedMonths++;
|
||||
continue;
|
||||
}
|
||||
|
||||
processingStats.districts.add(crimeRecord.district_id);
|
||||
processingStats.years.add(crimeRecord.year);
|
||||
|
||||
const incidents = await this.createMockIncidentsForCrime(
|
||||
crimeRecord,
|
||||
crimeCategories,
|
||||
);
|
||||
|
||||
const year = crimeRecord.year || 0;
|
||||
if (!yearlyStats.has(year)) {
|
||||
yearlyStats.set(year, {
|
||||
incidents: 0,
|
||||
resolved: 0,
|
||||
matched: 0,
|
||||
mismatched: 0,
|
||||
districts: new Set(),
|
||||
});
|
||||
}
|
||||
|
||||
const yearStats = yearlyStats.get(year)!;
|
||||
yearStats.incidents += incidents.length;
|
||||
yearStats.districts.add(crimeRecord.district_id);
|
||||
|
||||
const resolvedCount = incidents.filter((inc) =>
|
||||
inc.status === "resolved"
|
||||
).length;
|
||||
yearStats.resolved += resolvedCount;
|
||||
|
||||
if (crimeMonthlyInfo) {
|
||||
const categoryCount =
|
||||
Object.keys(crimeMonthlyInfo.categories).length;
|
||||
yearStats.matched += categoryCount - 1;
|
||||
yearStats.mismatched += 1;
|
||||
}
|
||||
|
||||
totalIncidentsCreated += incidents.length;
|
||||
}
|
||||
|
||||
for (const [year, stats] of Array.from(yearlyStats.entries())) {
|
||||
console.log(`\n📊 ${year} Data Summary:`);
|
||||
console.log(`├─ Total incidents created: ${stats.incidents}`);
|
||||
console.log(`├─ Districts processed: ${stats.districts.size}`);
|
||||
console.log(
|
||||
`├─ Categories: ${stats.matched} matched, ${stats.mismatched} mismatched`,
|
||||
);
|
||||
console.log(`└─ Total resolved cases: ${stats.resolved}`);
|
||||
}
|
||||
|
||||
await this.generateMock2025Incidents();
|
||||
|
||||
console.log("✅ Seeding selesai!");
|
||||
} catch (error) {
|
||||
console.error("❌ Error seeding crime incidents:", error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
private generateDistributedPoints(
|
||||
landArea: number,
|
||||
numPoints: number,
|
||||
districtId: string,
|
||||
districtName: string,
|
||||
): Array<{ latitude: number; longitude: number; radius: number }> {
|
||||
const points = [];
|
||||
const districtNameLower = districtName.toLowerCase();
|
||||
|
||||
const districtCenter = districtCenters.find(
|
||||
(center) => center.kecamatan.toLowerCase() === districtNameLower,
|
||||
);
|
||||
|
||||
if (!districtCenter) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const centerLat = districtCenter.lat;
|
||||
const centerLng = districtCenter.lng;
|
||||
|
||||
let scalingFactor = 0.3;
|
||||
|
||||
const effectiveLandArea = Math.max(landArea || 1, 1);
|
||||
|
||||
const radiusKm = Math.sqrt(effectiveLandArea) * scalingFactor;
|
||||
const radiusDeg = radiusKm / 111;
|
||||
|
||||
for (let i = 0; i < numPoints; i++) {
|
||||
const angle = Math.random() * 2 * Math.PI;
|
||||
const distanceFactor = Math.pow(Math.random(), 1.5);
|
||||
const distance = distanceFactor * radiusDeg;
|
||||
|
||||
const latitude = centerLat + distance * Math.cos(angle);
|
||||
const longitude = centerLng +
|
||||
distance * Math.sin(angle) /
|
||||
Math.cos(centerLat * Math.PI / 180);
|
||||
|
||||
const pointRadius = distance * 111000;
|
||||
|
||||
points.push({
|
||||
latitude,
|
||||
longitude,
|
||||
radius: pointRadius,
|
||||
});
|
||||
}
|
||||
|
||||
return points;
|
||||
}
|
||||
}
|
||||
|
||||
if (require.main === module) {
|
||||
const testSeeder = async () => {
|
||||
const prisma = new PrismaClient();
|
||||
const seeder = new CrimeIncidentsByTypeSeeder(prisma);
|
||||
try {
|
||||
await seeder.run();
|
||||
} catch (e) {
|
||||
console.error("Error during seeding:", e);
|
||||
process.exit(1);
|
||||
} finally {
|
||||
await prisma.$disconnect();
|
||||
}
|
||||
};
|
||||
|
||||
testSeeder();
|
||||
}
|
|
@ -9,6 +9,7 @@ import { createClient } from '../../app/_utils/supabase/client';
|
|||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
import csv from 'csv-parser';
|
||||
import { CRegex } from '../../app/_utils/const/regex';
|
||||
|
||||
type ICreateLocations = {
|
||||
id: string;
|
||||
|
@ -24,7 +25,7 @@ type ICreateLocations = {
|
|||
location: string;
|
||||
};
|
||||
|
||||
export class CrimeIncidentsSeeder {
|
||||
export class CrimeIncidentsByUnitSeeder {
|
||||
private crimeMonthlyData: Map<
|
||||
string,
|
||||
{ number_of_crime: number; crime_cleared: number }
|
||||
|
@ -187,7 +188,14 @@ private generateDistributedPoints(
|
|||
await this.loadCrimeMonthlyData();
|
||||
|
||||
// Check if crime incidents already exist
|
||||
const existingIncidents = await this.prisma.crime_incidents.findFirst();
|
||||
const existingIncidents = await this.prisma.crime_incidents.findFirst({
|
||||
where: {
|
||||
crimes: {
|
||||
source_type: "cbu"
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if (existingIncidents) {
|
||||
console.log('Crime incidents data already exists, skipping import.');
|
||||
return;
|
||||
|
@ -197,8 +205,8 @@ private generateDistributedPoints(
|
|||
const crimeRecords = await this.prisma.crimes.findMany({
|
||||
where: {
|
||||
month: { not: null },
|
||||
// Only process records with incidents (number_of_crime > 0)
|
||||
number_of_crime: { gt: 0 },
|
||||
source_type: "cbu"
|
||||
},
|
||||
orderBy: [{ district_id: 'asc' }, { year: 'asc' }, { month: 'asc' }],
|
||||
});
|
||||
|
@ -466,7 +474,7 @@ private generateDistributedPoints(
|
|||
separator: '-',
|
||||
uniquenessStrategy: 'counter',
|
||||
},
|
||||
/(\d{4})(?=-\d{4}$)/ // Pattern to extract the 4-digit counter
|
||||
CRegex.CR_YEAR_SEQUENCE
|
||||
);
|
||||
|
||||
// Determine status based on crime_cleared
|
||||
|
@ -582,7 +590,7 @@ private generateDistributedPoints(
|
|||
if (require.main === module) {
|
||||
const testSeeder = async () => {
|
||||
const prisma = new PrismaClient();
|
||||
const seeder = new CrimeIncidentsSeeder(prisma);
|
||||
const seeder = new CrimeIncidentsByUnitSeeder(prisma);
|
||||
try {
|
||||
await seeder.run();
|
||||
} catch (e) {
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
import {
|
||||
PrismaClient,
|
||||
crime_incidents,
|
||||
crime_rates,
|
||||
crimes,
|
||||
events,
|
||||
session_status,
|
||||
users,
|
||||
|
@ -9,6 +11,8 @@ import fs from 'fs';
|
|||
import path from 'path';
|
||||
import { parse } from 'csv-parse/sync';
|
||||
import { generateId, generateIdWithDbCounter } from '../../app/_utils/common';
|
||||
import { CRegex } from '../../app/_utils/const/regex';
|
||||
|
||||
|
||||
interface ICreateUser {
|
||||
id: string;
|
||||
|
@ -32,8 +36,6 @@ export class CrimesSeeder {
|
|||
async run(): Promise<void> {
|
||||
console.log('🌱 Seeding crimes data...');
|
||||
|
||||
// Clear existing data
|
||||
|
||||
try {
|
||||
// Create test user
|
||||
const user = await this.createUsers();
|
||||
|
@ -48,15 +50,16 @@ export class CrimesSeeder {
|
|||
// Create 5 sessions with completed status
|
||||
await this.createSessions(user, events);
|
||||
|
||||
// Import monthly crime data
|
||||
// Import original crime data with source_type = 'general'
|
||||
await this.importMonthlyCrimeData();
|
||||
|
||||
// Import yearly crime data from CSV file
|
||||
await this.importYearlyCrimeData();
|
||||
|
||||
// Import all-year crime summaries (2020-2024)
|
||||
await this.importAllYearSummaries();
|
||||
|
||||
// Import new crime data by type with appropriate source_type
|
||||
await this.importMonthlyCrimeDataByType();
|
||||
await this.importYearlyCrimeDataByType();
|
||||
await this.importSummaryByType();
|
||||
|
||||
console.log('✅ Crime seeding completed successfully.');
|
||||
} catch (error) {
|
||||
console.error('❌ Error seeding crimes:', error);
|
||||
|
@ -65,7 +68,6 @@ export class CrimesSeeder {
|
|||
}
|
||||
|
||||
private async createUsers() {
|
||||
// Check if test users already exist
|
||||
const existingUser = await this.prisma.users.findFirst({
|
||||
where: { email: 'sigapcompany@gmail.com' },
|
||||
});
|
||||
|
@ -75,7 +77,6 @@ export class CrimesSeeder {
|
|||
return existingUser;
|
||||
}
|
||||
|
||||
// Get admin role ID
|
||||
let roleId = await this.prisma.roles.findFirst({
|
||||
where: { name: 'admin' },
|
||||
});
|
||||
|
@ -89,7 +90,6 @@ export class CrimesSeeder {
|
|||
});
|
||||
}
|
||||
|
||||
// Create test user directly with Prisma (no Supabase)
|
||||
const newUser = await this.prisma.users.create({
|
||||
data: {
|
||||
email: 'sigapcompany@gmail.com',
|
||||
|
@ -118,7 +118,6 @@ export class CrimesSeeder {
|
|||
}
|
||||
|
||||
private async createEvents(user: ICreateUser) {
|
||||
// Check if events already exist
|
||||
const existingEvent = await this.prisma.events.findFirst({
|
||||
where: {
|
||||
user_id: user.id,
|
||||
|
@ -142,7 +141,6 @@ export class CrimesSeeder {
|
|||
}
|
||||
|
||||
private async createSessions(user: ICreateUser, events: events) {
|
||||
// Check if sessions already exist
|
||||
const existingSession = await this.prisma.sessions.findFirst();
|
||||
|
||||
if (existingSession) {
|
||||
|
@ -161,7 +159,6 @@ export class CrimesSeeder {
|
|||
return newSession;
|
||||
}
|
||||
|
||||
// Helper function for chunked insertion (with default chunk size 500)
|
||||
private async chunkedCreateMany(data: any[], chunkSize: number = 500) {
|
||||
for (let i = 0; i < data.length; i += chunkSize) {
|
||||
const chunk = data.slice(i, i + chunkSize);
|
||||
|
@ -174,33 +171,31 @@ export class CrimesSeeder {
|
|||
private async importMonthlyCrimeData() {
|
||||
console.log('Importing monthly crime data...');
|
||||
|
||||
// Check if crimes already exist
|
||||
const existingCrimes = await this.prisma.crimes.findFirst();
|
||||
const existingCrimes = await this.prisma.crimes.findFirst({
|
||||
where: {
|
||||
source_type: 'cbu',
|
||||
},
|
||||
});
|
||||
|
||||
if (existingCrimes) {
|
||||
console.log('Crimes data already exists, skipping import.');
|
||||
console.log('General crimes data already exists, skipping import.');
|
||||
return;
|
||||
}
|
||||
|
||||
// Read CSV file
|
||||
const csvFilePath = path.resolve(
|
||||
__dirname,
|
||||
'../data/excels/crimes/crime_monthly.csv'
|
||||
'../data/excels/crimes/crime_monthly_by_unit.csv'
|
||||
);
|
||||
const fileContent = fs.readFileSync(csvFilePath, { encoding: 'utf-8' });
|
||||
|
||||
// Parse CSV
|
||||
const records = parse(fileContent, {
|
||||
columns: true,
|
||||
skip_empty_lines: true,
|
||||
});
|
||||
|
||||
// Store unique district IDs to avoid duplicates
|
||||
const processedDistricts = new Set<string>();
|
||||
const crimesData: Array<Partial<crimes>> = [];
|
||||
|
||||
// Prepare batch data
|
||||
const crimesData = [];
|
||||
|
||||
// Process records
|
||||
for (const record of records) {
|
||||
const crimeRate = record.level.toLowerCase() as crime_rates;
|
||||
|
||||
|
@ -216,7 +211,6 @@ export class CrimesSeeder {
|
|||
}
|
||||
|
||||
const year = parseInt(record.year);
|
||||
// Create a unique ID for monthly crime data
|
||||
const crimeId = await generateIdWithDbCounter(
|
||||
'crimes',
|
||||
{
|
||||
|
@ -230,11 +224,9 @@ export class CrimesSeeder {
|
|||
separator: '-',
|
||||
uniquenessStrategy: 'counter',
|
||||
},
|
||||
/(\d{4})(?=-\d{4}$)/ // Pattern to extract the 4-digit counter
|
||||
CRegex.CR_YEAR_SEQUENCE
|
||||
);
|
||||
|
||||
// console.log('Creating crime ID:', crimeId);
|
||||
|
||||
crimesData.push({
|
||||
id: crimeId,
|
||||
district_id: record.district_id,
|
||||
|
@ -243,14 +235,14 @@ export class CrimesSeeder {
|
|||
month: parseInt(record.month_num),
|
||||
year: parseInt(record.year),
|
||||
number_of_crime: parseInt(record.number_of_crime),
|
||||
crime_cleared: parseInt(record.crime_cleared) || 0,
|
||||
score: parseFloat(record.score),
|
||||
source_type: 'cbu',
|
||||
});
|
||||
|
||||
// Keep track of unique districts for later creation of crime incidents
|
||||
processedDistricts.add(record.district_id);
|
||||
}
|
||||
|
||||
// Batch create all crimes in chunks
|
||||
await this.chunkedCreateMany(crimesData);
|
||||
|
||||
console.log(`Imported ${records.length} monthly crime records.`);
|
||||
|
@ -259,9 +251,11 @@ export class CrimesSeeder {
|
|||
private async importYearlyCrimeData() {
|
||||
console.log('Importing yearly crime data...');
|
||||
|
||||
// Check if yearly summaries already exist (records with null month)
|
||||
const existingYearlySummary = await this.prisma.crimes.findFirst({
|
||||
where: { month: null },
|
||||
where: {
|
||||
month: null,
|
||||
source_type: 'cbu',
|
||||
},
|
||||
});
|
||||
|
||||
if (existingYearlySummary) {
|
||||
|
@ -269,23 +263,19 @@ export class CrimesSeeder {
|
|||
return;
|
||||
}
|
||||
|
||||
// Read CSV file
|
||||
const csvFilePath = path.resolve(
|
||||
__dirname,
|
||||
'../data/excels/crimes/crime_yearly.csv'
|
||||
'../data/excels/crimes/crime_yearly_by_unit.csv'
|
||||
);
|
||||
const fileContent = fs.readFileSync(csvFilePath, { encoding: 'utf-8' });
|
||||
|
||||
// Parse CSV
|
||||
const records = parse(fileContent, {
|
||||
columns: true,
|
||||
skip_empty_lines: true,
|
||||
});
|
||||
|
||||
// Prepare batch data
|
||||
const crimesData = [];
|
||||
const crimesData: Array<Partial<crimes>> = [];
|
||||
|
||||
// Process records
|
||||
for (const record of records) {
|
||||
const crimeRate = record.level.toLowerCase() as crime_rates;
|
||||
const year = parseInt(record.year);
|
||||
|
@ -318,7 +308,7 @@ export class CrimesSeeder {
|
|||
separator: '-',
|
||||
uniquenessStrategy: 'counter',
|
||||
},
|
||||
/(\d{4})(?=-\d{4}$)/ // Pattern to extract the 4-digit counter
|
||||
CRegex.CR_YEAR_SEQUENCE
|
||||
);
|
||||
|
||||
crimesData.push({
|
||||
|
@ -329,11 +319,13 @@ export class CrimesSeeder {
|
|||
month: null,
|
||||
year: year,
|
||||
number_of_crime: parseInt(record.number_of_crime),
|
||||
crime_cleared: parseInt(record.crime_cleared) || 0,
|
||||
avg_crime: parseFloat(record.avg_crime) || 0,
|
||||
score: parseInt(record.score),
|
||||
source_type: 'cbu',
|
||||
});
|
||||
}
|
||||
|
||||
// Batch create all yearly crimes in chunks
|
||||
await this.chunkedCreateMany(crimesData);
|
||||
|
||||
console.log(`Imported ${records.length} yearly crime records.`);
|
||||
|
@ -342,9 +334,12 @@ export class CrimesSeeder {
|
|||
private async importAllYearSummaries() {
|
||||
console.log('Importing all-year (2020-2024) crime summaries...');
|
||||
|
||||
// Check if all-year summaries already exist (records with null month and null year)
|
||||
const existingAllYearSummaries = await this.prisma.crimes.findFirst({
|
||||
where: { month: null, year: null },
|
||||
where: {
|
||||
month: null,
|
||||
year: null,
|
||||
source_type: 'cbu',
|
||||
},
|
||||
});
|
||||
|
||||
if (existingAllYearSummaries) {
|
||||
|
@ -352,21 +347,18 @@ export class CrimesSeeder {
|
|||
return;
|
||||
}
|
||||
|
||||
// Read CSV file
|
||||
const csvFilePath = path.resolve(
|
||||
__dirname,
|
||||
'../data/excels/crimes/district_summary_2020_2024.csv'
|
||||
'../data/excels/crimes/crime_summary_by_unit.csv'
|
||||
);
|
||||
const fileContent = fs.readFileSync(csvFilePath, { encoding: 'utf-8' });
|
||||
|
||||
// Parse CSV
|
||||
const records = parse(fileContent, {
|
||||
columns: true,
|
||||
skip_empty_lines: true,
|
||||
});
|
||||
|
||||
// Prepare batch data
|
||||
const crimesData = [];
|
||||
const crimesData: Array<Partial<crimes>> = [];
|
||||
|
||||
for (const record of records) {
|
||||
const crimeRate = record.level.toLowerCase() as crime_rates;
|
||||
|
@ -387,7 +379,6 @@ export class CrimesSeeder {
|
|||
continue;
|
||||
}
|
||||
|
||||
// Create a unique ID for all-year summary data
|
||||
const crimeId = await generateIdWithDbCounter(
|
||||
'crimes',
|
||||
{
|
||||
|
@ -400,7 +391,7 @@ export class CrimesSeeder {
|
|||
separator: '-',
|
||||
uniquenessStrategy: 'counter',
|
||||
},
|
||||
/(\d{4})$/ // Pattern to extract the 4-digit counter at the end
|
||||
CRegex.CR_SEQUENCE_END
|
||||
);
|
||||
|
||||
crimesData.push({
|
||||
|
@ -411,15 +402,253 @@ export class CrimesSeeder {
|
|||
month: null,
|
||||
year: null,
|
||||
number_of_crime: parseInt(record.crime_total),
|
||||
score: parseFloat(record.avg_score),
|
||||
crime_cleared: parseInt(record.crime_cleared) || 0,
|
||||
avg_crime: parseFloat(record.avg_crime) || 0,
|
||||
score: parseFloat(record.score),
|
||||
source_type: 'cbu',
|
||||
});
|
||||
}
|
||||
|
||||
// Batch create all all-year summaries in chunks
|
||||
await this.chunkedCreateMany(crimesData);
|
||||
|
||||
console.log(`Imported ${records.length} all-year crime summaries.`);
|
||||
}
|
||||
|
||||
private async importMonthlyCrimeDataByType() {
|
||||
console.log('Importing monthly crime data by type...');
|
||||
|
||||
const existingCrimeByType = await this.prisma.crimes.findFirst({
|
||||
where: {
|
||||
source_type: 'cbt',
|
||||
},
|
||||
});
|
||||
|
||||
if (existingCrimeByType) {
|
||||
console.log('Crime data by type already exists, skipping import.');
|
||||
return;
|
||||
}
|
||||
|
||||
const csvFilePath = path.resolve(
|
||||
__dirname,
|
||||
'../data/excels/crimes/crime_monthly_by_type.csv'
|
||||
);
|
||||
const fileContent = fs.readFileSync(csvFilePath, { encoding: 'utf-8' });
|
||||
|
||||
const records = parse(fileContent, {
|
||||
columns: true,
|
||||
skip_empty_lines: true,
|
||||
});
|
||||
|
||||
const crimesData: Array<Partial<crimes>> = [];
|
||||
|
||||
for (const record of records) {
|
||||
const crimeRate = record.level.toLowerCase() as crime_rates;
|
||||
|
||||
const city = await this.prisma.cities.findFirst({
|
||||
where: {
|
||||
name: 'Jember',
|
||||
},
|
||||
});
|
||||
|
||||
if (!city) {
|
||||
console.error('City not found: Jember');
|
||||
continue;
|
||||
}
|
||||
|
||||
const year = parseInt(record.year);
|
||||
const crimeId = await generateIdWithDbCounter(
|
||||
'crimes',
|
||||
{
|
||||
prefix: 'CR',
|
||||
segments: {
|
||||
codes: [city.id],
|
||||
sequentialDigits: 4,
|
||||
year,
|
||||
},
|
||||
format: '{prefix}-{codes}-{sequence}-{year}',
|
||||
separator: '-',
|
||||
uniquenessStrategy: 'counter',
|
||||
},
|
||||
CRegex.CR_YEAR_SEQUENCE
|
||||
);
|
||||
|
||||
crimesData.push({
|
||||
id: crimeId,
|
||||
district_id: record.district_id,
|
||||
level: crimeRate,
|
||||
method: record.method || 'kmeans',
|
||||
month: parseInt(record.month_num),
|
||||
year: parseInt(record.year),
|
||||
number_of_crime: parseInt(record.number_of_crime),
|
||||
crime_cleared: parseInt(record.crime_cleared) || 0,
|
||||
score: parseFloat(record.score),
|
||||
source_type: 'cbt',
|
||||
});
|
||||
}
|
||||
|
||||
await this.chunkedCreateMany(crimesData);
|
||||
|
||||
console.log(`Imported ${records.length} monthly crime by type records.`);
|
||||
}
|
||||
|
||||
private async importYearlyCrimeDataByType() {
|
||||
console.log('Importing yearly crime data by type...');
|
||||
|
||||
const existingYearlySummary = await this.prisma.crimes.findFirst({
|
||||
where: {
|
||||
month: null,
|
||||
source_type: 'cbt',
|
||||
},
|
||||
});
|
||||
|
||||
if (existingYearlySummary) {
|
||||
console.log('Yearly crime data by type already exists, skipping import.');
|
||||
return;
|
||||
}
|
||||
|
||||
const csvFilePath = path.resolve(
|
||||
__dirname,
|
||||
'../data/excels/crimes/crime_yearly_by_type.csv'
|
||||
);
|
||||
const fileContent = fs.readFileSync(csvFilePath, { encoding: 'utf-8' });
|
||||
|
||||
const records = parse(fileContent, {
|
||||
columns: true,
|
||||
skip_empty_lines: true,
|
||||
});
|
||||
|
||||
const crimesData: Array<Partial<crimes>> = [];
|
||||
|
||||
for (const record of records) {
|
||||
const crimeRate = record.level.toLowerCase() as crime_rates;
|
||||
const year = parseInt(record.year);
|
||||
|
||||
const city = await this.prisma.cities.findFirst({
|
||||
where: {
|
||||
districts: {
|
||||
some: {
|
||||
id: record.district_id,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (!city) {
|
||||
console.error(`City not found for district ${record.district_id}`);
|
||||
continue;
|
||||
}
|
||||
|
||||
const crimeId = await generateIdWithDbCounter(
|
||||
'crimes',
|
||||
{
|
||||
prefix: 'CR',
|
||||
segments: {
|
||||
codes: [city.id],
|
||||
sequentialDigits: 4,
|
||||
year,
|
||||
},
|
||||
format: '{prefix}-{codes}-{sequence}-{year}',
|
||||
separator: '-',
|
||||
uniquenessStrategy: 'counter',
|
||||
},
|
||||
CRegex.CR_YEAR_SEQUENCE
|
||||
);
|
||||
|
||||
crimesData.push({
|
||||
id: crimeId,
|
||||
district_id: record.district_id,
|
||||
level: crimeRate,
|
||||
method: record.method || 'kmeans',
|
||||
month: null,
|
||||
year: year,
|
||||
number_of_crime: parseInt(record.number_of_crime),
|
||||
crime_cleared: parseInt(record.crime_cleared) || 0,
|
||||
avg_crime: parseFloat(record.avg_crime) || 0,
|
||||
score: parseInt(record.score),
|
||||
source_type: 'cbt',
|
||||
});
|
||||
}
|
||||
|
||||
await this.chunkedCreateMany(crimesData);
|
||||
|
||||
console.log(`Imported ${records.length} yearly crime by type records.`);
|
||||
}
|
||||
|
||||
private async importSummaryByType() {
|
||||
console.log('Importing crime summary by type...');
|
||||
|
||||
const existingSummary = await this.prisma.crimes.findFirst({
|
||||
where: {
|
||||
source_type: 'cbt',
|
||||
},
|
||||
});
|
||||
|
||||
if (existingSummary) {
|
||||
console.log('Crime summary by type already exists, skipping import.');
|
||||
return;
|
||||
}
|
||||
|
||||
const csvFilePath = path.resolve(
|
||||
__dirname,
|
||||
'../data/excels/crimes/crime_summary_by_type.csv'
|
||||
);
|
||||
const fileContent = fs.readFileSync(csvFilePath, { encoding: 'utf-8' });
|
||||
|
||||
const records = parse(fileContent, {
|
||||
columns: true,
|
||||
skip_empty_lines: true,
|
||||
});
|
||||
|
||||
const crimesData: Array<Partial<crimes>> = [];
|
||||
|
||||
for (const record of records) {
|
||||
const crimeRate = record.level.toLowerCase() as crime_rates;
|
||||
|
||||
const city = await this.prisma.cities.findFirst({
|
||||
where: {
|
||||
name: 'Jember',
|
||||
},
|
||||
});
|
||||
|
||||
if (!city) {
|
||||
console.error('City not found: Jember');
|
||||
continue;
|
||||
}
|
||||
|
||||
const crimeId = await generateIdWithDbCounter(
|
||||
'crimes',
|
||||
{
|
||||
prefix: 'CR',
|
||||
segments: {
|
||||
codes: [city.id],
|
||||
sequentialDigits: 4,
|
||||
},
|
||||
format: '{prefix}-{codes}-{sequence}',
|
||||
separator: '-',
|
||||
uniquenessStrategy: 'counter',
|
||||
},
|
||||
CRegex.CR_SEQUENCE_END
|
||||
);
|
||||
|
||||
crimesData.push({
|
||||
id: crimeId,
|
||||
district_id: record.district_id,
|
||||
level: crimeRate,
|
||||
method: 'kmeans',
|
||||
month: null,
|
||||
year: null,
|
||||
number_of_crime: parseInt(record.crime_total),
|
||||
crime_cleared: parseInt(record.crime_cleared) || 0,
|
||||
avg_crime: parseFloat(record.avg_crime) || 0,
|
||||
score: parseFloat(record.score),
|
||||
source_type: 'cbt',
|
||||
});
|
||||
}
|
||||
|
||||
await this.chunkedCreateMany(crimesData);
|
||||
|
||||
console.log(`Imported ${records.length} crime summary by type records.`);
|
||||
}
|
||||
}
|
||||
|
||||
// This allows the file to be executed standalone for testing
|
||||
|
|
|
@ -32,6 +32,9 @@ export class DemographicsSeeder {
|
|||
// Collect demographic data to be inserted in batch
|
||||
const demographicsToInsert = [];
|
||||
|
||||
// Track 2024 data to duplicate for 2025
|
||||
const data2024ByDistrict: Record<string, any> = {};
|
||||
|
||||
for (const row of data) {
|
||||
const districtName = String(row['Kecamatan']).trim();
|
||||
const year = Number(row['Tahun']);
|
||||
|
@ -52,17 +55,38 @@ export class DemographicsSeeder {
|
|||
const populationDensity =
|
||||
districtLandArea > 0 ? population / districtLandArea : 0;
|
||||
|
||||
demographicsToInsert.push({
|
||||
const demographicRecord = {
|
||||
district_id: district.id,
|
||||
year,
|
||||
population,
|
||||
population_density: populationDensity,
|
||||
number_of_unemployed: unemployed,
|
||||
});
|
||||
};
|
||||
|
||||
demographicsToInsert.push(demographicRecord);
|
||||
|
||||
// Store 2024 data for later duplication
|
||||
if (year === 2024) {
|
||||
data2024ByDistrict[district.id] = demographicRecord;
|
||||
}
|
||||
|
||||
counter++;
|
||||
}
|
||||
|
||||
// Create 2025 data based on 2024 data
|
||||
for (const districtId in data2024ByDistrict) {
|
||||
const record2024 = data2024ByDistrict[districtId];
|
||||
const record2025 = {
|
||||
...record2024,
|
||||
year: 2025 // Change year to 2025
|
||||
};
|
||||
|
||||
demographicsToInsert.push(record2025);
|
||||
counter++;
|
||||
}
|
||||
|
||||
console.log(`✅ Added ${Object.keys(data2024ByDistrict).length} demographic records for 2025 based on 2024 data`);
|
||||
|
||||
// Insert all demographic data at once
|
||||
await this.prisma.demographics.createMany({
|
||||
data: demographicsToInsert,
|
||||
|
|
|
@ -57,12 +57,16 @@ export class GeoJSONSeeder {
|
|||
jsonData.forEach((row: any) => {
|
||||
const districtName = row['Kecamatan'];
|
||||
if (districtName) {
|
||||
// Get 2024 value to use for 2025
|
||||
const value2024 = this.parseAreaValue(row['2024']);
|
||||
|
||||
this.areaData[districtName] = {
|
||||
'2020': this.parseAreaValue(row['2020']),
|
||||
'2021': this.parseAreaValue(row['2021']),
|
||||
'2022': this.parseAreaValue(row['2022']),
|
||||
'2023': this.parseAreaValue(row['2023']),
|
||||
'2024': this.parseAreaValue(row['2024']),
|
||||
'2025': value2024, // Use the same value as 2024
|
||||
};
|
||||
}
|
||||
});
|
||||
|
@ -196,7 +200,7 @@ export class GeoJSONSeeder {
|
|||
const districtsToCreate = [];
|
||||
const geographicsToCreate = [];
|
||||
const addressPromises = [];
|
||||
const years = [2020, 2021, 2022, 2023, 2024];
|
||||
const years = [2020, 2021, 2022, 2023, 2024, 2025]; // Added 2025
|
||||
|
||||
// 2. Process all districts first to prepare data
|
||||
for (let i = 0; i < districtGeoJson.features.length; i++) {
|
||||
|
|
|
@ -114,7 +114,6 @@ export class UnitSeeder {
|
|||
});
|
||||
|
||||
unitsToInsert.push({
|
||||
district_id: patrangDistrict.id,
|
||||
city_id: city.id,
|
||||
code_unit: polresId,
|
||||
name: `Polres ${city.name}`,
|
||||
|
|
|
@ -0,0 +1,157 @@
|
|||
|
||||
-- Function to calculate distance from location to its district's assigned police unit
|
||||
CREATE OR REPLACE FUNCTION gis.calculate_distance_to_district_unit()
|
||||
RETURNS TRIGGER AS $$
|
||||
BEGIN
|
||||
-- Calculate the distance to the unit assigned to this district
|
||||
SELECT ST_Distance(
|
||||
NEW.location::geography,
|
||||
u.location::geography
|
||||
) / 1000 -- Convert to kilometers
|
||||
INTO NEW.distance_to_unit
|
||||
FROM units u
|
||||
WHERE u.district_id = NEW.district_id;
|
||||
|
||||
-- If no unit found for this district, set distance_to_unit to NULL
|
||||
-- This indicates that the district doesn't have an assigned unit
|
||||
IF NEW.distance_to_unit IS NULL THEN
|
||||
RAISE NOTICE 'No assigned unit found for district id %', NEW.district_id;
|
||||
END IF;
|
||||
|
||||
RETURN NEW;
|
||||
END;
|
||||
$$ LANGUAGE plpgsql;
|
||||
|
||||
-- Create trigger to automatically calculate distance when a location is inserted or updated
|
||||
CREATE TRIGGER location_distance_to_unit_trigger
|
||||
BEFORE INSERT OR UPDATE ON locations
|
||||
FOR EACH ROW
|
||||
EXECUTE FUNCTION gis.calculate_distance_to_district_unit();
|
||||
|
||||
-- Comment explaining what this migration does
|
||||
COMMENT ON FUNCTION gis.calculate_distance_to_district_unit() IS 'Calculates the distance from a location to its district''s assigned police unit and populates the distance_to_unit field';
|
||||
COMMENT ON TRIGGER location_distance_to_unit_trigger ON locations IS 'Automatically calculates distance to district''s assigned police unit when a location is inserted or updated';
|
||||
|
||||
-- Function to automatically create resource entries when new tables are created
|
||||
-- and then create CRUD permissions for all admin users
|
||||
-- Modified to work without event triggers
|
||||
CREATE OR REPLACE FUNCTION gis.create_resource_and_permissions_for_table(table_name text)
|
||||
RETURNS void AS $$
|
||||
DECLARE
|
||||
resource_id uuid;
|
||||
admin_role_id uuid;
|
||||
BEGIN
|
||||
-- Get the admin role ID
|
||||
SELECT id INTO admin_role_id FROM roles WHERE name = 'admin';
|
||||
|
||||
-- If admin role doesn't exist, log a notice and exit
|
||||
IF admin_role_id IS NULL THEN
|
||||
RAISE NOTICE 'Admin role not found. No permissions created.';
|
||||
RETURN;
|
||||
END IF;
|
||||
|
||||
-- Skip system tables and migration tables
|
||||
IF table_name ~ '^(pg_|_|migrations)' THEN
|
||||
RETURN;
|
||||
END IF;
|
||||
|
||||
-- Check if resource already exists for this table
|
||||
IF NOT EXISTS (SELECT 1 FROM resources WHERE name = table_name) THEN
|
||||
-- Create new resource entry
|
||||
INSERT INTO resources (
|
||||
id,
|
||||
name,
|
||||
type,
|
||||
description,
|
||||
instance_role,
|
||||
relations,
|
||||
attributes,
|
||||
created_at,
|
||||
updated_at
|
||||
) VALUES (
|
||||
gen_random_uuid(),
|
||||
table_name,
|
||||
'table',
|
||||
'Auto-generated resource for table ' || table_name,
|
||||
NULL,
|
||||
NULL,
|
||||
'{"auto_generated": true}'::jsonb,
|
||||
NOW(),
|
||||
NOW()
|
||||
)
|
||||
RETURNING id INTO resource_id;
|
||||
|
||||
RAISE NOTICE 'Created new resource for table %', table_name;
|
||||
|
||||
-- Create CRUD permissions for admin role
|
||||
-- Create permission
|
||||
INSERT INTO permissions (id, action, resource_id, role_id, created_at, updated_at)
|
||||
VALUES (
|
||||
gen_random_uuid(),
|
||||
'create',
|
||||
resource_id,
|
||||
admin_role_id,
|
||||
NOW(),
|
||||
NOW()
|
||||
);
|
||||
|
||||
-- Read permission
|
||||
INSERT INTO permissions (id, action, resource_id, role_id, created_at, updated_at)
|
||||
VALUES (
|
||||
gen_random_uuid(),
|
||||
'read',
|
||||
resource_id,
|
||||
admin_role_id,
|
||||
NOW(),
|
||||
NOW()
|
||||
);
|
||||
|
||||
-- Update permission
|
||||
INSERT INTO permissions (id, action, resource_id, role_id, created_at, updated_at)
|
||||
VALUES (
|
||||
gen_random_uuid(),
|
||||
'update',
|
||||
resource_id,
|
||||
admin_role_id,
|
||||
NOW(),
|
||||
NOW()
|
||||
);
|
||||
|
||||
-- Delete permission
|
||||
INSERT INTO permissions (id, action, resource_id, role_id, created_at, updated_at)
|
||||
VALUES (
|
||||
gen_random_uuid(),
|
||||
'delete',
|
||||
resource_id,
|
||||
admin_role_id,
|
||||
NOW(),
|
||||
NOW()
|
||||
);
|
||||
|
||||
RAISE NOTICE 'Created CRUD permissions for admin role on resource %', table_name;
|
||||
END IF;
|
||||
END;
|
||||
$$ LANGUAGE plpgsql;
|
||||
|
||||
-- Function to backfill resources and permissions for existing tables
|
||||
CREATE OR REPLACE FUNCTION gis.backfill_resources_and_permissions()
|
||||
RETURNS void AS $$
|
||||
DECLARE
|
||||
table_record record;
|
||||
BEGIN
|
||||
-- For each existing table in the public schema
|
||||
FOR table_record IN
|
||||
SELECT table_name
|
||||
FROM information_schema.tables
|
||||
WHERE table_schema = 'public'
|
||||
AND table_type = 'BASE TABLE'
|
||||
LOOP
|
||||
-- Call the function to create resource and permissions for each table
|
||||
PERFORM gis.create_resource_and_permissions_for_table(table_record.table_name);
|
||||
END LOOP;
|
||||
END;
|
||||
$$ LANGUAGE plpgsql;
|
||||
|
||||
-- Comments explaining the functions
|
||||
COMMENT ON FUNCTION gis.create_resource_and_permissions_for_table(text) IS 'Creates resource entry and admin CRUD permissions for the specified table';
|
||||
COMMENT ON FUNCTION gis.backfill_resources_and_permissions() IS 'Function to backfill resources and permissions for existing tables';
|
Loading…
Reference in New Issue