diff --git a/sigap-website/app/_components/map/pop-up/crime-popup.tsx b/sigap-website/app/_components/map/pop-up/crime-popup.tsx index 35f628a..c2adc8e 100644 --- a/sigap-website/app/_components/map/pop-up/crime-popup.tsx +++ b/sigap-website/app/_components/map/pop-up/crime-popup.tsx @@ -1,7 +1,11 @@ -import { Popup } from 'react-map-gl/mapbox' -import { Badge } from '@/app/_components/ui/badge' -import { Card } from '@/app/_components/ui/card' -import { MapPin, AlertTriangle, Calendar, Clock, Tag, Bookmark, FileText } from 'lucide-react' +"use client" + +import { Popup } from "react-map-gl/mapbox" +import { Badge } from "@/app/_components/ui/badge" +import { Card } from "@/app/_components/ui/card" +import { Separator } from "@/app/_components/ui/separator" +import { Button } from "@/app/_components/ui/button" +import { MapPin, AlertTriangle, Calendar, Clock, Tag, Bookmark, FileText, Navigation, X } from "lucide-react" interface CrimePopupProps { longitude: number @@ -22,104 +26,157 @@ interface CrimePopupProps { } export default function CrimePopup({ longitude, latitude, onClose, crime }: CrimePopupProps) { - console.log("CrimePopup rendering with props:", { longitude, latitude, crime }) - const formatDate = (date?: Date) => { - if (!date) return 'Unknown date' - return new Date(date).toLocaleDateString() - } + if (!date) return "Unknown date" + return new Date(date).toLocaleDateString() + } const formatTime = (date?: Date) => { - if (!date) return 'Unknown time' - return new Date(date).toLocaleTimeString() - } + if (!date) return "Unknown time" + return new Date(date).toLocaleTimeString() + } const getStatusBadge = (status?: string) => { if (!status) return Unknown + const statusLower = status.toLowerCase() + if (statusLower.includes("resolv") || statusLower.includes("closed")) { + return Resolved + } + if (statusLower.includes("progress") || statusLower.includes("invest")) { + return In Progress + } + if (statusLower.includes("open") || statusLower.includes("new")) { + return Open + } + + return {status} + } + + // Determine border color based on status + const getBorderColor = (status?: string) => { + if (!status) return "border-l-gray-400" + const statusLower = status.toLowerCase() - if (statusLower.includes('resolv') || statusLower.includes('closed')) { - return Resolved + if (statusLower.includes("resolv") || statusLower.includes("closed")) { + return "border-l-emerald-600" } - if (statusLower.includes('progress') || statusLower.includes('invest')) { - return In Progress + if (statusLower.includes("progress") || statusLower.includes("invest")) { + return "border-l-amber-500" } - if (statusLower.includes('open') || statusLower.includes('new')) { - return Open + if (statusLower.includes("open") || statusLower.includes("new")) { + return "border-l-blue-600" } - return {status} + return "border-l-gray-400" } return ( - -
-

- - {crime.category || 'Unknown Incident'} -

- {getStatusBadge(crime.status)} -
+ closeButton={false} // Hide default close button + closeOnClick={false} + onClose={onClose} + anchor="top" + maxWidth="320px" + className="crime-popup z-50" + > + +
+ {/* Custom close button */} + - {crime.description && ( -
-

- - {crime.description} -

-
- )} +
+

+ + {crime.category || "Unknown Incident"} +

+ {getStatusBadge(crime.status)} +
-
- {crime.district && ( -

- - {crime.district} -

- )} + {crime.description && ( +
+

+ + {crime.description} +

+
+ )} - {crime.address && ( -

- - {crime.address} -

- )} + - {crime.timestamp && ( - <> -

- - {formatDate(crime.timestamp)} -

-

- - {formatTime(crime.timestamp)} -

- - )} + {/* Improved section headers */} +
+ {crime.district && ( +
+

District

+

+ + {crime.district} +

+
+ )} - {crime.type && ( -

- - Type: {crime.type} -

- )} + {crime.address && ( +
+

Location

+

+ + {crime.address} +

+
+ )} -

- ID: {crime.id} -

-
- - - ) + {crime.timestamp && ( + <> +
+

Date

+

+ + {formatDate(crime.timestamp)} +

+
+
+

Time

+

+ + {formatTime(crime.timestamp)} +

+
+ + )} + + {crime.type && ( +
+

Type

+

+ + {crime.type} +

+
+ )} +
+ +
+

+ + Coordinates: {latitude.toFixed(6)}, {longitude.toFixed(6)} +

+

ID: {crime.id}

+
+
+
+
+ ) } diff --git a/sigap-website/app/_components/map/pop-up/district-popup.tsx b/sigap-website/app/_components/map/pop-up/district-popup.tsx index 7fc11c8..93dcecb 100644 --- a/sigap-website/app/_components/map/pop-up/district-popup.tsx +++ b/sigap-website/app/_components/map/pop-up/district-popup.tsx @@ -1,27 +1,28 @@ -import { useState, useMemo } from 'react' -import { Popup } from 'react-map-gl/mapbox' -import { Badge } from '@/app/_components/ui/badge' -import { Button } from '@/app/_components/ui/button' -import { Card } from '@/app/_components/ui/card' -import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/app/_components/ui/tabs' -import { Separator } from '@/app/_components/ui/separator' -import { getMonthName } from '@/app/_utils/common' -import { BarChart, Map, Users, Home, FileBarChart, AlertTriangle } from 'lucide-react' -import type { DistrictFeature } from '../layers/district-layer' +"use client" + +import { useState, useMemo } from "react" +import { Popup } from "react-map-gl/mapbox" +import { Badge } from "@/app/_components/ui/badge" +import { Card } from "@/app/_components/ui/card" +import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/app/_components/ui/tabs" +import { Button } from "@/app/_components/ui/button" +import { getMonthName } from "@/app/_utils/common" +import { BarChart, Users, Home, AlertTriangle, ChevronRight, Building, Calendar, X } from 'lucide-react' +import type { DistrictFeature } from "../layers/district-layer" // Helper function to format numbers function formatNumber(num?: number): string { - if (num === undefined || num === null) return "N/A"; + if (num === undefined || num === null) return "N/A" if (num >= 1_000_000) { - return (num / 1_000_000).toFixed(1) + 'M'; + return (num / 1_000_000).toFixed(1) + "M" } if (num >= 1_000) { - return (num / 1_000).toFixed(1) + 'K'; + return (num / 1_000).toFixed(1) + "K" } - return num.toLocaleString(); + return num.toLocaleString() } interface DistrictPopupProps { @@ -41,169 +42,232 @@ export default function DistrictPopup({ district, year, month, - filterCategory = "all" + filterCategory = "all", }: DistrictPopupProps) { - console.log("DistrictPopup rendering with props:", { longitude, latitude, district, year, month }) - const [activeTab, setActiveTab] = useState('overview') + const [activeTab, setActiveTab] = useState("overview") // Extract all crime incidents from the district data and apply filtering if needed const allCrimeIncidents = useMemo(() => { // Check if there are crime incidents in the district object if (!Array.isArray(district.crime_incidents)) { - console.warn("No crime incidents array found in district data"); - return []; + console.warn("No crime incidents array found in district data") + return [] } // Return all incidents if filterCategory is 'all' - if (filterCategory === 'all') { - return district.crime_incidents; + if (filterCategory === "all") { + return district.crime_incidents } // Otherwise, filter by category - return district.crime_incidents.filter(incident => - incident.category === filterCategory - ); - }, [district, filterCategory]); - - // For debugging: log the actual crime incidents count vs number_of_crime - console.log(`District ${district.name} - Number of crime from data: ${district.number_of_crime}, Incidents array length: ${allCrimeIncidents.length}`); + return district.crime_incidents.filter((incident) => incident.category === filterCategory) + }, [district, filterCategory]) const getCrimeRateBadge = (level?: string) => { switch (level) { - case 'low': - return Low - case 'medium': - return Medium - case 'high': - return High - case 'critical': - return Critical + case "low": + return Low + case "medium": + return Medium + case "high": + return High + case "critical": + return Critical default: - return Unknown + return Unknown } } // Format a time period string from year and month const getTimePeriod = () => { - if (year && month && month !== 'all') { + if (year && month && month !== "all") { return `${getMonthName(Number(month))} ${year}` } - return year || 'All time' + return year || "All time" } return ( - -
-
-

{district.name}

+ +
+ {/* Custom close button */} + + +
+
+ +

{district.name}

+
{getCrimeRateBadge(district.level)}
-

- - District ID: {district.id} -

-

- - {district.number_of_crime || 0} crime incidents in {getTimePeriod()} - {filterCategory !== 'all' ? ` (${filterCategory} category)` : ''} -

+
+ + {getTimePeriod()} +
+
+ +
+
+ + {formatNumber(district.number_of_crime || 0)} + Incidents +
+ +
+ + {formatNumber(district.demographics?.population || 0)} + Population +
+ +
+ + {formatNumber(district.geographics?.land_area || 0)} + km² +
-
- - - Overview - - - Demographics - - - Incidents - - -
+ {/* Improved tab headers */} + + + Overview + + + Demographics + + + Incidents + + - -
-
-

Crime Level

-

- This area has a {district.level || 'unknown'} level of crime based on incident reports. -

+ {/* Tab content with improved section headers */} + +
+
+
+ +
+
+

Crime Level

+

+ This area has a {district.level || "unknown"} level of crime based on incident reports. +

+
{district.geographics && district.geographics.land_area && ( -
-

- Geography -

-

- Land area: {formatNumber(district.geographics.land_area)} km² -

- {district.geographics.address && ( +
+
+ +
+
+

Geography

- Address: {district.geographics.address} + Land area: {formatNumber(district.geographics.land_area)} km²

- )} + {district.geographics.address && ( +

Address: {district.geographics.address}

+ )} +
)} -
-

Time Period

-

- Data shown for {getTimePeriod()} -

+
+
+ +
+
+

Time Period

+

+ Data shown for {getTimePeriod()} + {filterCategory !== "all" ? ` (${filterCategory} category)` : ""} +

+
- + {district.demographics ? (
-
-

- Population -

-

- Total: {formatNumber(district.demographics.population || 0)} -

-

- Density: {formatNumber(district.demographics.population_density || 0)} people/km² -

+
+
+ +
+
+

Population

+

+ Total: {formatNumber(district.demographics.population || 0)} +

+

+ Density: {formatNumber(district.demographics.population_density || 0)} people/km² +

+
-
-

Unemployment

-

- {formatNumber(district.demographics.number_of_unemployed || 0)} unemployed people -

- {district.demographics.population && district.demographics.number_of_unemployed && ( +
+
+ +
+
+

Unemployment

- Rate: {((district.demographics.number_of_unemployed / district.demographics.population) * 100).toFixed(1)}% + {formatNumber(district.demographics.number_of_unemployed || 0)} unemployed people

- )} + {district.demographics.population && district.demographics.number_of_unemployed && ( +

+ Rate:{" "} + {( + (district.demographics.number_of_unemployed / district.demographics.population) * + 100 + ).toFixed(1)} + % +

+ )} +
-
-

Crime Rate

- {district.number_of_crime && district.demographics.population ? ( -

- {((district.number_of_crime / district.demographics.population) * 10000).toFixed(2)} crime incidents per 10,000 people -

- ) : ( -

No data available

- )} +
+
+ +
+
+

Crime Rate

+ {district.number_of_crime && district.demographics.population ? ( +

+ {((district.number_of_crime / district.demographics.population) * 10000).toFixed(2)} crime + incidents per 10,000 people +

+ ) : ( +

No data available

+ )} +
) : ( @@ -214,35 +278,38 @@ export default function DistrictPopup({ )} - {/* // Inside the TabsContent for crime_incidents */} {allCrimeIncidents && allCrimeIncidents.length > 0 ? (
{allCrimeIncidents.map((incident, index) => ( -
-
- +
+
+ + {incident.category || incident.type || "Unknown"} {incident.status || "unknown"}
-

- {incident.description || "No description"} -

-

- {incident.timestamp ? new Date(incident.timestamp).toLocaleString() : "Unknown date"} -

+

{incident.description || "No description"}

+
+

+ {incident.timestamp ? new Date(incident.timestamp).toLocaleString() : "Unknown date"} +

+ +
))} - {/* Show a note if we're missing some incidents */} {district.number_of_crime > allCrimeIncidents.length && (

Showing {allCrimeIncidents.length} of {district.number_of_crime} total incidents - {filterCategory !== 'all' ? ` for ${filterCategory} category` : ''} + {filterCategory !== "all" ? ` for ${filterCategory} category` : ""}

)} @@ -250,7 +317,9 @@ export default function DistrictPopup({ ) : (
-

No crime incidents available to display{filterCategory !== 'all' ? ` for ${filterCategory}` : ''}.

+

+ No crime incidents available to display{filterCategory !== "all" ? ` for ${filterCategory}` : ""}. +

Total reported incidents: {district.number_of_crime || 0}

)} @@ -259,4 +328,4 @@ export default function DistrictPopup({ ) -} \ No newline at end of file +} diff --git a/sigap-website/app/_components/map/sidebar/map-sidebar.tsx b/sigap-website/app/_components/map/sidebar/map-sidebar.tsx index 668802c..5f5d8de 100644 --- a/sigap-website/app/_components/map/sidebar/map-sidebar.tsx +++ b/sigap-website/app/_components/map/sidebar/map-sidebar.tsx @@ -1,22 +1,19 @@ "use client" import React, { useState, useEffect, useMemo } from "react" -import { - AlertTriangle, BarChart, ChevronRight, MapPin, Skull, Shield, FileText, - Clock, Calendar, MapIcon, Info, CheckCircle, AlertCircle, XCircle, - Bell, Users, Search, List, RefreshCw, Eye -} from "lucide-react" +import { AlertTriangle, BarChart, ChevronRight, MapPin, Skull, Shield, FileText, Clock, Calendar, MapIcon, Info, CheckCircle, AlertCircle, XCircle, Bell, Users, Search, List, RefreshCw, Eye, X, ChevronLeft, PieChart, LineChart, Activity, Filter, Layers, Settings, Menu } from 'lucide-react' import { Separator } from "@/app/_components/ui/separator" import { Card, CardContent, CardHeader, CardTitle, CardDescription } from "@/app/_components/ui/card" import { cn } from "@/app/_lib/utils" import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/app/_components/ui/tabs" import { Badge } from "@/app/_components/ui/badge" +import { Button } from "@/app/_components/ui/button" import { CRIME_RATE_COLORS } from "@/app/_utils/const/map" import { usePrefetchedCrimeData } from "@/app/(pages)/(admin)/dashboard/crime-management/crime-overview/_hooks/use-prefetch-crimes" import { getMonthName, formatDateString } from "@/app/_utils/common" import { Skeleton } from "@/app/_components/ui/skeleton" import { useGetCrimeCategories } from "@/app/(pages)/(admin)/dashboard/crime-management/crime-overview/_queries/queries" -import { $Enums, crime_categories, crime_incidents, crimes, demographics, districts, geographics, locations } from "@prisma/client" +import { $Enums } from "@prisma/client" interface CrimeSidebarProps { className?: string @@ -297,33 +294,72 @@ export default function CrimeSidebar({ return (
-
-
- - - - Crime Analysis - - {!isCrimesLoading && ( - - {getTimePeriodDisplay()} - - )} +
+
+ {/* Header with improved styling */} + +
+ +
+ +
+
+ +
+
+ + Crime Analysis + + {!isCrimesLoading && ( + + {getTimePeriodDisplay()} + + )} +
+
- - - Dashboard - Statistics - Information + {/* Improved tabs with pill style */} + + + + Dashboard + + + Statistics + + + Information + -
+
{isCrimesLoading ? (
@@ -342,24 +378,28 @@ export default function CrimeSidebar({
) : ( <> - - -
+ {/* Enhanced info card */} + + +
+
+ +
- + {formattedDate}
- + {formattedTime}
-
- - {location} +
+ + {location}
-
- +
+ {crimeStats.totalIncidents || 0} incidents reported {selectedMonth !== 'all' ? ` in ${getMonthName(Number(selectedMonth))}` : ` in ${selectedYear}`} @@ -368,33 +408,42 @@ export default function CrimeSidebar({ -
+ {/* Enhanced stat cards */} +
} - statusColor="text-blue-500" + statusIcon={} + statusColor="text-green-400" updatedTime={getTimePeriodDisplay()} + bgColor="bg-gradient-to-br from-sidebar-accent/30 to-sidebar-accent/20" + borderColor="border-sidebar-border" /> } - statusColor="text-amber-500" + statusIcon={} + statusColor="text-amber-400" updatedTime="Last 30 days" + bgColor="bg-gradient-to-br from-sidebar-accent/30 to-sidebar-accent/20" + borderColor="border-sidebar-border" /> 0 ? topCategories[0].type : "None"} - statusIcon={} - statusColor="text-green-500" + statusIcon={} + statusColor="text-green-400" + bgColor="bg-gradient-to-br from-sidebar-accent/30 to-sidebar-accent/20" + borderColor="border-sidebar-border" /> } - statusColor="text-purple-500" + statusIcon={} + statusColor="text-purple-400" updatedTime="Affected areas" + bgColor="bg-gradient-to-br from-sidebar-accent/30 to-sidebar-accent/20" + borderColor="border-sidebar-border" />
@@ -419,7 +468,7 @@ export default function CrimeSidebar({ ) : ( -
+
{crimeStats.recentIncidents.slice(0, 6).map((incident) => ( ) : ( <> - - - Monthly Incidents + + + + + Monthly Incidents + {selectedYear} - -
+ +
{crimeStats.incidentsByMonth.map((count, i) => { const maxCount = Math.max(...crimeStats.incidentsByMonth) const height = maxCount > 0 ? (count / maxCount) * 100 : 0 @@ -461,19 +513,19 @@ export default function CrimeSidebar({
) })}
-
+
Jan Feb Mar @@ -490,12 +542,14 @@ export default function CrimeSidebar({ - }> -
+ }> +
} + bgColor="bg-gradient-to-r from-blue-900/30 to-blue-800/20" /> } + bgColor="bg-gradient-to-r from-amber-900/30 to-amber-800/20" /> 50} + icon={} + bgColor="bg-gradient-to-r from-green-900/30 to-green-800/20" />
- + - }> -
+ }> +
{topCategories.length > 0 ? ( topCategories.map((category) => ( - }> - - + }> + +
-

Crime Severity

-
+

Crime Severity

+
Low Crime Rate
-
+
Medium Crime Rate
-
+
High Crime Rate
- +
-

Map Markers

-
+

Map Markers

+
Individual Incident
-
-
5
- Incident Cluster +
+
5
+ Incident Cluster
- }> - - -

+ }> + + +

SIGAP Crime Map provides real-time visualization and analysis of crime incidents across Jember region.

@@ -598,41 +656,58 @@ export default function CrimeSidebar({ Data is sourced from official police reports and updated daily to ensure accurate information.

-
+
Version - 1.2.4 + 1.2.4
-
+
Last Updated - June 18, 2024 + June 18, 2024
- }> - - -
- Filtering -

- Use the year, month, and category filters at the top to - refine the data shown on the map. -

+ }> + + +
+
+ +
+
+ Filtering +

+ Use the year, month, and category filters at the top to + refine the data shown on the map. +

+
-
- District Information -

- Click on any district to view detailed crime statistics for that area. -

+ +
+
+ +
+
+ District Information +

+ Click on any district to view detailed crime statistics for that area. +

+
-
- Incidents -

- Click on incident markers to view details about specific crime reports. -

+ +
+
+ +
+
+ Incidents +

+ Click on incident markers to view details about specific crime reports. +

+
@@ -646,14 +721,14 @@ export default function CrimeSidebar({ @@ -670,8 +745,8 @@ interface SidebarSectionProps { function SidebarSection({ title, children, icon }: SidebarSectionProps) { return ( -
-

+
+

{icon} {title}

@@ -686,19 +761,29 @@ interface SystemStatusCardProps { statusIcon: React.ReactNode statusColor: string updatedTime?: string + bgColor?: string + borderColor?: string } -function SystemStatusCard({ title, status, statusIcon, statusColor, updatedTime }: SystemStatusCardProps) { +function SystemStatusCard({ + title, + status, + statusIcon, + statusColor, + updatedTime, + bgColor = "bg-sidebar-accent/20", + borderColor = "border-sidebar-border" +}: SystemStatusCardProps) { return ( - - -
{title}
-
+ + +
{title}
+
{statusIcon} {status}
{updatedTime && ( -
{updatedTime}
+
{updatedTime}
)}
@@ -723,8 +808,18 @@ function IncidentCard({ title, time, location, severity }: EnhancedIncidentCardP } }; + const getBorderColor = () => { + switch (severity) { + case "Low": return "border-l-green-500"; + case "Medium": return "border-l-yellow-500"; + case "High": return "border-l-orange-500"; + case "Critical": return "border-l-red-500"; + default: return "border-l-gray-500"; + } + }; + return ( - +
@@ -733,11 +828,11 @@ function IncidentCard({ title, time, location, severity }: EnhancedIncidentCardP

{title}

{severity}
-
+
{location}
-
{time}
+
{time}
@@ -750,17 +845,29 @@ interface StatCardProps { value: string change: string isPositive?: boolean + icon?: React.ReactNode + bgColor?: string } -function StatCard({ title, value, change, isPositive = false }: StatCardProps) { +function StatCard({ + title, + value, + change, + isPositive = false, + icon, + bgColor = "bg-white/10" +}: StatCardProps) { return ( - +
- {title} - {change} + + {icon} + {title} + + {change}
-
{value}
+
{value}
) @@ -774,47 +881,20 @@ interface CrimeTypeCardProps { function CrimeTypeCard({ type, count, percentage }: CrimeTypeCardProps) { return ( - +
{type} {count} cases
-
-
+
+
{percentage}%
) } - -interface ReportCardProps { - title: string - date: string - author: string -} - -function ReportCard({ title, date, author }: ReportCardProps) { - return ( - - -
- -
-

{title}

-
- - {author} -
-
{date}
-
-
-
-
- ) -} - -function PieChart(props: any) { - return ; -} diff --git a/sigap-website/app/_styles/globals.css b/sigap-website/app/_styles/globals.css index 0324c7b..d11ab53 100644 --- a/sigap-website/app/_styles/globals.css +++ b/sigap-website/app/_styles/globals.css @@ -127,24 +127,7 @@ --sidebar-border: 0 0% 25%; /* #404040 */ --sidebar-ring: 155 100% 19%; /* #006239 */ } -} -@layer base { - :root { - --chart-1: 12 76% 61%; - --chart-2: 173 58% 39%; - --chart-3: 197 37% 24%; - --chart-4: 43 74% 66%; - --chart-5: 27 87% 67%; - } - - .dark { - --chart-1: 220 70% 50%; - --chart-2: 160 60% 45%; - --chart-3: 30 80% 55%; - --chart-4: 280 65% 60%; - --chart-5: 340 75% 55%; - } * { @apply border-border outline-ring/50; }