feat: enhance crime analytics and sidebar components with improved type definitions and additional data handling

This commit is contained in:
vergiLgood1 2025-05-04 05:04:19 +07:00
parent 0de70f9057
commit 75df66a621
9 changed files with 96 additions and 52 deletions

View File

@ -1,6 +1,19 @@
import { useMemo } from 'react'; import { useMemo } from 'react';
import { ICrimes } from '@/app/_utils/types/crimes'; import { ICrimes } from '@/app/_utils/types/crimes';
export interface ICrimeAnalytics {
todaysIncidents: number;
totalIncidents: number;
recentIncidents: any[];
filteredIncidents: any[];
categoryCounts: Record<string, number>;
districts: Record<string, number>;
incidentsByMonth: number[];
clearanceRate: number;
incidentsByMonthDetail: Record<string, any[]>;
availableMonths: string[];
}
export function useCrimeAnalytics(crimes: ICrimes[]) { export function useCrimeAnalytics(crimes: ICrimes[]) {
return useMemo(() => { return useMemo(() => {
if (!crimes || !Array.isArray(crimes) || crimes.length === 0) if (!crimes || !Array.isArray(crimes) || crimes.length === 0)

View File

@ -1,13 +1,14 @@
import React from 'react' import React from 'react'
import { Card, CardContent } from "@/app/_components/ui/card" import { Card, CardContent } from "@/app/_components/ui/card"
import { crime_categories } from '@prisma/client'
interface CrimeTypeCardProps { export interface ICrimeTypeCardProps {
type: string type: string
count: number count: number
percentage: number percentage: number
} }
export function CrimeTypeCard({ type, count, percentage }: CrimeTypeCardProps) { export function CrimeTypeCard({ type, count, percentage }: ICrimeTypeCardProps) {
return ( return (
<Card className="bg-white/5 hover:bg-white/10 border-0 text-white shadow-none transition-colors"> <Card className="bg-white/5 hover:bg-white/10 border-0 text-white shadow-none transition-colors">
<CardContent className="p-3"> <CardContent className="p-3">

View File

@ -3,7 +3,7 @@ import { AlertTriangle, MapPin, Calendar } from 'lucide-react'
import { Badge } from "@/app/_components/ui/badge" import { Badge } from "@/app/_components/ui/badge"
import { Card, CardContent } from "@/app/_components/ui/card" import { Card, CardContent } from "@/app/_components/ui/card"
interface EnhancedIncidentCardProps { interface IIncidentCardProps {
title: string title: string
time: string time: string
location: string location: string
@ -19,7 +19,7 @@ export function IncidentCard({
severity, severity,
onClick, onClick,
showTimeAgo = true showTimeAgo = true
}: EnhancedIncidentCardProps) { }: IIncidentCardProps) {
const getBadgeColor = () => { const getBadgeColor = () => {
switch (severity) { switch (severity) {
case "Low": return "bg-green-500/20 text-green-300"; case "Low": return "bg-green-500/20 text-green-300";

View File

@ -1,12 +1,12 @@
import React from 'react' import React from 'react'
interface SidebarSectionProps { interface ISidebarSectionProps {
title: string title: string
children: React.ReactNode children: React.ReactNode
icon?: React.ReactNode icon?: React.ReactNode
} }
export function SidebarSection({ title, children, icon }: SidebarSectionProps) { export function SidebarSection({ title, children, icon }: ISidebarSectionProps) {
return ( return (
<div> <div>
<h3 className="text-sm font-medium text-sidebar-foreground/90 mb-3 flex items-center gap-2 pl-1"> <h3 className="text-sm font-medium text-sidebar-foreground/90 mb-3 flex items-center gap-2 pl-1">

View File

@ -1,7 +1,7 @@
import React from 'react' import React from 'react'
import { Card, CardContent } from "@/app/_components/ui/card" import { Card, CardContent } from "@/app/_components/ui/card"
interface StatCardProps { interface IStatCardProps {
title: string title: string
value: string value: string
change: string change: string
@ -17,7 +17,7 @@ export function StatCard({
isPositive = false, isPositive = false,
icon, icon,
bgColor = "bg-white/10" bgColor = "bg-white/10"
}: StatCardProps) { }: IStatCardProps) {
return ( return (
<Card className={`${bgColor} hover:bg-white/15 border-0 text-white shadow-none transition-colors`}> <Card className={`${bgColor} hover:bg-white/15 border-0 text-white shadow-none transition-colors`}>
<CardContent className="p-3"> <CardContent className="p-3">

View File

@ -1,7 +1,7 @@
import React from 'react' import React from 'react'
import { Card, CardContent } from "@/app/_components/ui/card" import { Card, CardContent } from "@/app/_components/ui/card"
interface SystemStatusCardProps { interface ISystemStatusCardProps {
title: string title: string
status: string status: string
statusIcon: React.ReactNode statusIcon: React.ReactNode
@ -19,7 +19,7 @@ export function SystemStatusCard({
updatedTime, updatedTime,
bgColor = "bg-sidebar-accent/20", bgColor = "bg-sidebar-accent/20",
borderColor = "border-sidebar-border" borderColor = "border-sidebar-border"
}: SystemStatusCardProps) { }: ISystemStatusCardProps) {
return ( return (
<Card className={`${bgColor} border ${borderColor} hover:border-sidebar-border/80 transition-colors`}> <Card className={`${bgColor} border ${borderColor} hover:border-sidebar-border/80 transition-colors`}>
<CardContent className="p-3 text-xs"> <CardContent className="p-3 text-xs">

View File

@ -7,21 +7,43 @@ import { Button } from "@/app/_components/ui/button"
import { formatMonthKey, getIncidentSeverity, getMonthName, getTimeAgo } from "@/app/_utils/common" import { formatMonthKey, getIncidentSeverity, getMonthName, getTimeAgo } from "@/app/_utils/common"
import { SystemStatusCard } from "../components/system-status-card" import { SystemStatusCard } from "../components/system-status-card"
import { IncidentCard } from "../components/incident-card" import { IncidentCard } from "../components/incident-card"
import { ICrimeAnalytics } from '@/app/(pages)/(admin)/dashboard/crime-management/crime-overview/_hooks/use-crime-analytics'
interface Incident {
id: string;
category: string;
address: string;
timestamp: string;
district?: string;
severity?: number;
status?: string;
description?: string;
location?: {
lat: number;
lng: number;
};
}
interface SidebarIncidentsTabProps { interface SidebarIncidentsTabProps {
crimeStats: any crimeStats: ICrimeAnalytics;
formattedDate: string formattedDate: string;
formattedTime: string formattedTime: string;
location: string location: string;
selectedMonth?: number | "all" selectedMonth?: number | "all";
selectedYear: number selectedYear: number;
selectedCategory: string | "all" selectedCategory: string | "all";
getTimePeriodDisplay: () => string getTimePeriodDisplay: () => string;
paginationState: Record<string, number> paginationState: Record<string, number>;
handlePageChange: (monthKey: string, direction: 'next' | 'prev') => void handlePageChange: (monthKey: string, direction: 'next' | 'prev') => void;
handleIncidentClick: (incident: any) => void handleIncidentClick: (incident: Incident) => void;
activeIncidentTab: string activeIncidentTab: string;
setActiveIncidentTab: (tab: string) => void setActiveIncidentTab: (tab: string) => void;
}
interface CrimeCategory {
type: string;
count: number;
percentage: number;
} }
export function SidebarIncidentsTab({ export function SidebarIncidentsTab({
@ -41,12 +63,11 @@ export function SidebarIncidentsTab({
}: SidebarIncidentsTabProps) { }: SidebarIncidentsTabProps) {
const topCategories = crimeStats.categoryCounts ? const topCategories = crimeStats.categoryCounts ?
Object.entries(crimeStats.categoryCounts) Object.entries(crimeStats.categoryCounts)
.sort((a: any, b: any) => (b[1] as number) - (a[1] as number)) .sort((a, b) => b[1] - a[1])
.slice(0, 4) .slice(0, 4)
.map(([type, count]: [string, unknown]) => { .map(([type, count]) => {
const countAsNumber = count as number; const percentage = Math.round((count / crimeStats.totalIncidents) * 100) || 0
const percentage = Math.round(((countAsNumber) / crimeStats.totalIncidents) * 100) || 0 return { type, count, percentage }
return { type, count: countAsNumber, percentage }
}) : [] }) : []
return ( return (
@ -166,7 +187,7 @@ export function SidebarIncidentsTab({
</Card> </Card>
) : ( ) : (
<div className="space-y-3"> <div className="space-y-3">
{crimeStats.recentIncidents.slice(0, 6).map((incident: any) => ( {crimeStats.recentIncidents.slice(0, 6).map((incident: Incident) => (
<IncidentCard <IncidentCard
key={incident.id} key={incident.id}
title={`${incident.category || 'Unknown'} in ${incident.address?.split(',')[0] || 'Unknown Location'}`} title={`${incident.category || 'Unknown'} in ${incident.address?.split(',')[0] || 'Unknown Location'}`}
@ -231,7 +252,7 @@ export function SidebarIncidentsTab({
</div> </div>
<div className="space-y-2"> <div className="space-y-2">
{paginatedIncidents.map((incident: any) => ( {paginatedIncidents.map((incident: Incident) => (
<IncidentCard <IncidentCard
key={incident.id} key={incident.id}
title={`${incident.category || 'Unknown'} in ${incident.address?.split(',')[0] || 'Unknown Location'}`} title={`${incident.category || 'Unknown'} in ${incident.address?.split(',')[0] || 'Unknown Location'}`}

View File

@ -6,10 +6,13 @@ import { cn } from "@/app/_lib/utils"
import { getMonthName } from "@/app/_utils/common" import { getMonthName } from "@/app/_utils/common"
import { SidebarSection } from "../components/sidebar-section" import { SidebarSection } from "../components/sidebar-section"
import { StatCard } from "../components/stat-card" import { StatCard } from "../components/stat-card"
import { CrimeTypeCard } from "../components/crime-type-card" import { CrimeTypeCard, ICrimeTypeCardProps } from "../components/crime-type-card"
import { ICrimeAnalytics } from '@/app/(pages)/(admin)/dashboard/crime-management/crime-overview/_hooks/use-crime-analytics'
import { MONTHS } from '@/app/_utils/const/common'
interface SidebarStatisticsTabProps {
crimeStats: any interface ISidebarStatisticsTabProps {
crimeStats: ICrimeAnalytics
selectedMonth?: number | "all" selectedMonth?: number | "all"
selectedYear: number selectedYear: number
} }
@ -18,15 +21,14 @@ export function SidebarStatisticsTab({
crimeStats, crimeStats,
selectedMonth = "all", selectedMonth = "all",
selectedYear selectedYear
}: SidebarStatisticsTabProps) { }: ISidebarStatisticsTabProps) {
const topCategories = crimeStats.categoryCounts ? const topCategories = crimeStats.categoryCounts ?
Object.entries(crimeStats.categoryCounts) Object.entries(crimeStats.categoryCounts)
.sort((a: any, b: any) => (b[1] as number) - (a[1] as number)) .sort((a, b) => b[1] - a[1])
.slice(0, 4) .slice(0, 4)
.map(([type, count]: [string, unknown]) => { .map(([type, count]) => {
const countAsNumber = count as number; const percentage = Math.round(((count) / crimeStats.totalIncidents) * 100) || 0
const percentage = Math.round(((countAsNumber) / crimeStats.totalIncidents) * 100) || 0 return { type, count, percentage }
return { type, count: countAsNumber, percentage }
}) : [] }) : []
return ( return (
@ -62,18 +64,11 @@ export function SidebarStatisticsTab({
})} })}
</div> </div>
<div className="flex justify-between mt-2 text-[10px] text-white/60"> <div className="flex justify-between mt-2 text-[10px] text-white/60">
<span>Jan</span> {MONTHS.map((month, i) => (
<span>Feb</span> <span key={i} className={cn("w-1/12 text-center", selectedMonth !== 'all' && i + 1 === Number(selectedMonth) ? "text-amber-400" : "")}>
<span>Mar</span> {month.substring(0, 3)}
<span>Apr</span> </span>
<span>May</span> ))}
<span>Jun</span>
<span>Jul</span>
<span>Aug</span>
<span>Sep</span>
<span>Oct</span>
<span>Nov</span>
<span>Dec</span>
</div> </div>
</CardContent> </CardContent>
</Card> </Card>
@ -119,7 +114,7 @@ export function SidebarStatisticsTab({
<SidebarSection title="Most Common Crimes" icon={<PieChart className="h-4 w-4 text-amber-400" />}> <SidebarSection title="Most Common Crimes" icon={<PieChart className="h-4 w-4 text-amber-400" />}>
<div className="space-y-3"> <div className="space-y-3">
{topCategories.length > 0 ? ( {topCategories.length > 0 ? (
topCategories.map((category: any) => ( topCategories.map((category: ICrimeTypeCardProps) => (
<CrimeTypeCard <CrimeTypeCard
key={category.type} key={category.type}
type={category.type} type={category.type}

View File

@ -0,0 +1,14 @@
export const MONTHS = [
'January',
'February',
'March',
'April',
'May',
'June',
'July',
'August',
'September',
'October',
'November',
'December',
];