373 lines
11 KiB
TypeScript
373 lines
11 KiB
TypeScript
import {
|
|
PrismaClient,
|
|
crime_rates,
|
|
crime_status,
|
|
crimes,
|
|
} from '@prisma/client';
|
|
import { generateId } from '../../app/_utils/common';
|
|
import { createClient } from '../../app/_utils/supabase/client';
|
|
import * as fs from 'fs';
|
|
import * as path from 'path';
|
|
import csv from 'csv-parser';
|
|
|
|
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;
|
|
};
|
|
|
|
export class CrimeIncidentsSeeder {
|
|
private crimeMonthlyData: Map<
|
|
string,
|
|
{ number_of_crime: number; crime_cleared: number }
|
|
> = new Map();
|
|
|
|
constructor(
|
|
private prisma: PrismaClient,
|
|
private supabase = createClient()
|
|
) {}
|
|
|
|
private async loadCrimeMonthlyData(): Promise<void> {
|
|
const csvFilePath = path.resolve(
|
|
__dirname,
|
|
'../data/excels/crimes/crime_monthly.csv'
|
|
);
|
|
|
|
return new Promise((resolve, reject) => {
|
|
fs.createReadStream(csvFilePath)
|
|
.pipe(csv())
|
|
.on('data', (row) => {
|
|
const key = `${row.district_id}-${row.month_num}-${row.year}`;
|
|
this.crimeMonthlyData.set(key, {
|
|
number_of_crime: parseInt(row.number_of_crime),
|
|
crime_cleared: parseInt(row.crime_cleared),
|
|
});
|
|
})
|
|
.on('end', () => {
|
|
console.log(
|
|
`Loaded ${this.crimeMonthlyData.size} crime monthly records.`
|
|
);
|
|
resolve();
|
|
})
|
|
.on('error', (error) => {
|
|
console.error('Error loading crime monthly data:', error);
|
|
reject(error);
|
|
});
|
|
});
|
|
}
|
|
|
|
async run(): Promise<void> {
|
|
console.log('🌱 Seeding crime incidents data...');
|
|
|
|
try {
|
|
// Load crime monthly data first
|
|
await this.loadCrimeMonthlyData();
|
|
|
|
// Check if crime incidents already exist
|
|
const existingIncidents = await this.prisma.crime_incidents.findFirst();
|
|
if (existingIncidents) {
|
|
console.log('Crime incidents data already exists, skipping import.');
|
|
return;
|
|
}
|
|
|
|
// Get all monthly crime records (exclude yearly summaries)
|
|
const crimeRecords = await this.prisma.crimes.findMany({
|
|
where: {
|
|
month: { not: null },
|
|
// Only process records with incidents (number_of_crime > 0)
|
|
number_of_crime: { gt: 0 },
|
|
},
|
|
orderBy: [{ district_id: 'asc' }, { year: 'asc' }, { month: 'asc' }],
|
|
});
|
|
|
|
console.log(
|
|
`Found ${crimeRecords.length} monthly crime records with incidents to process.`
|
|
);
|
|
|
|
// Get all crime categories for random assignment
|
|
const crimeCategories = await this.prisma.crime_categories.findMany();
|
|
if (crimeCategories.length === 0) {
|
|
console.error(
|
|
'No crime categories found, please seed crime categories first.'
|
|
);
|
|
return;
|
|
}
|
|
|
|
// Process each crime record
|
|
let totalIncidentsCreated = 0;
|
|
for (const crimeRecord of crimeRecords) {
|
|
const incidents = await this.createIncidentsForCrime(
|
|
crimeRecord,
|
|
crimeCategories
|
|
);
|
|
totalIncidentsCreated += incidents.length;
|
|
|
|
// Log progress every 50 crime records
|
|
if (totalIncidentsCreated % 50 === 0) {
|
|
console.log(`Created ${totalIncidentsCreated} incidents so far...`);
|
|
}
|
|
}
|
|
|
|
console.log(
|
|
`✅ Successfully created ${totalIncidentsCreated} crime incidents.`
|
|
);
|
|
} catch (error) {
|
|
console.error('❌ Error seeding crime incidents:', error);
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
private async createIncidentsForCrime(
|
|
crime: crimes,
|
|
categories: any[]
|
|
): Promise<any[]> {
|
|
const incidentsCreated = [];
|
|
|
|
// Get district information
|
|
const district = await this.prisma.districts.findUnique({
|
|
where: { id: crime.district_id },
|
|
include: { cities: true },
|
|
});
|
|
|
|
if (!district) {
|
|
console.error(`District ${crime.district_id} not found, skipping.`);
|
|
return [];
|
|
}
|
|
|
|
const geo = await this.prisma.geographics.findFirst({
|
|
where: {
|
|
district_id: district.id,
|
|
year: crime.year,
|
|
},
|
|
select: {
|
|
latitude: true,
|
|
longitude: true,
|
|
},
|
|
});
|
|
|
|
if (!geo) {
|
|
console.error(
|
|
`Geographic data for district ${district.id} in year ${crime.year} not found, skipping.`
|
|
);
|
|
return [];
|
|
}
|
|
|
|
// Generate random coordinates within district boundary (with small variation)
|
|
const radius = 0.02; // Approximately 2km
|
|
const angle = Math.random() * Math.PI * 2; // Random angle in radians
|
|
const distance = Math.sqrt(Math.random()) * radius; // Random distance within circle
|
|
|
|
// Calculate offset using simple approximation (not exact but good enough for this purpose)
|
|
const latOffset = distance * Math.cos(angle);
|
|
const lngOffset = distance * Math.sin(angle);
|
|
|
|
// Apply offset to base coordinates
|
|
const latitude = geo.latitude + latOffset;
|
|
const longitude = geo.longitude + lngOffset;
|
|
|
|
// List of common street names in Jember
|
|
const jemberStreets = [
|
|
'Jalan Pahlawan',
|
|
'Jalan Merdeka',
|
|
'Jalan Cendrawasih',
|
|
'Jalan Srikandi',
|
|
'Jalan Sumbermujur',
|
|
'Jalan Taman Siswa',
|
|
'Jalan Pantai',
|
|
'Jalan Raya Sumberbaru',
|
|
'Jalan Abdurrahman Saleh',
|
|
];
|
|
|
|
const user = await this.prisma.users.findFirst({
|
|
where: {
|
|
email: 'admin@sigap.id',
|
|
},
|
|
select: {
|
|
id: true,
|
|
},
|
|
});
|
|
|
|
if (!user) {
|
|
console.error(`User not found, skipping.`);
|
|
return [];
|
|
}
|
|
|
|
const event = await this.prisma.events.findFirst({
|
|
where: {
|
|
user_id: user.id,
|
|
},
|
|
});
|
|
|
|
if (!event) {
|
|
console.error(`Event not found, skipping.`);
|
|
return [];
|
|
}
|
|
|
|
// Generate a random address in Jember
|
|
const streetName =
|
|
jemberStreets[Math.floor(Math.random() * jemberStreets.length)];
|
|
const buildingNumber = Math.floor(Math.random() * 200) + 1;
|
|
const randomAddress = `${streetName} No. ${buildingNumber}, ${district.name}, Jember`;
|
|
|
|
const locationData: Partial<ICreateLocations> = {
|
|
district_id: district.id,
|
|
event_id: event.id,
|
|
address: randomAddress,
|
|
type: 'crime',
|
|
latitude: latitude,
|
|
longitude: longitude,
|
|
location: `POINT(${longitude} ${latitude})`,
|
|
};
|
|
|
|
let { data: newLocation, error } = await this.supabase
|
|
.from('locations')
|
|
.insert([locationData])
|
|
.select();
|
|
|
|
if (error) {
|
|
console.error(
|
|
`Error inserting into locations for district ${district.name} (${crime.year}):`,
|
|
error
|
|
);
|
|
return [];
|
|
}
|
|
|
|
const location = await this.prisma.locations.findFirst({
|
|
where: {
|
|
event_id: event.id,
|
|
district_id: district.id,
|
|
},
|
|
select: {
|
|
id: true,
|
|
address: true,
|
|
},
|
|
});
|
|
|
|
if (!location) {
|
|
console.error(
|
|
`Location not found for district ${district.name} (${crime.year}), skipping.`
|
|
);
|
|
return [];
|
|
}
|
|
|
|
// Get crime_cleared data from the loaded CSV
|
|
const key = `${crime.district_id}-${crime.month}-${crime.year}`;
|
|
const crimeMonthlyInfo = this.crimeMonthlyData.get(key);
|
|
|
|
// Default values if not found in CSV
|
|
let crimesCleared = 0;
|
|
|
|
if (crimeMonthlyInfo) {
|
|
crimesCleared = crimeMonthlyInfo.crime_cleared;
|
|
// Safety check to ensure crime_cleared doesn't exceed number_of_crime
|
|
if (crimesCleared > crime.number_of_crime) {
|
|
crimesCleared = crime.number_of_crime;
|
|
}
|
|
} else {
|
|
console.warn(
|
|
`No crime monthly data found for ${key}, using default values.`
|
|
);
|
|
}
|
|
|
|
// Create incidents based on the number_of_crime value
|
|
for (let i = 0; i < crime.number_of_crime; i++) {
|
|
// Select random category
|
|
const randomCategory =
|
|
categories[Math.floor(Math.random() * categories.length)];
|
|
|
|
// Calculate a date within the crime's month
|
|
const year = crime.year;
|
|
const month = (crime.month as number) - 1; // JavaScript months are 0-indexed
|
|
const maxDay = new Date(year, month + 1, 0).getDate(); // Get last day of month
|
|
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);
|
|
|
|
// Generate a unique ID for the incident
|
|
const incidentId = generateId({
|
|
prefix: 'CI',
|
|
segments: {
|
|
codes: [district.cities.id],
|
|
sequentialDigits: 4,
|
|
year: crime.year,
|
|
},
|
|
format: '{prefix}-{codes}-{sequence}-{year}',
|
|
separator: '-',
|
|
randomSequence: false,
|
|
uniquenessStrategy: 'counter',
|
|
});
|
|
|
|
// Determine status based on crime_cleared
|
|
// If i < crimesCleared, this incident is resolved, otherwise unresolved
|
|
const status = i < crimesCleared ? 'resolved' : 'unresolved';
|
|
|
|
const randomLocation = [
|
|
`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}`,
|
|
];
|
|
|
|
const descriptions = [
|
|
`Kasus ${randomCategory.name.toLowerCase()} ${location.address}`,
|
|
`Laporan ${randomCategory.name.toLowerCase()} terjadi pada ${timestamp} ${randomLocation}`,
|
|
`${randomCategory.name} dilaporkan ${randomLocation}`,
|
|
`Insiden ${randomCategory.name.toLowerCase()} terjadi ${randomLocation}`,
|
|
`Kejadian ${randomCategory.name.toLowerCase()} ${randomLocation}`,
|
|
];
|
|
|
|
const randomDescription =
|
|
descriptions[Math.floor(Math.random() * descriptions.length)];
|
|
|
|
// Create the crime incident
|
|
const incident = await this.prisma.crime_incidents.create({
|
|
data: {
|
|
id: incidentId,
|
|
crime_id: crime.id,
|
|
crime_category_id: randomCategory.id,
|
|
location_id: location.id,
|
|
description: randomDescription,
|
|
victim_count: 0,
|
|
status: status,
|
|
timestamp: timestamp,
|
|
},
|
|
});
|
|
|
|
incidentsCreated.push(incident);
|
|
}
|
|
|
|
return incidentsCreated;
|
|
}
|
|
}
|
|
|
|
// This allows the file to be executed standalone for testing
|
|
if (require.main === module) {
|
|
const testSeeder = async () => {
|
|
const prisma = new PrismaClient();
|
|
const seeder = new CrimeIncidentsSeeder(prisma);
|
|
try {
|
|
await seeder.run();
|
|
} catch (e) {
|
|
console.error('Error during seeding:', e);
|
|
process.exit(1);
|
|
} finally {
|
|
await prisma.$disconnect();
|
|
}
|
|
};
|
|
|
|
testSeeder();
|
|
}
|