import { $Enums } from '@prisma/client'; import { CRIME_RATE_COLORS } from '@/app/_utils/const/map'; import type { ICrimes } from '@/app/_utils/types/crimes'; import { AlertTriangle, Shield } from "lucide-react"; import { IDistrictFeature } from '../types/map'; import { ITooltipsControl } from '@/app/_components/map/controls/top/tooltips'; // Process crime data by district export const processCrimeDataByDistrict = (crimes: ICrimes[]) => { return crimes.reduce( (acc, crime) => { const districtId = crime.district_id; acc[districtId] = { number_of_crime: crime.number_of_crime, level: crime.level, }; return acc; }, {} as Record< string, { number_of_crime?: number; level?: $Enums.crime_rates } > ); }; // Get color for crime rate level export const getCrimeRateColor = (level?: $Enums.crime_rates) => { if (!level) return CRIME_RATE_COLORS.default; switch (level) { case 'low': return CRIME_RATE_COLORS.low; case 'medium': return CRIME_RATE_COLORS.medium; case 'high': return CRIME_RATE_COLORS.high; default: return CRIME_RATE_COLORS.default; } }; // Create fill color expression for district layer export const createFillColorExpression = ( focusedDistrictId: string | null, crimeDataByDistrict: Record< string, { number_of_crime?: number; level?: $Enums.crime_rates } > ) => { const colorEntries = focusedDistrictId ? [ [ focusedDistrictId, getCrimeRateColor(crimeDataByDistrict[focusedDistrictId]?.level), ], 'rgba(0,0,0,0.05)', ] : Object.entries(crimeDataByDistrict).flatMap(([districtId, data]) => { return [districtId, getCrimeRateColor(data.level)]; }); return [ 'case', ['has', 'kode_kec'], [ 'match', ['get', 'kode_kec'], ...colorEntries, focusedDistrictId ? 'rgba(0,0,0,0.05)' : CRIME_RATE_COLORS.default, ], CRIME_RATE_COLORS.default, ]; }; // Extract crime incidents for GeoJSON export const extractCrimeIncidents = ( crimes: ICrimes[], filterCategory: string | 'all' ) => { return crimes.flatMap((crime) => { if (!crime.crime_incidents) return []; let filteredIncidents = crime.crime_incidents; if (filterCategory !== 'all') { filteredIncidents = crime.crime_incidents.filter( (incident) => incident.crime_categories && incident.crime_categories.name === filterCategory ); } return filteredIncidents .map((incident) => { if (!incident.locations) { console.warn('Missing location for incident:', incident.id); return null; } return { type: 'Feature' as const, properties: { id: incident.id, district: crime.districts?.name || 'Unknown', category: incident.crime_categories?.name || 'Unknown', incidentType: incident.crime_categories?.type || 'Unknown', level: crime.level || 'low', description: incident.description || '', status: incident.status || '', }, geometry: { type: 'Point' as const, coordinates: [ incident.locations.longitude || 0, incident.locations.latitude || 0, ], }, }; }) .filter(Boolean); }); }; // Process district feature from map click export const processDistrictFeature = ( feature: any, e: any, districtId: string, crimeDataByDistrict: Record< string, { number_of_crime?: number; level?: $Enums.crime_rates } >, crimes: ICrimes[], year: string, month: string ): IDistrictFeature | null => { const crimeData = crimeDataByDistrict[districtId] || {}; let crime_incidents: Array<{ id: string; timestamp: Date; description: string; status: string; category: string; type: string; address: string; latitude: number; longitude: number; }> = []; const districtCrimes = crimes.filter( (crime) => crime.district_id === districtId ); districtCrimes.forEach((crimeRecord) => { if (crimeRecord && crimeRecord.crime_incidents) { const incidents = crimeRecord.crime_incidents.map((incident) => ({ id: incident.id, timestamp: incident.timestamp, description: incident.description || '', status: incident.status || '', category: incident.crime_categories?.name || '', type: incident.crime_categories?.type || '', address: incident.locations?.address || '', latitude: incident.locations?.latitude || 0, longitude: incident.locations?.longitude || 0, })); crime_incidents = [...crime_incidents, ...incidents]; } }); const firstDistrictCrime = districtCrimes.length > 0 ? districtCrimes[0] : null; if (!firstDistrictCrime) return null; const selectedYearNum = year ? Number.parseInt(year) : new Date().getFullYear(); let demographics = firstDistrictCrime?.districts.demographics?.find( (d) => d.year === selectedYearNum ); if (!demographics && firstDistrictCrime?.districts.demographics?.length) { demographics = firstDistrictCrime.districts.demographics.sort( (a, b) => b.year - a.year )[0]; console.log( `Tidak ada data demografis untuk tahun ${selectedYearNum}, menggunakan data tahun ${demographics.year}` ); } let geographics = firstDistrictCrime?.districts.geographics?.find( (g) => g.year === selectedYearNum ); if (!geographics && firstDistrictCrime?.districts.geographics?.length) { const validGeographics = firstDistrictCrime.districts.geographics .filter((g) => g.year !== null) .sort((a, b) => (b.year || 0) - (a.year || 0)); geographics = validGeographics.length > 0 ? validGeographics[0] : firstDistrictCrime.districts.geographics[0]; console.log( `Tidak ada data geografis untuk tahun ${selectedYearNum}, menggunakan data ${geographics.year ? `tahun ${geographics.year}` : 'tanpa tahun'}` ); } const clickLng = e.lngLat ? e.lngLat.lng : null; const clickLat = e.lngLat ? e.lngLat.lat : null; if (!geographics) { console.error('Missing geographics data for district:', districtId); return null; } if (!demographics) { console.error('Missing demographics data for district:', districtId); return null; } return { id: districtId, name: feature.properties.nama || feature.properties.kecamatan || 'Unknown District', longitude: geographics.longitude || clickLng || 0, latitude: geographics.latitude || clickLat || 0, number_of_crime: crimeData.number_of_crime || 0, level: crimeData.level || $Enums.crime_rates.low, demographics: { number_of_unemployed: demographics.number_of_unemployed, population: demographics.population, population_density: demographics.population_density, year: demographics.year, }, geographics: { address: geographics.address || '', land_area: geographics.land_area || 0, year: geographics.year || 0, latitude: geographics.latitude, longitude: geographics.longitude, }, crime_incidents: crime_incidents || [], selectedYear: year, selectedMonth: month, isFocused: true, }; }; /** * Format distance in a human-readable way * @param meters Distance in meters * @returns Formatted distance string */ export function formatDistance(meters: number): string { if (meters < 1000) { return `${Math.round(meters)} m`; } else { return `${(meters / 1000).toFixed(1)} km`; } } // Helper function to determine fill opacity based on active control export function getFillOpacity(activeControl?: ITooltipsControl, showFill?: boolean): number { if (!showFill) return 0 // Full opacity for incidents and clusters if (activeControl === "incidents" || activeControl === "clusters" || activeControl === "units") { return 0.6 } // Low opacity for timeline to show markers but still see district boundaries if (activeControl === "timeline") { return 0.1 } // No fill for other controls, but keep boundaries visible return 0 } // Helper to normalize severity to a number export const normalizeSeverity = (sev: number | "Low" | "Medium" | "High" | "Critical"): number => { if (typeof sev === "number") return sev switch (sev) { case "Critical": return 4 case "High": return 3 case "Medium": return 2 case "Low": default: return 1 } } export const normalizeSeverityNumber = (sev?: number | "Low" | "Medium" | "High" | "Critical"): string => { if (!sev) return "Medium"; if (typeof sev === "number") { switch (sev) { case 4: return "Critical"; case 3: return "High"; case 2: return "Medium"; case 1: return "Low"; default: return "Medium"; } } return sev; }; export const getSeverityGradient = (severity: number) => { switch (severity) { case 4: return "bg-gradient-to-r from-purple-500/10 to-purple-600/5 dark:from-purple-900/20 dark:to-purple-800/10" case 3: return "bg-gradient-to-r from-red-500/10 to-red-600/5 dark:from-red-900/20 dark:to-red-800/10" case 2: return "bg-gradient-to-r from-yellow-500/10 to-yellow-600/5 dark:from-yellow-900/20 dark:to-yellow-800/10" case 1: default: return "bg-gradient-to-r from-blue-500/10 to-blue-600/5 dark:from-blue-900/20 dark:to-blue-800/10" } } export const getSeverityIconColor = (severity: number) => { switch (severity) { case 4: return "text-purple-600 dark:text-purple-400" case 3: return "text-red-500 dark:text-red-400" case 2: return "text-yellow-600 dark:text-yellow-400" case 1: default: return "text-blue-500 dark:text-blue-400" } } export const getSeverityIcon = (severity: number) => { switch (severity) { case 4: case 3: return case 2: case 1: default: return } } export const getStatusInfo = (status?: string | true) => { if (status === true) { return { text: "Verified", color: "bg-green-100 text-green-800 border-green-200 dark:bg-green-900/30 dark:text-green-400" }; } if (!status) { return { text: "Pending", color: "bg-blue-100 text-blue-800 border-blue-200 dark:bg-blue-900/30 dark:text-blue-400" }; } switch (status.toLowerCase()) { case "resolved": return { text: "Resolved", color: "bg-green-100 text-green-800 border-green-200 dark:bg-green-900/30 dark:text-green-400" }; case "in progress": return { text: "In Progress", color: "bg-amber-100 text-amber-800 border-amber-200 dark:bg-amber-900/30 dark:text-amber-400" }; case "pending": return { text: "Pending", color: "bg-blue-100 text-blue-800 border-blue-200 dark:bg-blue-900/30 dark:text-blue-400" }; default: return { text: status.charAt(0).toUpperCase() + status.slice(1), color: "bg-gray-100 text-gray-800 border-gray-200 dark:bg-gray-900/30 dark:text-gray-400" }; } }; // Get appropriate color for severity badge export const getSeverityColor = (severity: string) => { switch (severity) { case "Critical": return "bg-red-100 text-red-800 border-red-200 dark:bg-red-900/30 dark:text-red-400"; case "High": return "bg-orange-100 text-orange-800 border-orange-200 dark:bg-orange-900/30 dark:text-orange-400"; case "Medium": return "bg-yellow-100 text-yellow-800 border-yellow-200 dark:bg-yellow-900/30 dark:text-yellow-400"; case "Low": return "bg-green-100 text-green-800 border-green-200 dark:bg-green-900/30 dark:text-green-400"; default: return "bg-blue-100 text-blue-800 border-blue-200 dark:bg-blue-900/30 dark:text-blue-400"; } };