feat: enhance crime analytics and sidebar components with improved type definitions and additional data handling
This commit is contained in:
parent
0de70f9057
commit
75df66a621
|
@ -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)
|
||||||
|
|
|
@ -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">
|
||||||
|
|
|
@ -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";
|
||||||
|
|
|
@ -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">
|
||||||
|
|
|
@ -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">
|
||||||
|
|
|
@ -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">
|
||||||
|
|
|
@ -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'}`}
|
||||||
|
|
|
@ -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}
|
||||||
|
|
|
@ -0,0 +1,14 @@
|
||||||
|
export const MONTHS = [
|
||||||
|
'January',
|
||||||
|
'February',
|
||||||
|
'March',
|
||||||
|
'April',
|
||||||
|
'May',
|
||||||
|
'June',
|
||||||
|
'July',
|
||||||
|
'August',
|
||||||
|
'September',
|
||||||
|
'October',
|
||||||
|
'November',
|
||||||
|
'December',
|
||||||
|
];
|
Loading…
Reference in New Issue