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 {
|
export class CRegex {
|
||||||
static readonly PHONE_REGEX = /^(\+62|62|0)[8][1-9][0-9]{6,11}$/;
|
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 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
|
district_id,district_name,crime_total,crime_cleared,avg_crime,score,level
|
||||||
350901,Jombang,41,41,8.2,99,low
|
350901,Jombang,112,41,10.25,99,low
|
||||||
350902,Kencong,38,37,7.6,100,low
|
350902,Kencong,95,37,9.5,100,low
|
||||||
350903,Sumberbaru,400,332,80.0,26,medium
|
350903,Sumberbaru,458,332,100,26,medium
|
||||||
350904,Gumukmas,401,337,80.2,26,medium
|
350904,Gumukmas,456,337,100.25,26,medium
|
||||||
350905,Umbulsari,45,44,9.0,98,low
|
350905,Umbulsari,124,44,11.25,98,low
|
||||||
350906,Tanggul,388,337,77.6,28,medium
|
350906,Tanggul,501,337,97,28,medium
|
||||||
350907,Semboro,49,47,9.8,98,low
|
350907,Semboro,127,47,12.25,98,low
|
||||||
350908,Puger,448,385,89.6,16,medium
|
350908,Puger,553,385,112,16,medium
|
||||||
350909,Bangsalsari,473,403,94.6,11,medium
|
350909,Bangsalsari,568,403,118.25,11,medium
|
||||||
350910,Balung,524,422,104.8,0,medium
|
350910,Balung,582,422,131,0,medium
|
||||||
350911,Wuluhan,458,379,91.6,14,medium
|
350911,Wuluhan,520,379,114.5,14,medium
|
||||||
350912,Ambulu,442,366,88.4,17,medium
|
350912,Ambulu,534,366,110.5,17,medium
|
||||||
350913,Rambipuji,426,373,85.2,21,medium
|
350913,Rambipuji,500,373,106.5,21,medium
|
||||||
350914,Panti,45,43,9.0,98,low
|
350914,Panti,135,43,11.25,98,low
|
||||||
350915,Sukorambi,45,44,9.0,98,low
|
350915,Sukorambi,134,44,11.25,98,low
|
||||||
350916,Jenggawah,438,362,87.6,18,medium
|
350916,Jenggawah,515,362,109.5,18,medium
|
||||||
350917,Ajung,429,363,85.8,20,medium
|
350917,Ajung,516,363,107.25,20,medium
|
||||||
350918,Tempurejo,147,143,29.4,78,low
|
350918,Tempurejo,208,143,36.75,78,low
|
||||||
350919,Kaliwates,476,400,95.2,10,high
|
350919,Kaliwates,547,400,119,10,high
|
||||||
350920,Patrang,508,416,101.6,4,high
|
350920,Patrang,574,416,127,4,high
|
||||||
350921,Sumbersari,422,359,84.4,21,high
|
350921,Sumbersari,529,359,105.5,21,high
|
||||||
350922,Arjasa,57,56,11.4,96,low
|
350922,Arjasa,156,56,14.25,96,low
|
||||||
350923,Mumbulsari,35,35,7.0,100,low
|
350923,Mumbulsari,103,35,8.75,100,low
|
||||||
350924,Pakusari,52,52,10.4,97,low
|
350924,Pakusari,113,52,13,97,low
|
||||||
350925,Jelbuk,42,41,8.4,99,low
|
350925,Jelbuk,144,41,10.5,99,low
|
||||||
350926,Mayang,51,51,10.2,97,low
|
350926,Mayang,141,51,12.75,97,low
|
||||||
350927,Kalisat,45,45,9.0,98,low
|
350927,Kalisat,112,45,11.25,98,low
|
||||||
350928,Ledokombo,47,45,9.4,98,low
|
350928,Ledokombo,118,45,11.75,98,low
|
||||||
350929,Sukowono,46,46,9.2,98,low
|
350929,Sukowono,112,46,11.5,98,low
|
||||||
350930,Silo,433,381,86.6,19,medium
|
350930,Silo,522,381,108.25,19,medium
|
||||||
350931,Sumberjambe,43,43,8.6,99,low
|
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
|
district_id,district_name,number_of_crime,avg_crime,crime_cleared,level,score,method,year
|
||||||
350901,Jombang,10,low,93,kmeans,2020
|
350901,Jombang,10,0.83,10,low,93,kmeans,2020
|
||||||
350901,Jombang,19,low,87,kmeans,2021
|
350901,Jombang,19,1.58,19,low,87,kmeans,2021
|
||||||
350901,Jombang,4,low,98,kmeans,2022
|
350901,Jombang,4,0.33,4,low,98,kmeans,2022
|
||||||
350901,Jombang,4,low,98,kmeans,2023
|
350901,Jombang,4,0.33,4,low,98,kmeans,2023
|
||||||
350901,Jombang,4,low,98,kmeans,2024
|
350901,Jombang,4,0.33,4,low,98,kmeans,2024
|
||||||
350902,Kencong,6,low,96,kmeans,2022
|
350901,Jombang,71,5.92,40,medium,50,kmeans,2025
|
||||||
350902,Kencong,10,low,93,kmeans,2021
|
350902,Kencong,11,0.92,9,low,93,kmeans,2020
|
||||||
350902,Kencong,11,low,93,kmeans,2020
|
350902,Kencong,10,0.83,10,low,93,kmeans,2021
|
||||||
350902,Kencong,7,low,95,kmeans,2023
|
350902,Kencong,6,0.5,6,low,96,kmeans,2022
|
||||||
350902,Kencong,4,low,98,kmeans,2024
|
350902,Kencong,7,0.58,7,low,95,kmeans,2023
|
||||||
350903,Sumberbaru,82,medium,42,kmeans,2024
|
350902,Kencong,4,0.33,5,low,98,kmeans,2024
|
||||||
350903,Sumberbaru,75,medium,47,kmeans,2022
|
350902,Kencong,57,4.75,7,medium,60,kmeans,2025
|
||||||
350903,Sumberbaru,109,medium,23,kmeans,2021
|
350903,Sumberbaru,85,7.08,73,medium,40,kmeans,2020
|
||||||
350903,Sumberbaru,49,medium,65,kmeans,2023
|
350903,Sumberbaru,109,9.08,87,medium,23,kmeans,2021
|
||||||
350903,Sumberbaru,85,medium,40,kmeans,2020
|
350903,Sumberbaru,75,6.25,63,medium,47,kmeans,2022
|
||||||
350904,Gumukmas,101,medium,28,kmeans,2020
|
350903,Sumberbaru,49,4.08,39,medium,65,kmeans,2023
|
||||||
350904,Gumukmas,73,medium,48,kmeans,2022
|
350903,Sumberbaru,82,6.83,70,medium,42,kmeans,2024
|
||||||
350904,Gumukmas,104,medium,26,kmeans,2021
|
350903,Sumberbaru,58,4.83,38,medium,59,kmeans,2025
|
||||||
350904,Gumukmas,74,low,48,kmeans,2024
|
350904,Gumukmas,101,8.42,80,medium,28,kmeans,2020
|
||||||
350904,Gumukmas,49,low,65,kmeans,2023
|
350904,Gumukmas,104,8.67,88,medium,26,kmeans,2021
|
||||||
350905,Umbulsari,7,low,95,kmeans,2022
|
350904,Gumukmas,73,6.08,69,medium,48,kmeans,2022
|
||||||
350905,Umbulsari,5,low,97,kmeans,2023
|
350904,Gumukmas,49,4.08,40,medium,65,kmeans,2023
|
||||||
350905,Umbulsari,17,low,88,kmeans,2021
|
350904,Gumukmas,74,6.17,60,low,48,kmeans,2024
|
||||||
350905,Umbulsari,14,low,90,kmeans,2020
|
350904,Gumukmas,55,4.58,32,medium,61,kmeans,2025
|
||||||
350905,Umbulsari,2,low,99,kmeans,2024
|
350905,Umbulsari,14,1.17,13,low,90,kmeans,2020
|
||||||
350906,Tanggul,102,medium,28,kmeans,2020
|
350905,Umbulsari,17,1.42,17,low,88,kmeans,2021
|
||||||
350906,Tanggul,69,medium,51,kmeans,2023
|
350905,Umbulsari,7,0.58,7,low,95,kmeans,2022
|
||||||
350906,Tanggul,95,medium,33,kmeans,2021
|
350905,Umbulsari,5,0.42,5,low,97,kmeans,2023
|
||||||
350906,Tanggul,29,low,80,kmeans,2024
|
350905,Umbulsari,2,0.17,2,low,99,kmeans,2024
|
||||||
350906,Tanggul,93,medium,34,kmeans,2022
|
350905,Umbulsari,79,6.58,33,medium,44,kmeans,2025
|
||||||
350907,Semboro,6,low,96,kmeans,2022
|
350906,Tanggul,102,8.5,78,medium,28,kmeans,2020
|
||||||
350907,Semboro,5,low,97,kmeans,2023
|
350906,Tanggul,95,7.92,86,medium,33,kmeans,2021
|
||||||
350907,Semboro,21,low,85,kmeans,2021
|
350906,Tanggul,93,7.75,83,medium,34,kmeans,2022
|
||||||
350907,Semboro,4,low,98,kmeans,2024
|
350906,Tanggul,69,5.75,63,medium,51,kmeans,2023
|
||||||
350907,Semboro,13,low,91,kmeans,2020
|
350906,Tanggul,29,2.42,27,low,80,kmeans,2024
|
||||||
350908,Puger,102,medium,28,kmeans,2020
|
350906,Tanggul,113,9.42,20,medium,20,kmeans,2025
|
||||||
350908,Puger,72,medium,49,kmeans,2024
|
350907,Semboro,13,1.08,11,low,91,kmeans,2020
|
||||||
350908,Puger,98,medium,30,kmeans,2021
|
350907,Semboro,21,1.75,21,low,85,kmeans,2021
|
||||||
350908,Puger,94,medium,33,kmeans,2023
|
350907,Semboro,6,0.5,6,low,96,kmeans,2022
|
||||||
350908,Puger,82,medium,42,kmeans,2022
|
350907,Semboro,5,0.42,5,low,97,kmeans,2023
|
||||||
350909,Bangsalsari,116,medium,18,kmeans,2022
|
350907,Semboro,4,0.33,4,low,98,kmeans,2024
|
||||||
350909,Bangsalsari,75,medium,47,kmeans,2023
|
350907,Semboro,78,6.5,32,medium,45,kmeans,2025
|
||||||
350909,Bangsalsari,121,medium,14,kmeans,2021
|
350908,Puger,102,8.5,85,medium,28,kmeans,2020
|
||||||
350909,Bangsalsari,64,medium,55,kmeans,2024
|
350908,Puger,98,8.17,84,medium,30,kmeans,2021
|
||||||
350909,Bangsalsari,97,medium,31,kmeans,2020
|
350908,Puger,82,6.83,74,medium,42,kmeans,2022
|
||||||
350910,Balung,122,medium,13,kmeans,2020
|
350908,Puger,94,7.83,76,medium,33,kmeans,2023
|
||||||
350910,Balung,92,medium,35,kmeans,2023
|
350908,Puger,72,6,66,medium,49,kmeans,2024
|
||||||
350910,Balung,127,medium,10,kmeans,2021
|
350908,Puger,105,8.75,64,medium,25,kmeans,2025
|
||||||
350910,Balung,102,medium,28,kmeans,2024
|
350909,Bangsalsari,97,8.08,80,medium,31,kmeans,2020
|
||||||
350910,Balung,81,medium,43,kmeans,2022
|
350909,Bangsalsari,121,10.08,103,medium,14,kmeans,2021
|
||||||
350911,Wuluhan,72,medium,49,kmeans,2022
|
350909,Bangsalsari,116,9.67,102,medium,18,kmeans,2022
|
||||||
350911,Wuluhan,74,medium,48,kmeans,2024
|
350909,Bangsalsari,75,6.25,65,medium,47,kmeans,2023
|
||||||
350911,Wuluhan,132,medium,6,kmeans,2021
|
350909,Bangsalsari,64,5.33,53,medium,55,kmeans,2024
|
||||||
350911,Wuluhan,84,medium,40,kmeans,2023
|
350909,Bangsalsari,95,7.92,55,medium,33,kmeans,2025
|
||||||
350911,Wuluhan,96,medium,32,kmeans,2020
|
350910,Balung,122,10.17,94,medium,13,kmeans,2020
|
||||||
350912,Ambulu,99,medium,30,kmeans,2020
|
350910,Balung,127,10.58,104,medium,10,kmeans,2021
|
||||||
350912,Ambulu,70,medium,50,kmeans,2024
|
350910,Balung,81,6.75,71,medium,43,kmeans,2022
|
||||||
350912,Ambulu,97,medium,31,kmeans,2021
|
350910,Balung,92,7.67,74,medium,35,kmeans,2023
|
||||||
350912,Ambulu,99,medium,30,kmeans,2023
|
350910,Balung,102,8.5,79,medium,28,kmeans,2024
|
||||||
350912,Ambulu,77,medium,45,kmeans,2022
|
350910,Balung,58,4.83,16,medium,59,kmeans,2025
|
||||||
350913,Rambipuji,104,medium,26,kmeans,2022
|
350911,Wuluhan,96,8,77,medium,32,kmeans,2020
|
||||||
350913,Rambipuji,68,medium,52,kmeans,2023
|
350911,Wuluhan,132,11,103,medium,6,kmeans,2021
|
||||||
350913,Rambipuji,103,medium,27,kmeans,2021
|
350911,Wuluhan,72,6,67,medium,49,kmeans,2022
|
||||||
350913,Rambipuji,103,medium,27,kmeans,2020
|
350911,Wuluhan,84,7,70,medium,40,kmeans,2023
|
||||||
350913,Rambipuji,48,medium,66,kmeans,2024
|
350911,Wuluhan,74,6.17,62,medium,48,kmeans,2024
|
||||||
350914,Panti,11,low,93,kmeans,2020
|
350911,Wuluhan,62,5.17,31,medium,56,kmeans,2025
|
||||||
350914,Panti,5,low,97,kmeans,2023
|
350912,Ambulu,99,8.25,79,medium,30,kmeans,2020
|
||||||
350914,Panti,19,low,87,kmeans,2021
|
350912,Ambulu,97,8.08,83,medium,31,kmeans,2021
|
||||||
350914,Panti,3,low,98,kmeans,2024
|
350912,Ambulu,77,6.42,73,medium,45,kmeans,2022
|
||||||
350914,Panti,7,low,95,kmeans,2022
|
350912,Ambulu,99,8.25,75,medium,30,kmeans,2023
|
||||||
350915,Sukorambi,4,low,98,kmeans,2022
|
350912,Ambulu,70,5.83,56,medium,50,kmeans,2024
|
||||||
350915,Sukorambi,5,low,97,kmeans,2024
|
350912,Ambulu,92,7.67,55,medium,35,kmeans,2025
|
||||||
350915,Sukorambi,19,low,87,kmeans,2021
|
350913,Rambipuji,103,8.58,86,medium,27,kmeans,2020
|
||||||
350915,Sukorambi,6,low,96,kmeans,2023
|
350913,Rambipuji,103,8.58,91,medium,27,kmeans,2021
|
||||||
350915,Sukorambi,11,low,93,kmeans,2020
|
350913,Rambipuji,104,8.67,96,medium,26,kmeans,2022
|
||||||
350916,Jenggawah,59,medium,58,kmeans,2023
|
350913,Rambipuji,68,5.67,58,medium,52,kmeans,2023
|
||||||
350916,Jenggawah,66,medium,53,kmeans,2024
|
350913,Rambipuji,48,4,42,medium,66,kmeans,2024
|
||||||
350916,Jenggawah,96,medium,32,kmeans,2022
|
350913,Rambipuji,74,6.17,47,medium,48,kmeans,2025
|
||||||
350916,Jenggawah,106,medium,25,kmeans,2020
|
350914,Panti,11,0.92,9,low,93,kmeans,2020
|
||||||
350916,Jenggawah,111,medium,21,kmeans,2021
|
350914,Panti,19,1.58,19,low,87,kmeans,2021
|
||||||
350917,Ajung,107,medium,24,kmeans,2021
|
350914,Panti,7,0.58,7,low,95,kmeans,2022
|
||||||
350917,Ajung,82,medium,42,kmeans,2020
|
350914,Panti,5,0.42,5,low,97,kmeans,2023
|
||||||
350917,Ajung,95,medium,33,kmeans,2022
|
350914,Panti,3,0.25,3,low,98,kmeans,2024
|
||||||
350917,Ajung,82,medium,42,kmeans,2024
|
350914,Panti,90,7.5,54,medium,36,kmeans,2025
|
||||||
350917,Ajung,63,medium,55,kmeans,2023
|
350915,Sukorambi,11,0.92,10,low,93,kmeans,2020
|
||||||
350918,Tempurejo,15,low,90,kmeans,2023
|
350915,Sukorambi,19,1.58,19,low,87,kmeans,2021
|
||||||
350918,Tempurejo,17,low,88,kmeans,2024
|
350915,Sukorambi,4,0.33,4,low,98,kmeans,2022
|
||||||
350918,Tempurejo,27,low,81,kmeans,2022
|
350915,Sukorambi,6,0.5,6,low,96,kmeans,2023
|
||||||
350918,Tempurejo,39,low,73,kmeans,2020
|
350915,Sukorambi,5,0.42,5,low,97,kmeans,2024
|
||||||
350918,Tempurejo,49,low,65,kmeans,2021
|
350915,Sukorambi,89,7.42,21,medium,37,kmeans,2025
|
||||||
350919,Kaliwates,124,high,12,kmeans,2021
|
350916,Jenggawah,106,8.83,83,medium,25,kmeans,2020
|
||||||
350919,Kaliwates,100,high,29,kmeans,2024
|
350916,Jenggawah,111,9.25,92,medium,21,kmeans,2021
|
||||||
350919,Kaliwates,93,high,34,kmeans,2022
|
350916,Jenggawah,96,8,80,medium,32,kmeans,2022
|
||||||
350919,Kaliwates,89,high,37,kmeans,2020
|
350916,Jenggawah,59,4.92,54,medium,58,kmeans,2023
|
||||||
350919,Kaliwates,70,high,50,kmeans,2023
|
350916,Jenggawah,66,5.5,53,medium,53,kmeans,2024
|
||||||
350920,Patrang,52,high,63,kmeans,2023
|
350916,Jenggawah,77,6.42,27,medium,45,kmeans,2025
|
||||||
350920,Patrang,104,high,26,kmeans,2024
|
350917,Ajung,82,6.83,71,medium,42,kmeans,2020
|
||||||
350920,Patrang,88,high,38,kmeans,2022
|
350917,Ajung,107,8.92,92,medium,24,kmeans,2021
|
||||||
350920,Patrang,124,high,12,kmeans,2020
|
350917,Ajung,95,7.92,87,medium,33,kmeans,2022
|
||||||
350920,Patrang,140,high,0,kmeans,2021
|
350917,Ajung,63,5.25,56,medium,55,kmeans,2023
|
||||||
350921,Sumbersari,89,high,37,kmeans,2021
|
350917,Ajung,82,6.83,57,medium,42,kmeans,2024
|
||||||
350921,Sumbersari,94,medium,33,kmeans,2020
|
350917,Ajung,87,7.25,66,medium,38,kmeans,2025
|
||||||
350921,Sumbersari,107,high,24,kmeans,2022
|
350918,Tempurejo,39,3.25,35,low,73,kmeans,2020
|
||||||
350921,Sumbersari,53,high,63,kmeans,2023
|
350918,Tempurejo,49,4.08,48,low,65,kmeans,2021
|
||||||
350921,Sumbersari,79,high,44,kmeans,2024
|
350918,Tempurejo,27,2.25,27,low,81,kmeans,2022
|
||||||
350922,Arjasa,6,low,96,kmeans,2023
|
350918,Tempurejo,15,1.25,16,low,90,kmeans,2023
|
||||||
350922,Arjasa,8,low,95,kmeans,2020
|
350918,Tempurejo,17,1.42,17,low,88,kmeans,2024
|
||||||
350922,Arjasa,14,low,90,kmeans,2022
|
350918,Tempurejo,61,5.08,26,medium,57,kmeans,2025
|
||||||
350922,Arjasa,3,low,98,kmeans,2024
|
350919,Kaliwates,89,7.42,73,high,37,kmeans,2020
|
||||||
350922,Arjasa,26,low,82,kmeans,2021
|
350919,Kaliwates,124,10.33,105,high,12,kmeans,2021
|
||||||
350923,Mumbulsari,17,low,88,kmeans,2021
|
350919,Kaliwates,93,7.75,80,high,34,kmeans,2022
|
||||||
350923,Mumbulsari,4,low,98,kmeans,2024
|
350919,Kaliwates,70,5.83,60,high,50,kmeans,2023
|
||||||
350923,Mumbulsari,2,low,99,kmeans,2022
|
350919,Kaliwates,100,8.33,82,high,29,kmeans,2024
|
||||||
350923,Mumbulsari,10,low,93,kmeans,2020
|
350919,Kaliwates,71,5.92,46,medium,50,kmeans,2025
|
||||||
350923,Mumbulsari,2,low,99,kmeans,2023
|
350920,Patrang,124,10.33,98,high,12,kmeans,2020
|
||||||
350924,Pakusari,7,low,95,kmeans,2023
|
350920,Patrang,140,11.67,110,high,0,kmeans,2021
|
||||||
350924,Pakusari,3,low,98,kmeans,2024
|
350920,Patrang,88,7.33,83,high,38,kmeans,2022
|
||||||
350924,Pakusari,10,low,93,kmeans,2022
|
350920,Patrang,52,4.33,48,high,63,kmeans,2023
|
||||||
350924,Pakusari,11,low,93,kmeans,2020
|
350920,Patrang,104,8.67,77,high,26,kmeans,2024
|
||||||
350924,Pakusari,21,low,85,kmeans,2021
|
350920,Patrang,66,5.5,27,medium,53,kmeans,2025
|
||||||
350925,Jelbuk,21,low,85,kmeans,2021
|
350921,Sumbersari,94,7.83,77,medium,33,kmeans,2020
|
||||||
350925,Jelbuk,12,low,92,kmeans,2020
|
350921,Sumbersari,89,7.42,79,high,37,kmeans,2021
|
||||||
350925,Jelbuk,6,low,96,kmeans,2022
|
350921,Sumbersari,107,8.92,89,high,24,kmeans,2022
|
||||||
350925,Jelbuk,0,low,100,kmeans,2024
|
350921,Sumbersari,53,4.42,51,high,63,kmeans,2023
|
||||||
350925,Jelbuk,3,low,98,kmeans,2023
|
350921,Sumbersari,79,6.58,63,high,44,kmeans,2024
|
||||||
350926,Mayang,7,low,95,kmeans,2023
|
350921,Sumbersari,107,8.92,42,medium,24,kmeans,2025
|
||||||
350926,Mayang,16,low,89,kmeans,2020
|
350922,Arjasa,8,0.67,6,low,95,kmeans,2020
|
||||||
350926,Mayang,6,low,96,kmeans,2022
|
350922,Arjasa,26,2.17,26,low,82,kmeans,2021
|
||||||
350926,Mayang,18,low,88,kmeans,2021
|
350922,Arjasa,14,1.17,14,low,90,kmeans,2022
|
||||||
350926,Mayang,4,low,98,kmeans,2024
|
350922,Arjasa,6,0.5,6,low,96,kmeans,2023
|
||||||
350927,Kalisat,16,low,89,kmeans,2020
|
350922,Arjasa,3,0.25,4,low,98,kmeans,2024
|
||||||
350927,Kalisat,15,low,90,kmeans,2021
|
350922,Arjasa,99,8.25,56,medium,30,kmeans,2025
|
||||||
350927,Kalisat,4,low,98,kmeans,2023
|
350923,Mumbulsari,10,0.83,9,low,93,kmeans,2020
|
||||||
350927,Kalisat,5,low,97,kmeans,2022
|
350923,Mumbulsari,17,1.42,18,low,88,kmeans,2021
|
||||||
350927,Kalisat,5,low,97,kmeans,2024
|
350923,Mumbulsari,2,0.17,2,low,99,kmeans,2022
|
||||||
350928,Ledokombo,5,low,97,kmeans,2024
|
350923,Mumbulsari,2,0.17,2,low,99,kmeans,2023
|
||||||
350928,Ledokombo,18,low,88,kmeans,2021
|
350923,Mumbulsari,4,0.33,4,low,98,kmeans,2024
|
||||||
350928,Ledokombo,10,low,93,kmeans,2020
|
350923,Mumbulsari,68,5.67,35,medium,52,kmeans,2025
|
||||||
350928,Ledokombo,10,low,93,kmeans,2022
|
350924,Pakusari,11,0.92,12,low,93,kmeans,2020
|
||||||
350928,Ledokombo,4,low,98,kmeans,2023
|
350924,Pakusari,21,1.75,21,low,85,kmeans,2021
|
||||||
350929,Sukowono,6,low,96,kmeans,2023
|
350924,Pakusari,10,0.83,10,low,93,kmeans,2022
|
||||||
350929,Sukowono,7,low,95,kmeans,2022
|
350924,Pakusari,7,0.58,7,low,95,kmeans,2023
|
||||||
350929,Sukowono,11,low,93,kmeans,2020
|
350924,Pakusari,3,0.25,3,low,98,kmeans,2024
|
||||||
350929,Sukowono,20,low,86,kmeans,2021
|
350924,Pakusari,61,5.08,43,medium,57,kmeans,2025
|
||||||
350929,Sukowono,2,low,99,kmeans,2024
|
350925,Jelbuk,12,1,11,low,92,kmeans,2020
|
||||||
350930,Silo,68,medium,52,kmeans,2024
|
350925,Jelbuk,21,1.75,21,low,85,kmeans,2021
|
||||||
350930,Silo,86,medium,39,kmeans,2022
|
350925,Jelbuk,6,0.5,6,low,96,kmeans,2022
|
||||||
350930,Silo,81,medium,43,kmeans,2023
|
350925,Jelbuk,3,0.25,3,low,98,kmeans,2023
|
||||||
350930,Silo,89,medium,37,kmeans,2021
|
350925,Jelbuk,0,0,0,low,100,kmeans,2024
|
||||||
350930,Silo,109,medium,23,kmeans,2020
|
350925,Jelbuk,102,8.5,76,medium,28,kmeans,2025
|
||||||
350931,Sumberjambe,7,low,95,kmeans,2020
|
350926,Mayang,16,1.33,15,low,89,kmeans,2020
|
||||||
350931,Sumberjambe,19,low,87,kmeans,2021
|
350926,Mayang,18,1.5,19,low,88,kmeans,2021
|
||||||
350931,Sumberjambe,8,low,95,kmeans,2023
|
350926,Mayang,6,0.5,6,low,96,kmeans,2022
|
||||||
350931,Sumberjambe,5,low,97,kmeans,2022
|
350926,Mayang,7,0.58,7,low,95,kmeans,2023
|
||||||
350931,Sumberjambe,4,low,98,kmeans,2024
|
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)
|
created_at DateTime? @default(now()) @db.Timestamptz(6)
|
||||||
updated_at DateTime? @default(now()) @db.Timestamptz(6)
|
updated_at DateTime? @default(now()) @db.Timestamptz(6)
|
||||||
districts districts[]
|
districts districts[]
|
||||||
|
units units[]
|
||||||
|
|
||||||
@@index([name], map: "idx_cities_name")
|
@@index([name], map: "idx_cities_name")
|
||||||
}
|
}
|
||||||
|
@ -172,9 +173,11 @@ model crimes {
|
||||||
method String? @db.VarChar(100)
|
method String? @db.VarChar(100)
|
||||||
month Int?
|
month Int?
|
||||||
number_of_crime Int @default(0)
|
number_of_crime Int @default(0)
|
||||||
|
crime_cleared Int @default(0)
|
||||||
|
avg_crime Float @default(0)
|
||||||
score Float @default(0)
|
score Float @default(0)
|
||||||
updated_at DateTime? @default(now()) @db.Timestamptz(6)
|
updated_at DateTime? @default(now()) @db.Timestamptz(6)
|
||||||
year Int
|
year Int?
|
||||||
source_type String? @db.VarChar(100)
|
source_type String? @db.VarChar(100)
|
||||||
crime_incidents crime_incidents[]
|
crime_incidents crime_incidents[]
|
||||||
districts districts @relation(fields: [district_id], references: [id], onDelete: Cascade, onUpdate: NoAction)
|
districts districts @relation(fields: [district_id], references: [id], onDelete: Cascade, onUpdate: NoAction)
|
||||||
|
@ -265,19 +268,22 @@ model incident_logs {
|
||||||
model units {
|
model units {
|
||||||
id String @id @default(dbgenerated("gen_random_uuid()")) @db.Uuid
|
id String @id @default(dbgenerated("gen_random_uuid()")) @db.Uuid
|
||||||
code_unit String @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)
|
||||||
name String @db.VarChar(100)
|
name String @db.VarChar(100)
|
||||||
description String?
|
description String?
|
||||||
type unit_type
|
type unit_type
|
||||||
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)
|
||||||
address String?
|
address String?
|
||||||
|
phone String?
|
||||||
land_area Float?
|
land_area Float?
|
||||||
latitude Float
|
latitude Float
|
||||||
longitude Float
|
longitude Float
|
||||||
location Unsupported("geography")
|
location Unsupported("geography")
|
||||||
unit_statistics unit_statistics[]
|
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([name], map: "idx_units_name")
|
||||||
@@index([type], map: "idx_units_type")
|
@@index([type], map: "idx_units_type")
|
||||||
|
|
|
@ -10,7 +10,8 @@ import { CrimeCategoriesSeeder } from './seeds/crime-category';
|
||||||
|
|
||||||
import { UnitSeeder } from './seeds/units';
|
import { UnitSeeder } from './seeds/units';
|
||||||
import { CrimesSeeder } from './seeds/crimes';
|
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();
|
const prisma = new PrismaClient();
|
||||||
|
|
||||||
|
@ -37,8 +38,8 @@ class DatabaseSeeder {
|
||||||
new UnitSeeder(prisma),
|
new UnitSeeder(prisma),
|
||||||
new DemographicsSeeder(prisma),
|
new DemographicsSeeder(prisma),
|
||||||
new CrimesSeeder(prisma),
|
new CrimesSeeder(prisma),
|
||||||
new DetailedCrimeIncidentsSeeder(prisma), // Add the new crime incidents seeder
|
// new CrimeIncidentsByUnitSeeder(prisma),
|
||||||
// new CrimeIncidentsSeederV2(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 fs from 'fs';
|
||||||
import * as path from 'path';
|
import * as path from 'path';
|
||||||
import csv from 'csv-parser';
|
import csv from 'csv-parser';
|
||||||
|
import { CRegex } from '../../app/_utils/const/regex';
|
||||||
|
|
||||||
type ICreateLocations = {
|
type ICreateLocations = {
|
||||||
id: string;
|
id: string;
|
||||||
|
@ -24,7 +25,7 @@ type ICreateLocations = {
|
||||||
location: string;
|
location: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export class CrimeIncidentsSeeder {
|
export class CrimeIncidentsByUnitSeeder {
|
||||||
private crimeMonthlyData: Map<
|
private crimeMonthlyData: Map<
|
||||||
string,
|
string,
|
||||||
{ number_of_crime: number; crime_cleared: number }
|
{ number_of_crime: number; crime_cleared: number }
|
||||||
|
@ -187,7 +188,14 @@ private generateDistributedPoints(
|
||||||
await this.loadCrimeMonthlyData();
|
await this.loadCrimeMonthlyData();
|
||||||
|
|
||||||
// Check if crime incidents already exist
|
// 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) {
|
if (existingIncidents) {
|
||||||
console.log('Crime incidents data already exists, skipping import.');
|
console.log('Crime incidents data already exists, skipping import.');
|
||||||
return;
|
return;
|
||||||
|
@ -197,8 +205,8 @@ private generateDistributedPoints(
|
||||||
const crimeRecords = await this.prisma.crimes.findMany({
|
const crimeRecords = await this.prisma.crimes.findMany({
|
||||||
where: {
|
where: {
|
||||||
month: { not: null },
|
month: { not: null },
|
||||||
// Only process records with incidents (number_of_crime > 0)
|
|
||||||
number_of_crime: { gt: 0 },
|
number_of_crime: { gt: 0 },
|
||||||
|
source_type: "cbu"
|
||||||
},
|
},
|
||||||
orderBy: [{ district_id: 'asc' }, { year: 'asc' }, { month: 'asc' }],
|
orderBy: [{ district_id: 'asc' }, { year: 'asc' }, { month: 'asc' }],
|
||||||
});
|
});
|
||||||
|
@ -466,7 +474,7 @@ private generateDistributedPoints(
|
||||||
separator: '-',
|
separator: '-',
|
||||||
uniquenessStrategy: 'counter',
|
uniquenessStrategy: 'counter',
|
||||||
},
|
},
|
||||||
/(\d{4})(?=-\d{4}$)/ // Pattern to extract the 4-digit counter
|
CRegex.CR_YEAR_SEQUENCE
|
||||||
);
|
);
|
||||||
|
|
||||||
// Determine status based on crime_cleared
|
// Determine status based on crime_cleared
|
||||||
|
@ -582,7 +590,7 @@ private generateDistributedPoints(
|
||||||
if (require.main === module) {
|
if (require.main === module) {
|
||||||
const testSeeder = async () => {
|
const testSeeder = async () => {
|
||||||
const prisma = new PrismaClient();
|
const prisma = new PrismaClient();
|
||||||
const seeder = new CrimeIncidentsSeeder(prisma);
|
const seeder = new CrimeIncidentsByUnitSeeder(prisma);
|
||||||
try {
|
try {
|
||||||
await seeder.run();
|
await seeder.run();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
|
|
@ -1,6 +1,8 @@
|
||||||
import {
|
import {
|
||||||
PrismaClient,
|
PrismaClient,
|
||||||
|
crime_incidents,
|
||||||
crime_rates,
|
crime_rates,
|
||||||
|
crimes,
|
||||||
events,
|
events,
|
||||||
session_status,
|
session_status,
|
||||||
users,
|
users,
|
||||||
|
@ -9,6 +11,8 @@ import fs from 'fs';
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
import { parse } from 'csv-parse/sync';
|
import { parse } from 'csv-parse/sync';
|
||||||
import { generateId, generateIdWithDbCounter } from '../../app/_utils/common';
|
import { generateId, generateIdWithDbCounter } from '../../app/_utils/common';
|
||||||
|
import { CRegex } from '../../app/_utils/const/regex';
|
||||||
|
|
||||||
|
|
||||||
interface ICreateUser {
|
interface ICreateUser {
|
||||||
id: string;
|
id: string;
|
||||||
|
@ -32,8 +36,6 @@ export class CrimesSeeder {
|
||||||
async run(): Promise<void> {
|
async run(): Promise<void> {
|
||||||
console.log('🌱 Seeding crimes data...');
|
console.log('🌱 Seeding crimes data...');
|
||||||
|
|
||||||
// Clear existing data
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Create test user
|
// Create test user
|
||||||
const user = await this.createUsers();
|
const user = await this.createUsers();
|
||||||
|
@ -48,15 +50,16 @@ export class CrimesSeeder {
|
||||||
// Create 5 sessions with completed status
|
// Create 5 sessions with completed status
|
||||||
await this.createSessions(user, events);
|
await this.createSessions(user, events);
|
||||||
|
|
||||||
// Import monthly crime data
|
// Import original crime data with source_type = 'general'
|
||||||
await this.importMonthlyCrimeData();
|
await this.importMonthlyCrimeData();
|
||||||
|
|
||||||
// Import yearly crime data from CSV file
|
|
||||||
await this.importYearlyCrimeData();
|
await this.importYearlyCrimeData();
|
||||||
|
|
||||||
// Import all-year crime summaries (2020-2024)
|
|
||||||
await this.importAllYearSummaries();
|
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.');
|
console.log('✅ Crime seeding completed successfully.');
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('❌ Error seeding crimes:', error);
|
console.error('❌ Error seeding crimes:', error);
|
||||||
|
@ -65,7 +68,6 @@ export class CrimesSeeder {
|
||||||
}
|
}
|
||||||
|
|
||||||
private async createUsers() {
|
private async createUsers() {
|
||||||
// Check if test users already exist
|
|
||||||
const existingUser = await this.prisma.users.findFirst({
|
const existingUser = await this.prisma.users.findFirst({
|
||||||
where: { email: 'sigapcompany@gmail.com' },
|
where: { email: 'sigapcompany@gmail.com' },
|
||||||
});
|
});
|
||||||
|
@ -75,7 +77,6 @@ export class CrimesSeeder {
|
||||||
return existingUser;
|
return existingUser;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get admin role ID
|
|
||||||
let roleId = await this.prisma.roles.findFirst({
|
let roleId = await this.prisma.roles.findFirst({
|
||||||
where: { name: 'admin' },
|
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({
|
const newUser = await this.prisma.users.create({
|
||||||
data: {
|
data: {
|
||||||
email: 'sigapcompany@gmail.com',
|
email: 'sigapcompany@gmail.com',
|
||||||
|
@ -118,7 +118,6 @@ export class CrimesSeeder {
|
||||||
}
|
}
|
||||||
|
|
||||||
private async createEvents(user: ICreateUser) {
|
private async createEvents(user: ICreateUser) {
|
||||||
// Check if events already exist
|
|
||||||
const existingEvent = await this.prisma.events.findFirst({
|
const existingEvent = await this.prisma.events.findFirst({
|
||||||
where: {
|
where: {
|
||||||
user_id: user.id,
|
user_id: user.id,
|
||||||
|
@ -142,7 +141,6 @@ export class CrimesSeeder {
|
||||||
}
|
}
|
||||||
|
|
||||||
private async createSessions(user: ICreateUser, events: events) {
|
private async createSessions(user: ICreateUser, events: events) {
|
||||||
// Check if sessions already exist
|
|
||||||
const existingSession = await this.prisma.sessions.findFirst();
|
const existingSession = await this.prisma.sessions.findFirst();
|
||||||
|
|
||||||
if (existingSession) {
|
if (existingSession) {
|
||||||
|
@ -161,7 +159,6 @@ export class CrimesSeeder {
|
||||||
return newSession;
|
return newSession;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Helper function for chunked insertion (with default chunk size 500)
|
|
||||||
private async chunkedCreateMany(data: any[], chunkSize: number = 500) {
|
private async chunkedCreateMany(data: any[], chunkSize: number = 500) {
|
||||||
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);
|
||||||
|
@ -174,33 +171,31 @@ export class CrimesSeeder {
|
||||||
private async importMonthlyCrimeData() {
|
private async importMonthlyCrimeData() {
|
||||||
console.log('Importing monthly crime data...');
|
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) {
|
if (existingCrimes) {
|
||||||
console.log('Crimes data already exists, skipping import.');
|
console.log('General crimes data already exists, skipping import.');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Read CSV file
|
|
||||||
const csvFilePath = path.resolve(
|
const csvFilePath = path.resolve(
|
||||||
__dirname,
|
__dirname,
|
||||||
'../data/excels/crimes/crime_monthly.csv'
|
'../data/excels/crimes/crime_monthly_by_unit.csv'
|
||||||
);
|
);
|
||||||
const fileContent = fs.readFileSync(csvFilePath, { encoding: 'utf-8' });
|
const fileContent = fs.readFileSync(csvFilePath, { encoding: 'utf-8' });
|
||||||
|
|
||||||
// Parse CSV
|
|
||||||
const records = parse(fileContent, {
|
const records = parse(fileContent, {
|
||||||
columns: true,
|
columns: true,
|
||||||
skip_empty_lines: true,
|
skip_empty_lines: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
// Store unique district IDs to avoid duplicates
|
|
||||||
const processedDistricts = new Set<string>();
|
const processedDistricts = new Set<string>();
|
||||||
|
const crimesData: Array<Partial<crimes>> = [];
|
||||||
|
|
||||||
// Prepare batch data
|
|
||||||
const crimesData = [];
|
|
||||||
|
|
||||||
// Process records
|
|
||||||
for (const record of records) {
|
for (const record of records) {
|
||||||
const crimeRate = record.level.toLowerCase() as crime_rates;
|
const crimeRate = record.level.toLowerCase() as crime_rates;
|
||||||
|
|
||||||
|
@ -216,7 +211,6 @@ export class CrimesSeeder {
|
||||||
}
|
}
|
||||||
|
|
||||||
const year = parseInt(record.year);
|
const year = parseInt(record.year);
|
||||||
// Create a unique ID for monthly crime data
|
|
||||||
const crimeId = await generateIdWithDbCounter(
|
const crimeId = await generateIdWithDbCounter(
|
||||||
'crimes',
|
'crimes',
|
||||||
{
|
{
|
||||||
|
@ -230,11 +224,9 @@ export class CrimesSeeder {
|
||||||
separator: '-',
|
separator: '-',
|
||||||
uniquenessStrategy: 'counter',
|
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({
|
crimesData.push({
|
||||||
id: crimeId,
|
id: crimeId,
|
||||||
district_id: record.district_id,
|
district_id: record.district_id,
|
||||||
|
@ -243,14 +235,14 @@ export class CrimesSeeder {
|
||||||
month: parseInt(record.month_num),
|
month: parseInt(record.month_num),
|
||||||
year: parseInt(record.year),
|
year: parseInt(record.year),
|
||||||
number_of_crime: parseInt(record.number_of_crime),
|
number_of_crime: parseInt(record.number_of_crime),
|
||||||
|
crime_cleared: parseInt(record.crime_cleared) || 0,
|
||||||
score: parseFloat(record.score),
|
score: parseFloat(record.score),
|
||||||
|
source_type: 'cbu',
|
||||||
});
|
});
|
||||||
|
|
||||||
// Keep track of unique districts for later creation of crime incidents
|
|
||||||
processedDistricts.add(record.district_id);
|
processedDistricts.add(record.district_id);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Batch create all crimes in chunks
|
|
||||||
await this.chunkedCreateMany(crimesData);
|
await this.chunkedCreateMany(crimesData);
|
||||||
|
|
||||||
console.log(`Imported ${records.length} monthly crime records.`);
|
console.log(`Imported ${records.length} monthly crime records.`);
|
||||||
|
@ -259,9 +251,11 @@ export class CrimesSeeder {
|
||||||
private async importYearlyCrimeData() {
|
private async importYearlyCrimeData() {
|
||||||
console.log('Importing yearly crime data...');
|
console.log('Importing yearly crime data...');
|
||||||
|
|
||||||
// Check if yearly summaries already exist (records with null month)
|
|
||||||
const existingYearlySummary = await this.prisma.crimes.findFirst({
|
const existingYearlySummary = await this.prisma.crimes.findFirst({
|
||||||
where: { month: null },
|
where: {
|
||||||
|
month: null,
|
||||||
|
source_type: 'cbu',
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
if (existingYearlySummary) {
|
if (existingYearlySummary) {
|
||||||
|
@ -269,23 +263,19 @@ export class CrimesSeeder {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Read CSV file
|
|
||||||
const csvFilePath = path.resolve(
|
const csvFilePath = path.resolve(
|
||||||
__dirname,
|
__dirname,
|
||||||
'../data/excels/crimes/crime_yearly.csv'
|
'../data/excels/crimes/crime_yearly_by_unit.csv'
|
||||||
);
|
);
|
||||||
const fileContent = fs.readFileSync(csvFilePath, { encoding: 'utf-8' });
|
const fileContent = fs.readFileSync(csvFilePath, { encoding: 'utf-8' });
|
||||||
|
|
||||||
// Parse CSV
|
|
||||||
const records = parse(fileContent, {
|
const records = parse(fileContent, {
|
||||||
columns: true,
|
columns: true,
|
||||||
skip_empty_lines: true,
|
skip_empty_lines: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
// Prepare batch data
|
const crimesData: Array<Partial<crimes>> = [];
|
||||||
const crimesData = [];
|
|
||||||
|
|
||||||
// Process records
|
|
||||||
for (const record of records) {
|
for (const record of records) {
|
||||||
const crimeRate = record.level.toLowerCase() as crime_rates;
|
const crimeRate = record.level.toLowerCase() as crime_rates;
|
||||||
const year = parseInt(record.year);
|
const year = parseInt(record.year);
|
||||||
|
@ -318,7 +308,7 @@ export class CrimesSeeder {
|
||||||
separator: '-',
|
separator: '-',
|
||||||
uniquenessStrategy: 'counter',
|
uniquenessStrategy: 'counter',
|
||||||
},
|
},
|
||||||
/(\d{4})(?=-\d{4}$)/ // Pattern to extract the 4-digit counter
|
CRegex.CR_YEAR_SEQUENCE
|
||||||
);
|
);
|
||||||
|
|
||||||
crimesData.push({
|
crimesData.push({
|
||||||
|
@ -329,11 +319,13 @@ export class CrimesSeeder {
|
||||||
month: null,
|
month: null,
|
||||||
year: year,
|
year: year,
|
||||||
number_of_crime: parseInt(record.number_of_crime),
|
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),
|
score: parseInt(record.score),
|
||||||
|
source_type: 'cbu',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Batch create all yearly crimes in chunks
|
|
||||||
await this.chunkedCreateMany(crimesData);
|
await this.chunkedCreateMany(crimesData);
|
||||||
|
|
||||||
console.log(`Imported ${records.length} yearly crime records.`);
|
console.log(`Imported ${records.length} yearly crime records.`);
|
||||||
|
@ -342,9 +334,12 @@ 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...');
|
||||||
|
|
||||||
// Check if all-year summaries already exist (records with null month and null year)
|
|
||||||
const existingAllYearSummaries = await this.prisma.crimes.findFirst({
|
const existingAllYearSummaries = await this.prisma.crimes.findFirst({
|
||||||
where: { month: null, year: null },
|
where: {
|
||||||
|
month: null,
|
||||||
|
year: null,
|
||||||
|
source_type: 'cbu',
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
if (existingAllYearSummaries) {
|
if (existingAllYearSummaries) {
|
||||||
|
@ -352,21 +347,18 @@ export class CrimesSeeder {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Read CSV file
|
|
||||||
const csvFilePath = path.resolve(
|
const csvFilePath = path.resolve(
|
||||||
__dirname,
|
__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' });
|
const fileContent = fs.readFileSync(csvFilePath, { encoding: 'utf-8' });
|
||||||
|
|
||||||
// Parse CSV
|
|
||||||
const records = parse(fileContent, {
|
const records = parse(fileContent, {
|
||||||
columns: true,
|
columns: true,
|
||||||
skip_empty_lines: true,
|
skip_empty_lines: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
// Prepare batch data
|
const crimesData: Array<Partial<crimes>> = [];
|
||||||
const crimesData = [];
|
|
||||||
|
|
||||||
for (const record of records) {
|
for (const record of records) {
|
||||||
const crimeRate = record.level.toLowerCase() as crime_rates;
|
const crimeRate = record.level.toLowerCase() as crime_rates;
|
||||||
|
@ -387,7 +379,6 @@ export class CrimesSeeder {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create a unique ID for all-year summary data
|
|
||||||
const crimeId = await generateIdWithDbCounter(
|
const crimeId = await generateIdWithDbCounter(
|
||||||
'crimes',
|
'crimes',
|
||||||
{
|
{
|
||||||
|
@ -400,7 +391,7 @@ export class CrimesSeeder {
|
||||||
separator: '-',
|
separator: '-',
|
||||||
uniquenessStrategy: 'counter',
|
uniquenessStrategy: 'counter',
|
||||||
},
|
},
|
||||||
/(\d{4})$/ // Pattern to extract the 4-digit counter at the end
|
CRegex.CR_SEQUENCE_END
|
||||||
);
|
);
|
||||||
|
|
||||||
crimesData.push({
|
crimesData.push({
|
||||||
|
@ -411,15 +402,253 @@ export class CrimesSeeder {
|
||||||
month: null,
|
month: null,
|
||||||
year: null,
|
year: null,
|
||||||
number_of_crime: parseInt(record.crime_total),
|
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);
|
await this.chunkedCreateMany(crimesData);
|
||||||
|
|
||||||
console.log(`Imported ${records.length} all-year crime summaries.`);
|
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
|
// This allows the file to be executed standalone for testing
|
||||||
|
|
|
@ -31,6 +31,9 @@ export class DemographicsSeeder {
|
||||||
|
|
||||||
// Collect demographic data to be inserted in batch
|
// Collect demographic data to be inserted in batch
|
||||||
const demographicsToInsert = [];
|
const demographicsToInsert = [];
|
||||||
|
|
||||||
|
// Track 2024 data to duplicate for 2025
|
||||||
|
const data2024ByDistrict: Record<string, any> = {};
|
||||||
|
|
||||||
for (const row of data) {
|
for (const row of data) {
|
||||||
const districtName = String(row['Kecamatan']).trim();
|
const districtName = String(row['Kecamatan']).trim();
|
||||||
|
@ -51,17 +54,38 @@ export class DemographicsSeeder {
|
||||||
|
|
||||||
const populationDensity =
|
const populationDensity =
|
||||||
districtLandArea > 0 ? population / districtLandArea : 0;
|
districtLandArea > 0 ? population / districtLandArea : 0;
|
||||||
|
|
||||||
demographicsToInsert.push({
|
const demographicRecord = {
|
||||||
district_id: district.id,
|
district_id: district.id,
|
||||||
year,
|
year,
|
||||||
population,
|
population,
|
||||||
population_density: populationDensity,
|
population_density: populationDensity,
|
||||||
number_of_unemployed: unemployed,
|
number_of_unemployed: unemployed,
|
||||||
});
|
};
|
||||||
|
|
||||||
|
demographicsToInsert.push(demographicRecord);
|
||||||
|
|
||||||
|
// Store 2024 data for later duplication
|
||||||
|
if (year === 2024) {
|
||||||
|
data2024ByDistrict[district.id] = demographicRecord;
|
||||||
|
}
|
||||||
|
|
||||||
counter++;
|
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
|
// Insert all demographic data at once
|
||||||
await this.prisma.demographics.createMany({
|
await this.prisma.demographics.createMany({
|
||||||
|
|
|
@ -57,12 +57,16 @@ export class GeoJSONSeeder {
|
||||||
jsonData.forEach((row: any) => {
|
jsonData.forEach((row: any) => {
|
||||||
const districtName = row['Kecamatan'];
|
const districtName = row['Kecamatan'];
|
||||||
if (districtName) {
|
if (districtName) {
|
||||||
|
// Get 2024 value to use for 2025
|
||||||
|
const value2024 = this.parseAreaValue(row['2024']);
|
||||||
|
|
||||||
this.areaData[districtName] = {
|
this.areaData[districtName] = {
|
||||||
'2020': this.parseAreaValue(row['2020']),
|
'2020': this.parseAreaValue(row['2020']),
|
||||||
'2021': this.parseAreaValue(row['2021']),
|
'2021': this.parseAreaValue(row['2021']),
|
||||||
'2022': this.parseAreaValue(row['2022']),
|
'2022': this.parseAreaValue(row['2022']),
|
||||||
'2023': this.parseAreaValue(row['2023']),
|
'2023': this.parseAreaValue(row['2023']),
|
||||||
'2024': this.parseAreaValue(row['2024']),
|
'2024': this.parseAreaValue(row['2024']),
|
||||||
|
'2025': value2024, // Use the same value as 2024
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
@ -196,7 +200,7 @@ export class GeoJSONSeeder {
|
||||||
const districtsToCreate = [];
|
const districtsToCreate = [];
|
||||||
const geographicsToCreate = [];
|
const geographicsToCreate = [];
|
||||||
const addressPromises = [];
|
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
|
// 2. Process all districts first to prepare data
|
||||||
for (let i = 0; i < districtGeoJson.features.length; i++) {
|
for (let i = 0; i < districtGeoJson.features.length; i++) {
|
||||||
|
|
|
@ -114,7 +114,6 @@ export class UnitSeeder {
|
||||||
});
|
});
|
||||||
|
|
||||||
unitsToInsert.push({
|
unitsToInsert.push({
|
||||||
district_id: patrangDistrict.id,
|
|
||||||
city_id: city.id,
|
city_id: city.id,
|
||||||
code_unit: polresId,
|
code_unit: polresId,
|
||||||
name: `Polres ${city.name}`,
|
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