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 { 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 { 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 { 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 = { 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; if (!year) { console.error( `Year is not defined for crime record ${crime.id}, skipping.` ); return []; } 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, }, 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(); }