feat: add initial data seeding for resources, roles, demographics, crime incidents, and geographic data
- Created resources data structure in `resources.ts` - Added roles data structure in `roles.ts` - Implemented seeding for crime categories and incidents with detailed logic in `crime-category.ts` and `crime-incident.ts` - Developed demographic data seeding logic in `demographic.ts` - Implemented geographic data seeding from GeoJSON files in `geographic.ts` - Added permission seeding logic in `permission.ts` - Created resource and role seeding scripts in `resource.ts` and `role.ts`
This commit is contained in:
parent
410535e1d9
commit
63b0721859
|
@ -14,7 +14,7 @@ import {
|
|||
SidebarRail,
|
||||
} from "@/app/_components/ui/sidebar";
|
||||
import { NavPreMain } from "./navigations/nav-pre-main";
|
||||
import { navData } from "@/prisma/data/nav";
|
||||
import { navData } from "@/prisma/data/jsons/nav";
|
||||
import { TeamSwitcher } from "../../../_components/team-switcher";
|
||||
import { useGetCurrentUserQuery } from "../dashboard/user-management/_queries/queries";
|
||||
|
||||
|
|
|
@ -23,8 +23,8 @@ import {
|
|||
applyCookiePreferences,
|
||||
} from "@/app/_utils/cookies/cookies-manager";
|
||||
import { toast } from "sonner";
|
||||
import { initialTimezones, TimezoneType } from "@/prisma/data/timezones";
|
||||
import { languages, LanguageType } from "@/prisma/data/languages";
|
||||
import { initialTimezones, TimezoneType } from "@/prisma/data/jsons/timezones";
|
||||
import { languages, LanguageType } from "@/prisma/data/jsons/languages";
|
||||
|
||||
export default function PreferencesSettings() {
|
||||
const [language, setLanguage] = useState("en-US");
|
||||
|
|
|
@ -0,0 +1,26 @@
|
|||
import { Badge } from "@/app/_components/ui/badge"
|
||||
import { Button } from "@/app/_components/ui/button"
|
||||
import { Download, Calendar } from "lucide-react"
|
||||
|
||||
export default function AnalyticsHeader() {
|
||||
return (
|
||||
<div className="flex flex-col md:flex-row justify-between items-start md:items-center gap-4 mb-8">
|
||||
<div>
|
||||
<h2 className="text-3xl font-bold tracking-tight">Analytics & Reporting</h2>
|
||||
<p className="text-muted-foreground">Crime statistics, trends, and performance metrics</p>
|
||||
</div>
|
||||
<div className="flex flex-col sm:flex-row items-start sm:items-center gap-4">
|
||||
<div className="flex items-center gap-2">
|
||||
<Badge variant="outline" className="bg-blue-100 text-blue-800 hover:bg-blue-100 flex items-center">
|
||||
<Calendar className="h-3 w-3 mr-1" />
|
||||
Apr 1 - Apr 30, 2023
|
||||
</Badge>
|
||||
</div>
|
||||
<Button className="flex items-center">
|
||||
<Download className="h-4 w-4 mr-2" />
|
||||
Export Reports
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
|
@ -0,0 +1,79 @@
|
|||
"use client"
|
||||
|
||||
import { Progress } from "@/app/_components/ui/progress"
|
||||
import { Badge } from "@/app/_components/ui/badge"
|
||||
import { TrendingUp, TrendingDown } from "lucide-react"
|
||||
|
||||
export default function CaseResolutionRates() {
|
||||
const resolutionData = [
|
||||
{
|
||||
type: "Homicide",
|
||||
rate: 72,
|
||||
trend: "+5%",
|
||||
direction: "up",
|
||||
},
|
||||
{
|
||||
type: "Assault",
|
||||
rate: 65,
|
||||
trend: "+3%",
|
||||
direction: "up",
|
||||
},
|
||||
{
|
||||
type: "Robbery",
|
||||
rate: 48,
|
||||
trend: "-2%",
|
||||
direction: "down",
|
||||
},
|
||||
{
|
||||
type: "Theft",
|
||||
rate: 35,
|
||||
trend: "+1%",
|
||||
direction: "up",
|
||||
},
|
||||
{
|
||||
type: "Cybercrime",
|
||||
rate: 28,
|
||||
trend: "-4%",
|
||||
direction: "down",
|
||||
},
|
||||
]
|
||||
|
||||
return (
|
||||
<div className="mt-4 space-y-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<div className="text-2xl font-bold">52%</div>
|
||||
<div className="text-xs text-muted-foreground">Overall Case Clearance</div>
|
||||
</div>
|
||||
<Badge variant="outline" className="bg-green-100 text-green-800">
|
||||
+2% from last month
|
||||
</Badge>
|
||||
</div>
|
||||
|
||||
<div className="space-y-3">
|
||||
{resolutionData.map((item) => (
|
||||
<div key={item.type}>
|
||||
<div className="flex justify-between text-xs mb-1">
|
||||
<span>{item.type}</span>
|
||||
<div className="flex items-center">
|
||||
<span>{item.rate}%</span>
|
||||
<span className={`ml-1 ${item.direction === "up" ? "text-green-600" : "text-red-600"}`}>
|
||||
{item.direction === "up" ? (
|
||||
<TrendingUp className="h-3 w-3 ml-1" />
|
||||
) : (
|
||||
<TrendingDown className="h-3 w-3 ml-1" />
|
||||
)}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<Progress
|
||||
value={item.rate}
|
||||
className="h-2"
|
||||
indicatorClassName={item.rate > 60 ? "bg-green-500" : item.rate > 40 ? "bg-yellow-500" : "bg-red-500"}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
|
@ -0,0 +1,198 @@
|
|||
"use client"
|
||||
|
||||
import { useState } from "react"
|
||||
import { Button } from "@/app/_components/ui/button"
|
||||
import { Tabs, TabsList, TabsTrigger } from "@/app/_components/ui/tabs"
|
||||
import {
|
||||
ChartContainer,
|
||||
ChartTooltip,
|
||||
ChartTooltipContent,
|
||||
ChartLegend,
|
||||
ChartConfig,
|
||||
ChartLegendContent,
|
||||
} from "@/app/_components/ui/chart"
|
||||
import { Line, LineChart, XAxis, YAxis, CartesianGrid, ResponsiveContainer, Tooltip, BarChart } from "recharts"
|
||||
|
||||
const chartConfig = {
|
||||
desktop: {
|
||||
label: "Desktop",
|
||||
color: "#2563eb",
|
||||
},
|
||||
mobile: {
|
||||
label: "Mobile",
|
||||
color: "#60a5fa",
|
||||
},
|
||||
} satisfies ChartConfig
|
||||
|
||||
export default function CrimeTrends() {
|
||||
const [timeRange, setTimeRange] = useState("6m")
|
||||
const [chartType, setChartType] = useState("all")
|
||||
|
||||
// Sample data for the chart
|
||||
const data = [
|
||||
{ month: "Jan", violent: 42, property: 85, cyber: 18, other: 30 },
|
||||
{ month: "Feb", violent: 38, property: 78, cyber: 22, other: 28 },
|
||||
{ month: "Mar", violent: 45, property: 82, cyber: 25, other: 32 },
|
||||
{ month: "Apr", violent: 40, property: 75, cyber: 30, other: 35 },
|
||||
{ month: "May", violent: 35, property: 72, cyber: 28, other: 30 },
|
||||
{ month: "Jun", violent: 32, property: 68, cyber: 32, other: 28 },
|
||||
]
|
||||
|
||||
return (
|
||||
<div className="mt-4 h-[300px]">
|
||||
<div className="flex flex-col sm:flex-row justify-between items-start sm:items-center gap-4 mb-4">
|
||||
<Tabs defaultValue={chartType} onValueChange={setChartType} className="w-full sm:w-auto">
|
||||
<TabsList className="grid grid-cols-4 w-full sm:w-auto">
|
||||
<TabsTrigger value="all">All Crimes</TabsTrigger>
|
||||
<TabsTrigger value="violent">Violent</TabsTrigger>
|
||||
<TabsTrigger value="property">Property</TabsTrigger>
|
||||
<TabsTrigger value="cyber">Cyber</TabsTrigger>
|
||||
</TabsList>
|
||||
</Tabs>
|
||||
|
||||
<div className="flex gap-2">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => setTimeRange("3m")}
|
||||
className={timeRange === "3m" ? "bg-muted" : ""}
|
||||
>
|
||||
3M
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => setTimeRange("6m")}
|
||||
className={timeRange === "6m" ? "bg-muted" : ""}
|
||||
>
|
||||
6M
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => setTimeRange("1y")}
|
||||
className={timeRange === "1y" ? "bg-muted" : ""}
|
||||
>
|
||||
1Y
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => setTimeRange("all")}
|
||||
className={timeRange === "all" ? "bg-muted" : ""}
|
||||
>
|
||||
All
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<ChartContainer className="h-[250px]" config={chartConfig}>
|
||||
<>
|
||||
<ResponsiveContainer width="100%" height="100%">
|
||||
<LineChart data={data} margin={{ top: 5, right: 10, left: 0, bottom: 5 }}>
|
||||
<CartesianGrid strokeDasharray="3 3" className="stroke-muted" />
|
||||
<XAxis dataKey="month" className="text-xs" />
|
||||
<YAxis className="text-xs" />
|
||||
<Tooltip content={<CustomTooltip />} />
|
||||
{chartType === "all" || chartType === "violent" ? (
|
||||
<Line
|
||||
type="monotone"
|
||||
dataKey="violent"
|
||||
stroke="#ef4444"
|
||||
strokeWidth={2}
|
||||
dot={{ r: 4 }}
|
||||
activeDot={{ r: 6 }}
|
||||
/>
|
||||
) : null}
|
||||
{chartType === "all" || chartType === "property" ? (
|
||||
<Line
|
||||
type="monotone"
|
||||
dataKey="property"
|
||||
stroke="#3b82f6"
|
||||
strokeWidth={2}
|
||||
dot={{ r: 4 }}
|
||||
activeDot={{ r: 6 }}
|
||||
/>
|
||||
) : null}
|
||||
{chartType === "all" || chartType === "cyber" ? (
|
||||
<Line
|
||||
type="monotone"
|
||||
dataKey="cyber"
|
||||
stroke="#10b981"
|
||||
strokeWidth={2}
|
||||
dot={{ r: 4 }}
|
||||
activeDot={{ r: 6 }}
|
||||
/>
|
||||
) : null}
|
||||
{chartType === "all" ? (
|
||||
<Line
|
||||
type="monotone"
|
||||
dataKey="other"
|
||||
stroke="#a855f7"
|
||||
strokeWidth={2}
|
||||
dot={{ r: 4 }}
|
||||
activeDot={{ r: 6 }}
|
||||
/>
|
||||
) : null}
|
||||
</LineChart>
|
||||
</ResponsiveContainer>
|
||||
<ChartLegend className="justify-center mt-2">
|
||||
{chartType === "all" || chartType === "violent" ? (
|
||||
<ChartLegendContent>
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="h-2 w-2 rounded-full" style={{ backgroundColor: "#ef4444" }}></div>
|
||||
<span>Violent Crime</span>
|
||||
</div>
|
||||
</ChartLegendContent>
|
||||
) : null}
|
||||
{chartType === "all" || chartType === "property" ? (
|
||||
<ChartLegendContent>
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="h-2 w-2 rounded-full" style={{ backgroundColor: "#3b82f6" }}></div>
|
||||
<span>Property Crime</span>
|
||||
</div>
|
||||
</ChartLegendContent>
|
||||
) : null}
|
||||
{chartType === "all" || chartType === "cyber" ? (
|
||||
<ChartLegendContent>
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="h-2 w-2 rounded-full" style={{ backgroundColor: "#10b981" }}></div>
|
||||
<span>Cybercrime</span>
|
||||
</div>
|
||||
</ChartLegendContent>
|
||||
) : null}
|
||||
{chartType === "all" ? (
|
||||
<ChartLegendContent>
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="h-2 w-2 rounded-full" style={{ backgroundColor: "#a855f7" }}></div>
|
||||
<span>Other</span>
|
||||
</div>
|
||||
</ChartLegendContent>
|
||||
) : null}
|
||||
</ChartLegend>
|
||||
</>
|
||||
</ChartContainer>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function CustomTooltip({ active, payload, label }: any) {
|
||||
if (active && payload && payload.length) {
|
||||
return (
|
||||
<ChartTooltip content={
|
||||
<ChartTooltipContent>
|
||||
<div className="font-medium">{label}</div>
|
||||
{payload.map((entry: any, index: number) => (
|
||||
<div key={`item-${index}`} className="flex items-center gap-2 text-sm">
|
||||
<div className="h-2 w-2 rounded-full" style={{ backgroundColor: entry.color }}></div>
|
||||
<span className="font-medium">{entry.name}:</span>
|
||||
<span>{entry.value} incidents</span>
|
||||
</div>
|
||||
))}
|
||||
</ChartTooltipContent>
|
||||
} />
|
||||
)
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
|
@ -0,0 +1,92 @@
|
|||
import { Button } from "@/app/_components/ui/button"
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/app/_components/ui/select"
|
||||
import { Checkbox } from "@/app/_components/ui/checkbox"
|
||||
import { Download, FileText } from "lucide-react"
|
||||
|
||||
export default function CustomReportBuilder() {
|
||||
const reportTypes = [
|
||||
{ id: "crime-stats", label: "Crime Statistics" },
|
||||
{ id: "officer-perf", label: "Officer Performance" },
|
||||
{ id: "response-time", label: "Response Times" },
|
||||
{ id: "case-resolution", label: "Case Resolution" },
|
||||
{ id: "geographic", label: "Geographic Analysis" },
|
||||
]
|
||||
|
||||
const savedReports = [
|
||||
{ name: "Monthly Crime Summary", date: "Apr 30, 2023", type: "PDF" },
|
||||
{ name: "Q1 Performance Review", date: "Mar 31, 2023", type: "XLSX" },
|
||||
]
|
||||
|
||||
return (
|
||||
<div className="mt-4 space-y-4">
|
||||
<div className="space-y-3">
|
||||
<div>
|
||||
<label className="text-xs text-muted-foreground mb-1 block">Report Type</label>
|
||||
<Select>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="Select report type" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="comprehensive">Comprehensive Report</SelectItem>
|
||||
<SelectItem value="summary">Summary Report</SelectItem>
|
||||
<SelectItem value="comparative">Comparative Analysis</SelectItem>
|
||||
<SelectItem value="trend">Trend Analysis</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="text-xs text-muted-foreground mb-1 block">Time Period</label>
|
||||
<Select>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="Select time period" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="current-month">Current Month</SelectItem>
|
||||
<SelectItem value="previous-month">Previous Month</SelectItem>
|
||||
<SelectItem value="quarter">Last Quarter</SelectItem>
|
||||
<SelectItem value="year">Year to Date</SelectItem>
|
||||
<SelectItem value="custom">Custom Range</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="text-xs text-muted-foreground mb-1 block">Include Sections</label>
|
||||
<div className="grid grid-cols-2 gap-2 mt-1">
|
||||
{reportTypes.map((type) => (
|
||||
<div key={type.id} className="flex items-center space-x-2">
|
||||
<Checkbox id={type.id} />
|
||||
<label htmlFor={type.id} className="text-xs">
|
||||
{type.label}
|
||||
</label>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Button className="w-full mt-2">
|
||||
<FileText className="h-4 w-4 mr-2" />
|
||||
Generate Report
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className="border-t pt-3">
|
||||
<h4 className="text-sm font-medium mb-2">Saved Reports</h4>
|
||||
<div className="space-y-2">
|
||||
{savedReports.map((report, index) => (
|
||||
<div key={index} className="flex items-center justify-between p-2 border rounded-lg">
|
||||
<div>
|
||||
<div className="text-sm font-medium">{report.name}</div>
|
||||
<div className="text-xs text-muted-foreground">{report.date}</div>
|
||||
</div>
|
||||
<Button variant="ghost" size="sm">
|
||||
<Download className="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
|
@ -0,0 +1,98 @@
|
|||
import { Badge } from "@/app/_components/ui/badge"
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/app/_components/ui/select"
|
||||
|
||||
export default function GeographicalAnalysis() {
|
||||
return (
|
||||
<div className="mt-4 h-full">
|
||||
<div className="flex justify-between items-center mb-4">
|
||||
<Select defaultValue="all">
|
||||
<SelectTrigger className="w-[180px]">
|
||||
<SelectValue placeholder="Crime Type" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="all">All Crimes</SelectItem>
|
||||
<SelectItem value="violent">Violent Crimes</SelectItem>
|
||||
<SelectItem value="property">Property Crimes</SelectItem>
|
||||
<SelectItem value="cyber">Cybercrimes</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
|
||||
<Select defaultValue="heat">
|
||||
<SelectTrigger className="w-[180px]">
|
||||
<SelectValue placeholder="Map Type" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="heat">Heat Map</SelectItem>
|
||||
<SelectItem value="pin">Pin Map</SelectItem>
|
||||
<SelectItem value="district">District Map</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
<div className="h-[400px] rounded-md bg-slate-100 border flex items-center justify-center relative overflow-hidden">
|
||||
<div className="absolute inset-0 opacity-50 bg-[url('/placeholder.svg?height=400&width=600')] bg-center bg-cover"></div>
|
||||
|
||||
<div className="absolute top-2 right-2 bg-background/80 backdrop-blur-sm p-2 rounded-md text-xs font-medium">
|
||||
<div className="flex items-center gap-1">
|
||||
<span className="w-2 h-2 rounded-full bg-red-500"></span> High Density
|
||||
</div>
|
||||
<div className="flex items-center gap-1">
|
||||
<span className="w-2 h-2 rounded-full bg-yellow-500"></span> Medium Density
|
||||
</div>
|
||||
<div className="flex items-center gap-1">
|
||||
<span className="w-2 h-2 rounded-full bg-green-500"></span> Low Density
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="z-10 bg-background/80 backdrop-blur-sm p-3 rounded-lg">
|
||||
<span className="font-medium">Crime Hotspot Map</span>
|
||||
<div className="text-xs text-muted-foreground mt-1">Showing data for April 2023</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mt-4 grid grid-cols-2 gap-2">
|
||||
<div className="border rounded-lg p-3">
|
||||
<h4 className="text-sm font-medium">Highest Crime Areas</h4>
|
||||
<div className="mt-2 space-y-2">
|
||||
<div className="flex justify-between items-center">
|
||||
<span className="text-xs">Downtown District</span>
|
||||
<Badge variant="outline" className="bg-red-100 text-red-800">
|
||||
High
|
||||
</Badge>
|
||||
</div>
|
||||
<div className="flex justify-between items-center">
|
||||
<span className="text-xs">West Side Commercial</span>
|
||||
<Badge variant="outline" className="bg-yellow-100 text-yellow-800">
|
||||
Medium
|
||||
</Badge>
|
||||
</div>
|
||||
<div className="flex justify-between items-center">
|
||||
<span className="text-xs">North Transit Hub</span>
|
||||
<Badge variant="outline" className="bg-yellow-100 text-yellow-800">
|
||||
Medium
|
||||
</Badge>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="border rounded-lg p-3">
|
||||
<h4 className="text-sm font-medium">Crime Reduction</h4>
|
||||
<div className="mt-2 space-y-2">
|
||||
<div className="flex justify-between items-center">
|
||||
<span className="text-xs">East Residential</span>
|
||||
<span className="text-xs text-green-600">-15% MoM</span>
|
||||
</div>
|
||||
<div className="flex justify-between items-center">
|
||||
<span className="text-xs">South Park Area</span>
|
||||
<span className="text-xs text-green-600">-8% MoM</span>
|
||||
</div>
|
||||
<div className="flex justify-between items-center">
|
||||
<span className="text-xs">Central Business</span>
|
||||
<span className="text-xs text-red-600">+5% MoM</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
|
@ -0,0 +1,145 @@
|
|||
"use client"
|
||||
|
||||
import { Tabs, TabsList, TabsTrigger } from "@/app/_components/ui/tabs"
|
||||
import {
|
||||
ChartContainer,
|
||||
ChartTooltip,
|
||||
ChartTooltipContent,
|
||||
ChartLegend,
|
||||
ChartLegendContent,
|
||||
ChartConfig,
|
||||
} from "@/app/_components/ui/chart"
|
||||
import { Cell, Pie, PieChart, ResponsiveContainer, Tooltip } from "recharts"
|
||||
import { BarChart } from "lucide-react"
|
||||
|
||||
const chartConfig = {
|
||||
desktop: {
|
||||
label: "Desktop",
|
||||
color: "#2563eb",
|
||||
},
|
||||
mobile: {
|
||||
label: "Mobile",
|
||||
color: "#60a5fa",
|
||||
},
|
||||
} satisfies ChartConfig
|
||||
|
||||
export default function IncidentTypeBreakdown() {
|
||||
// Sample data for the chart
|
||||
const data = [
|
||||
{ name: "Theft", value: 35, color: "#3b82f6" },
|
||||
{ name: "Assault", value: 20, color: "#ef4444" },
|
||||
{ name: "Vandalism", value: 15, color: "#f59e0b" },
|
||||
{ name: "Fraud", value: 10, color: "#10b981" },
|
||||
{ name: "Drugs", value: 8, color: "#8b5cf6" },
|
||||
{ name: "Other", value: 12, color: "#6b7280" },
|
||||
]
|
||||
|
||||
return (
|
||||
<div className="mt-4">
|
||||
<div className="flex justify-between items-center mb-4">
|
||||
<Tabs defaultValue="pie">
|
||||
<TabsList>
|
||||
<TabsTrigger value="pie">Pie Chart</TabsTrigger>
|
||||
<TabsTrigger value="bar">Bar Chart</TabsTrigger>
|
||||
</TabsList>
|
||||
</Tabs>
|
||||
|
||||
<Tabs defaultValue="all">
|
||||
<TabsList>
|
||||
<TabsTrigger value="all">All Time</TabsTrigger>
|
||||
<TabsTrigger value="month">This Month</TabsTrigger>
|
||||
<TabsTrigger value="week">This Week</TabsTrigger>
|
||||
</TabsList>
|
||||
</Tabs>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<ChartContainer config={chartConfig} className="h-[250px]">
|
||||
<>
|
||||
|
||||
<ResponsiveContainer width="100%" height="100%">
|
||||
<PieChart>
|
||||
<Pie data={data} cx="50%" cy="50%" innerRadius={60} outerRadius={80} paddingAngle={2} dataKey="value">
|
||||
{data.map((entry, index) => (
|
||||
<Cell key={`cell-${index}`} fill={entry.color} />
|
||||
))}
|
||||
</Pie>
|
||||
<Tooltip content={<CustomTooltip />} />
|
||||
</PieChart>
|
||||
</ResponsiveContainer>
|
||||
|
||||
<ChartLegend className="justify-center mt-2">
|
||||
{data.map((item) => (
|
||||
<ChartLegendContent key={item.name}>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="w-2 h-2 rounded-full" style={{ backgroundColor: item.color }}></span>
|
||||
<span>{item.name}</span>
|
||||
</div>
|
||||
</ChartLegendContent>
|
||||
))}
|
||||
</ChartLegend>
|
||||
</>
|
||||
</ChartContainer>
|
||||
|
||||
<div className="space-y-3">
|
||||
<div className="border rounded-lg p-3">
|
||||
<h4 className="text-sm font-medium mb-2">Key Insights</h4>
|
||||
<ul className="text-xs space-y-2">
|
||||
<li className="flex items-start gap-2">
|
||||
<span className="w-2 h-2 rounded-full bg-blue-500 mt-1"></span>
|
||||
<span>
|
||||
Theft accounts for the largest portion of incidents (35%), with a 5% increase from the previous month.
|
||||
</span>
|
||||
</li>
|
||||
<li className="flex items-start gap-2">
|
||||
<span className="w-2 h-2 rounded-full bg-red-500 mt-1"></span>
|
||||
<span>Assault incidents have decreased by 3% compared to the previous month.</span>
|
||||
</li>
|
||||
<li className="flex items-start gap-2">
|
||||
<span className="w-2 h-2 rounded-full bg-green-500 mt-1"></span>
|
||||
<span>Fraud reports have increased by 8%, indicating a growing trend in financial crimes.</span>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div className="border rounded-lg p-3">
|
||||
<h4 className="text-sm font-medium mb-2">Recommendations</h4>
|
||||
<ul className="text-xs space-y-2">
|
||||
<li className="flex items-start gap-2">
|
||||
<span className="w-2 h-2 rounded-full bg-purple-500 mt-1"></span>
|
||||
<span>Increase patrols in high-theft areas, particularly in commercial districts.</span>
|
||||
</li>
|
||||
<li className="flex items-start gap-2">
|
||||
<span className="w-2 h-2 rounded-full bg-purple-500 mt-1"></span>
|
||||
<span>Launch a public awareness campaign about fraud prevention.</span>
|
||||
</li>
|
||||
<li className="flex items-start gap-2">
|
||||
<span className="w-2 h-2 rounded-full bg-purple-500 mt-1"></span>
|
||||
<span>Continue community engagement programs that have helped reduce assault incidents.</span>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function CustomTooltip({ active, payload }: any) {
|
||||
if (active && payload && payload.length) {
|
||||
return (
|
||||
<ChartTooltip content={
|
||||
<ChartTooltipContent>
|
||||
<div className="font-medium">{payload[0].name}</div>
|
||||
<div className="flex items-center gap-2 text-sm">
|
||||
<span>{payload[0].value}% of total incidents</span>
|
||||
</div>
|
||||
</ChartTooltipContent>
|
||||
}>
|
||||
|
||||
</ChartTooltip>
|
||||
)
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
|
@ -0,0 +1,85 @@
|
|||
"use client"
|
||||
|
||||
import { Progress } from "@/app/_components/ui/progress"
|
||||
import { Award } from "lucide-react"
|
||||
|
||||
export default function OfficerPerformanceMetrics() {
|
||||
const officers = [
|
||||
{
|
||||
name: "Emily Parker",
|
||||
metric: "Case Clearance",
|
||||
value: 92,
|
||||
rank: 1,
|
||||
},
|
||||
{
|
||||
name: "Michael Chen",
|
||||
metric: "Response Time",
|
||||
value: 88,
|
||||
rank: 2,
|
||||
},
|
||||
{
|
||||
name: "Sarah Johnson",
|
||||
metric: "Evidence Processing",
|
||||
value: 95,
|
||||
rank: 3,
|
||||
},
|
||||
]
|
||||
|
||||
const departmentMetrics = [
|
||||
{
|
||||
name: "Cases Assigned",
|
||||
value: 245,
|
||||
change: "+12",
|
||||
period: "this month",
|
||||
},
|
||||
{
|
||||
name: "Cases Closed",
|
||||
value: 182,
|
||||
change: "+8",
|
||||
period: "this month",
|
||||
},
|
||||
{
|
||||
name: "Avg. Case Duration",
|
||||
value: "18.5 days",
|
||||
change: "-2.3",
|
||||
period: "from last month",
|
||||
},
|
||||
]
|
||||
|
||||
return (
|
||||
<div className="mt-4 space-y-4">
|
||||
<div className="border rounded-lg p-3">
|
||||
<h4 className="font-medium text-sm mb-3 flex items-center">
|
||||
<Award className="h-4 w-4 mr-1" />
|
||||
Top Performers
|
||||
</h4>
|
||||
|
||||
<div className="space-y-3">
|
||||
{officers.map((officer) => (
|
||||
<div key={officer.name}>
|
||||
<div className="flex justify-between text-sm mb-1">
|
||||
<span>{officer.name}</span>
|
||||
<span>
|
||||
{officer.metric}: {officer.value}%
|
||||
</span>
|
||||
</div>
|
||||
<Progress value={officer.value} className="h-2" />
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 gap-2">
|
||||
{departmentMetrics.map((metric) => (
|
||||
<div key={metric.name} className="border rounded-lg p-3">
|
||||
<div className="text-sm text-muted-foreground">{metric.name}</div>
|
||||
<div className="text-xl font-bold mt-1">{metric.value}</div>
|
||||
<div className="text-xs text-green-600 mt-1">
|
||||
{metric.change} {metric.period}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
|
@ -0,0 +1,84 @@
|
|||
import { Badge } from "@/app/_components/ui/badge"
|
||||
import { AlertTriangle, TrendingUp } from "lucide-react"
|
||||
|
||||
export default function PredictiveAnalytics() {
|
||||
const predictions = [
|
||||
{
|
||||
area: "Downtown District",
|
||||
crimeType: "Theft",
|
||||
riskLevel: "High",
|
||||
confidence: "85%",
|
||||
trend: "Increasing",
|
||||
},
|
||||
{
|
||||
area: "West Side Commercial",
|
||||
crimeType: "Vandalism",
|
||||
riskLevel: "Medium",
|
||||
confidence: "72%",
|
||||
trend: "Stable",
|
||||
},
|
||||
{
|
||||
area: "North Transit Hub",
|
||||
crimeType: "Assault",
|
||||
riskLevel: "Medium",
|
||||
confidence: "68%",
|
||||
trend: "Increasing",
|
||||
},
|
||||
]
|
||||
|
||||
return (
|
||||
<div className="mt-4 space-y-3">
|
||||
<div className="flex items-center justify-between">
|
||||
<h4 className="text-sm font-medium">7-Day Forecast</h4>
|
||||
<Badge variant="outline" className="bg-blue-100 text-blue-800">
|
||||
AI-Powered
|
||||
</Badge>
|
||||
</div>
|
||||
|
||||
{predictions.map((prediction, index) => (
|
||||
<div key={index} className="border rounded-lg p-3">
|
||||
<div className="flex items-center gap-2">
|
||||
<AlertTriangle
|
||||
className={`h-4 w-4 ${prediction.riskLevel === "High" ? "text-red-500" : "text-yellow-500"}`}
|
||||
/>
|
||||
<h4 className="font-medium text-sm">{prediction.area}</h4>
|
||||
<Badge
|
||||
variant="outline"
|
||||
className={
|
||||
prediction.riskLevel === "High"
|
||||
? "bg-red-100 text-red-800 ml-auto"
|
||||
: "bg-yellow-100 text-yellow-800 ml-auto"
|
||||
}
|
||||
>
|
||||
{prediction.riskLevel} Risk
|
||||
</Badge>
|
||||
</div>
|
||||
|
||||
<div className="mt-2 text-xs">
|
||||
<div className="flex justify-between">
|
||||
<span>Predicted Crime:</span>
|
||||
<span className="font-medium">{prediction.crimeType}</span>
|
||||
</div>
|
||||
<div className="flex justify-between mt-1">
|
||||
<span>Confidence:</span>
|
||||
<span className="font-medium">{prediction.confidence}</span>
|
||||
</div>
|
||||
<div className="flex justify-between mt-1">
|
||||
<span>Trend:</span>
|
||||
<span
|
||||
className={`font-medium flex items-center ${prediction.trend === "Increasing" ? "text-red-600" : "text-blue-600"}`}
|
||||
>
|
||||
{prediction.trend}
|
||||
{prediction.trend === "Increasing" && <TrendingUp className="h-3 w-3 ml-1" />}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
|
||||
<div className="text-xs text-center text-muted-foreground mt-2">
|
||||
Predictions based on historical data, weather patterns, and local events
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
|
@ -0,0 +1,103 @@
|
|||
"use client"
|
||||
|
||||
import { Progress } from "@/app/_components/ui/progress"
|
||||
import { Clock, TrendingDown } from "lucide-react"
|
||||
import { ChartContainer, ChartTooltip, ChartTooltipContent } from "@/app/_components/ui/chart"
|
||||
import { Bar, BarChart, XAxis, YAxis, CartesianGrid, ResponsiveContainer, Tooltip } from "recharts"
|
||||
|
||||
export default function ResponseTimeMetrics() {
|
||||
// Sample data for the chart
|
||||
const data = [
|
||||
{ name: "Violent", time: 4.2 },
|
||||
{ name: "Theft", time: 8.5 },
|
||||
{ name: "Domestic", time: 5.1 },
|
||||
{ name: "Traffic", time: 9.8 },
|
||||
{ name: "Noise", time: 12.3 },
|
||||
]
|
||||
|
||||
return (
|
||||
<div className="mt-4 space-y-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<div className="text-2xl font-bold">4.2m</div>
|
||||
<div className="text-xs text-muted-foreground">Average Response Time</div>
|
||||
</div>
|
||||
<div className="text-xs text-green-600 flex items-center">
|
||||
<TrendingDown className="h-3 w-3 mr-1" />
|
||||
-0.3m from last month
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<div>
|
||||
<div className="flex justify-between text-xs mb-1">
|
||||
<span>Priority 1 (Emergency)</span>
|
||||
<div className="flex items-center">
|
||||
<Clock className="h-3 w-3 mr-1" />
|
||||
<span>3.2 min avg</span>
|
||||
</div>
|
||||
</div>
|
||||
<Progress value={80} className="h-2" indicatorClassName="bg-red-500" />
|
||||
</div>
|
||||
<div>
|
||||
<div className="flex justify-between text-xs mb-1">
|
||||
<span>Priority 2 (Urgent)</span>
|
||||
<div className="flex items-center">
|
||||
<Clock className="h-3 w-3 mr-1" />
|
||||
<span>5.8 min avg</span>
|
||||
</div>
|
||||
</div>
|
||||
<Progress value={70} className="h-2" indicatorClassName="bg-orange-500" />
|
||||
</div>
|
||||
<div>
|
||||
<div className="flex justify-between text-xs mb-1">
|
||||
<span>Priority 3 (Standard)</span>
|
||||
<div className="flex items-center">
|
||||
<Clock className="h-3 w-3 mr-1" />
|
||||
<span>12.5 min avg</span>
|
||||
</div>
|
||||
</div>
|
||||
<Progress value={60} className="h-2" indicatorClassName="bg-blue-500" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="h-[120px]">
|
||||
<ChartContainer>
|
||||
|
||||
<ResponsiveContainer width="100%" height="100%">
|
||||
<BarChart data={data} margin={{ top: 5, right: 5, left: 0, bottom: 5 }}>
|
||||
<CartesianGrid strokeDasharray="3 3" className="stroke-muted" />
|
||||
<XAxis dataKey="name" className="text-xs" />
|
||||
<YAxis className="text-xs" />
|
||||
<Tooltip content={<CustomTooltip />} />
|
||||
<Bar dataKey="time" fill="#3b82f6" radius={[4, 4, 0, 0]} />
|
||||
</BarChart>
|
||||
</ResponsiveContainer>
|
||||
|
||||
</ChartContainer>
|
||||
</div>
|
||||
|
||||
<div className="text-xs text-center text-muted-foreground">Response time by incident type (minutes)</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function CustomTooltip({ active, payload, label }: any) {
|
||||
if (active && payload && payload.length) {
|
||||
return (
|
||||
<ChartTooltip content={
|
||||
<ChartTooltipContent>
|
||||
<div className="font-medium">{label} Incidents</div>
|
||||
<div className="flex items-center gap-2 text-sm">
|
||||
<span>Avg. Response Time:</span>
|
||||
<span className="font-medium">{payload[0].value} minutes</span>
|
||||
</div>
|
||||
</ChartTooltipContent>
|
||||
}>
|
||||
|
||||
</ChartTooltip>
|
||||
)
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
|
@ -0,0 +1,80 @@
|
|||
"use client"
|
||||
|
||||
import { Badge } from "@/app/_components/ui/badge"
|
||||
import { ChartContainer, ChartTooltip, ChartTooltipContent } from "@/app/_components/ui/chart"
|
||||
import { BarChart, Cell, Pie, PieChart, ResponsiveContainer, Tooltip } from "recharts"
|
||||
|
||||
export default function TimeOfDayAnalysis() {
|
||||
// Sample data for the chart
|
||||
const data = [
|
||||
{ name: "Morning (6AM-12PM)", value: 15, color: "#3b82f6" },
|
||||
{ name: "Afternoon (12PM-6PM)", value: 25, color: "#f59e0b" },
|
||||
{ name: "Evening (6PM-12AM)", value: 40, color: "#8b5cf6" },
|
||||
{ name: "Night (12AM-6AM)", value: 20, color: "#1e293b" },
|
||||
]
|
||||
|
||||
const highRiskTimes = [
|
||||
{ day: "Friday", time: "10PM - 2AM", risk: "High" },
|
||||
{ day: "Saturday", time: "11PM - 3AM", risk: "High" },
|
||||
{ day: "Sunday", time: "12AM - 4AM", risk: "Medium" },
|
||||
]
|
||||
|
||||
return (
|
||||
<div className="mt-4 space-y-4">
|
||||
<ChartContainer className="h-[150px]">
|
||||
<ResponsiveContainer width="100%" height="100%">
|
||||
<PieChart>
|
||||
<Pie data={data} cx="50%" cy="50%" innerRadius={30} outerRadius={50} paddingAngle={2} dataKey="value">
|
||||
{data.map((entry, index) => (
|
||||
<Cell key={`cell-${index}`} fill={entry.color} />
|
||||
))}
|
||||
</Pie>
|
||||
<Tooltip content={<CustomTooltip />} />
|
||||
</PieChart>
|
||||
</ResponsiveContainer>
|
||||
</ChartContainer>
|
||||
|
||||
<div className="text-xs text-center">
|
||||
<div className="font-medium">Crime Distribution by Time of Day</div>
|
||||
<div className="text-muted-foreground mt-1">Evening hours show highest incident rates</div>
|
||||
</div>
|
||||
|
||||
<div className="border rounded-lg p-3">
|
||||
<h4 className="text-sm font-medium mb-2">High-Risk Time Periods</h4>
|
||||
<div className="space-y-2">
|
||||
{highRiskTimes.map((item, index) => (
|
||||
<div key={index} className="flex justify-between items-center">
|
||||
<div className="text-xs">
|
||||
<span className="font-medium">{item.day}:</span> {item.time}
|
||||
</div>
|
||||
<Badge
|
||||
variant="outline"
|
||||
className={item.risk === "High" ? "bg-red-100 text-red-800" : "bg-yellow-100 text-yellow-800"}
|
||||
>
|
||||
{item.risk} Risk
|
||||
</Badge>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function CustomTooltip({ active, payload }: any) {
|
||||
if (active && payload && payload.length) {
|
||||
return (
|
||||
<ChartTooltip content={
|
||||
<ChartTooltipContent>
|
||||
<div className="font-medium">{payload[0].name}</div>
|
||||
<div className="flex items-center gap-2 text-sm">
|
||||
<span>{payload[0].value}% of incidents</span>
|
||||
</div>
|
||||
</ChartTooltipContent>
|
||||
}>
|
||||
</ChartTooltip>
|
||||
)
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
|
@ -0,0 +1,101 @@
|
|||
"use client"
|
||||
|
||||
import { BentoGrid, BentoGridItem } from "@/app/_components/ui/bento-grid"
|
||||
import { BarChart3, TrendingUp, Map, Calendar, Clock, FileText, AlertTriangle, Filter } from "lucide-react"
|
||||
import AnalyticsHeader from "./_components/analytics-header"
|
||||
import CrimeTrends from "./_components/crime-trends"
|
||||
import GeographicalAnalysis from "./_components/geographical-analysis"
|
||||
import ResponseTimeMetrics from "./_components/response-time-metrics"
|
||||
import CaseResolutionRates from "./_components/case-resolution-rates"
|
||||
import IncidentTypeBreakdown from "./_components/incident-type-breakdown"
|
||||
import OfficerPerformanceMetrics from "./_components/officer-performance-metrics"
|
||||
import TimeOfDayAnalysis from "./_components/time-of-day-analysis"
|
||||
import CustomReportBuilder from "./_components/custom-report-builder"
|
||||
import PredictiveAnalytics from "./_components/predictive-analytics"
|
||||
|
||||
export default function AnalyticsReportingPage() {
|
||||
return (
|
||||
<div className="container py-4 min-h-screen">
|
||||
<div className="max-w-7xl mx-auto">
|
||||
<AnalyticsHeader />
|
||||
|
||||
<BentoGrid>
|
||||
<BentoGridItem
|
||||
title="Crime Trends"
|
||||
description="Monthly and yearly crime statistics"
|
||||
icon={<TrendingUp className="w-5 h-5" />}
|
||||
colSpan="2"
|
||||
>
|
||||
<CrimeTrends />
|
||||
</BentoGridItem>
|
||||
|
||||
<BentoGridItem
|
||||
title="Geographical Analysis"
|
||||
description="Crime hotspots and patterns"
|
||||
icon={<Map className="w-5 h-5" />}
|
||||
rowSpan="2"
|
||||
>
|
||||
<GeographicalAnalysis />
|
||||
</BentoGridItem>
|
||||
|
||||
<BentoGridItem
|
||||
title="Response Time Metrics"
|
||||
description="Emergency response efficiency"
|
||||
icon={<Clock className="w-5 h-5" />}
|
||||
>
|
||||
<ResponseTimeMetrics />
|
||||
</BentoGridItem>
|
||||
|
||||
<BentoGridItem
|
||||
title="Case Resolution Rates"
|
||||
description="Clearance rates by crime type"
|
||||
icon={<FileText className="w-5 h-5" />}
|
||||
>
|
||||
<CaseResolutionRates />
|
||||
</BentoGridItem>
|
||||
|
||||
<BentoGridItem
|
||||
title="Incident Type Breakdown"
|
||||
description="Distribution of crime categories"
|
||||
icon={<BarChart3 className="w-5 h-5" />}
|
||||
colSpan="2"
|
||||
>
|
||||
<IncidentTypeBreakdown />
|
||||
</BentoGridItem>
|
||||
|
||||
<BentoGridItem
|
||||
title="Officer Performance Metrics"
|
||||
description="Productivity and efficiency analysis"
|
||||
icon={<BarChart3 className="w-5 h-5" />}
|
||||
>
|
||||
<OfficerPerformanceMetrics />
|
||||
</BentoGridItem>
|
||||
|
||||
<BentoGridItem
|
||||
title="Time of Day Analysis"
|
||||
description="Crime patterns by hour and day"
|
||||
icon={<Calendar className="w-5 h-5" />}
|
||||
>
|
||||
<TimeOfDayAnalysis />
|
||||
</BentoGridItem>
|
||||
|
||||
<BentoGridItem
|
||||
title="Predictive Analytics"
|
||||
description="Crime forecasting and risk assessment"
|
||||
icon={<AlertTriangle className="w-5 h-5" />}
|
||||
>
|
||||
<PredictiveAnalytics />
|
||||
</BentoGridItem>
|
||||
|
||||
<BentoGridItem
|
||||
title="Custom Report Builder"
|
||||
description="Generate tailored reports"
|
||||
icon={<Filter className="w-5 h-5" />}
|
||||
>
|
||||
<CustomReportBuilder />
|
||||
</BentoGridItem>
|
||||
</BentoGrid>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
|
@ -0,0 +1,68 @@
|
|||
import { Badge } from "@/app/_components/ui/badge"
|
||||
import { Calendar, MapPin } from "lucide-react"
|
||||
|
||||
export default function CommunityEvents() {
|
||||
const events = [
|
||||
{
|
||||
title: "Community Safety Workshop",
|
||||
date: "Apr 28, 2023",
|
||||
time: "6:00 PM - 8:00 PM",
|
||||
location: "Community Center",
|
||||
type: "Workshop",
|
||||
},
|
||||
{
|
||||
title: "Coffee with a Cop",
|
||||
date: "May 5, 2023",
|
||||
time: "9:00 AM - 11:00 AM",
|
||||
location: "Downtown Café",
|
||||
type: "Meet & Greet",
|
||||
},
|
||||
{
|
||||
title: "Neighborhood Watch Meeting",
|
||||
date: "May 12, 2023",
|
||||
time: "7:00 PM - 8:30 PM",
|
||||
location: "Public Library",
|
||||
type: "Meeting",
|
||||
},
|
||||
]
|
||||
|
||||
return (
|
||||
<div className="mt-4 space-y-3">
|
||||
{events.map((event, index) => (
|
||||
<div key={index} className="border rounded-lg p-3">
|
||||
<div className="flex justify-between items-start">
|
||||
<h4 className="font-medium text-sm">{event.title}</h4>
|
||||
<Badge variant="outline" className="bg-blue-100 text-blue-800">
|
||||
{event.type}
|
||||
</Badge>
|
||||
</div>
|
||||
|
||||
<div className="mt-2 space-y-1 text-xs text-muted-foreground">
|
||||
<div className="flex items-center">
|
||||
<Calendar className="h-3 w-3 mr-1" />
|
||||
{event.date}, {event.time}
|
||||
</div>
|
||||
<div className="flex items-center">
|
||||
<MapPin className="h-3 w-3 mr-1" />
|
||||
{event.location}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mt-2 flex justify-between items-center">
|
||||
<div className="flex -space-x-2">
|
||||
{[1, 2, 3].map((i) => (
|
||||
<div key={i} className="w-5 h-5 rounded-full border border-background bg-muted"></div>
|
||||
))}
|
||||
<div className="w-5 h-5 rounded-full border border-background bg-muted flex items-center justify-center text-[10px]">
|
||||
+8
|
||||
</div>
|
||||
</div>
|
||||
<button className="text-xs text-primary">RSVP</button>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
|
||||
<button className="w-full text-sm text-primary mt-2 py-1">View All Events</button>
|
||||
</div>
|
||||
)
|
||||
}
|
|
@ -0,0 +1,50 @@
|
|||
import { Button } from "@/app/_components/ui/button"
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/app/_components/ui/select"
|
||||
import { MessageSquare } from "lucide-react"
|
||||
|
||||
export default function CommunityFeedback() {
|
||||
return (
|
||||
<div className="mt-4 space-y-4">
|
||||
<div className="text-xs text-muted-foreground">
|
||||
We value your input! Share your thoughts, concerns, or suggestions to help us better serve the community.
|
||||
</div>
|
||||
|
||||
<div className="space-y-3">
|
||||
<div>
|
||||
<label className="text-xs text-muted-foreground mb-1 block">Feedback Type</label>
|
||||
<Select>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="Select feedback type" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="suggestion">Suggestion</SelectItem>
|
||||
<SelectItem value="concern">Concern</SelectItem>
|
||||
<SelectItem value="compliment">Compliment</SelectItem>
|
||||
<SelectItem value="question">Question</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="text-xs text-muted-foreground mb-1 block">Your Message</label>
|
||||
<textarea
|
||||
placeholder="Share your feedback..."
|
||||
className="w-full rounded-md border border-input bg-background px-3 py-2 text-sm min-h-[80px] ring-offset-background placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
|
||||
></textarea>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-2">
|
||||
<input type="checkbox" id="contact-me" className="rounded border-input" />
|
||||
<label htmlFor="contact-me" className="text-xs">
|
||||
I would like to be contacted about my feedback
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<Button className="w-full flex items-center justify-center">
|
||||
<MessageSquare className="h-4 w-4 mr-2" />
|
||||
Submit Feedback
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
|
@ -0,0 +1,27 @@
|
|||
import { Button } from "@/app/_components/ui/button"
|
||||
import { Search, Phone } from "lucide-react"
|
||||
|
||||
export default function CommunityHeader() {
|
||||
return (
|
||||
<div className="flex flex-col md:flex-row justify-between items-start md:items-center gap-4 mb-8">
|
||||
<div>
|
||||
<h2 className="text-3xl font-bold tracking-tight">Community Engagement Portal</h2>
|
||||
<p className="text-muted-foreground">Stay informed, connected, and engaged with your local law enforcement</p>
|
||||
</div>
|
||||
<div className="flex flex-col sm:flex-row items-start sm:items-center gap-4">
|
||||
<div className="relative">
|
||||
<Search className="absolute left-2.5 top-2.5 h-4 w-4 text-muted-foreground" />
|
||||
<input
|
||||
type="search"
|
||||
placeholder="Search resources..."
|
||||
className="w-full rounded-md border border-input bg-background px-9 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
|
||||
/>
|
||||
</div>
|
||||
<Button className="flex items-center">
|
||||
<Phone className="h-4 w-4 mr-2" />
|
||||
Emergency: 911
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
|
@ -0,0 +1,72 @@
|
|||
import { Badge } from "@/app/_components/ui/badge"
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/app/_components/ui/select"
|
||||
|
||||
export default function CommunityMap() {
|
||||
return (
|
||||
<div className="mt-4 h-full">
|
||||
<div className="flex justify-between items-center mb-4">
|
||||
<Select defaultValue="all">
|
||||
<SelectTrigger className="w-[180px]">
|
||||
<SelectValue placeholder="Map View" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="all">All Incidents</SelectItem>
|
||||
<SelectItem value="theft">Theft</SelectItem>
|
||||
<SelectItem value="vandalism">Vandalism</SelectItem>
|
||||
<SelectItem value="traffic">Traffic</SelectItem>
|
||||
<SelectItem value="resources">Community Resources</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
|
||||
<div className="flex gap-2">
|
||||
<Badge variant="outline" className="bg-blue-100 text-blue-800">
|
||||
Last 7 Days
|
||||
</Badge>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="h-[350px] rounded-md bg-slate-100 border flex items-center justify-center relative overflow-hidden">
|
||||
<div className="absolute inset-0 opacity-50 bg-[url('/placeholder.svg?height=400&width=600')] bg-center bg-cover"></div>
|
||||
|
||||
<div className="absolute top-2 right-2 bg-background/80 backdrop-blur-sm p-2 rounded-md text-xs font-medium">
|
||||
<div className="flex items-center gap-1">
|
||||
<span className="w-2 h-2 rounded-full bg-red-500"></span> Theft
|
||||
</div>
|
||||
<div className="flex items-center gap-1">
|
||||
<span className="w-2 h-2 rounded-full bg-yellow-500"></span> Vandalism
|
||||
</div>
|
||||
<div className="flex items-center gap-1">
|
||||
<span className="w-2 h-2 rounded-full bg-blue-500"></span> Traffic
|
||||
</div>
|
||||
<div className="flex items-center gap-1">
|
||||
<span className="w-2 h-2 rounded-full bg-green-500"></span> Resources
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="z-10 bg-background/80 backdrop-blur-sm p-3 rounded-lg">
|
||||
<span className="font-medium">Community Safety Map</span>
|
||||
<div className="text-xs text-muted-foreground mt-1">Showing incidents from Apr 17-24, 2023</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mt-4 border rounded-lg p-3">
|
||||
<h4 className="text-sm font-medium mb-2">Your Neighborhood</h4>
|
||||
<div className="space-y-2">
|
||||
<div className="flex justify-between items-center">
|
||||
<span className="text-xs">Downtown District</span>
|
||||
<Badge variant="outline" className="bg-yellow-100 text-yellow-800">
|
||||
Medium Risk
|
||||
</Badge>
|
||||
</div>
|
||||
<div className="text-xs text-muted-foreground">Recent incidents: 5 thefts, 2 vandalism reports</div>
|
||||
<div className="text-xs text-muted-foreground">
|
||||
Trend: <span className="text-red-600">+12% from last month</span>
|
||||
</div>
|
||||
<div className="mt-2">
|
||||
<button className="text-xs text-primary">View Safety Recommendations</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
|
@ -0,0 +1,117 @@
|
|||
import { Badge } from "@/app/_components/ui/badge"
|
||||
import { Button } from "@/app/_components/ui/button"
|
||||
import { AlertTriangle, MapPin, Clock, Bell } from "lucide-react"
|
||||
|
||||
export default function CrimeAlerts() {
|
||||
const alerts = [
|
||||
{
|
||||
id: "ALT-1234",
|
||||
type: "Theft",
|
||||
description:
|
||||
"Multiple vehicle break-ins reported in the Downtown area. Please secure valuables and lock vehicles.",
|
||||
location: "Downtown District",
|
||||
date: "Apr 23, 2023",
|
||||
time: "Posted 2 hours ago",
|
||||
severity: "Medium",
|
||||
},
|
||||
{
|
||||
id: "ALT-1235",
|
||||
type: "Suspicious Activity",
|
||||
description:
|
||||
"Residents report unknown individuals going door-to-door claiming to represent utility companies. Always ask for proper identification.",
|
||||
location: "North Residential Area",
|
||||
date: "Apr 22, 2023",
|
||||
time: "Posted 1 day ago",
|
||||
severity: "Low",
|
||||
},
|
||||
{
|
||||
id: "ALT-1236",
|
||||
type: "Scam Alert",
|
||||
description:
|
||||
"Phone scammers impersonating police officers requesting payment for 'outstanding warrants'. Police will never request payment over the phone.",
|
||||
location: "Citywide",
|
||||
date: "Apr 21, 2023",
|
||||
time: "Posted 2 days ago",
|
||||
severity: "High",
|
||||
},
|
||||
]
|
||||
|
||||
return (
|
||||
<div className="mt-4">
|
||||
<div className="flex justify-between items-center mb-4">
|
||||
<div className="flex gap-2">
|
||||
<Button variant="outline" size="sm">
|
||||
All Alerts
|
||||
</Button>
|
||||
<Button variant="outline" size="sm">
|
||||
My Neighborhood
|
||||
</Button>
|
||||
</div>
|
||||
<Button variant="outline" size="sm" className="flex items-center">
|
||||
<Bell className="h-4 w-4 mr-2" />
|
||||
Subscribe to Alerts
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className="space-y-4">
|
||||
{alerts.map((alert) => (
|
||||
<div
|
||||
key={alert.id}
|
||||
className="flex items-start gap-3 p-4 border rounded-lg hover:bg-muted/50 transition-colors"
|
||||
>
|
||||
<div
|
||||
className={`w-10 h-10 rounded-full flex items-center justify-center shrink-0 ${
|
||||
alert.severity === "High" ? "bg-red-100" : alert.severity === "Medium" ? "bg-yellow-100" : "bg-blue-100"
|
||||
}`}
|
||||
>
|
||||
<AlertTriangle
|
||||
className={`h-5 w-5 ${
|
||||
alert.severity === "High"
|
||||
? "text-red-600"
|
||||
: alert.severity === "Medium"
|
||||
? "text-yellow-600"
|
||||
: "text-blue-600"
|
||||
}`}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
<h4 className="font-medium">{alert.type}</h4>
|
||||
<Badge
|
||||
variant="outline"
|
||||
className={
|
||||
alert.severity === "High"
|
||||
? "bg-red-100 text-red-800"
|
||||
: alert.severity === "Medium"
|
||||
? "bg-yellow-100 text-yellow-800"
|
||||
: "bg-blue-100 text-blue-800"
|
||||
}
|
||||
>
|
||||
{alert.severity} Priority
|
||||
</Badge>
|
||||
</div>
|
||||
|
||||
<p className="text-sm mt-2">{alert.description}</p>
|
||||
|
||||
<div className="flex flex-wrap gap-x-4 gap-y-1 mt-3 text-xs text-muted-foreground">
|
||||
<div className="flex items-center">
|
||||
<MapPin className="h-3 w-3 mr-1" />
|
||||
{alert.location}
|
||||
</div>
|
||||
<div className="flex items-center">
|
||||
<Clock className="h-3 w-3 mr-1" />
|
||||
{alert.time}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Button size="sm" variant="outline">
|
||||
Details
|
||||
</Button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
|
@ -0,0 +1,155 @@
|
|||
"use client"
|
||||
|
||||
import { Tabs, TabsList, TabsTrigger } from "@/app/_components/ui/tabs"
|
||||
import {
|
||||
ChartContainer,
|
||||
ChartTooltip,
|
||||
ChartTooltipContent,
|
||||
ChartLegend,
|
||||
ChartLegendContent,
|
||||
ChartConfig,
|
||||
} from "@/app/_components/ui/chart"
|
||||
import { Bar, BarChart, Line, LineChart, XAxis, YAxis, CartesianGrid, ResponsiveContainer, Tooltip } from "recharts"
|
||||
|
||||
const chartConfig = {
|
||||
desktop: {
|
||||
label: "Desktop",
|
||||
color: "#2563eb",
|
||||
},
|
||||
mobile: {
|
||||
label: "Mobile",
|
||||
color: "#60a5fa",
|
||||
},
|
||||
} satisfies ChartConfig
|
||||
|
||||
export default function CrimeStatistics() {
|
||||
// Sample data for the chart
|
||||
const monthlyData = [
|
||||
{ month: "Jan", violent: 12, property: 45, other: 20 },
|
||||
{ month: "Feb", violent: 10, property: 42, other: 18 },
|
||||
{ month: "Mar", violent: 15, property: 48, other: 22 },
|
||||
{ month: "Apr", violent: 8, property: 40, other: 15 },
|
||||
{ month: "May", violent: 12, property: 38, other: 20 },
|
||||
{ month: "Jun", violent: 10, property: 35, other: 18 },
|
||||
]
|
||||
|
||||
const neighborhoodData = [
|
||||
{ name: "Downtown", incidents: 45 },
|
||||
{ name: "Westside", incidents: 32 },
|
||||
{ name: "North", incidents: 28 },
|
||||
{ name: "East", incidents: 22 },
|
||||
{ name: "South", incidents: 18 },
|
||||
]
|
||||
|
||||
return (
|
||||
<div className="mt-4">
|
||||
<div className="flex justify-between items-center mb-4">
|
||||
<Tabs defaultValue="trends">
|
||||
<TabsList>
|
||||
<TabsTrigger value="trends">Crime Trends</TabsTrigger>
|
||||
<TabsTrigger value="neighborhood">By Neighborhood</TabsTrigger>
|
||||
</TabsList>
|
||||
</Tabs>
|
||||
|
||||
<Tabs defaultValue="6m">
|
||||
<TabsList>
|
||||
<TabsTrigger value="3m">3 Months</TabsTrigger>
|
||||
<TabsTrigger value="6m">6 Months</TabsTrigger>
|
||||
<TabsTrigger value="1y">1 Year</TabsTrigger>
|
||||
</TabsList>
|
||||
</Tabs>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<ChartContainer config={chartConfig} className="h-[250px]">
|
||||
<>
|
||||
<ResponsiveContainer width="100%" height="100%">
|
||||
<LineChart data={monthlyData} margin={{ top: 5, right: 10, left: 0, bottom: 5 }}>
|
||||
<CartesianGrid strokeDasharray="3 3" className="stroke-muted" />
|
||||
<XAxis dataKey="month" className="text-xs" />
|
||||
<YAxis className="text-xs" />
|
||||
<Tooltip content={<CustomLineTooltip />} />
|
||||
<Line type="monotone" dataKey="violent" stroke="#ef4444" strokeWidth={2} dot={{ r: 4 }} />
|
||||
<Line type="monotone" dataKey="property" stroke="#3b82f6" strokeWidth={2} dot={{ r: 4 }} />
|
||||
<Line type="monotone" dataKey="other" stroke="#10b981" strokeWidth={2} dot={{ r: 4 }} />
|
||||
</LineChart>
|
||||
</ResponsiveContainer>
|
||||
|
||||
<ChartLegend className="justify-center mt-2">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="h-2 w-2 rounded-full" style={{ backgroundColor: "#ef4444" }}></div>
|
||||
<span>Violent Crime</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="h-2 w-2 rounded-full" style={{ backgroundColor: "#3b82f6" }}></div>
|
||||
<span>Property Crime</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="h-2 w-2 rounded-full" style={{ backgroundColor: "#10b981" }}></div>
|
||||
<span>Other</span>
|
||||
</div>
|
||||
</ChartLegend>
|
||||
|
||||
</>
|
||||
</ChartContainer>
|
||||
|
||||
<ChartContainer className="h-[250px]">
|
||||
<ResponsiveContainer width="100%" height="100%">
|
||||
<BarChart data={neighborhoodData} margin={{ top: 5, right: 10, left: 0, bottom: 5 }}>
|
||||
<CartesianGrid strokeDasharray="3 3" className="stroke-muted" />
|
||||
<XAxis dataKey="name" className="text-xs" />
|
||||
<YAxis className="text-xs" />
|
||||
<Tooltip content={<CustomBarTooltip />} />
|
||||
<Bar dataKey="incidents" fill="#3b82f6" radius={[4, 4, 0, 0]} />
|
||||
</BarChart>
|
||||
</ResponsiveContainer>
|
||||
</ChartContainer>
|
||||
</div>
|
||||
|
||||
<div className="mt-4 text-xs text-center text-muted-foreground">
|
||||
Data source: City Police Department. Last updated: April 24, 2023
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function CustomLineTooltip({ active, payload, label }: any) {
|
||||
if (active && payload && payload.length) {
|
||||
return (
|
||||
<ChartTooltip content={
|
||||
<ChartTooltipContent>
|
||||
<div className="font-medium">{label}</div>
|
||||
{payload.map((entry: any, index: number) => (
|
||||
<div key={`item-${index}`} className="flex items-center gap-2 text-sm">
|
||||
<div className="h-2 w-2 rounded-full" style={{ backgroundColor: entry.color }}></div>
|
||||
<span className="font-medium">{entry.name}:</span>
|
||||
<span>{entry.value} incidents</span>
|
||||
</div>
|
||||
))}
|
||||
</ChartTooltipContent>
|
||||
}>
|
||||
</ChartTooltip>
|
||||
)
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
function CustomBarTooltip({ active, payload, label }: any) {
|
||||
if (active && payload && payload.length) {
|
||||
return (
|
||||
<ChartTooltip content={
|
||||
<ChartTooltipContent>
|
||||
<div className="font-medium">{label} Neighborhood</div>
|
||||
<div className="flex items-center gap-2 text-sm">
|
||||
<span>Total Incidents:</span>
|
||||
<span className="font-medium">{payload[0].value}</span>
|
||||
</div>
|
||||
</ChartTooltipContent>
|
||||
}>
|
||||
</ChartTooltip>
|
||||
)
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
|
@ -0,0 +1,54 @@
|
|||
"use client"
|
||||
|
||||
import { useState } from "react"
|
||||
import { ChevronDown } from "lucide-react"
|
||||
|
||||
export default function FAQSection() {
|
||||
const faqs = [
|
||||
{
|
||||
question: "How do I report a non-emergency incident?",
|
||||
answer:
|
||||
"You can use the 'Report an Incident' form on this portal, call the non-emergency number at 555-123-4567, or visit your local police station in person.",
|
||||
},
|
||||
{
|
||||
question: "How can I join a Neighborhood Watch group?",
|
||||
answer:
|
||||
"Check the 'Neighborhood Watch' section for groups in your area. You can contact the coordinator directly or click 'Start or Join a Group' for more information.",
|
||||
},
|
||||
{
|
||||
question: "What should I do if I witness a crime?",
|
||||
answer:
|
||||
"If it's an emergency or crime in progress, call 911 immediately. For non-emergencies, use the non-emergency line or the reporting form on this portal.",
|
||||
},
|
||||
{
|
||||
question: "How can I request extra patrols in my neighborhood?",
|
||||
answer:
|
||||
"Contact your local precinct or submit a request through the 'Community Feedback' section of this portal.",
|
||||
},
|
||||
]
|
||||
|
||||
const [openIndex, setOpenIndex] = useState<number | null>(null)
|
||||
|
||||
const toggleFAQ = (index: number) => {
|
||||
setOpenIndex(openIndex === index ? null : index)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="mt-4 space-y-2">
|
||||
{faqs.map((faq, index) => (
|
||||
<div key={index} className="border rounded-lg overflow-hidden">
|
||||
<button
|
||||
className="flex justify-between items-center w-full p-3 text-left text-sm font-medium"
|
||||
onClick={() => toggleFAQ(index)}
|
||||
>
|
||||
{faq.question}
|
||||
<ChevronDown className={`h-4 w-4 transition-transform ${openIndex === index ? "rotate-180" : ""}`} />
|
||||
</button>
|
||||
{openIndex === index && <div className="px-3 pb-3 text-xs text-muted-foreground">{faq.answer}</div>}
|
||||
</div>
|
||||
))}
|
||||
|
||||
<button className="w-full text-sm text-primary mt-2 py-1">View All FAQs</button>
|
||||
</div>
|
||||
)
|
||||
}
|
|
@ -0,0 +1,62 @@
|
|||
import { Badge } from "@/app/_components/ui/badge"
|
||||
import { Users, MapPin, Phone } from "lucide-react"
|
||||
|
||||
export default function NeighborhoodWatch() {
|
||||
const groups = [
|
||||
{
|
||||
name: "Downtown Watch Group",
|
||||
coordinator: "Sarah Johnson",
|
||||
members: 24,
|
||||
area: "Downtown District",
|
||||
contact: "555-123-4567",
|
||||
},
|
||||
{
|
||||
name: "Westside Neighbors",
|
||||
coordinator: "Michael Chen",
|
||||
members: 18,
|
||||
area: "West Residential Area",
|
||||
contact: "555-234-5678",
|
||||
},
|
||||
{
|
||||
name: "Parkview Safety",
|
||||
coordinator: "Emily Parker",
|
||||
members: 15,
|
||||
area: "Park District",
|
||||
contact: "555-345-6789",
|
||||
},
|
||||
]
|
||||
|
||||
return (
|
||||
<div className="mt-4 space-y-3">
|
||||
{groups.map((group, index) => (
|
||||
<div key={index} className="border rounded-lg p-3">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="w-8 h-8 rounded-full bg-muted flex items-center justify-center shrink-0">
|
||||
<Users className="h-4 w-4" />
|
||||
</div>
|
||||
<div>
|
||||
<h4 className="font-medium text-sm">{group.name}</h4>
|
||||
<div className="text-xs text-muted-foreground">Coordinator: {group.coordinator}</div>
|
||||
</div>
|
||||
<Badge variant="outline" className="ml-auto">
|
||||
{group.members} members
|
||||
</Badge>
|
||||
</div>
|
||||
|
||||
<div className="mt-2 space-y-1 text-xs text-muted-foreground pl-10">
|
||||
<div className="flex items-center">
|
||||
<MapPin className="h-3 w-3 mr-1" />
|
||||
{group.area}
|
||||
</div>
|
||||
<div className="flex items-center">
|
||||
<Phone className="h-3 w-3 mr-1" />
|
||||
{group.contact}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
|
||||
<button className="w-full text-sm text-primary mt-2 py-1">Start or Join a Group</button>
|
||||
</div>
|
||||
)
|
||||
}
|
|
@ -0,0 +1,62 @@
|
|||
import { Button } from "@/app/_components/ui/button"
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/app/_components/ui/select"
|
||||
import { FileText, MapPin, Camera } from "lucide-react"
|
||||
|
||||
export default function ReportIncident() {
|
||||
return (
|
||||
<div className="mt-4 space-y-4">
|
||||
<div className="text-xs text-muted-foreground">
|
||||
Use this form to report non-emergency incidents. For emergencies, please call 911.
|
||||
</div>
|
||||
|
||||
<div className="space-y-3">
|
||||
<div>
|
||||
<label className="text-xs text-muted-foreground mb-1 block">Incident Type</label>
|
||||
<Select>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="Select incident type" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="theft">Theft/Burglary</SelectItem>
|
||||
<SelectItem value="vandalism">Vandalism</SelectItem>
|
||||
<SelectItem value="suspicious">Suspicious Activity</SelectItem>
|
||||
<SelectItem value="noise">Noise Complaint</SelectItem>
|
||||
<SelectItem value="other">Other</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="text-xs text-muted-foreground mb-1 block">Location</label>
|
||||
<div className="relative">
|
||||
<MapPin className="absolute left-2.5 top-2.5 h-4 w-4 text-muted-foreground" />
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Enter address or location"
|
||||
className="w-full rounded-md border border-input bg-background px-9 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="text-xs text-muted-foreground mb-1 block">Description</label>
|
||||
<textarea
|
||||
placeholder="Describe what happened..."
|
||||
className="w-full rounded-md border border-input bg-background px-3 py-2 text-sm min-h-[80px] ring-offset-background placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
|
||||
></textarea>
|
||||
</div>
|
||||
|
||||
<div className="flex gap-2">
|
||||
<Button variant="outline" className="flex-1 flex items-center justify-center">
|
||||
<Camera className="h-4 w-4 mr-2" />
|
||||
Add Photo
|
||||
</Button>
|
||||
<Button className="flex-1 flex items-center justify-center">
|
||||
<FileText className="h-4 w-4 mr-2" />
|
||||
Submit Report
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
|
@ -0,0 +1,58 @@
|
|||
import { Phone, Building, Heart, Shield } from "lucide-react"
|
||||
|
||||
export default function ResourceDirectory() {
|
||||
const resources = [
|
||||
{
|
||||
name: "Police Non-Emergency",
|
||||
category: "Law Enforcement",
|
||||
contact: "555-123-4567",
|
||||
icon: Shield,
|
||||
},
|
||||
{
|
||||
name: "Victim Services",
|
||||
category: "Support",
|
||||
contact: "555-234-5678",
|
||||
icon: Heart,
|
||||
},
|
||||
{
|
||||
name: "City Hall",
|
||||
category: "Government",
|
||||
contact: "555-345-6789",
|
||||
icon: Building,
|
||||
},
|
||||
{
|
||||
name: "Mental Health Hotline",
|
||||
category: "Support",
|
||||
contact: "555-456-7890",
|
||||
icon: Heart,
|
||||
},
|
||||
]
|
||||
|
||||
return (
|
||||
<div className="mt-4 space-y-3">
|
||||
{resources.map((resource, index) => (
|
||||
<div key={index} className="border rounded-lg p-3">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="w-8 h-8 rounded-full bg-muted flex items-center justify-center shrink-0">
|
||||
<resource.icon className="h-4 w-4" />
|
||||
</div>
|
||||
<div>
|
||||
<h4 className="font-medium text-sm">{resource.name}</h4>
|
||||
<div className="text-xs text-muted-foreground">{resource.category}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mt-2 flex justify-between items-center">
|
||||
<div className="flex items-center text-xs">
|
||||
<Phone className="h-3 w-3 mr-1" />
|
||||
{resource.contact}
|
||||
</div>
|
||||
<button className="text-xs text-primary">Call</button>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
|
||||
<button className="w-full text-sm text-primary mt-2 py-1">View Full Directory</button>
|
||||
</div>
|
||||
)
|
||||
}
|
|
@ -0,0 +1,46 @@
|
|||
import { Home, Car, Smartphone } from "lucide-react"
|
||||
|
||||
export default function SafetyTips() {
|
||||
const categories = [
|
||||
{
|
||||
name: "Home Security",
|
||||
icon: Home,
|
||||
tips: ["Lock all doors and windows when away", "Install motion-sensor lighting", "Consider a security system"],
|
||||
},
|
||||
{
|
||||
name: "Vehicle Protection",
|
||||
icon: Car,
|
||||
tips: ["Always lock your vehicle", "Don't leave valuables visible", "Park in well-lit areas"],
|
||||
},
|
||||
{
|
||||
name: "Digital Safety",
|
||||
icon: Smartphone,
|
||||
tips: ["Use strong, unique passwords", "Be cautious of suspicious emails", "Keep software updated"],
|
||||
},
|
||||
]
|
||||
|
||||
return (
|
||||
<div className="mt-4 space-y-3">
|
||||
{categories.map((category) => (
|
||||
<div key={category.name} className="border rounded-lg p-3">
|
||||
<div className="flex items-center gap-2 mb-2">
|
||||
<div className="w-8 h-8 rounded-full bg-muted flex items-center justify-center shrink-0">
|
||||
<category.icon className="h-4 w-4" />
|
||||
</div>
|
||||
<h4 className="font-medium text-sm">{category.name}</h4>
|
||||
</div>
|
||||
|
||||
<ul className="space-y-1 pl-10">
|
||||
{category.tips.map((tip, index) => (
|
||||
<li key={index} className="text-xs list-disc">
|
||||
{tip}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
))}
|
||||
|
||||
<button className="w-full text-sm text-primary mt-2 py-1">View All Safety Tips</button>
|
||||
</div>
|
||||
)
|
||||
}
|
|
@ -0,0 +1,110 @@
|
|||
"use client"
|
||||
|
||||
import { BentoGrid, BentoGridItem } from "@/app/_components/ui/bento-grid"
|
||||
import { Bell, Calendar, FileText, MapPin, MessageSquare, Phone, Users, HelpCircle, BarChart3 } from "lucide-react"
|
||||
import CommunityHeader from "./_components/community-header"
|
||||
import CrimeAlerts from "./_components/crime-alerts"
|
||||
import SafetyTips from "./_components/safety-tips"
|
||||
import CommunityEvents from "./_components/community-events"
|
||||
import NeighborhoodWatch from "./_components/neighborhood-watch"
|
||||
import ReportIncident from "./_components/report-incident"
|
||||
import CrimeStatistics from "./_components/crime-statistics"
|
||||
import ResourceDirectory from "./_components/resource-directory"
|
||||
import FAQSection from "./_components/faq-section"
|
||||
import CommunityFeedback from "./_components/community-feedback"
|
||||
import CommunityMap from "./_components/community-map"
|
||||
|
||||
export default function CommunityEngagementPage() {
|
||||
return (
|
||||
<div className="container py-4 min-h-screen">
|
||||
<div className="max-w-7xl mx-auto">
|
||||
<CommunityHeader />
|
||||
|
||||
<BentoGrid>
|
||||
<BentoGridItem
|
||||
title="Crime Alerts"
|
||||
description="Recent incidents in your area"
|
||||
icon={<Bell className="w-5 h-5" />}
|
||||
colSpan="2"
|
||||
>
|
||||
<CrimeAlerts />
|
||||
</BentoGridItem>
|
||||
|
||||
<BentoGridItem
|
||||
title="Community Map"
|
||||
description="Explore your neighborhood"
|
||||
icon={<MapPin className="w-5 h-5" />}
|
||||
rowSpan="2"
|
||||
>
|
||||
<CommunityMap />
|
||||
</BentoGridItem>
|
||||
|
||||
<BentoGridItem
|
||||
title="Safety Tips"
|
||||
description="Protect yourself and your property"
|
||||
icon={<HelpCircle className="w-5 h-5" />}
|
||||
>
|
||||
<SafetyTips />
|
||||
</BentoGridItem>
|
||||
|
||||
<BentoGridItem
|
||||
title="Community Events"
|
||||
description="Upcoming meetings and activities"
|
||||
icon={<Calendar className="w-5 h-5" />}
|
||||
>
|
||||
<CommunityEvents />
|
||||
</BentoGridItem>
|
||||
|
||||
<BentoGridItem
|
||||
title="Neighborhood Watch"
|
||||
description="Local groups and coordinators"
|
||||
icon={<Users className="w-5 h-5" />}
|
||||
>
|
||||
<NeighborhoodWatch />
|
||||
</BentoGridItem>
|
||||
|
||||
<BentoGridItem
|
||||
title="Report an Incident"
|
||||
description="Submit non-emergency reports"
|
||||
icon={<FileText className="w-5 h-5" />}
|
||||
>
|
||||
<ReportIncident />
|
||||
</BentoGridItem>
|
||||
|
||||
<BentoGridItem
|
||||
title="Crime Statistics"
|
||||
description="Data and trends for your area"
|
||||
icon={<BarChart3 className="w-5 h-5" />}
|
||||
colSpan="2"
|
||||
>
|
||||
<CrimeStatistics />
|
||||
</BentoGridItem>
|
||||
|
||||
<BentoGridItem
|
||||
title="Resource Directory"
|
||||
description="Community services and contacts"
|
||||
icon={<Phone className="w-5 h-5" />}
|
||||
>
|
||||
<ResourceDirectory />
|
||||
</BentoGridItem>
|
||||
|
||||
<BentoGridItem
|
||||
title="FAQ"
|
||||
description="Common questions and answers"
|
||||
icon={<HelpCircle className="w-5 h-5" />}
|
||||
>
|
||||
<FAQSection />
|
||||
</BentoGridItem>
|
||||
|
||||
<BentoGridItem
|
||||
title="Community Feedback"
|
||||
description="Share your thoughts and suggestions"
|
||||
icon={<MessageSquare className="w-5 h-5" />}
|
||||
>
|
||||
<CommunityFeedback />
|
||||
</BentoGridItem>
|
||||
</BentoGrid>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
|
@ -0,0 +1,69 @@
|
|||
import { User } from "lucide-react"
|
||||
import { Badge } from "@/app/_components/ui/badge"
|
||||
|
||||
export default function CaseAssignees() {
|
||||
const assignees = [
|
||||
{
|
||||
id: "OFF-1234",
|
||||
name: "Michael Chen",
|
||||
role: "Lead Detective",
|
||||
badge: "ID-5678",
|
||||
contact: "555-123-4567",
|
||||
status: "Active",
|
||||
},
|
||||
{
|
||||
id: "OFF-2345",
|
||||
name: "Sarah Johnson",
|
||||
role: "Forensic Specialist",
|
||||
badge: "ID-6789",
|
||||
contact: "555-234-5678",
|
||||
status: "Active",
|
||||
},
|
||||
{
|
||||
id: "OFF-3456",
|
||||
name: "Robert Wilson",
|
||||
role: "Evidence Technician",
|
||||
badge: "ID-7890",
|
||||
contact: "555-345-6789",
|
||||
status: "Active",
|
||||
},
|
||||
{
|
||||
id: "OFF-4567",
|
||||
name: "James Rodriguez",
|
||||
role: "Patrol Officer",
|
||||
badge: "ID-8901",
|
||||
contact: "555-456-7890",
|
||||
status: "Standby",
|
||||
},
|
||||
]
|
||||
|
||||
return (
|
||||
<div className="space-y-3">
|
||||
{assignees.map((officer) => (
|
||||
<div key={officer.id} className="flex items-center gap-3 p-2 rounded-lg hover:bg-muted/50 transition-colors">
|
||||
<div className="w-8 h-8 rounded-full bg-muted flex items-center justify-center shrink-0">
|
||||
<User className="h-4 w-4" />
|
||||
</div>
|
||||
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="flex items-center gap-2">
|
||||
<h4 className="font-medium text-sm">{officer.name}</h4>
|
||||
<Badge
|
||||
variant="outline"
|
||||
className={officer.status === "Active" ? "bg-green-100 text-green-800" : "bg-blue-100 text-blue-800"}
|
||||
>
|
||||
{officer.status}
|
||||
</Badge>
|
||||
</div>
|
||||
|
||||
<div className="text-xs text-muted-foreground">
|
||||
{officer.role} • Badge #{officer.badge}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
|
||||
<button className="w-full text-sm text-primary mt-2 py-1">+ Assign Personnel</button>
|
||||
</div>
|
||||
)
|
||||
}
|
|
@ -0,0 +1,92 @@
|
|||
import { Download, FileText, FilePlus, FileImage, FileSpreadsheet } from "lucide-react"
|
||||
import { Badge } from "@/app/_components/ui/badge"
|
||||
|
||||
export default function CaseDocuments() {
|
||||
const documents = [
|
||||
{
|
||||
id: "DOC-3421",
|
||||
name: "Initial Police Report",
|
||||
type: "PDF",
|
||||
size: "1.2 MB",
|
||||
uploadedBy: "James Rodriguez",
|
||||
uploadDate: "Apr 15, 2023",
|
||||
icon: FileText,
|
||||
},
|
||||
{
|
||||
id: "DOC-3422",
|
||||
name: "Autopsy Report",
|
||||
type: "PDF",
|
||||
size: "3.5 MB",
|
||||
uploadedBy: "Dr. Lisa Wong",
|
||||
uploadDate: "Apr 17, 2023",
|
||||
icon: FileText,
|
||||
},
|
||||
{
|
||||
id: "DOC-3423",
|
||||
name: "Crime Scene Photos",
|
||||
type: "ZIP",
|
||||
size: "24.7 MB",
|
||||
uploadedBy: "Robert Wilson",
|
||||
uploadDate: "Apr 15, 2023",
|
||||
icon: FileImage,
|
||||
},
|
||||
{
|
||||
id: "DOC-3424",
|
||||
name: "Witness Statement - Emily Parker",
|
||||
type: "DOCX",
|
||||
size: "285 KB",
|
||||
uploadedBy: "Michael Chen",
|
||||
uploadDate: "Apr 20, 2023",
|
||||
icon: FilePlus,
|
||||
},
|
||||
{
|
||||
id: "DOC-3425",
|
||||
name: "Evidence Log",
|
||||
type: "XLSX",
|
||||
size: "420 KB",
|
||||
uploadedBy: "Sarah Johnson",
|
||||
uploadDate: "Apr 22, 2023",
|
||||
icon: FileSpreadsheet,
|
||||
},
|
||||
]
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<div className="flex justify-between items-center">
|
||||
<h3 className="text-lg font-semibold">Case Documents ({documents.length})</h3>
|
||||
<button className="text-sm text-primary">Upload Document</button>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
{documents.map((doc) => (
|
||||
<div
|
||||
key={doc.id}
|
||||
className="flex items-center gap-3 p-3 border rounded-lg hover:bg-muted/50 transition-colors"
|
||||
>
|
||||
<div className="w-10 h-10 rounded bg-muted flex items-center justify-center shrink-0">
|
||||
<doc.icon className="h-5 w-5" />
|
||||
</div>
|
||||
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="flex items-center gap-2">
|
||||
<h4 className="font-medium truncate">{doc.name}</h4>
|
||||
<Badge variant="outline">{doc.type}</Badge>
|
||||
</div>
|
||||
|
||||
<div className="flex gap-4 mt-1 text-xs text-muted-foreground">
|
||||
<span>{doc.size}</span>
|
||||
<span>Uploaded: {doc.uploadDate}</span>
|
||||
<span>By: {doc.uploadedBy}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button className="flex items-center text-sm text-primary shrink-0">
|
||||
<Download className="h-4 w-4 mr-1" />
|
||||
Download
|
||||
</button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
|
@ -0,0 +1,93 @@
|
|||
import { FileText, ImageIcon, Package } from "lucide-react"
|
||||
import { Badge } from "@/app/_components/ui/badge"
|
||||
|
||||
export default function CaseEvidence() {
|
||||
const evidenceItems = [
|
||||
{
|
||||
id: "EV-4523",
|
||||
type: "Weapon",
|
||||
description: "Kitchen knife, 8-inch blade with wooden handle",
|
||||
dateCollected: "Apr 18, 2023",
|
||||
status: "Processing",
|
||||
location: "Evidence Locker B-12",
|
||||
icon: Package,
|
||||
},
|
||||
{
|
||||
id: "EV-4524",
|
||||
type: "Photograph",
|
||||
description: "Crime scene photos - living room area",
|
||||
dateCollected: "Apr 15, 2023",
|
||||
status: "Analyzed",
|
||||
location: "Digital Storage",
|
||||
icon: ImageIcon,
|
||||
},
|
||||
{
|
||||
id: "EV-4525",
|
||||
type: "Document",
|
||||
description: "Victim's personal diary",
|
||||
dateCollected: "Apr 15, 2023",
|
||||
status: "Analyzed",
|
||||
location: "Evidence Locker A-7",
|
||||
icon: FileText,
|
||||
},
|
||||
{
|
||||
id: "EV-4526",
|
||||
type: "DNA Sample",
|
||||
description: "Blood sample from kitchen floor",
|
||||
dateCollected: "Apr 15, 2023",
|
||||
status: "Lab Analysis",
|
||||
location: "Forensic Lab",
|
||||
icon: Package,
|
||||
},
|
||||
]
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<div className="flex justify-between items-center">
|
||||
<h3 className="text-lg font-semibold">Evidence Items ({evidenceItems.length})</h3>
|
||||
<button className="text-sm text-primary">Add Evidence</button>
|
||||
</div>
|
||||
|
||||
<div className="space-y-3">
|
||||
{evidenceItems.map((item) => (
|
||||
<div
|
||||
key={item.id}
|
||||
className="flex items-start gap-3 p-3 border rounded-lg hover:bg-muted/50 transition-colors"
|
||||
>
|
||||
<div className="w-10 h-10 rounded-full bg-muted flex items-center justify-center shrink-0">
|
||||
<item.icon className="h-5 w-5" />
|
||||
</div>
|
||||
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
<h4 className="font-medium">{item.id}</h4>
|
||||
<Badge variant="outline">{item.type}</Badge>
|
||||
<Badge
|
||||
variant="outline"
|
||||
className={
|
||||
item.status === "Analyzed"
|
||||
? "bg-green-100 text-green-800"
|
||||
: item.status === "Processing"
|
||||
? "bg-yellow-100 text-yellow-800"
|
||||
: "bg-blue-100 text-blue-800"
|
||||
}
|
||||
>
|
||||
{item.status}
|
||||
</Badge>
|
||||
</div>
|
||||
|
||||
<p className="text-sm mt-1">{item.description}</p>
|
||||
|
||||
<div className="flex flex-wrap gap-x-4 gap-y-1 mt-2 text-xs text-muted-foreground">
|
||||
<span>Collected: {item.dateCollected}</span>
|
||||
<span>Location: {item.location}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button className="text-sm text-primary shrink-0">View Details</button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
|
@ -0,0 +1,47 @@
|
|||
import { AlertTriangle, Calendar, Clock } from "lucide-react"
|
||||
import { Badge } from "@/app/_components/ui/badge"
|
||||
import { Button } from "@/app/_components/ui/button"
|
||||
|
||||
interface CaseHeaderProps {
|
||||
caseId: string
|
||||
title: string
|
||||
status: string
|
||||
priority: string
|
||||
dateOpened: string
|
||||
lastUpdated: string
|
||||
}
|
||||
|
||||
export default function CaseHeader({ caseId, title, status, priority, dateOpened, lastUpdated }: CaseHeaderProps) {
|
||||
return (
|
||||
<div className="flex flex-col md:flex-row justify-between gap-4">
|
||||
<div>
|
||||
<div className="flex items-center gap-2">
|
||||
<h1 className="text-2xl font-bold">Case #{caseId}</h1>
|
||||
<Badge variant="outline" className="bg-blue-100 text-blue-800">
|
||||
{status}
|
||||
</Badge>
|
||||
<Badge variant="outline" className="bg-red-100 text-red-800">
|
||||
<AlertTriangle className="h-3 w-3 mr-1" />
|
||||
{priority}
|
||||
</Badge>
|
||||
</div>
|
||||
<h2 className="text-xl mt-1">{title}</h2>
|
||||
<div className="flex items-center gap-4 mt-2 text-sm text-muted-foreground">
|
||||
<div className="flex items-center">
|
||||
<Calendar className="h-4 w-4 mr-1" />
|
||||
Opened: {dateOpened}
|
||||
</div>
|
||||
<div className="flex items-center">
|
||||
<Clock className="h-4 w-4 mr-1" />
|
||||
Last updated: {lastUpdated}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-start gap-2">
|
||||
<Button>Update Status</Button>
|
||||
<Button variant="outline">Export Case</Button>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
|
@ -0,0 +1,72 @@
|
|||
import { User } from "lucide-react"
|
||||
import { Button } from "@/app/_components/ui/button"
|
||||
import { Textarea } from "@/app/_components/ui/textarea"
|
||||
|
||||
export default function CaseNotes() {
|
||||
const notes = [
|
||||
{
|
||||
id: "N-5621",
|
||||
content:
|
||||
"Forensic analysis suggests the murder weapon was wiped clean before being discarded. No fingerprints were recovered, but DNA traces were found on the handle.",
|
||||
author: "Sarah Johnson",
|
||||
role: "Forensic Specialist",
|
||||
timestamp: "Apr 22, 2023 • 15:10",
|
||||
},
|
||||
{
|
||||
id: "N-5620",
|
||||
content:
|
||||
"Witness Emily Parker's statement corroborates the timeline established by the medical examiner. Victim was likely killed between 9:00 PM and 11:00 PM on April 14.",
|
||||
author: "Michael Chen",
|
||||
role: "Detective",
|
||||
timestamp: "Apr 20, 2023 • 14:35",
|
||||
},
|
||||
{
|
||||
id: "N-5619",
|
||||
content:
|
||||
"Background check on suspect John Doe shows prior assault charges that were dropped in 2021. Need to follow up with previous complainant.",
|
||||
author: "Michael Chen",
|
||||
role: "Detective",
|
||||
timestamp: "Apr 19, 2023 • 10:22",
|
||||
},
|
||||
{
|
||||
id: "N-5618",
|
||||
content:
|
||||
"Initial canvas of the neighborhood complete. Three potential witnesses identified and scheduled for interviews.",
|
||||
author: "James Rodriguez",
|
||||
role: "Patrol Officer",
|
||||
timestamp: "Apr 16, 2023 • 08:45",
|
||||
},
|
||||
]
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<div>
|
||||
<h3 className="text-lg font-semibold mb-4">Case Notes</h3>
|
||||
|
||||
<div className="mb-6">
|
||||
<Textarea placeholder="Add a note about this case..." className="min-h-[100px] mb-2" />
|
||||
<div className="flex justify-end">
|
||||
<Button>Add Note</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-4">
|
||||
{notes.map((note) => (
|
||||
<div key={note.id} className="p-3 border rounded-lg">
|
||||
<div className="flex items-center gap-2 mb-2">
|
||||
<div className="w-6 h-6 rounded-full bg-muted flex items-center justify-center shrink-0">
|
||||
<User className="h-3 w-3" />
|
||||
</div>
|
||||
<span className="font-medium">{note.author}</span>
|
||||
<span className="text-xs text-muted-foreground">({note.role})</span>
|
||||
<span className="text-xs text-muted-foreground ml-auto">{note.timestamp}</span>
|
||||
</div>
|
||||
|
||||
<p className="text-sm">{note.content}</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
|
@ -0,0 +1,59 @@
|
|||
import { ArrowRight } from "lucide-react"
|
||||
import { Badge } from "@/app/_components/ui/badge"
|
||||
|
||||
export default function CaseRelated() {
|
||||
const relatedCases = [
|
||||
{
|
||||
id: "CR-7456",
|
||||
title: "Assault - Downtown District",
|
||||
date: "Mar 12, 2023",
|
||||
status: "Closed",
|
||||
relationship: "Same suspect",
|
||||
},
|
||||
{
|
||||
id: "CR-7689",
|
||||
title: "Breaking & Entering - Westside",
|
||||
date: "Apr 02, 2023",
|
||||
status: "Active",
|
||||
relationship: "Similar MO",
|
||||
},
|
||||
{
|
||||
id: "CR-7712",
|
||||
title: "Theft - Downtown District",
|
||||
date: "Apr 10, 2023",
|
||||
status: "Active",
|
||||
relationship: "Same location",
|
||||
},
|
||||
]
|
||||
|
||||
return (
|
||||
<div className="space-y-3">
|
||||
{relatedCases.map((case_) => (
|
||||
<div key={case_.id} className="flex items-center gap-2 p-2 rounded-lg hover:bg-muted/50 transition-colors">
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="flex items-center gap-2">
|
||||
<h4 className="font-medium text-sm">Case #{case_.id}</h4>
|
||||
<Badge
|
||||
variant="outline"
|
||||
className={case_.status === "Active" ? "bg-blue-100 text-blue-800" : "bg-green-100 text-green-800"}
|
||||
>
|
||||
{case_.status}
|
||||
</Badge>
|
||||
</div>
|
||||
|
||||
<p className="text-xs truncate">{case_.title}</p>
|
||||
|
||||
<div className="flex justify-between text-xs text-muted-foreground mt-1">
|
||||
<span>{case_.date}</span>
|
||||
<span>{case_.relationship}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<ArrowRight className="h-4 w-4 text-muted-foreground" />
|
||||
</div>
|
||||
))}
|
||||
|
||||
<button className="w-full text-sm text-primary mt-2 py-1">+ Link Related Case</button>
|
||||
</div>
|
||||
)
|
||||
}
|
|
@ -0,0 +1,78 @@
|
|||
import { Circle } from "lucide-react"
|
||||
|
||||
export default function CaseTimeline() {
|
||||
const timelineEvents = [
|
||||
{
|
||||
date: "Apr 22, 2023",
|
||||
time: "14:30",
|
||||
title: "Forensic Report Received",
|
||||
description: "DNA analysis results from the lab confirm suspect's presence at the crime scene.",
|
||||
user: "Sarah Johnson",
|
||||
role: "Forensic Specialist",
|
||||
},
|
||||
{
|
||||
date: "Apr 20, 2023",
|
||||
time: "09:15",
|
||||
title: "Witness Interview Conducted",
|
||||
description: "Key witness provided detailed description of events and potential suspect.",
|
||||
user: "Michael Chen",
|
||||
role: "Detective",
|
||||
},
|
||||
{
|
||||
date: "Apr 18, 2023",
|
||||
time: "16:45",
|
||||
title: "Evidence Collected",
|
||||
description: "Weapon recovered from dumpster 2 blocks from crime scene. Sent for fingerprint analysis.",
|
||||
user: "Robert Wilson",
|
||||
role: "Evidence Technician",
|
||||
},
|
||||
{
|
||||
date: "Apr 15, 2023",
|
||||
time: "23:10",
|
||||
title: "Case Opened",
|
||||
description: "Officers responded to 911 call. Victim found deceased at the scene.",
|
||||
user: "James Rodriguez",
|
||||
role: "Patrol Officer",
|
||||
},
|
||||
]
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<div className="flex justify-between items-center">
|
||||
<h3 className="text-lg font-semibold">Case Timeline</h3>
|
||||
<button className="text-sm text-primary">Add Event</button>
|
||||
</div>
|
||||
|
||||
<div className="relative">
|
||||
{timelineEvents.map((event, index) => (
|
||||
<div key={index} className="mb-8 relative pl-6">
|
||||
{/* Timeline connector */}
|
||||
{index < timelineEvents.length - 1 && (
|
||||
<div className="absolute left-[0.4375rem] top-3 bottom-0 w-0.5 bg-muted" />
|
||||
)}
|
||||
|
||||
{/* Timeline dot */}
|
||||
<div className="absolute left-0 top-1">
|
||||
<Circle className="h-3.5 w-3.5 fill-primary text-primary" />
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col sm:flex-row sm:items-start gap-2">
|
||||
<div className="min-w-[140px] text-sm text-muted-foreground">
|
||||
<div>{event.date}</div>
|
||||
<div>{event.time}</div>
|
||||
</div>
|
||||
|
||||
<div className="bg-muted/50 rounded-lg p-3 flex-1">
|
||||
<h4 className="font-medium">{event.title}</h4>
|
||||
<p className="text-sm mt-1">{event.description}</p>
|
||||
<div className="text-xs text-muted-foreground mt-2">
|
||||
Added by {event.user} ({event.role})
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
|
@ -0,0 +1,87 @@
|
|||
import { User } from "lucide-react"
|
||||
import { Badge } from "@/app/_components/ui/badge"
|
||||
|
||||
export default function CaseWitnesses() {
|
||||
const witnesses = [
|
||||
{
|
||||
id: "W-1201",
|
||||
name: "Emily Parker",
|
||||
type: "Eyewitness",
|
||||
contactInfo: "555-123-4567",
|
||||
interviewDate: "Apr 20, 2023",
|
||||
status: "Interviewed",
|
||||
reliability: "High",
|
||||
notes: "Neighbor who heard argument and saw suspect leaving the scene.",
|
||||
},
|
||||
{
|
||||
id: "W-1202",
|
||||
name: "Thomas Grant",
|
||||
type: "Character Witness",
|
||||
contactInfo: "555-987-6543",
|
||||
interviewDate: "Apr 19, 2023",
|
||||
status: "Interviewed",
|
||||
reliability: "Medium",
|
||||
notes: "Victim's coworker who provided information about recent conflicts.",
|
||||
},
|
||||
{
|
||||
id: "W-1203",
|
||||
name: "Maria Sanchez",
|
||||
type: "Eyewitness",
|
||||
contactInfo: "555-456-7890",
|
||||
interviewDate: "Pending",
|
||||
status: "Scheduled",
|
||||
reliability: "Unknown",
|
||||
notes: "Passerby who may have seen suspect in the area before the incident.",
|
||||
},
|
||||
]
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<div className="flex justify-between items-center">
|
||||
<h3 className="text-lg font-semibold">Witnesses & Interviews ({witnesses.length})</h3>
|
||||
<button className="text-sm text-primary">Add Witness</button>
|
||||
</div>
|
||||
|
||||
<div className="space-y-3">
|
||||
{witnesses.map((witness) => (
|
||||
<div
|
||||
key={witness.id}
|
||||
className="flex items-start gap-3 p-3 border rounded-lg hover:bg-muted/50 transition-colors"
|
||||
>
|
||||
<div className="w-10 h-10 rounded-full bg-muted flex items-center justify-center shrink-0">
|
||||
<User className="h-5 w-5" />
|
||||
</div>
|
||||
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
<h4 className="font-medium">{witness.name}</h4>
|
||||
<Badge variant="outline">{witness.type}</Badge>
|
||||
<Badge
|
||||
variant="outline"
|
||||
className={
|
||||
witness.status === "Interviewed" ? "bg-green-100 text-green-800" : "bg-yellow-100 text-yellow-800"
|
||||
}
|
||||
>
|
||||
{witness.status}
|
||||
</Badge>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-wrap gap-x-4 gap-y-1 mt-1 text-xs text-muted-foreground">
|
||||
<span>ID: {witness.id}</span>
|
||||
<span>Contact: {witness.contactInfo}</span>
|
||||
<span>Reliability: {witness.reliability}</span>
|
||||
{witness.interviewDate !== "Pending" && <span>Interviewed: {witness.interviewDate}</span>}
|
||||
</div>
|
||||
|
||||
<p className="text-sm mt-2">{witness.notes}</p>
|
||||
</div>
|
||||
|
||||
<button className="text-sm text-primary shrink-0">
|
||||
{witness.status === "Interviewed" ? "View Statement" : "Schedule Interview"}
|
||||
</button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
|
@ -0,0 +1,101 @@
|
|||
"use client"
|
||||
|
||||
import { ArrowLeft, Calendar, FileText, MessageSquare, Paperclip, Shield, User } from "lucide-react"
|
||||
import { Button } from "@/app/_components/ui/button"
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/app/_components/ui/tabs"
|
||||
import CaseHeader from "./_components/case-header"
|
||||
import CaseTimeline from "./_components/case-timeline"
|
||||
import CaseEvidence from "./_components/case-evidence"
|
||||
import CaseWitnesses from "./_components/case-witnesses"
|
||||
import CaseDocuments from "./_components/case-documents"
|
||||
import CaseNotes from "./_components/case-notes"
|
||||
import CaseAssignees from "./_components/case-assignees"
|
||||
import CaseRelated from "./_components/case-related"
|
||||
|
||||
export default function CrimeIncidentPage() {
|
||||
return (
|
||||
<div className="container py-4 min-h-screen">
|
||||
<div className="max-w-7xl mx-auto">
|
||||
<div className="mb-6">
|
||||
<Button variant="ghost" size="sm" className="mb-4">
|
||||
<ArrowLeft className="mr-2 h-4 w-4" />
|
||||
Back to Cases
|
||||
</Button>
|
||||
|
||||
<CaseHeader
|
||||
caseId="CR-7823"
|
||||
title="Homicide Investigation - Downtown District"
|
||||
status="Active"
|
||||
priority="Critical"
|
||||
dateOpened="2023-04-15"
|
||||
lastUpdated="2023-04-22"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
|
||||
<div className="lg:col-span-2">
|
||||
<Tabs defaultValue="timeline" className="w-full">
|
||||
<TabsList className="grid grid-cols-5 mb-4">
|
||||
<TabsTrigger value="timeline">
|
||||
<Calendar className="h-4 w-4 mr-2" />
|
||||
Timeline
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="evidence">
|
||||
<Paperclip className="h-4 w-4 mr-2" />
|
||||
Evidence
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="witnesses">
|
||||
<User className="h-4 w-4 mr-2" />
|
||||
Witnesses
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="documents">
|
||||
<FileText className="h-4 w-4 mr-2" />
|
||||
Documents
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="notes">
|
||||
<MessageSquare className="h-4 w-4 mr-2" />
|
||||
Notes
|
||||
</TabsTrigger>
|
||||
</TabsList>
|
||||
|
||||
<TabsContent value="timeline" className="border rounded-lg p-4">
|
||||
<CaseTimeline />
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="evidence" className="border rounded-lg p-4">
|
||||
<CaseEvidence />
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="witnesses" className="border rounded-lg p-4">
|
||||
<CaseWitnesses />
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="documents" className="border rounded-lg p-4">
|
||||
<CaseDocuments />
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="notes" className="border rounded-lg p-4">
|
||||
<CaseNotes />
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
</div>
|
||||
|
||||
<div className="space-y-6">
|
||||
<div className="border rounded-lg p-4">
|
||||
<h3 className="text-lg font-semibold mb-4 flex items-center">
|
||||
<Shield className="h-5 w-5 mr-2" />
|
||||
Assigned Personnel
|
||||
</h3>
|
||||
<CaseAssignees />
|
||||
</div>
|
||||
|
||||
<div className="border rounded-lg p-4">
|
||||
<h3 className="text-lg font-semibold mb-4">Related Cases</h3>
|
||||
<CaseRelated />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
|
@ -0,0 +1,84 @@
|
|||
import { Badge } from "@/app/_components/ui/badge"
|
||||
import { ArrowRight, Clock, User } from "lucide-react"
|
||||
|
||||
export default function ChainOfCustody() {
|
||||
const custodyEvents = [
|
||||
{
|
||||
evidenceId: "EV-4523",
|
||||
events: [
|
||||
{
|
||||
action: "Collected",
|
||||
by: "Officer Wilson",
|
||||
date: "Apr 18, 2023",
|
||||
time: "16:45",
|
||||
},
|
||||
{
|
||||
action: "Transferred",
|
||||
by: "Officer Wilson",
|
||||
to: "Evidence Room",
|
||||
date: "Apr 18, 2023",
|
||||
time: "18:30",
|
||||
},
|
||||
{
|
||||
action: "Checked Out",
|
||||
by: "Sarah Johnson",
|
||||
date: "Apr 20, 2023",
|
||||
time: "09:15",
|
||||
},
|
||||
{
|
||||
action: "Returned",
|
||||
by: "Sarah Johnson",
|
||||
date: "Apr 22, 2023",
|
||||
time: "14:30",
|
||||
},
|
||||
],
|
||||
},
|
||||
]
|
||||
|
||||
return (
|
||||
<div className="mt-4 space-y-4">
|
||||
<div className="flex justify-between items-center">
|
||||
<h3 className="text-sm font-medium">Recent Activity</h3>
|
||||
<button className="text-xs text-primary">View All</button>
|
||||
</div>
|
||||
|
||||
{custodyEvents.map((item) => (
|
||||
<div key={item.evidenceId} className="border rounded-lg p-3">
|
||||
<div className="flex justify-between items-center mb-3">
|
||||
<h4 className="font-medium text-sm">Evidence #{item.evidenceId}</h4>
|
||||
<Badge variant="outline" className="bg-blue-100 text-blue-800">
|
||||
{item.events.length} transfers
|
||||
</Badge>
|
||||
</div>
|
||||
|
||||
<div className="space-y-3">
|
||||
{item.events.map((event, index) => (
|
||||
<div key={index} className="flex items-center gap-2 text-sm">
|
||||
<div className="w-6 h-6 rounded-full bg-muted flex items-center justify-center shrink-0">
|
||||
{event.action === "Collected" || event.action === "Checked Out" ? (
|
||||
<User className="h-3 w-3" />
|
||||
) : (
|
||||
<ArrowRight className="h-3 w-3" />
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="flex items-center gap-1">
|
||||
<span className="font-medium">{event.action}</span>
|
||||
<span>by {event.by}</span>
|
||||
{event.to && <span>to {event.to}</span>}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="text-xs text-muted-foreground flex items-center">
|
||||
<Clock className="h-3 w-3 mr-1" />
|
||||
{event.date}, {event.time}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
}
|
|
@ -0,0 +1,66 @@
|
|||
import { Badge } from "@/app/_components/ui/badge"
|
||||
import { Download, FileText, ImageIcon, Video } from "lucide-react"
|
||||
|
||||
export default function DigitalEvidence() {
|
||||
const digitalItems = [
|
||||
{
|
||||
id: "EV-4524",
|
||||
type: "Images",
|
||||
description: "Crime scene photos (24 files)",
|
||||
case: "CR-7823",
|
||||
size: "156 MB",
|
||||
format: "JPG",
|
||||
icon: ImageIcon,
|
||||
},
|
||||
{
|
||||
id: "EV-4530",
|
||||
type: "Video",
|
||||
description: "Security camera footage",
|
||||
case: "CR-7825",
|
||||
size: "1.2 GB",
|
||||
format: "MP4",
|
||||
icon: Video,
|
||||
},
|
||||
{
|
||||
id: "EV-4532",
|
||||
type: "Document",
|
||||
description: "Forensic report",
|
||||
case: "CR-7823",
|
||||
size: "2.4 MB",
|
||||
format: "PDF",
|
||||
icon: FileText,
|
||||
},
|
||||
]
|
||||
|
||||
return (
|
||||
<div className="mt-4 space-y-3">
|
||||
{digitalItems.map((item) => (
|
||||
<div key={item.id} className="flex items-center gap-3 p-2 border rounded-lg">
|
||||
<div className="w-8 h-8 rounded bg-muted flex items-center justify-center shrink-0">
|
||||
<item.icon className="h-4 w-4" />
|
||||
</div>
|
||||
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="flex items-center gap-2">
|
||||
<h4 className="font-medium text-sm">{item.id}</h4>
|
||||
<Badge variant="outline">{item.format}</Badge>
|
||||
</div>
|
||||
|
||||
<p className="text-xs truncate">{item.description}</p>
|
||||
|
||||
<div className="flex gap-2 text-xs text-muted-foreground mt-1">
|
||||
<span>Case: {item.case}</span>
|
||||
<span>{item.size}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button className="text-primary p-1 rounded-full hover:bg-muted">
|
||||
<Download className="h-4 w-4" />
|
||||
</button>
|
||||
</div>
|
||||
))}
|
||||
|
||||
<button className="w-full text-sm text-primary mt-2 py-1">Upload Digital Evidence</button>
|
||||
</div>
|
||||
)
|
||||
}
|
|
@ -0,0 +1,74 @@
|
|||
import { Badge } from "@/app/_components/ui/badge"
|
||||
import { FileText } from "lucide-react"
|
||||
|
||||
export default function EvidenceByCase() {
|
||||
const cases = [
|
||||
{
|
||||
id: "CR-7823",
|
||||
title: "Homicide Investigation",
|
||||
evidenceCount: 12,
|
||||
recentItems: [
|
||||
{ id: "EV-4523", type: "Weapon" },
|
||||
{ id: "EV-4524", type: "Photograph" },
|
||||
{ id: "EV-4525", type: "Document" },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: "CR-7825",
|
||||
title: "Armed Robbery",
|
||||
evidenceCount: 8,
|
||||
recentItems: [
|
||||
{ id: "EV-4527", type: "Fingerprint" },
|
||||
{ id: "EV-4528", type: "Clothing" },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: "CR-7830",
|
||||
title: "Kidnapping",
|
||||
evidenceCount: 15,
|
||||
recentItems: [
|
||||
{ id: "EV-4535", type: "Vehicle" },
|
||||
{ id: "EV-4536", type: "DNA Sample" },
|
||||
],
|
||||
},
|
||||
]
|
||||
|
||||
return (
|
||||
<div className="mt-4 space-y-3">
|
||||
{cases.map((case_) => (
|
||||
<div key={case_.id} className="border rounded-lg p-3">
|
||||
<div className="flex items-center gap-2 mb-2">
|
||||
<div className="w-8 h-8 rounded bg-muted flex items-center justify-center shrink-0">
|
||||
<FileText className="h-4 w-4" />
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h4 className="font-medium text-sm">Case #{case_.id}</h4>
|
||||
<div className="text-xs text-muted-foreground truncate">{case_.title}</div>
|
||||
</div>
|
||||
|
||||
<Badge variant="outline" className="ml-auto">
|
||||
{case_.evidenceCount} items
|
||||
</Badge>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-wrap gap-2 mt-3">
|
||||
{case_.recentItems.map((item) => (
|
||||
<div key={item.id} className="text-xs px-2 py-1 bg-muted rounded-md">
|
||||
{item.id} ({item.type})
|
||||
</div>
|
||||
))}
|
||||
|
||||
{case_.evidenceCount > case_.recentItems.length && (
|
||||
<div className="text-xs px-2 py-1 bg-muted rounded-md">
|
||||
+{case_.evidenceCount - case_.recentItems.length} more
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
|
||||
<button className="w-full text-sm text-primary mt-2 py-1">View All Cases</button>
|
||||
</div>
|
||||
)
|
||||
}
|
|
@ -0,0 +1,129 @@
|
|||
import { Badge } from "@/app/_components/ui/badge"
|
||||
import { FileText, ImageIcon, Package, Search } from "lucide-react"
|
||||
|
||||
export default function EvidenceCatalog() {
|
||||
const evidenceItems = [
|
||||
{
|
||||
id: "EV-4523",
|
||||
type: "Weapon",
|
||||
description: "Kitchen knife, 8-inch blade with wooden handle",
|
||||
case: "CR-7823",
|
||||
dateCollected: "Apr 18, 2023",
|
||||
status: "Processing",
|
||||
location: "Evidence Locker B-12",
|
||||
icon: Package,
|
||||
},
|
||||
{
|
||||
id: "EV-4524",
|
||||
type: "Photograph",
|
||||
description: "Crime scene photos - living room area",
|
||||
case: "CR-7823",
|
||||
dateCollected: "Apr 15, 2023",
|
||||
status: "Analyzed",
|
||||
location: "Digital Storage",
|
||||
icon: ImageIcon,
|
||||
},
|
||||
{
|
||||
id: "EV-4525",
|
||||
type: "Document",
|
||||
description: "Victim's personal diary",
|
||||
case: "CR-7823",
|
||||
dateCollected: "Apr 15, 2023",
|
||||
status: "Analyzed",
|
||||
location: "Evidence Locker A-7",
|
||||
icon: FileText,
|
||||
},
|
||||
{
|
||||
id: "EV-4526",
|
||||
type: "DNA Sample",
|
||||
description: "Blood sample from kitchen floor",
|
||||
case: "CR-7823",
|
||||
dateCollected: "Apr 15, 2023",
|
||||
status: "Lab Analysis",
|
||||
location: "Forensic Lab",
|
||||
icon: Package,
|
||||
},
|
||||
{
|
||||
id: "EV-4527",
|
||||
type: "Fingerprint",
|
||||
description: "Prints lifted from doorknob",
|
||||
case: "CR-7825",
|
||||
dateCollected: "Apr 20, 2023",
|
||||
status: "Processing",
|
||||
location: "Forensic Lab",
|
||||
icon: FileText,
|
||||
},
|
||||
{
|
||||
id: "EV-4528",
|
||||
type: "Clothing",
|
||||
description: "Victim's jacket with possible blood stains",
|
||||
case: "CR-7825",
|
||||
dateCollected: "Apr 20, 2023",
|
||||
status: "Lab Analysis",
|
||||
location: "Evidence Locker C-5",
|
||||
icon: Package,
|
||||
},
|
||||
]
|
||||
|
||||
return (
|
||||
<div className="mt-4">
|
||||
<div className="flex justify-between items-center mb-4">
|
||||
<div className="flex gap-2">
|
||||
<button className="text-sm font-medium">All</button>
|
||||
<button className="text-sm text-muted-foreground">Physical</button>
|
||||
<button className="text-sm text-muted-foreground">Digital</button>
|
||||
</div>
|
||||
<div className="relative">
|
||||
<input
|
||||
type="search"
|
||||
placeholder="Search evidence..."
|
||||
className="w-full rounded-md border border-input bg-background px-9 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
|
||||
/>
|
||||
<Search className="absolute left-2.5 top-2.5 h-4 w-4 text-muted-foreground" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-3">
|
||||
{evidenceItems.map((item) => (
|
||||
<div
|
||||
key={item.id}
|
||||
className="flex items-start gap-3 p-3 border rounded-lg hover:bg-muted/50 transition-colors"
|
||||
>
|
||||
<div className="w-10 h-10 rounded-full bg-muted flex items-center justify-center shrink-0">
|
||||
<item.icon className="h-5 w-5" />
|
||||
</div>
|
||||
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
<h4 className="font-medium">{item.id}</h4>
|
||||
<Badge variant="outline">{item.type}</Badge>
|
||||
<Badge
|
||||
variant="outline"
|
||||
className={
|
||||
item.status === "Analyzed"
|
||||
? "bg-green-100 text-green-800"
|
||||
: item.status === "Processing"
|
||||
? "bg-yellow-100 text-yellow-800"
|
||||
: "bg-blue-100 text-blue-800"
|
||||
}
|
||||
>
|
||||
{item.status}
|
||||
</Badge>
|
||||
</div>
|
||||
|
||||
<p className="text-sm mt-1">{item.description}</p>
|
||||
|
||||
<div className="flex flex-wrap gap-x-4 gap-y-1 mt-2 text-xs text-muted-foreground">
|
||||
<span>Case: {item.case}</span>
|
||||
<span>Collected: {item.dateCollected}</span>
|
||||
<span>Location: {item.location}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button className="text-sm text-primary shrink-0">View Details</button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
|
@ -0,0 +1,74 @@
|
|||
import { Badge } from "@/app/_components/ui/badge"
|
||||
import { AlertTriangle, Calendar } from "lucide-react"
|
||||
|
||||
export default function EvidenceDisposal() {
|
||||
const disposalItems = [
|
||||
{
|
||||
id: "EV-3245",
|
||||
type: "Clothing",
|
||||
case: "CR-6578",
|
||||
scheduledDate: "May 15, 2023",
|
||||
disposalType: "Return to Owner",
|
||||
status: "Scheduled",
|
||||
},
|
||||
{
|
||||
id: "EV-3246",
|
||||
type: "Drug Sample",
|
||||
case: "CR-6580",
|
||||
scheduledDate: "May 20, 2023",
|
||||
disposalType: "Destruction",
|
||||
status: "Pending Approval",
|
||||
},
|
||||
{
|
||||
id: "EV-3250",
|
||||
type: "Electronics",
|
||||
case: "CR-6590",
|
||||
scheduledDate: "May 25, 2023",
|
||||
disposalType: "Return to Owner",
|
||||
status: "Scheduled",
|
||||
},
|
||||
]
|
||||
|
||||
return (
|
||||
<div className="mt-4 space-y-3">
|
||||
{disposalItems.map((item) => (
|
||||
<div key={item.id} className="flex items-center gap-3 p-2 border rounded-lg">
|
||||
<div className="w-8 h-8 rounded-full bg-muted flex items-center justify-center shrink-0">
|
||||
<AlertTriangle className="h-4 w-4" />
|
||||
</div>
|
||||
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="flex items-center gap-2">
|
||||
<h4 className="font-medium text-sm">{item.id}</h4>
|
||||
<Badge variant="outline">{item.type}</Badge>
|
||||
</div>
|
||||
|
||||
<div className="flex gap-2 text-xs text-muted-foreground mt-1">
|
||||
<span>Case: {item.case}</span>
|
||||
<span>Method: {item.disposalType}</span>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-1 text-xs mt-1">
|
||||
<Calendar className="h-3 w-3" />
|
||||
<span>Scheduled: {item.scheduledDate}</span>
|
||||
<Badge
|
||||
variant="outline"
|
||||
className={
|
||||
item.status === "Scheduled"
|
||||
? "bg-green-100 text-green-800 ml-2"
|
||||
: "bg-yellow-100 text-yellow-800 ml-2"
|
||||
}
|
||||
>
|
||||
{item.status}
|
||||
</Badge>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button className="text-xs text-primary shrink-0">Review</button>
|
||||
</div>
|
||||
))}
|
||||
|
||||
<button className="w-full text-sm text-primary mt-2 py-1">Schedule Disposal</button>
|
||||
</div>
|
||||
)
|
||||
}
|
|
@ -0,0 +1,24 @@
|
|||
import { Badge } from "@/app/_components/ui/badge"
|
||||
import { Button } from "@/app/_components/ui/button"
|
||||
|
||||
export default function EvidenceHeader() {
|
||||
return (
|
||||
<div className="flex flex-col md:flex-row justify-between items-start md:items-center gap-4 mb-8">
|
||||
<div>
|
||||
<h2 className="text-3xl font-bold tracking-tight">Evidence Management</h2>
|
||||
<p className="text-muted-foreground">Track, analyze, and manage physical and digital evidence</p>
|
||||
</div>
|
||||
<div className="flex flex-col sm:flex-row items-start sm:items-center gap-4">
|
||||
<div className="flex items-center gap-2">
|
||||
<Badge variant="outline" className="bg-blue-100 text-blue-800 hover:bg-blue-100">
|
||||
Items: 1,245
|
||||
</Badge>
|
||||
<Badge variant="outline" className="bg-yellow-100 text-yellow-800 hover:bg-yellow-100">
|
||||
Processing: 32
|
||||
</Badge>
|
||||
</div>
|
||||
<Button>Log New Evidence</Button>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
|
@ -0,0 +1,57 @@
|
|||
import { Search } from "lucide-react"
|
||||
import { Button } from "@/app/_components/ui/button"
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/app/_components/ui/select"
|
||||
|
||||
export default function EvidenceSearch() {
|
||||
return (
|
||||
<div className="mt-4 space-y-4">
|
||||
<div className="relative">
|
||||
<Search className="absolute left-2.5 top-2.5 h-4 w-4 text-muted-foreground" />
|
||||
<input
|
||||
type="search"
|
||||
placeholder="Search by ID, description, or case..."
|
||||
className="w-full rounded-md border border-input bg-background px-9 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 gap-2">
|
||||
<div>
|
||||
<label className="text-xs text-muted-foreground mb-1 block">Evidence Type</label>
|
||||
<Select>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="All Types" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="all">All Types</SelectItem>
|
||||
<SelectItem value="weapon">Weapon</SelectItem>
|
||||
<SelectItem value="dna">DNA Sample</SelectItem>
|
||||
<SelectItem value="document">Document</SelectItem>
|
||||
<SelectItem value="photo">Photograph</SelectItem>
|
||||
<SelectItem value="clothing">Clothing</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="text-xs text-muted-foreground mb-1 block">Status</label>
|
||||
<Select>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="Any Status" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="all">Any Status</SelectItem>
|
||||
<SelectItem value="processing">Processing</SelectItem>
|
||||
<SelectItem value="analyzed">Analyzed</SelectItem>
|
||||
<SelectItem value="stored">Stored</SelectItem>
|
||||
<SelectItem value="disposal">Scheduled for Disposal</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Button className="w-full">Search Evidence</Button>
|
||||
|
||||
<div className="text-xs text-muted-foreground">Recent searches: EV-4523, CR-7823, "knife", "blood sample"</div>
|
||||
</div>
|
||||
)
|
||||
}
|
|
@ -0,0 +1,98 @@
|
|||
import { Badge } from "@/app/_components/ui/badge"
|
||||
import { Progress } from "@/app/_components/ui/progress"
|
||||
import { FlaskRoundIcon as Flask } from "lucide-react"
|
||||
|
||||
export default function LabAnalysis() {
|
||||
const labItems = [
|
||||
{
|
||||
id: "EV-4526",
|
||||
type: "DNA Sample",
|
||||
description: "Blood sample from kitchen floor",
|
||||
case: "CR-7823",
|
||||
submittedDate: "Apr 15, 2023",
|
||||
status: "Processing",
|
||||
progress: 65,
|
||||
estimatedCompletion: "Apr 25, 2023",
|
||||
priority: "High",
|
||||
},
|
||||
{
|
||||
id: "EV-4528",
|
||||
type: "Clothing",
|
||||
description: "Victim's jacket with possible blood stains",
|
||||
case: "CR-7825",
|
||||
submittedDate: "Apr 20, 2023",
|
||||
status: "Queue",
|
||||
progress: 10,
|
||||
estimatedCompletion: "Apr 30, 2023",
|
||||
priority: "Medium",
|
||||
},
|
||||
{
|
||||
id: "EV-4527",
|
||||
type: "Fingerprint",
|
||||
description: "Prints lifted from doorknob",
|
||||
case: "CR-7825",
|
||||
submittedDate: "Apr 20, 2023",
|
||||
status: "Processing",
|
||||
progress: 40,
|
||||
estimatedCompletion: "Apr 27, 2023",
|
||||
priority: "High",
|
||||
},
|
||||
]
|
||||
|
||||
return (
|
||||
<div className="mt-4">
|
||||
<div className="flex justify-between items-center mb-4">
|
||||
<div className="flex gap-2">
|
||||
<button className="text-sm font-medium">All</button>
|
||||
<button className="text-sm text-muted-foreground">Processing</button>
|
||||
<button className="text-sm text-muted-foreground">Completed</button>
|
||||
</div>
|
||||
<button className="text-sm text-primary">Submit to Lab</button>
|
||||
</div>
|
||||
|
||||
<div className="space-y-3">
|
||||
{labItems.map((item) => (
|
||||
<div
|
||||
key={item.id}
|
||||
className="flex items-start gap-3 p-3 border rounded-lg hover:bg-muted/50 transition-colors"
|
||||
>
|
||||
<div className="w-10 h-10 rounded-full bg-muted flex items-center justify-center shrink-0">
|
||||
<Flask className="h-5 w-5" />
|
||||
</div>
|
||||
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
<h4 className="font-medium">{item.id}</h4>
|
||||
<Badge variant="outline">{item.type}</Badge>
|
||||
<Badge
|
||||
variant="outline"
|
||||
className={item.priority === "High" ? "bg-red-100 text-red-800" : "bg-yellow-100 text-yellow-800"}
|
||||
>
|
||||
{item.priority}
|
||||
</Badge>
|
||||
</div>
|
||||
|
||||
<p className="text-sm mt-1">{item.description}</p>
|
||||
|
||||
<div className="mt-2">
|
||||
<div className="flex justify-between text-xs mb-1">
|
||||
<span>Analysis Progress</span>
|
||||
<span>{item.progress}%</span>
|
||||
</div>
|
||||
<Progress value={item.progress} className="h-2" />
|
||||
</div>
|
||||
|
||||
<div className="flex flex-wrap gap-x-4 gap-y-1 mt-2 text-xs text-muted-foreground">
|
||||
<span>Case: {item.case}</span>
|
||||
<span>Submitted: {item.submittedDate}</span>
|
||||
<span>Est. Completion: {item.estimatedCompletion}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button className="text-sm text-primary shrink-0">View Results</button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
|
@ -0,0 +1,82 @@
|
|||
import { Badge } from "@/app/_components/ui/badge"
|
||||
import { Progress } from "@/app/_components/ui/progress"
|
||||
import { Package } from "lucide-react"
|
||||
|
||||
export default function StorageLocations() {
|
||||
const locations = [
|
||||
{
|
||||
id: "A",
|
||||
name: "Evidence Locker A",
|
||||
capacity: 85,
|
||||
items: 42,
|
||||
securityLevel: "High",
|
||||
},
|
||||
{
|
||||
id: "B",
|
||||
name: "Evidence Locker B",
|
||||
capacity: 90,
|
||||
items: 81,
|
||||
securityLevel: "High",
|
||||
},
|
||||
{
|
||||
id: "C",
|
||||
name: "Evidence Locker C",
|
||||
capacity: 75,
|
||||
items: 45,
|
||||
securityLevel: "Medium",
|
||||
},
|
||||
{
|
||||
id: "D",
|
||||
name: "Digital Storage",
|
||||
capacity: 40,
|
||||
items: 32,
|
||||
securityLevel: "High",
|
||||
},
|
||||
]
|
||||
|
||||
return (
|
||||
<div className="mt-4 space-y-3">
|
||||
{locations.map((location) => (
|
||||
<div key={location.id} className="border rounded-lg p-3">
|
||||
<div className="flex items-center gap-2 mb-2">
|
||||
<div className="w-8 h-8 rounded bg-muted flex items-center justify-center shrink-0">
|
||||
<Package className="h-4 w-4" />
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h4 className="font-medium text-sm">{location.name}</h4>
|
||||
<div className="text-xs text-muted-foreground">Security: {location.securityLevel}</div>
|
||||
</div>
|
||||
|
||||
<Badge
|
||||
variant="outline"
|
||||
className={
|
||||
location.items / location.capacity > 0.9
|
||||
? "bg-red-100 text-red-800 ml-auto"
|
||||
: location.items / location.capacity > 0.7
|
||||
? "bg-yellow-100 text-yellow-800 ml-auto"
|
||||
: "bg-green-100 text-green-800 ml-auto"
|
||||
}
|
||||
>
|
||||
{location.items}/{location.capacity} items
|
||||
</Badge>
|
||||
</div>
|
||||
|
||||
<Progress
|
||||
value={(location.items / location.capacity) * 100}
|
||||
className="h-2"
|
||||
indicatorClassName={
|
||||
location.items / location.capacity > 0.9
|
||||
? "bg-red-500"
|
||||
: location.items / location.capacity > 0.7
|
||||
? "bg-yellow-500"
|
||||
: "bg-green-500"
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
|
||||
<button className="w-full text-sm text-primary mt-2 py-1">Manage Locations</button>
|
||||
</div>
|
||||
)
|
||||
}
|
|
@ -0,0 +1,92 @@
|
|||
"use client"
|
||||
|
||||
import { BentoGrid, BentoGridItem } from "@/app/_components/ui/bento-grid"
|
||||
import { Briefcase, FileText, Package, ImageIcon, Database, Clock, AlertTriangle, Search } from "lucide-react"
|
||||
|
||||
import EvidenceHeader from "./_components/evidence-header"
|
||||
import EvidenceCatalog from "./_components/evidence-catalog"
|
||||
import ChainOfCustody from "./_components/chain-of-custody"
|
||||
import LabAnalysis from "./_components/lab-analysis"
|
||||
import StorageLocations from "./_components/storage-locations"
|
||||
import EvidenceByCase from "./_components/evidence-by-case"
|
||||
import DigitalEvidence from "./_components/digital-evidence"
|
||||
import EvidenceDisposal from "./_components/evidence-disposal"
|
||||
import EvidenceSearch from "./_components/evidence-search"
|
||||
|
||||
export default function EvidenceManagementPage() {
|
||||
return (
|
||||
<div className="container py-4 min-h-screen">
|
||||
<div className="max-w-7xl mx-auto">
|
||||
<EvidenceHeader />
|
||||
|
||||
<BentoGrid>
|
||||
<BentoGridItem
|
||||
title="Evidence Catalog"
|
||||
description="Recently logged items"
|
||||
icon={<Briefcase className="w-5 h-5" />}
|
||||
colSpan="2"
|
||||
>
|
||||
<EvidenceCatalog />
|
||||
</BentoGridItem>
|
||||
|
||||
<BentoGridItem
|
||||
title="Chain of Custody"
|
||||
description="Evidence handling records"
|
||||
icon={<Clock className="w-5 h-5" />}
|
||||
>
|
||||
<ChainOfCustody />
|
||||
</BentoGridItem>
|
||||
|
||||
<BentoGridItem
|
||||
title="Lab Analysis"
|
||||
description="Processing status and results"
|
||||
icon={<FileText className="w-5 h-5" />}
|
||||
colSpan="2"
|
||||
>
|
||||
<LabAnalysis />
|
||||
</BentoGridItem>
|
||||
|
||||
<BentoGridItem
|
||||
title="Storage Locations"
|
||||
description="Evidence storage management"
|
||||
icon={<Package className="w-5 h-5" />}
|
||||
>
|
||||
<StorageLocations />
|
||||
</BentoGridItem>
|
||||
|
||||
<BentoGridItem
|
||||
title="Evidence by Case"
|
||||
description="Items grouped by case"
|
||||
icon={<Database className="w-5 h-5" />}
|
||||
>
|
||||
<EvidenceByCase />
|
||||
</BentoGridItem>
|
||||
|
||||
<BentoGridItem
|
||||
title="Digital Evidence"
|
||||
description="Files, recordings, and media"
|
||||
icon={<ImageIcon className="w-5 h-5" />}
|
||||
>
|
||||
<DigitalEvidence />
|
||||
</BentoGridItem>
|
||||
|
||||
<BentoGridItem
|
||||
title="Evidence Disposal"
|
||||
description="Scheduled for destruction or return"
|
||||
icon={<AlertTriangle className="w-5 h-5" />}
|
||||
>
|
||||
<EvidenceDisposal />
|
||||
</BentoGridItem>
|
||||
|
||||
<BentoGridItem
|
||||
title="Evidence Search"
|
||||
description="Find items by ID, case, or type"
|
||||
icon={<Search className="w-5 h-5" />}
|
||||
>
|
||||
<EvidenceSearch />
|
||||
</BentoGridItem>
|
||||
</BentoGrid>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
|
@ -0,0 +1,24 @@
|
|||
import { Badge } from "@/app/_components/ui/badge"
|
||||
import { Button } from "@/app/_components/ui/button"
|
||||
|
||||
export default function DepartmentHeader() {
|
||||
return (
|
||||
<div className="flex flex-col md:flex-row justify-between items-start md:items-center gap-4 mb-8">
|
||||
<div>
|
||||
<h2 className="text-3xl font-bold tracking-tight">Officer Management</h2>
|
||||
<p className="text-muted-foreground">Personnel, scheduling, and department resources</p>
|
||||
</div>
|
||||
<div className="flex flex-col sm:flex-row items-start sm:items-center gap-4">
|
||||
<div className="flex items-center gap-2">
|
||||
<Badge variant="outline" className="bg-green-100 text-green-800 hover:bg-green-100">
|
||||
On Duty: 18
|
||||
</Badge>
|
||||
<Badge variant="outline" className="bg-yellow-100 text-yellow-800 hover:bg-yellow-100">
|
||||
Off Duty: 12
|
||||
</Badge>
|
||||
</div>
|
||||
<Button>Add Officer</Button>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
|
@ -0,0 +1,65 @@
|
|||
import { Badge } from "@/app/_components/ui/badge"
|
||||
import { Car, Radio, Shield } from "lucide-react"
|
||||
|
||||
export default function EquipmentAssignments() {
|
||||
const equipment = [
|
||||
{
|
||||
type: "Vehicle",
|
||||
id: "VEH-1234",
|
||||
assignedTo: "Emily Parker",
|
||||
status: "In Service",
|
||||
icon: Car,
|
||||
},
|
||||
{
|
||||
type: "Radio",
|
||||
id: "RAD-5678",
|
||||
assignedTo: "Michael Chen",
|
||||
status: "In Service",
|
||||
icon: Radio,
|
||||
},
|
||||
{
|
||||
type: "Body Camera",
|
||||
id: "CAM-9012",
|
||||
assignedTo: "Robert Wilson",
|
||||
status: "Maintenance",
|
||||
icon: Shield,
|
||||
},
|
||||
{
|
||||
type: "Vehicle",
|
||||
id: "VEH-3456",
|
||||
assignedTo: "David Thompson",
|
||||
status: "In Service",
|
||||
icon: Car,
|
||||
},
|
||||
]
|
||||
|
||||
return (
|
||||
<div className="mt-4 space-y-2">
|
||||
{equipment.map((item, index) => (
|
||||
<div key={index} className="flex items-center gap-3 p-2 border rounded-lg">
|
||||
<div className="w-8 h-8 rounded bg-muted flex items-center justify-center shrink-0">
|
||||
<item.icon className="h-4 w-4" />
|
||||
</div>
|
||||
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="flex items-center gap-2">
|
||||
<h4 className="font-medium text-sm">{item.type}</h4>
|
||||
<span className="text-xs text-muted-foreground">#{item.id}</span>
|
||||
</div>
|
||||
|
||||
<div className="text-xs text-muted-foreground">Assigned to: {item.assignedTo}</div>
|
||||
</div>
|
||||
|
||||
<Badge
|
||||
variant="outline"
|
||||
className={item.status === "In Service" ? "bg-green-100 text-green-800" : "bg-yellow-100 text-yellow-800"}
|
||||
>
|
||||
{item.status}
|
||||
</Badge>
|
||||
</div>
|
||||
))}
|
||||
|
||||
<button className="w-full text-sm text-primary mt-2 py-1">Manage Equipment</button>
|
||||
</div>
|
||||
)
|
||||
}
|
|
@ -0,0 +1,92 @@
|
|||
import { Badge } from "@/app/_components/ui/badge"
|
||||
import { FileText } from "lucide-react"
|
||||
|
||||
export default function IncidentReports() {
|
||||
const reports = [
|
||||
{
|
||||
id: "IR-7823",
|
||||
title: "Traffic Stop - Speeding",
|
||||
officer: "David Thompson",
|
||||
date: "Apr 22, 2023",
|
||||
status: "Submitted",
|
||||
location: "Highway 101, Mile 23",
|
||||
},
|
||||
{
|
||||
id: "IR-7824",
|
||||
title: "Domestic Disturbance",
|
||||
officer: "Emily Parker",
|
||||
date: "Apr 22, 2023",
|
||||
status: "Pending Review",
|
||||
location: "123 Main St, Apt 4B",
|
||||
},
|
||||
{
|
||||
id: "IR-7825",
|
||||
title: "Shoplifting",
|
||||
officer: "James Rodriguez",
|
||||
date: "Apr 21, 2023",
|
||||
status: "Approved",
|
||||
location: "Downtown Mall, Store #12",
|
||||
},
|
||||
{
|
||||
id: "IR-7826",
|
||||
title: "Noise Complaint",
|
||||
officer: "Emily Parker",
|
||||
date: "Apr 21, 2023",
|
||||
status: "Approved",
|
||||
location: "456 Oak Ave",
|
||||
},
|
||||
]
|
||||
|
||||
return (
|
||||
<div className="mt-4">
|
||||
<div className="flex justify-between items-center mb-4">
|
||||
<div className="flex gap-2">
|
||||
<button className="text-sm font-medium">All</button>
|
||||
<button className="text-sm text-muted-foreground">Pending</button>
|
||||
<button className="text-sm text-muted-foreground">Approved</button>
|
||||
</div>
|
||||
<button className="text-sm text-primary">New Report</button>
|
||||
</div>
|
||||
|
||||
<div className="space-y-3">
|
||||
{reports.map((report) => (
|
||||
<div
|
||||
key={report.id}
|
||||
className="flex items-start gap-3 p-3 border rounded-lg hover:bg-muted/50 transition-colors"
|
||||
>
|
||||
<div className="w-10 h-10 rounded bg-muted flex items-center justify-center shrink-0">
|
||||
<FileText className="h-5 w-5" />
|
||||
</div>
|
||||
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
<h4 className="font-medium">{report.title}</h4>
|
||||
<span className="text-sm text-muted-foreground">#{report.id}</span>
|
||||
<Badge
|
||||
variant="outline"
|
||||
className={
|
||||
report.status === "Approved"
|
||||
? "bg-green-100 text-green-800"
|
||||
: report.status === "Pending Review"
|
||||
? "bg-yellow-100 text-yellow-800"
|
||||
: "bg-blue-100 text-blue-800"
|
||||
}
|
||||
>
|
||||
{report.status}
|
||||
</Badge>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-wrap gap-x-4 gap-y-1 mt-1 text-xs text-muted-foreground">
|
||||
<span>Officer: {report.officer}</span>
|
||||
<span>Date: {report.date}</span>
|
||||
<span>Location: {report.location}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button className="text-sm text-primary shrink-0">View</button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
|
@ -0,0 +1,62 @@
|
|||
import { Badge } from "@/app/_components/ui/badge"
|
||||
import { Calendar } from "lucide-react"
|
||||
|
||||
export default function LeaveManagement() {
|
||||
const leaveRequests = [
|
||||
{
|
||||
officer: "Sarah Johnson",
|
||||
type: "Vacation",
|
||||
dates: "May 10-15, 2023",
|
||||
status: "Approved",
|
||||
requestDate: "Apr 15, 2023",
|
||||
},
|
||||
{
|
||||
officer: "James Rodriguez",
|
||||
type: "Sick Leave",
|
||||
dates: "Apr 25, 2023",
|
||||
status: "Pending",
|
||||
requestDate: "Apr 24, 2023",
|
||||
},
|
||||
{
|
||||
officer: "Michael Chen",
|
||||
type: "Personal",
|
||||
dates: "May 5, 2023",
|
||||
status: "Pending",
|
||||
requestDate: "Apr 20, 2023",
|
||||
},
|
||||
]
|
||||
|
||||
return (
|
||||
<div className="mt-4 space-y-3">
|
||||
{leaveRequests.map((request, index) => (
|
||||
<div key={index} className="flex items-start gap-3 p-2 border rounded-lg">
|
||||
<div className="w-8 h-8 rounded bg-muted flex items-center justify-center shrink-0">
|
||||
<Calendar className="h-4 w-4" />
|
||||
</div>
|
||||
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="flex items-center gap-2">
|
||||
<h4 className="font-medium text-sm">{request.officer}</h4>
|
||||
<Badge
|
||||
variant="outline"
|
||||
className={
|
||||
request.status === "Approved" ? "bg-green-100 text-green-800" : "bg-yellow-100 text-yellow-800"
|
||||
}
|
||||
>
|
||||
{request.status}
|
||||
</Badge>
|
||||
</div>
|
||||
|
||||
<div className="text-xs text-muted-foreground">
|
||||
{request.type} • {request.dates}
|
||||
</div>
|
||||
|
||||
<div className="text-xs text-muted-foreground mt-1">Requested: {request.requestDate}</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
|
||||
<button className="w-full text-sm text-primary mt-2 py-1">Request Leave</button>
|
||||
</div>
|
||||
)
|
||||
}
|
|
@ -0,0 +1,55 @@
|
|||
import { Progress } from "@/app/_components/ui/progress"
|
||||
import { Award, TrendingUp } from "lucide-react"
|
||||
|
||||
export default function OfficerPerformance() {
|
||||
const topPerformers = [
|
||||
{ name: "Emily Parker", metric: "Case Clearance", value: 92 },
|
||||
{ name: "Michael Chen", metric: "Response Time", value: 88 },
|
||||
{ name: "Sarah Johnson", metric: "Evidence Processing", value: 95 },
|
||||
]
|
||||
|
||||
return (
|
||||
<div className="mt-4 space-y-4">
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div className="border rounded-lg p-3">
|
||||
<div className="text-sm text-muted-foreground mb-1">Avg. Case Clearance</div>
|
||||
<div className="text-2xl font-bold">68%</div>
|
||||
<div className="text-xs text-green-600 flex items-center mt-1">
|
||||
<TrendingUp className="h-3 w-3 mr-1" />
|
||||
+5% from last month
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="border rounded-lg p-3">
|
||||
<div className="text-sm text-muted-foreground mb-1">Avg. Response Time</div>
|
||||
<div className="text-2xl font-bold">4.2m</div>
|
||||
<div className="text-xs text-green-600 flex items-center mt-1">
|
||||
<TrendingUp className="h-3 w-3 mr-1" />
|
||||
-0.3m from last month
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="border rounded-lg p-3">
|
||||
<h4 className="font-medium text-sm mb-3 flex items-center">
|
||||
<Award className="h-4 w-4 mr-1" />
|
||||
Top Performers
|
||||
</h4>
|
||||
|
||||
<div className="space-y-3">
|
||||
{topPerformers.map((performer, index) => (
|
||||
<div key={index}>
|
||||
<div className="flex justify-between text-sm mb-1">
|
||||
<span>{performer.name}</span>
|
||||
<span>
|
||||
{performer.metric}: {performer.value}%
|
||||
</span>
|
||||
</div>
|
||||
<Progress value={performer.value} className="h-2" />
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
|
@ -0,0 +1,132 @@
|
|||
import { User, Shield, Phone } from "lucide-react"
|
||||
import { Badge } from "@/app/_components/ui/badge"
|
||||
import { Button } from "@/app/_components/ui/button"
|
||||
|
||||
export default function OfficerRoster() {
|
||||
const officers = [
|
||||
{
|
||||
id: "OFF-1234",
|
||||
name: "Michael Chen",
|
||||
badge: "ID-5678",
|
||||
rank: "Detective",
|
||||
unit: "Homicide",
|
||||
status: "On Duty",
|
||||
contact: "555-123-4567",
|
||||
shift: "Day",
|
||||
},
|
||||
{
|
||||
id: "OFF-2345",
|
||||
name: "Sarah Johnson",
|
||||
badge: "ID-6789",
|
||||
rank: "Specialist",
|
||||
unit: "Forensics",
|
||||
status: "On Duty",
|
||||
contact: "555-234-5678",
|
||||
shift: "Day",
|
||||
},
|
||||
{
|
||||
id: "OFF-3456",
|
||||
name: "Robert Wilson",
|
||||
badge: "ID-7890",
|
||||
rank: "Technician",
|
||||
unit: "Evidence",
|
||||
status: "On Duty",
|
||||
contact: "555-345-6789",
|
||||
shift: "Day",
|
||||
},
|
||||
{
|
||||
id: "OFF-4567",
|
||||
name: "James Rodriguez",
|
||||
badge: "ID-8901",
|
||||
rank: "Officer",
|
||||
unit: "Patrol",
|
||||
status: "Off Duty",
|
||||
contact: "555-456-7890",
|
||||
shift: "Night",
|
||||
},
|
||||
{
|
||||
id: "OFF-5678",
|
||||
name: "Emily Parker",
|
||||
badge: "ID-9012",
|
||||
rank: "Sergeant",
|
||||
unit: "Patrol",
|
||||
status: "On Duty",
|
||||
contact: "555-567-8901",
|
||||
shift: "Day",
|
||||
},
|
||||
{
|
||||
id: "OFF-6789",
|
||||
name: "David Thompson",
|
||||
badge: "ID-0123",
|
||||
rank: "Officer",
|
||||
unit: "Traffic",
|
||||
status: "On Duty",
|
||||
contact: "555-678-9012",
|
||||
shift: "Day",
|
||||
},
|
||||
]
|
||||
|
||||
return (
|
||||
<div className="mt-4">
|
||||
<div className="flex justify-between items-center mb-4">
|
||||
<div className="flex gap-2">
|
||||
<Button variant="outline" size="sm">
|
||||
All
|
||||
</Button>
|
||||
<Button variant="outline" size="sm">
|
||||
On Duty
|
||||
</Button>
|
||||
<Button variant="outline" size="sm">
|
||||
Off Duty
|
||||
</Button>
|
||||
</div>
|
||||
<div className="relative">
|
||||
<input
|
||||
type="search"
|
||||
placeholder="Search officers..."
|
||||
className="w-full rounded-md border border-input bg-background px-9 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
|
||||
/>
|
||||
<User className="absolute left-2.5 top-2.5 h-4 w-4 text-muted-foreground" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-3">
|
||||
{officers.map((officer) => (
|
||||
<div
|
||||
key={officer.id}
|
||||
className="flex items-center gap-3 p-3 border rounded-lg hover:bg-muted/50 transition-colors"
|
||||
>
|
||||
<div className="w-10 h-10 rounded-full bg-muted flex items-center justify-center shrink-0">
|
||||
<User className="h-5 w-5" />
|
||||
</div>
|
||||
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="flex items-center gap-2">
|
||||
<h4 className="font-medium">{officer.name}</h4>
|
||||
<Badge
|
||||
variant="outline"
|
||||
className={
|
||||
officer.status === "On Duty" ? "bg-green-100 text-green-800" : "bg-yellow-100 text-yellow-800"
|
||||
}
|
||||
>
|
||||
{officer.status}
|
||||
</Badge>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-wrap gap-x-3 gap-y-1 mt-1 text-xs text-muted-foreground">
|
||||
<div className="flex items-center">
|
||||
<Shield className="h-3 w-3 mr-1" />
|
||||
{officer.rank} • {officer.unit}
|
||||
</div>
|
||||
<div className="flex items-center">
|
||||
<Phone className="h-3 w-3 mr-1" />
|
||||
{officer.contact}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
|
@ -0,0 +1,85 @@
|
|||
import { Calendar, Clock } from "lucide-react"
|
||||
|
||||
export default function ShiftSchedule() {
|
||||
const days = ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"]
|
||||
const shifts = [
|
||||
{ time: "Day (7AM-3PM)", color: "bg-blue-100" },
|
||||
{ time: "Evening (3PM-11PM)", color: "bg-purple-100" },
|
||||
{ time: "Night (11PM-7AM)", color: "bg-indigo-100" },
|
||||
]
|
||||
|
||||
// Sample schedule data
|
||||
const schedule = {
|
||||
"OFF-1234": [0, 0, 0, 0, 0, null, null], // Day shift Mon-Fri
|
||||
"OFF-2345": [0, 0, 0, 0, 0, null, null], // Day shift Mon-Fri
|
||||
"OFF-3456": [0, 0, 0, 0, 0, null, null], // Day shift Mon-Fri
|
||||
"OFF-4567": [null, null, 2, 2, 2, 2, 2], // Night shift Wed-Sun
|
||||
"OFF-5678": [0, 0, 0, 0, 0, null, null], // Day shift Mon-Fri
|
||||
"OFF-6789": [1, 1, 1, 1, 1, null, null], // Evening shift Mon-Fri
|
||||
}
|
||||
|
||||
const officers = [
|
||||
{ id: "OFF-1234", name: "Michael Chen" },
|
||||
{ id: "OFF-2345", name: "Sarah Johnson" },
|
||||
{ id: "OFF-3456", name: "Robert Wilson" },
|
||||
{ id: "OFF-4567", name: "James Rodriguez" },
|
||||
{ id: "OFF-5678", name: "Emily Parker" },
|
||||
{ id: "OFF-6789", name: "David Thompson" },
|
||||
]
|
||||
|
||||
return (
|
||||
<div className="mt-4">
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<div className="flex items-center">
|
||||
<Calendar className="h-4 w-4 mr-2" />
|
||||
<span className="font-medium">Week of April 24-30, 2023</span>
|
||||
</div>
|
||||
<div className="flex gap-1">
|
||||
<button className="p-1 rounded hover:bg-muted">
|
||||
<Clock className="h-4 w-4" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="border rounded-lg overflow-hidden">
|
||||
<div className="grid grid-cols-8 border-b bg-muted/50">
|
||||
<div className="p-2 font-medium text-sm border-r">Officer</div>
|
||||
{days.map((day, i) => (
|
||||
<div key={day} className="p-2 font-medium text-sm text-center">
|
||||
{day}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{officers.map((officer) => (
|
||||
<div key={officer.id} className="grid grid-cols-8 border-b last:border-b-0">
|
||||
<div className="p-2 text-sm border-r truncate">{officer.name}</div>
|
||||
{days.map((day, dayIndex) => {
|
||||
const shiftIndex = schedule[officer.id]?.[dayIndex]
|
||||
const shift = shifts[shiftIndex]
|
||||
|
||||
return (
|
||||
<div key={`${officer.id}-${day}`} className="p-2 text-xs text-center">
|
||||
{shift ? (
|
||||
<div className={`${shift.color} rounded-md py-1 px-2`}>{shift.time.split(" ")[0]}</div>
|
||||
) : (
|
||||
<div className="text-muted-foreground">Off</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="mt-4 flex flex-wrap gap-2">
|
||||
{shifts.map((shift) => (
|
||||
<div key={shift.time} className="flex items-center text-xs">
|
||||
<div className={`w-3 h-3 rounded-full ${shift.color} mr-1`}></div>
|
||||
{shift.time}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
|
@ -0,0 +1,67 @@
|
|||
import { Badge } from "@/app/_components/ui/badge"
|
||||
import { Shield, Users } from "lucide-react"
|
||||
|
||||
export default function SpecializedUnits() {
|
||||
const units = [
|
||||
{
|
||||
name: "SWAT",
|
||||
members: 8,
|
||||
status: "Available",
|
||||
lead: "Capt. Anderson",
|
||||
},
|
||||
{
|
||||
name: "K-9 Unit",
|
||||
members: 5,
|
||||
status: "On Call",
|
||||
lead: "Lt. Martinez",
|
||||
},
|
||||
{
|
||||
name: "Narcotics",
|
||||
members: 6,
|
||||
status: "Deployed",
|
||||
lead: "Sgt. Williams",
|
||||
},
|
||||
]
|
||||
|
||||
return (
|
||||
<div className="mt-4 space-y-3">
|
||||
{units.map((unit, index) => (
|
||||
<div key={index} className="flex items-start gap-3 p-2 border rounded-lg">
|
||||
<div className="w-8 h-8 rounded bg-muted flex items-center justify-center shrink-0">
|
||||
<Shield className="h-4 w-4" />
|
||||
</div>
|
||||
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="flex items-center gap-2">
|
||||
<h4 className="font-medium text-sm">{unit.name}</h4>
|
||||
<Badge
|
||||
variant="outline"
|
||||
className={
|
||||
unit.status === "Available"
|
||||
? "bg-green-100 text-green-800"
|
||||
: unit.status === "On Call"
|
||||
? "bg-blue-100 text-blue-800"
|
||||
: "bg-yellow-100 text-yellow-800"
|
||||
}
|
||||
>
|
||||
{unit.status}
|
||||
</Badge>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-2 text-xs text-muted-foreground">
|
||||
<div className="flex items-center">
|
||||
<Users className="h-3 w-3 mr-1" />
|
||||
{unit.members} members
|
||||
</div>
|
||||
<div>Lead: {unit.lead}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button className="text-xs text-primary shrink-0">Details</button>
|
||||
</div>
|
||||
))}
|
||||
|
||||
<button className="w-full text-sm text-primary mt-2 py-1">View All Units</button>
|
||||
</div>
|
||||
)
|
||||
}
|
|
@ -0,0 +1,61 @@
|
|||
import { Badge } from "@/app/_components/ui/badge"
|
||||
import { Progress } from "@/app/_components/ui/progress"
|
||||
|
||||
export default function TrainingStatus() {
|
||||
const trainings = [
|
||||
{
|
||||
name: "Firearms Qualification",
|
||||
dueDate: "May 15, 2023",
|
||||
status: "Completed",
|
||||
completion: 100,
|
||||
},
|
||||
{
|
||||
name: "De-escalation Techniques",
|
||||
dueDate: "June 10, 2023",
|
||||
status: "In Progress",
|
||||
completion: 60,
|
||||
},
|
||||
{
|
||||
name: "Emergency Response",
|
||||
dueDate: "July 22, 2023",
|
||||
status: "Not Started",
|
||||
completion: 0,
|
||||
},
|
||||
{
|
||||
name: "Evidence Handling",
|
||||
dueDate: "May 30, 2023",
|
||||
status: "In Progress",
|
||||
completion: 75,
|
||||
},
|
||||
]
|
||||
|
||||
return (
|
||||
<div className="mt-4 space-y-3">
|
||||
{trainings.map((training, index) => (
|
||||
<div key={index} className="border rounded-lg p-3">
|
||||
<div className="flex justify-between items-start mb-2">
|
||||
<h4 className="font-medium text-sm">{training.name}</h4>
|
||||
<Badge
|
||||
variant="outline"
|
||||
className={
|
||||
training.status === "Completed"
|
||||
? "bg-green-100 text-green-800"
|
||||
: training.status === "In Progress"
|
||||
? "bg-yellow-100 text-yellow-800"
|
||||
: "bg-red-100 text-red-800"
|
||||
}
|
||||
>
|
||||
{training.status}
|
||||
</Badge>
|
||||
</div>
|
||||
|
||||
<Progress value={training.completion} className="h-2 mb-2" />
|
||||
|
||||
<div className="text-xs text-muted-foreground">Due: {training.dueDate}</div>
|
||||
</div>
|
||||
))}
|
||||
|
||||
<button className="w-full text-sm text-primary mt-2 py-1">View All Trainings</button>
|
||||
</div>
|
||||
)
|
||||
}
|
|
@ -0,0 +1,92 @@
|
|||
"use client"
|
||||
|
||||
import { BentoGrid, BentoGridItem } from "@/app/_components/ui/bento-grid"
|
||||
import { Shield, Users, Calendar, Award, Clock, FileText, Briefcase } from "lucide-react"
|
||||
import DepartmentHeader from "./_components/department-header"
|
||||
import OfficerRoster from "./_components/officer-roster"
|
||||
import ShiftSchedule from "./_components/shift-schedule"
|
||||
import OfficerPerformance from "./_components/officer-performance"
|
||||
import TrainingStatus from "./_components/training-status"
|
||||
import EquipmentAssignments from "./_components/equipment-assignments"
|
||||
import IncidentReports from "./_components/incident-reports"
|
||||
import LeaveManagement from "./_components/leave-management"
|
||||
import SpecializedUnits from "./_components/specialized-units"
|
||||
|
||||
export default function OfficerManagementPage() {
|
||||
return (
|
||||
<div className="container py-4 min-h-screen">
|
||||
<div className="max-w-7xl mx-auto">
|
||||
<DepartmentHeader />
|
||||
|
||||
<BentoGrid>
|
||||
<BentoGridItem
|
||||
title="Officer Roster"
|
||||
description="Active personnel and status"
|
||||
icon={<Shield className="w-5 h-5" />}
|
||||
colSpan="2"
|
||||
>
|
||||
<OfficerRoster />
|
||||
</BentoGridItem>
|
||||
|
||||
<BentoGridItem
|
||||
title="Shift Schedule"
|
||||
description="Current and upcoming shifts"
|
||||
icon={<Calendar className="w-5 h-5" />}
|
||||
rowSpan="2"
|
||||
>
|
||||
<ShiftSchedule />
|
||||
</BentoGridItem>
|
||||
|
||||
<BentoGridItem
|
||||
title="Officer Performance"
|
||||
description="Case clearance and metrics"
|
||||
icon={<Award className="w-5 h-5" />}
|
||||
>
|
||||
<OfficerPerformance />
|
||||
</BentoGridItem>
|
||||
|
||||
<BentoGridItem
|
||||
title="Training Status"
|
||||
description="Certifications and requirements"
|
||||
icon={<Award className="w-5 h-5" />}
|
||||
>
|
||||
<TrainingStatus />
|
||||
</BentoGridItem>
|
||||
|
||||
<BentoGridItem
|
||||
title="Equipment Assignments"
|
||||
description="Issued gear and vehicles"
|
||||
icon={<Briefcase className="w-5 h-5" />}
|
||||
>
|
||||
<EquipmentAssignments />
|
||||
</BentoGridItem>
|
||||
|
||||
<BentoGridItem
|
||||
title="Incident Reports"
|
||||
description="Recently filed reports"
|
||||
icon={<FileText className="w-5 h-5" />}
|
||||
colSpan="2"
|
||||
>
|
||||
<IncidentReports />
|
||||
</BentoGridItem>
|
||||
|
||||
<BentoGridItem
|
||||
title="Leave Management"
|
||||
description="Time-off requests and approvals"
|
||||
icon={<Clock className="w-5 h-5" />}
|
||||
>
|
||||
<LeaveManagement />
|
||||
</BentoGridItem>
|
||||
|
||||
<BentoGridItem
|
||||
title="Specialized Units"
|
||||
description="Tactical teams and special operations"
|
||||
icon={<Users className="w-5 h-5" />}
|
||||
>
|
||||
<SpecializedUnits />
|
||||
</BentoGridItem>
|
||||
</BentoGrid>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
|
@ -0,0 +1,143 @@
|
|||
import { Badge } from "@/app/_components/ui/badge"
|
||||
import { Button } from "@/app/_components/ui/button"
|
||||
import { AlertTriangle, MapPin, Clock, Users } from "lucide-react"
|
||||
|
||||
export default function ActiveIncidents() {
|
||||
const incidents = [
|
||||
{
|
||||
id: "INC-4523",
|
||||
type: "Domestic Disturbance",
|
||||
location: "123 Main St, Apt 4B",
|
||||
priority: "High",
|
||||
status: "Units Responding",
|
||||
timeReceived: "10:23 AM",
|
||||
responseTime: "2m 15s",
|
||||
assignedUnits: ["Unit 12", "Unit 15"],
|
||||
},
|
||||
{
|
||||
id: "INC-4524",
|
||||
type: "Traffic Accident",
|
||||
location: "Interstate 95, Mile 42",
|
||||
priority: "Medium",
|
||||
status: "On Scene",
|
||||
timeReceived: "10:15 AM",
|
||||
responseTime: "5m 30s",
|
||||
assignedUnits: ["Unit 8", "Unit 22"],
|
||||
},
|
||||
{
|
||||
id: "INC-4525",
|
||||
type: "Burglary",
|
||||
location: "456 Oak Ave",
|
||||
priority: "Medium",
|
||||
status: "Units Responding",
|
||||
timeReceived: "10:05 AM",
|
||||
responseTime: "4m 45s",
|
||||
assignedUnits: ["Unit 17"],
|
||||
},
|
||||
{
|
||||
id: "INC-4526",
|
||||
type: "Medical Emergency",
|
||||
location: "789 Pine St",
|
||||
priority: "Critical",
|
||||
status: "On Scene",
|
||||
timeReceived: "9:58 AM",
|
||||
responseTime: "3m 10s",
|
||||
assignedUnits: ["Unit 5", "Unit 9", "Ambulance 3"],
|
||||
},
|
||||
]
|
||||
|
||||
return (
|
||||
<div className="mt-4">
|
||||
<div className="flex justify-between items-center mb-4">
|
||||
<div className="flex gap-2">
|
||||
<Button variant="outline" size="sm">
|
||||
All
|
||||
</Button>
|
||||
<Button variant="outline" size="sm">
|
||||
Critical
|
||||
</Button>
|
||||
<Button variant="outline" size="sm">
|
||||
High
|
||||
</Button>
|
||||
<Button variant="outline" size="sm">
|
||||
Medium
|
||||
</Button>
|
||||
</div>
|
||||
<div className="text-sm text-muted-foreground">Showing {incidents.length} active incidents</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-3">
|
||||
{incidents.map((incident) => (
|
||||
<div
|
||||
key={incident.id}
|
||||
className="flex items-start gap-3 p-3 border rounded-lg hover:bg-muted/50 transition-colors"
|
||||
>
|
||||
<div
|
||||
className={`w-10 h-10 rounded-full flex items-center justify-center shrink-0 ${
|
||||
incident.priority === "Critical"
|
||||
? "bg-red-100"
|
||||
: incident.priority === "High"
|
||||
? "bg-orange-100"
|
||||
: "bg-yellow-100"
|
||||
}`}
|
||||
>
|
||||
<AlertTriangle
|
||||
className={`h-5 w-5 ${
|
||||
incident.priority === "Critical"
|
||||
? "text-red-600"
|
||||
: incident.priority === "High"
|
||||
? "text-orange-600"
|
||||
: "text-yellow-600"
|
||||
}`}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
<h4 className="font-medium">{incident.type}</h4>
|
||||
<span className="text-sm text-muted-foreground">#{incident.id}</span>
|
||||
<Badge
|
||||
variant="outline"
|
||||
className={
|
||||
incident.priority === "Critical"
|
||||
? "bg-red-100 text-red-800"
|
||||
: incident.priority === "High"
|
||||
? "bg-orange-100 text-orange-800"
|
||||
: "bg-yellow-100 text-yellow-800"
|
||||
}
|
||||
>
|
||||
{incident.priority}
|
||||
</Badge>
|
||||
<Badge variant="outline" className="bg-blue-100 text-blue-800">
|
||||
{incident.status}
|
||||
</Badge>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-wrap gap-x-4 gap-y-1 mt-2 text-xs text-muted-foreground">
|
||||
<div className="flex items-center">
|
||||
<MapPin className="h-3 w-3 mr-1" />
|
||||
{incident.location}
|
||||
</div>
|
||||
<div className="flex items-center">
|
||||
<Clock className="h-3 w-3 mr-1" />
|
||||
Received: {incident.timeReceived} (Response: {incident.responseTime})
|
||||
</div>
|
||||
<div className="flex items-center">
|
||||
<Users className="h-3 w-3 mr-1" />
|
||||
Units: {incident.assignedUnits.join(", ")}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-2">
|
||||
<Button size="sm">Update</Button>
|
||||
<Button size="sm" variant="outline">
|
||||
Details
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
|
@ -0,0 +1,87 @@
|
|||
import { Badge } from "@/app/_components/ui/badge"
|
||||
import { Radio, MessageSquare, User } from "lucide-react"
|
||||
|
||||
export default function DispatchCommunications() {
|
||||
const communications = [
|
||||
{
|
||||
id: 1,
|
||||
type: "Radio",
|
||||
from: "Unit 12",
|
||||
message: "Arriving on scene at 123 Main St.",
|
||||
time: "10:32 AM",
|
||||
priority: "Normal",
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
type: "Radio",
|
||||
from: "Unit 8",
|
||||
message: "Requesting additional unit for traffic control at accident scene.",
|
||||
time: "10:28 AM",
|
||||
priority: "High",
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
type: "Message",
|
||||
from: "Dispatch",
|
||||
message: "Be advised, suspect description updated for INC-4525.",
|
||||
time: "10:25 AM",
|
||||
priority: "Normal",
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
type: "Radio",
|
||||
from: "Unit 5",
|
||||
message: "Medical assistance required at 789 Pine St.",
|
||||
time: "10:20 AM",
|
||||
priority: "High",
|
||||
},
|
||||
]
|
||||
|
||||
return (
|
||||
<div className="mt-4 space-y-3">
|
||||
<div className="flex justify-between items-center">
|
||||
<div className="text-sm font-medium">Recent Communications</div>
|
||||
<Badge variant="outline" className="bg-blue-100 text-blue-800">
|
||||
Channel: Main Dispatch
|
||||
</Badge>
|
||||
</div>
|
||||
|
||||
{communications.map((comm) => (
|
||||
<div key={comm.id} className="border rounded-lg p-2">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="w-6 h-6 rounded-full bg-muted flex items-center justify-center shrink-0">
|
||||
{comm.type === "Radio" ? <Radio className="h-3 w-3" /> : <MessageSquare className="h-3 w-3" />}
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-1">
|
||||
<span className="font-medium text-xs">{comm.from}</span>
|
||||
<span className="text-xs text-muted-foreground">• {comm.time}</span>
|
||||
</div>
|
||||
|
||||
<Badge
|
||||
variant="outline"
|
||||
className={
|
||||
comm.priority === "High" ? "bg-red-100 text-red-800 ml-auto" : "bg-blue-100 text-blue-800 ml-auto"
|
||||
}
|
||||
>
|
||||
{comm.priority}
|
||||
</Badge>
|
||||
</div>
|
||||
|
||||
<p className="text-xs mt-1 ml-8">{comm.message}</p>
|
||||
</div>
|
||||
))}
|
||||
|
||||
<div className="flex items-center gap-2 mt-2">
|
||||
<div className="w-8 h-8 rounded-full bg-muted flex items-center justify-center shrink-0">
|
||||
<User className="h-4 w-4" />
|
||||
</div>
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Send message to all units..."
|
||||
className="flex-1 text-sm rounded-md border border-input bg-background px-3 py-1 ring-offset-background placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
|
@ -0,0 +1,29 @@
|
|||
import { Badge } from "@/app/_components/ui/badge"
|
||||
import { Button } from "@/app/_components/ui/button"
|
||||
import { AlertTriangle, Phone } from "lucide-react"
|
||||
|
||||
export default function DispatchHeader() {
|
||||
return (
|
||||
<div className="flex flex-col md:flex-row justify-between items-start md:items-center gap-4 mb-8">
|
||||
<div>
|
||||
<h2 className="text-3xl font-bold tracking-tight">Resource Dispatch Center</h2>
|
||||
<p className="text-muted-foreground">Emergency response coordination and unit management</p>
|
||||
</div>
|
||||
<div className="flex flex-col sm:flex-row items-start sm:items-center gap-4">
|
||||
<div className="flex items-center gap-2">
|
||||
<Badge variant="outline" className="bg-green-100 text-green-800 hover:bg-green-100">
|
||||
Active Units: 18/24
|
||||
</Badge>
|
||||
<Badge variant="outline" className="bg-red-100 text-red-800 hover:bg-red-100 flex items-center">
|
||||
<AlertTriangle className="h-3 w-3 mr-1" />
|
||||
High Call Volume
|
||||
</Badge>
|
||||
</div>
|
||||
<Button className="flex items-center">
|
||||
<Phone className="h-4 w-4 mr-2" />
|
||||
New Dispatch
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
|
@ -0,0 +1,110 @@
|
|||
import { Badge } from "@/app/_components/ui/badge"
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/app/_components/ui/select"
|
||||
|
||||
export default function DispatchMap() {
|
||||
return (
|
||||
<div className="mt-4 h-full">
|
||||
<div className="flex justify-between items-center mb-4">
|
||||
<div className="flex gap-2">
|
||||
<Badge variant="outline" className="bg-blue-100 text-blue-800">
|
||||
Available Units
|
||||
</Badge>
|
||||
<Badge variant="outline" className="bg-green-100 text-green-800">
|
||||
On Scene
|
||||
</Badge>
|
||||
<Badge variant="outline" className="bg-yellow-100 text-yellow-800">
|
||||
Responding
|
||||
</Badge>
|
||||
<Badge variant="outline" className="bg-red-100 text-red-800">
|
||||
Incidents
|
||||
</Badge>
|
||||
</div>
|
||||
|
||||
<Select defaultValue="all">
|
||||
<SelectTrigger className="w-[180px]">
|
||||
<SelectValue placeholder="Filter Units" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="all">All Units</SelectItem>
|
||||
<SelectItem value="patrol">Patrol Units</SelectItem>
|
||||
<SelectItem value="traffic">Traffic Units</SelectItem>
|
||||
<SelectItem value="k9">K-9 Units</SelectItem>
|
||||
<SelectItem value="medical">Medical Units</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
<div className="h-[400px] rounded-md bg-slate-100 border flex items-center justify-center relative overflow-hidden">
|
||||
<div className="absolute inset-0 opacity-50 bg-[url('/placeholder.svg?height=400&width=600')] bg-center bg-cover"></div>
|
||||
|
||||
<div className="absolute top-2 right-2 bg-background/80 backdrop-blur-sm p-2 rounded-md text-xs font-medium">
|
||||
<div className="flex items-center gap-1">
|
||||
<span className="w-2 h-2 rounded-full bg-blue-500"></span> Available Units: 12
|
||||
</div>
|
||||
<div className="flex items-center gap-1">
|
||||
<span className="w-2 h-2 rounded-full bg-yellow-500"></span> Responding: 6
|
||||
</div>
|
||||
<div className="flex items-center gap-1">
|
||||
<span className="w-2 h-2 rounded-full bg-green-500"></span> On Scene: 6
|
||||
</div>
|
||||
<div className="flex items-center gap-1">
|
||||
<span className="w-2 h-2 rounded-full bg-red-500"></span> Active Incidents: 4
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="z-10 bg-background/80 backdrop-blur-sm p-3 rounded-lg">
|
||||
<span className="font-medium">Real-Time Dispatch Map</span>
|
||||
<div className="text-xs text-muted-foreground mt-1">Last updated: Just now</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mt-4 grid grid-cols-2 gap-2">
|
||||
<div className="border rounded-lg p-3">
|
||||
<h4 className="text-sm font-medium">Coverage Analysis</h4>
|
||||
<div className="mt-2 space-y-2">
|
||||
<div className="flex justify-between items-center">
|
||||
<span className="text-xs">Downtown District</span>
|
||||
<Badge variant="outline" className="bg-green-100 text-green-800">
|
||||
Good
|
||||
</Badge>
|
||||
</div>
|
||||
<div className="flex justify-between items-center">
|
||||
<span className="text-xs">West Side</span>
|
||||
<Badge variant="outline" className="bg-yellow-100 text-yellow-800">
|
||||
Limited
|
||||
</Badge>
|
||||
</div>
|
||||
<div className="flex justify-between items-center">
|
||||
<span className="text-xs">North Area</span>
|
||||
<Badge variant="outline" className="bg-red-100 text-red-800">
|
||||
Understaffed
|
||||
</Badge>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="border rounded-lg p-3">
|
||||
<h4 className="text-sm font-medium">Response Zones</h4>
|
||||
<div className="mt-2 space-y-2">
|
||||
<div className="flex justify-between items-center">
|
||||
<span className="text-xs">Zone 1 (Downtown)</span>
|
||||
<span className="text-xs">4 units</span>
|
||||
</div>
|
||||
<div className="flex justify-between items-center">
|
||||
<span className="text-xs">Zone 2 (West)</span>
|
||||
<span className="text-xs">2 units</span>
|
||||
</div>
|
||||
<div className="flex justify-between items-center">
|
||||
<span className="text-xs">Zone 3 (North)</span>
|
||||
<span className="text-xs">1 unit</span>
|
||||
</div>
|
||||
<div className="flex justify-between items-center">
|
||||
<span className="text-xs">Zone 4 (East)</span>
|
||||
<span className="text-xs">3 units</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
|
@ -0,0 +1,73 @@
|
|||
import { Badge } from "@/app/_components/ui/badge"
|
||||
import { Clock, FileText } from "lucide-react"
|
||||
|
||||
export default function IncidentHistory() {
|
||||
const closedIncidents = [
|
||||
{
|
||||
id: "INC-4520",
|
||||
type: "Traffic Stop",
|
||||
location: "Highway 101, Mile 35",
|
||||
resolution: "Citation Issued",
|
||||
timeReceived: "9:15 AM",
|
||||
timeClosed: "9:45 AM",
|
||||
units: ["Unit 22"],
|
||||
},
|
||||
{
|
||||
id: "INC-4521",
|
||||
type: "Alarm Activation",
|
||||
location: "First National Bank",
|
||||
resolution: "False Alarm",
|
||||
timeReceived: "9:22 AM",
|
||||
timeClosed: "9:40 AM",
|
||||
units: ["Unit 7", "Unit 10"],
|
||||
},
|
||||
{
|
||||
id: "INC-4522",
|
||||
type: "Welfare Check",
|
||||
location: "234 Cedar Lane",
|
||||
resolution: "Assistance Provided",
|
||||
timeReceived: "9:30 AM",
|
||||
timeClosed: "9:55 AM",
|
||||
units: ["Unit 15"],
|
||||
},
|
||||
]
|
||||
|
||||
return (
|
||||
<div className="mt-4 space-y-3">
|
||||
<div className="flex justify-between items-center">
|
||||
<div className="text-sm font-medium">Recently Closed</div>
|
||||
<button className="text-xs text-primary">View All</button>
|
||||
</div>
|
||||
|
||||
{closedIncidents.map((incident) => (
|
||||
<div key={incident.id} className="border rounded-lg p-2">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="w-6 h-6 rounded bg-muted flex items-center justify-center shrink-0">
|
||||
<FileText className="h-3 w-3" />
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div className="flex items-center gap-1">
|
||||
<span className="font-medium text-xs">{incident.type}</span>
|
||||
<span className="text-xs text-muted-foreground">#{incident.id}</span>
|
||||
</div>
|
||||
<div className="text-xs text-muted-foreground">{incident.location}</div>
|
||||
</div>
|
||||
|
||||
<Badge variant="outline" className="bg-green-100 text-green-800 ml-auto">
|
||||
{incident.resolution}
|
||||
</Badge>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-between mt-2 text-xs text-muted-foreground">
|
||||
<div className="flex items-center">
|
||||
<Clock className="h-3 w-3 mr-1" />
|
||||
{incident.timeReceived} - {incident.timeClosed}
|
||||
</div>
|
||||
<div>Units: {incident.units.join(", ")}</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
}
|
|
@ -0,0 +1,86 @@
|
|||
import { Badge } from "@/app/_components/ui/badge"
|
||||
import { Button } from "@/app/_components/ui/button"
|
||||
import { MapPin, Clock, ArrowRight } from "lucide-react"
|
||||
|
||||
export default function PriorityQueue() {
|
||||
const queuedCalls = [
|
||||
{
|
||||
id: "INC-4527",
|
||||
type: "Noise Complaint",
|
||||
location: "567 Elm St, Apt 12",
|
||||
priority: "Low",
|
||||
timeReceived: "10:28 AM",
|
||||
waitTime: "5m 12s",
|
||||
},
|
||||
{
|
||||
id: "INC-4528",
|
||||
type: "Suspicious Person",
|
||||
location: "Main Street Park",
|
||||
priority: "Medium",
|
||||
timeReceived: "10:25 AM",
|
||||
waitTime: "8m 35s",
|
||||
},
|
||||
{
|
||||
id: "INC-4529",
|
||||
type: "Shoplifting",
|
||||
location: "Downtown Mall, Store #5",
|
||||
priority: "Medium",
|
||||
timeReceived: "10:20 AM",
|
||||
waitTime: "13m 40s",
|
||||
},
|
||||
]
|
||||
|
||||
return (
|
||||
<div className="mt-4 space-y-3">
|
||||
<div className="flex justify-between items-center">
|
||||
<div className="text-sm font-medium">Pending Calls: {queuedCalls.length}</div>
|
||||
<Badge variant="outline" className="bg-yellow-100 text-yellow-800">
|
||||
Avg Wait: 9m 15s
|
||||
</Badge>
|
||||
</div>
|
||||
|
||||
{queuedCalls.map((call) => (
|
||||
<div key={call.id} className="border rounded-lg p-3">
|
||||
<div className="flex items-center gap-2">
|
||||
<div
|
||||
className={`w-2 h-2 rounded-full ${
|
||||
call.priority === "Low" ? "bg-blue-500" : call.priority === "Medium" ? "bg-yellow-500" : "bg-red-500"
|
||||
}`}
|
||||
></div>
|
||||
<h4 className="font-medium text-sm">{call.type}</h4>
|
||||
<Badge
|
||||
variant="outline"
|
||||
className={
|
||||
call.priority === "Low"
|
||||
? "bg-blue-100 text-blue-800 ml-auto"
|
||||
: call.priority === "Medium"
|
||||
? "bg-yellow-100 text-yellow-800 ml-auto"
|
||||
: "bg-red-100 text-red-800 ml-auto"
|
||||
}
|
||||
>
|
||||
{call.priority}
|
||||
</Badge>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-wrap gap-x-4 gap-y-1 mt-2 text-xs text-muted-foreground">
|
||||
<div className="flex items-center">
|
||||
<MapPin className="h-3 w-3 mr-1" />
|
||||
{call.location}
|
||||
</div>
|
||||
<div className="flex items-center">
|
||||
<Clock className="h-3 w-3 mr-1" />
|
||||
Waiting: {call.waitTime}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-end mt-2">
|
||||
<Button size="sm" className="flex items-center">
|
||||
Dispatch
|
||||
<ArrowRight className="ml-1 h-3 w-3" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
}
|
|
@ -0,0 +1,75 @@
|
|||
import { Badge } from "@/app/_components/ui/badge"
|
||||
import { Progress } from "@/app/_components/ui/progress"
|
||||
import { Car, Truck, Ambulance, Shield } from "lucide-react"
|
||||
|
||||
export default function ResourceAvailability() {
|
||||
const resources = [
|
||||
{
|
||||
type: "Patrol Cars",
|
||||
total: 24,
|
||||
available: 12,
|
||||
icon: Car,
|
||||
},
|
||||
{
|
||||
type: "K-9 Units",
|
||||
total: 4,
|
||||
available: 2,
|
||||
icon: Shield,
|
||||
},
|
||||
{
|
||||
type: "Ambulances",
|
||||
total: 8,
|
||||
available: 5,
|
||||
icon: Ambulance,
|
||||
},
|
||||
{
|
||||
type: "SWAT Team",
|
||||
total: 1,
|
||||
available: 1,
|
||||
icon: Truck,
|
||||
},
|
||||
]
|
||||
|
||||
return (
|
||||
<div className="mt-4 space-y-3">
|
||||
{resources.map((resource) => (
|
||||
<div key={resource.type} className="border rounded-lg p-3">
|
||||
<div className="flex items-center gap-2 mb-2">
|
||||
<div className="w-8 h-8 rounded bg-muted flex items-center justify-center shrink-0">
|
||||
<resource.icon className="h-4 w-4" />
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h4 className="font-medium text-sm">{resource.type}</h4>
|
||||
</div>
|
||||
|
||||
<Badge
|
||||
variant="outline"
|
||||
className={
|
||||
resource.available / resource.total > 0.5
|
||||
? "bg-green-100 text-green-800 ml-auto"
|
||||
: resource.available / resource.total > 0.25
|
||||
? "bg-yellow-100 text-yellow-800 ml-auto"
|
||||
: "bg-red-100 text-red-800 ml-auto"
|
||||
}
|
||||
>
|
||||
{resource.available}/{resource.total} Available
|
||||
</Badge>
|
||||
</div>
|
||||
|
||||
<Progress
|
||||
value={(resource.available / resource.total) * 100}
|
||||
className="h-2"
|
||||
indicatorClassName={
|
||||
resource.available / resource.total > 0.5
|
||||
? "bg-green-500"
|
||||
: resource.available / resource.total > 0.25
|
||||
? "bg-yellow-500"
|
||||
: "bg-red-500"
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
}
|
|
@ -0,0 +1,97 @@
|
|||
"use client"
|
||||
|
||||
import { Progress } from "@/app/_components/ui/progress"
|
||||
import { Clock } from "lucide-react"
|
||||
import { ChartContainer, ChartTooltip, ChartTooltipContent } from "@/app/_components/ui/chart"
|
||||
import { Bar, BarChart, XAxis, YAxis, CartesianGrid, ResponsiveContainer, Tooltip } from "recharts"
|
||||
|
||||
export default function ResponseTimes() {
|
||||
// Sample data for the chart
|
||||
const data = [
|
||||
{ name: "Critical", time: 3.2 },
|
||||
{ name: "High", time: 5.8 },
|
||||
{ name: "Medium", time: 8.5 },
|
||||
{ name: "Low", time: 12.3 },
|
||||
]
|
||||
|
||||
return (
|
||||
<div className="mt-4 space-y-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<div className="text-2xl font-bold">4.2m</div>
|
||||
<div className="text-xs text-muted-foreground">Average Response Time</div>
|
||||
</div>
|
||||
<div className="text-xs text-green-600 flex items-center">
|
||||
<Clock className="h-3 w-3 mr-1" />
|
||||
Target: 5.0m
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<div>
|
||||
<div className="flex justify-between text-xs mb-1">
|
||||
<span>Priority 1 (Critical)</span>
|
||||
<div className="flex items-center">
|
||||
<Clock className="h-3 w-3 mr-1" />
|
||||
<span>3.2 min avg</span>
|
||||
</div>
|
||||
</div>
|
||||
<Progress value={80} className="h-2" indicatorClassName="bg-red-500" />
|
||||
</div>
|
||||
<div>
|
||||
<div className="flex justify-between text-xs mb-1">
|
||||
<span>Priority 2 (High)</span>
|
||||
<div className="flex items-center">
|
||||
<Clock className="h-3 w-3 mr-1" />
|
||||
<span>5.8 min avg</span>
|
||||
</div>
|
||||
</div>
|
||||
<Progress value={70} className="h-2" indicatorClassName="bg-orange-500" />
|
||||
</div>
|
||||
<div>
|
||||
<div className="flex justify-between text-xs mb-1">
|
||||
<span>Priority 3 (Medium)</span>
|
||||
<div className="flex items-center">
|
||||
<Clock className="h-3 w-3 mr-1" />
|
||||
<span>8.5 min avg</span>
|
||||
</div>
|
||||
</div>
|
||||
<Progress value={60} className="h-2" indicatorClassName="bg-blue-500" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="h-[100px]">
|
||||
<ChartContainer>
|
||||
<ResponsiveContainer width="100%" height="100%">
|
||||
<BarChart data={data} margin={{ top: 5, right: 5, left: 0, bottom: 5 }}>
|
||||
<CartesianGrid strokeDasharray="3 3" className="stroke-muted" />
|
||||
<XAxis dataKey="name" className="text-xs" />
|
||||
<YAxis className="text-xs" />
|
||||
<Tooltip content={<CustomTooltip />} />
|
||||
<Bar dataKey="time" fill="#3b82f6" radius={[4, 4, 0, 0]} />
|
||||
</BarChart>
|
||||
</ResponsiveContainer>
|
||||
</ChartContainer>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function CustomTooltip({ active, payload, label }: any) {
|
||||
if (active && payload && payload.length) {
|
||||
return (
|
||||
<ChartTooltip content={
|
||||
<ChartTooltipContent>
|
||||
<div className="font-medium">{label} Priority</div>
|
||||
<div className="flex items-center gap-2 text-sm">
|
||||
<span>Avg. Response Time:</span>
|
||||
<span className="font-medium">{payload[0].value} minutes</span>
|
||||
</div>
|
||||
</ChartTooltipContent>
|
||||
}>
|
||||
</ChartTooltip>
|
||||
)
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
|
@ -0,0 +1,80 @@
|
|||
import { Badge } from "@/app/_components/ui/badge"
|
||||
import { Calendar, Clock } from "lucide-react"
|
||||
|
||||
export default function ShiftSchedule() {
|
||||
const currentShift = {
|
||||
name: "Day Shift",
|
||||
hours: "7:00 AM - 3:00 PM",
|
||||
supervisor: "Sgt. Parker",
|
||||
officers: 18,
|
||||
status: "Active",
|
||||
}
|
||||
|
||||
const upcomingShift = {
|
||||
name: "Evening Shift",
|
||||
hours: "3:00 PM - 11:00 PM",
|
||||
supervisor: "Sgt. Rodriguez",
|
||||
officers: 16,
|
||||
status: "Upcoming",
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="mt-4 space-y-3">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center">
|
||||
<Calendar className="h-4 w-4 mr-2" />
|
||||
<span className="font-medium text-sm">Current Schedule</span>
|
||||
</div>
|
||||
<div className="text-xs text-muted-foreground">April 24, 2023</div>
|
||||
</div>
|
||||
|
||||
<div className="border rounded-lg p-3">
|
||||
<div className="flex justify-between items-center">
|
||||
<h4 className="font-medium">{currentShift.name}</h4>
|
||||
<Badge variant="outline" className="bg-green-100 text-green-800">
|
||||
{currentShift.status}
|
||||
</Badge>
|
||||
</div>
|
||||
|
||||
<div className="mt-2 space-y-1 text-sm">
|
||||
<div className="flex items-center">
|
||||
<Clock className="h-3 w-3 mr-1 text-muted-foreground" />
|
||||
<span>{currentShift.hours}</span>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<span className="text-muted-foreground">Supervisor:</span>
|
||||
<span>{currentShift.supervisor}</span>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<span className="text-muted-foreground">Officers on duty:</span>
|
||||
<span>{currentShift.officers}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="border rounded-lg p-3">
|
||||
<div className="flex justify-between items-center">
|
||||
<h4 className="font-medium">{upcomingShift.name}</h4>
|
||||
<Badge variant="outline" className="bg-blue-100 text-blue-800">
|
||||
{upcomingShift.status}
|
||||
</Badge>
|
||||
</div>
|
||||
|
||||
<div className="mt-2 space-y-1 text-sm">
|
||||
<div className="flex items-center">
|
||||
<Clock className="h-3 w-3 mr-1 text-muted-foreground" />
|
||||
<span>{upcomingShift.hours}</span>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<span className="text-muted-foreground">Supervisor:</span>
|
||||
<span>{upcomingShift.supervisor}</span>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<span className="text-muted-foreground">Officers scheduled:</span>
|
||||
<span>{upcomingShift.officers}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
|
@ -0,0 +1,65 @@
|
|||
import { Badge } from "@/app/_components/ui/badge"
|
||||
import { Car, User } from "lucide-react"
|
||||
|
||||
export default function UnitStatus() {
|
||||
const units = [
|
||||
{ id: "Unit 5", officer: "Parker", status: "On Scene", location: "789 Pine St" },
|
||||
{ id: "Unit 8", officer: "Rodriguez", status: "On Scene", location: "I-95, Mile 42" },
|
||||
{ id: "Unit 9", officer: "Johnson", status: "On Scene", location: "789 Pine St" },
|
||||
{ id: "Unit 12", officer: "Chen", status: "Responding", location: "123 Main St" },
|
||||
{ id: "Unit 15", officer: "Wilson", status: "Responding", location: "123 Main St" },
|
||||
{ id: "Unit 17", officer: "Thompson", status: "Responding", location: "456 Oak Ave" },
|
||||
]
|
||||
|
||||
return (
|
||||
<div className="mt-4 space-y-3">
|
||||
<div className="grid grid-cols-2 gap-2 text-center">
|
||||
<div className="border rounded-lg p-2">
|
||||
<div className="text-2xl font-bold">12</div>
|
||||
<div className="text-xs text-muted-foreground">Available</div>
|
||||
</div>
|
||||
<div className="border rounded-lg p-2">
|
||||
<div className="text-2xl font-bold">12</div>
|
||||
<div className="text-xs text-muted-foreground">Assigned</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
{units.map((unit) => (
|
||||
<div key={unit.id} className="flex items-center gap-2 p-2 border rounded-lg">
|
||||
<div className="w-8 h-8 rounded-full bg-muted flex items-center justify-center shrink-0">
|
||||
<Car className="h-4 w-4" />
|
||||
</div>
|
||||
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="flex items-center gap-2">
|
||||
<h4 className="font-medium text-sm">{unit.id}</h4>
|
||||
<div className="text-xs text-muted-foreground flex items-center">
|
||||
<User className="h-3 w-3 mr-1" />
|
||||
{unit.officer}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="text-xs text-muted-foreground truncate">{unit.location}</div>
|
||||
</div>
|
||||
|
||||
<Badge
|
||||
variant="outline"
|
||||
className={
|
||||
unit.status === "On Scene"
|
||||
? "bg-green-100 text-green-800"
|
||||
: unit.status === "Responding"
|
||||
? "bg-yellow-100 text-yellow-800"
|
||||
: "bg-blue-100 text-blue-800"
|
||||
}
|
||||
>
|
||||
{unit.status}
|
||||
</Badge>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<button className="w-full text-sm text-primary mt-2 py-1">View All Units</button>
|
||||
</div>
|
||||
)
|
||||
}
|
|
@ -0,0 +1,101 @@
|
|||
"use client"
|
||||
|
||||
import { BentoGrid, BentoGridItem } from "@/app/_components/ui/bento-grid"
|
||||
import { MapPin, Phone, Users, Car, Clock, AlertTriangle, Radio, Calendar, MessageSquare } from "lucide-react"
|
||||
import DispatchHeader from "./_components/dispatch-header"
|
||||
import ActiveIncidents from "./_components/active-incidents"
|
||||
import DispatchMap from "./_components/dispatch-map"
|
||||
import UnitStatus from "./_components/unit-status"
|
||||
import ResponseTimes from "./_components/response-times"
|
||||
import PriorityQueue from "./_components/priority-queue"
|
||||
import ResourceAvailability from "./_components/resource-availability"
|
||||
import ShiftSchedule from "./_components/shift-schedule"
|
||||
import DispatchCommunications from "./_components/dispatch-communications"
|
||||
import IncidentHistory from "./_components/incident-history"
|
||||
|
||||
export default function ResourceDispatchPage() {
|
||||
return (
|
||||
<div className="container py-4 min-h-screen">
|
||||
<div className="max-w-7xl mx-auto">
|
||||
<DispatchHeader />
|
||||
|
||||
<BentoGrid>
|
||||
<BentoGridItem
|
||||
title="Active Incidents"
|
||||
description="Currently responding calls"
|
||||
icon={<AlertTriangle className="w-5 h-5" />}
|
||||
colSpan="2"
|
||||
>
|
||||
<ActiveIncidents />
|
||||
</BentoGridItem>
|
||||
|
||||
<BentoGridItem
|
||||
title="Dispatch Map"
|
||||
description="Real-time unit locations"
|
||||
icon={<MapPin className="w-5 h-5" />}
|
||||
rowSpan="2"
|
||||
colSpan="2"
|
||||
>
|
||||
<DispatchMap />
|
||||
</BentoGridItem>
|
||||
|
||||
<BentoGridItem
|
||||
title="Unit Status"
|
||||
description="Available and assigned units"
|
||||
icon={<Users className="w-5 h-5" />}
|
||||
>
|
||||
<UnitStatus />
|
||||
</BentoGridItem>
|
||||
|
||||
<BentoGridItem
|
||||
title="Response Times"
|
||||
description="Current performance metrics"
|
||||
icon={<Clock className="w-5 h-5" />}
|
||||
>
|
||||
<ResponseTimes />
|
||||
</BentoGridItem>
|
||||
|
||||
<BentoGridItem
|
||||
title="Priority Queue"
|
||||
description="Pending dispatch requests"
|
||||
icon={<Phone className="w-5 h-5" />}
|
||||
>
|
||||
<PriorityQueue />
|
||||
</BentoGridItem>
|
||||
|
||||
<BentoGridItem
|
||||
title="Resource Availability"
|
||||
description="Vehicles and specialized equipment"
|
||||
icon={<Car className="w-5 h-5" />}
|
||||
>
|
||||
<ResourceAvailability />
|
||||
</BentoGridItem>
|
||||
|
||||
<BentoGridItem
|
||||
title="Shift Schedule"
|
||||
description="Current and upcoming shifts"
|
||||
icon={<Calendar className="w-5 h-5" />}
|
||||
>
|
||||
<ShiftSchedule />
|
||||
</BentoGridItem>
|
||||
|
||||
<BentoGridItem
|
||||
title="Dispatch Communications"
|
||||
description="Recent radio traffic and messages"
|
||||
icon={<Radio className="w-5 h-5" />}
|
||||
>
|
||||
<DispatchCommunications />
|
||||
</BentoGridItem>
|
||||
|
||||
<BentoGridItem
|
||||
title="Incident History"
|
||||
description="Recently closed calls"
|
||||
icon={<MessageSquare className="w-5 h-5" />}
|
||||
>
|
||||
<IncidentHistory />
|
||||
</BentoGridItem>
|
||||
</BentoGrid>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
|
@ -10,89 +10,102 @@ import { createClient } from "@/app/_utils/supabase/server"
|
|||
import db from "@/prisma/db";
|
||||
|
||||
export async function signInPasswordless(formData: FormData) {
|
||||
const instrumentationService = getInjection("IInstrumentationService")
|
||||
return await instrumentationService.instrumentServerAction("signIn", {
|
||||
recordResponse: true
|
||||
const instrumentationService = getInjection('IInstrumentationService');
|
||||
return await instrumentationService.instrumentServerAction(
|
||||
'signIn',
|
||||
{
|
||||
recordResponse: true,
|
||||
},
|
||||
async () => {
|
||||
async () => {
|
||||
try {
|
||||
const email = formData.get('email')?.toString();
|
||||
|
||||
try {
|
||||
const email = formData.get("email")?.toString()
|
||||
const signInPasswordlessController = getInjection(
|
||||
'ISignInPasswordlessController'
|
||||
);
|
||||
return await signInPasswordlessController({ email });
|
||||
|
||||
const signInPasswordlessController = getInjection("ISignInPasswordlessController")
|
||||
return await signInPasswordlessController({ email })
|
||||
// if (email) {
|
||||
// redirect(`/verify-otp?email=${encodeURIComponent(email)}`)
|
||||
// }
|
||||
} catch (err) {
|
||||
if (err instanceof InputParseError) {
|
||||
return { error: err.message };
|
||||
}
|
||||
|
||||
// if (email) {
|
||||
// redirect(`/verify-otp?email=${encodeURIComponent(email)}`)
|
||||
// }
|
||||
if (err instanceof AuthenticationError) {
|
||||
return { error: 'Invalid credential. Please try again.' };
|
||||
}
|
||||
|
||||
} catch (err) {
|
||||
if (err instanceof InputParseError) {
|
||||
return { error: err.message }
|
||||
}
|
||||
if (
|
||||
err instanceof UnauthenticatedError ||
|
||||
err instanceof NotFoundError
|
||||
) {
|
||||
return {
|
||||
error: err.message,
|
||||
};
|
||||
}
|
||||
|
||||
if (err instanceof AuthenticationError) {
|
||||
return { error: "Invalid credential. Please try again." }
|
||||
}
|
||||
const crashReporterService = getInjection('ICrashReporterService');
|
||||
crashReporterService.report(err);
|
||||
|
||||
if (err instanceof UnauthenticatedError || err instanceof NotFoundError) {
|
||||
return {
|
||||
error: 'User not found. Please tell your admin to create an account for you.',
|
||||
};
|
||||
}
|
||||
|
||||
const crashReporterService = getInjection('ICrashReporterService');
|
||||
crashReporterService.report(err);
|
||||
|
||||
return {
|
||||
error:
|
||||
'An error happened. The developers have been notified. Please try again later.',
|
||||
};
|
||||
}
|
||||
|
||||
})
|
||||
return {
|
||||
error:
|
||||
'An error happened. The developers have been notified. Please try again later.',
|
||||
};
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
export async function signInWithPassword(formData: FormData) {
|
||||
const instrumentationService = getInjection("IInstrumentationService")
|
||||
return await instrumentationService.instrumentServerAction("signInWithPassword", {
|
||||
recordResponse: true
|
||||
}, async () => {
|
||||
try {
|
||||
const email = formData.get("email")?.toString()
|
||||
const password = formData.get("password")?.toString()
|
||||
const instrumentationService = getInjection('IInstrumentationService');
|
||||
return await instrumentationService.instrumentServerAction(
|
||||
'signInWithPassword',
|
||||
{
|
||||
recordResponse: true,
|
||||
},
|
||||
async () => {
|
||||
try {
|
||||
const email = formData.get('email')?.toString();
|
||||
const password = formData.get('password')?.toString();
|
||||
|
||||
console.log("woi:", email + " " + password)
|
||||
// console.log("woi:", email + " " + password)
|
||||
|
||||
const signInWithPasswordController = getInjection("ISignInWithPasswordController")
|
||||
await signInWithPasswordController({ email, password })
|
||||
const signInWithPasswordController = getInjection(
|
||||
'ISignInWithPasswordController'
|
||||
);
|
||||
await signInWithPasswordController({ email, password });
|
||||
|
||||
return { success: true }
|
||||
} catch (err) {
|
||||
if (err instanceof InputParseError) {
|
||||
return { error: err.message }
|
||||
}
|
||||
|
||||
if (err instanceof AuthenticationError) {
|
||||
return { error: "Invalid credential. Please try again." }
|
||||
}
|
||||
|
||||
if (err instanceof UnauthenticatedError || err instanceof NotFoundError) {
|
||||
return {
|
||||
error: 'User not found. Please tell your admin to create an account for you.',
|
||||
};
|
||||
}
|
||||
|
||||
const crashReporterService = getInjection('ICrashReporterService');
|
||||
crashReporterService.report(err);
|
||||
|
||||
return {
|
||||
error:
|
||||
'An error happened. The developers have been notified. Please try again later.',
|
||||
};
|
||||
return { success: true };
|
||||
} catch (err) {
|
||||
if (err instanceof InputParseError) {
|
||||
return { error: err.message };
|
||||
}
|
||||
})
|
||||
|
||||
if (err instanceof AuthenticationError) {
|
||||
return { error: 'Invalid credential. Please try again.' };
|
||||
}
|
||||
|
||||
if (
|
||||
err instanceof UnauthenticatedError ||
|
||||
err instanceof NotFoundError
|
||||
) {
|
||||
return {
|
||||
error: err.message,
|
||||
};
|
||||
}
|
||||
|
||||
const crashReporterService = getInjection('ICrashReporterService');
|
||||
crashReporterService.report(err);
|
||||
|
||||
return {
|
||||
error:
|
||||
'An error happened. The developers have been notified. Please try again later.',
|
||||
};
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
// export async function signUp(formData: FormData) {
|
||||
// const instrumentationService = getInjection("IInstrumentationService")
|
||||
|
|
|
@ -0,0 +1,364 @@
|
|||
"use client"
|
||||
|
||||
import { cn } from "@/app/_lib/utils"
|
||||
import * as React from "react"
|
||||
import * as RechartsPrimitive from "recharts"
|
||||
|
||||
// Format: { THEME_NAME: CSS_SELECTOR }
|
||||
const THEMES = { light: "", dark: ".dark" } as const
|
||||
|
||||
export type ChartConfig = {
|
||||
[k in string]: {
|
||||
label?: React.ReactNode
|
||||
icon?: React.ComponentType
|
||||
} & (
|
||||
| { color?: string; theme?: never }
|
||||
| { color?: never; theme: Record<keyof typeof THEMES, string> }
|
||||
)
|
||||
}
|
||||
|
||||
type ChartContextProps = {
|
||||
config: ChartConfig
|
||||
}
|
||||
|
||||
const ChartContext = React.createContext<ChartContextProps | null>(null)
|
||||
|
||||
function useChart() {
|
||||
const context = React.useContext(ChartContext)
|
||||
|
||||
if (!context) {
|
||||
throw new Error("useChart must be used within a <ChartContainer />")
|
||||
}
|
||||
|
||||
return context
|
||||
}
|
||||
|
||||
const ChartContainer = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
React.ComponentProps<"div"> & {
|
||||
config?: ChartConfig // Jadikan opsional
|
||||
children: React.ComponentProps<
|
||||
typeof RechartsPrimitive.ResponsiveContainer
|
||||
>["children"]
|
||||
}
|
||||
>(({ id, className, children, config = {}, ...props }, ref) => { // Nilai default adalah objek kosong
|
||||
const uniqueId = React.useId()
|
||||
const chartId = `chart-${id || uniqueId.replace(/:/g, "")}`
|
||||
|
||||
return (
|
||||
<ChartContext.Provider value={{ config }}>
|
||||
<div
|
||||
data-chart={chartId}
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"flex aspect-video justify-center text-xs [&_.recharts-cartesian-axis-tick_text]:fill-muted-foreground [&_.recharts-cartesian-grid_line[stroke='#ccc']]:stroke-border/50 [&_.recharts-curve.recharts-tooltip-cursor]:stroke-border [&_.recharts-dot[stroke='#fff']]:stroke-transparent [&_.recharts-layer]:outline-none [&_.recharts-polar-grid_[stroke='#ccc']]:stroke-border [&_.recharts-radial-bar-background-sector]:fill-muted [&_.recharts-rectangle.recharts-tooltip-cursor]:fill-muted [&_.recharts-reference-line_[stroke='#ccc']]:stroke-border [&_.recharts-sector[stroke='#fff']]:stroke-transparent [&_.recharts-sector]:outline-none [&_.recharts-surface]:outline-none",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<ChartStyle id={chartId} config={config} />
|
||||
<RechartsPrimitive.ResponsiveContainer>
|
||||
{children}
|
||||
</RechartsPrimitive.ResponsiveContainer>
|
||||
</div>
|
||||
</ChartContext.Provider>
|
||||
)
|
||||
})
|
||||
ChartContainer.displayName = "Chart"
|
||||
|
||||
const ChartStyle = ({ id, config }: { id: string; config: ChartConfig }) => {
|
||||
const colorConfig = Object.entries(config).filter(
|
||||
([, config]) => config.theme || config.color
|
||||
)
|
||||
|
||||
if (!colorConfig.length) {
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
<style
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: Object.entries(THEMES)
|
||||
.map(
|
||||
([theme, prefix]) => `
|
||||
${prefix} [data-chart=${id}] {
|
||||
${colorConfig
|
||||
.map(([key, itemConfig]) => {
|
||||
const color =
|
||||
itemConfig.theme?.[theme as keyof typeof itemConfig.theme] ||
|
||||
itemConfig.color
|
||||
return color ? ` --color-${key}: ${color};` : null
|
||||
})
|
||||
.join("\n")}
|
||||
}
|
||||
`
|
||||
)
|
||||
.join("\n"),
|
||||
}}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
const ChartTooltip = RechartsPrimitive.Tooltip
|
||||
|
||||
const ChartTooltipContent = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
React.ComponentProps<typeof RechartsPrimitive.Tooltip> &
|
||||
React.ComponentProps<"div"> & {
|
||||
hideLabel?: boolean
|
||||
hideIndicator?: boolean
|
||||
indicator?: "line" | "dot" | "dashed"
|
||||
nameKey?: string
|
||||
labelKey?: string
|
||||
}
|
||||
>(
|
||||
(
|
||||
{
|
||||
active,
|
||||
payload,
|
||||
className,
|
||||
indicator = "dot",
|
||||
hideLabel = false,
|
||||
hideIndicator = false,
|
||||
label,
|
||||
labelFormatter,
|
||||
labelClassName,
|
||||
formatter,
|
||||
color,
|
||||
nameKey,
|
||||
labelKey,
|
||||
},
|
||||
ref
|
||||
) => {
|
||||
const { config } = useChart()
|
||||
|
||||
const tooltipLabel = React.useMemo(() => {
|
||||
if (hideLabel || !payload?.length) {
|
||||
return null
|
||||
}
|
||||
|
||||
const [item] = payload
|
||||
const key = `${labelKey || item?.dataKey || item?.name || "value"}`
|
||||
const itemConfig = getPayloadConfigFromPayload(config, item, key)
|
||||
const value =
|
||||
!labelKey && typeof label === "string"
|
||||
? config[label as keyof typeof config]?.label || label
|
||||
: itemConfig?.label
|
||||
|
||||
if (labelFormatter) {
|
||||
return (
|
||||
<div className={cn("font-medium", labelClassName)}>
|
||||
{labelFormatter(value, payload)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
if (!value) {
|
||||
return null
|
||||
}
|
||||
|
||||
return <div className={cn("font-medium", labelClassName)}>{value}</div>
|
||||
}, [
|
||||
label,
|
||||
labelFormatter,
|
||||
payload,
|
||||
hideLabel,
|
||||
labelClassName,
|
||||
config,
|
||||
labelKey,
|
||||
])
|
||||
|
||||
if (!active || !payload?.length) {
|
||||
return null
|
||||
}
|
||||
|
||||
const nestLabel = payload.length === 1 && indicator !== "dot"
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"grid min-w-[8rem] items-start gap-1.5 rounded-lg border border-border/50 bg-background px-2.5 py-1.5 text-xs shadow-xl",
|
||||
className
|
||||
)}
|
||||
>
|
||||
{!nestLabel ? tooltipLabel : null}
|
||||
<div className="grid gap-1.5">
|
||||
{payload.map((item, index) => {
|
||||
const key = `${nameKey || item.name || item.dataKey || "value"}`
|
||||
const itemConfig = getPayloadConfigFromPayload(config, item, key)
|
||||
const indicatorColor = color || item.payload.fill || item.color
|
||||
|
||||
return (
|
||||
<div
|
||||
key={item.dataKey}
|
||||
className={cn(
|
||||
"flex w-full flex-wrap items-stretch gap-2 [&>svg]:h-2.5 [&>svg]:w-2.5 [&>svg]:text-muted-foreground",
|
||||
indicator === "dot" && "items-center"
|
||||
)}
|
||||
>
|
||||
{formatter && item?.value !== undefined && item.name ? (
|
||||
formatter(item.value, item.name, item, index, item.payload)
|
||||
) : (
|
||||
<>
|
||||
{itemConfig?.icon ? (
|
||||
<itemConfig.icon />
|
||||
) : (
|
||||
!hideIndicator && (
|
||||
<div
|
||||
className={cn(
|
||||
"shrink-0 rounded-[2px] border-[--color-border] bg-[--color-bg]",
|
||||
{
|
||||
"h-2.5 w-2.5": indicator === "dot",
|
||||
"w-1": indicator === "line",
|
||||
"w-0 border-[1.5px] border-dashed bg-transparent":
|
||||
indicator === "dashed",
|
||||
"my-0.5": nestLabel && indicator === "dashed",
|
||||
}
|
||||
)}
|
||||
style={
|
||||
{
|
||||
"--color-bg": indicatorColor,
|
||||
"--color-border": indicatorColor,
|
||||
} as React.CSSProperties
|
||||
}
|
||||
/>
|
||||
)
|
||||
)}
|
||||
<div
|
||||
className={cn(
|
||||
"flex flex-1 justify-between leading-none",
|
||||
nestLabel ? "items-end" : "items-center"
|
||||
)}
|
||||
>
|
||||
<div className="grid gap-1.5">
|
||||
{nestLabel ? tooltipLabel : null}
|
||||
<span className="text-muted-foreground">
|
||||
{itemConfig?.label || item.name}
|
||||
</span>
|
||||
</div>
|
||||
{item.value && (
|
||||
<span className="font-mono font-medium tabular-nums text-foreground">
|
||||
{item.value.toLocaleString()}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
)
|
||||
ChartTooltipContent.displayName = "ChartTooltip"
|
||||
|
||||
const ChartLegend = RechartsPrimitive.Legend
|
||||
|
||||
const ChartLegendContent = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
React.ComponentProps<"div"> &
|
||||
Pick<RechartsPrimitive.LegendProps, "payload" | "verticalAlign"> & {
|
||||
hideIcon?: boolean
|
||||
nameKey?: string
|
||||
}
|
||||
>(
|
||||
(
|
||||
{ className, hideIcon = false, payload, verticalAlign = "bottom", nameKey },
|
||||
ref
|
||||
) => {
|
||||
const { config } = useChart()
|
||||
|
||||
if (!payload?.length) {
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"flex items-center justify-center gap-4",
|
||||
verticalAlign === "top" ? "pb-3" : "pt-3",
|
||||
className
|
||||
)}
|
||||
>
|
||||
{payload.map((item) => {
|
||||
const key = `${nameKey || item.dataKey || "value"}`
|
||||
const itemConfig = getPayloadConfigFromPayload(config, item, key)
|
||||
|
||||
return (
|
||||
<div
|
||||
key={item.value}
|
||||
className={cn(
|
||||
"flex items-center gap-1.5 [&>svg]:h-3 [&>svg]:w-3 [&>svg]:text-muted-foreground"
|
||||
)}
|
||||
>
|
||||
{itemConfig?.icon && !hideIcon ? (
|
||||
<itemConfig.icon />
|
||||
) : (
|
||||
<div
|
||||
className="h-2 w-2 shrink-0 rounded-[2px]"
|
||||
style={{
|
||||
backgroundColor: item.color,
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{itemConfig?.label}
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
)
|
||||
ChartLegendContent.displayName = "ChartLegend"
|
||||
|
||||
// Helper to extract item config from a payload.
|
||||
function getPayloadConfigFromPayload(
|
||||
config: ChartConfig,
|
||||
payload: unknown,
|
||||
key: string
|
||||
) {
|
||||
if (typeof payload !== "object" || payload === null) {
|
||||
return undefined
|
||||
}
|
||||
|
||||
const payloadPayload =
|
||||
"payload" in payload &&
|
||||
typeof payload.payload === "object" &&
|
||||
payload.payload !== null
|
||||
? payload.payload
|
||||
: undefined
|
||||
|
||||
let configLabelKey: string = key
|
||||
|
||||
if (
|
||||
key in payload &&
|
||||
typeof payload[key as keyof typeof payload] === "string"
|
||||
) {
|
||||
configLabelKey = payload[key as keyof typeof payload] as string
|
||||
} else if (
|
||||
payloadPayload &&
|
||||
key in payloadPayload &&
|
||||
typeof payloadPayload[key as keyof typeof payloadPayload] === "string"
|
||||
) {
|
||||
configLabelKey = payloadPayload[
|
||||
key as keyof typeof payloadPayload
|
||||
] as string
|
||||
}
|
||||
|
||||
return configLabelKey in config
|
||||
? config[configLabelKey]
|
||||
: config[key as keyof typeof config]
|
||||
}
|
||||
|
||||
export {
|
||||
ChartContainer,
|
||||
ChartTooltip,
|
||||
ChartTooltipContent,
|
||||
ChartLegend,
|
||||
ChartLegendContent,
|
||||
ChartStyle,
|
||||
}
|
|
@ -90,7 +90,6 @@
|
|||
--tertiary: 0 0% 12%; /* #1F1F1F1 */
|
||||
--tertiary-foreground: 0 0% 85%; /* #d9d9d9 */
|
||||
--tertiary-border: 0 0% 20%; /* #333333 */
|
||||
|
||||
|
||||
/* Muted: abu-abu gelap untuk teks pendukung */
|
||||
--muted: 0 0% 20%; /* #333333 */
|
||||
|
@ -131,10 +130,25 @@
|
|||
}
|
||||
|
||||
@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;
|
||||
}
|
||||
body {
|
||||
@apply bg-background text-foreground;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -10,10 +10,10 @@ import db from '../../prisma/db';
|
|||
import { v4 as uuidv4 } from 'uuid';
|
||||
import * as crypto from 'crypto';
|
||||
|
||||
// Maintain a registry of used IDs to prevent duplicates
|
||||
// Used to track generated IDs
|
||||
const usedIdRegistry = new Set<string>();
|
||||
|
||||
// Type definition for the global counter
|
||||
// Add type definition for global counter
|
||||
declare global {
|
||||
var __idCounter: number;
|
||||
}
|
||||
|
@ -359,6 +359,21 @@ export const createRoute = (
|
|||
return `${baseRoute}?${queryString}`;
|
||||
};
|
||||
|
||||
|
||||
|
||||
// Format date helper function
|
||||
function formatDateV2(date: Date, formatStr: string): string {
|
||||
const pad = (num: number) => String(num).padStart(2, '0');
|
||||
|
||||
return formatStr
|
||||
.replace('yyyy', String(date.getFullYear()))
|
||||
.replace('MM', pad(date.getMonth() + 1))
|
||||
.replace('dd', pad(date.getDate()))
|
||||
.replace('HH', pad(date.getHours()))
|
||||
.replace('mm', pad(date.getMinutes()))
|
||||
.replace('ss', pad(date.getSeconds()));
|
||||
}
|
||||
|
||||
/**
|
||||
* Universal Custom ID Generator
|
||||
* Creates structured, readable IDs for any system or entity
|
||||
|
@ -377,23 +392,19 @@ export const createRoute = (
|
|||
* @param {boolean} options.upperCase - Convert result to uppercase
|
||||
* @returns {string} - Generated custom ID
|
||||
*/
|
||||
|
||||
/**
|
||||
* Generate a unique ID with multiple options to reduce collision risk
|
||||
*/
|
||||
export function generateId(
|
||||
options: {
|
||||
prefix?: string;
|
||||
segments?: {
|
||||
codes?: string[];
|
||||
year?: number;
|
||||
year?: number | boolean; // Year diubah menjadi number | boolean
|
||||
sequentialDigits?: number;
|
||||
includeDate?: boolean;
|
||||
dateFormat?: string;
|
||||
includeTime?: boolean;
|
||||
includeMilliseconds?: boolean;
|
||||
};
|
||||
format?: string;
|
||||
format?: string | null;
|
||||
separator?: string;
|
||||
upperCase?: boolean;
|
||||
randomSequence?: boolean;
|
||||
|
@ -402,13 +413,18 @@ export function generateId(
|
|||
maxRetries?: number;
|
||||
} = {}
|
||||
): string {
|
||||
// Default options
|
||||
// Jika uniquenessStrategy tidak diatur dan randomSequence = false,
|
||||
// gunakan counter sebagai strategi default
|
||||
if (!options.uniquenessStrategy && options.randomSequence === false) {
|
||||
options.uniquenessStrategy = 'counter';
|
||||
}
|
||||
|
||||
const config = {
|
||||
prefix: options.prefix || 'ID',
|
||||
segments: {
|
||||
codes: options.segments?.codes || [],
|
||||
year: options.segments?.year,
|
||||
sequentialDigits: options.segments?.sequentialDigits || 6, // Increased to 6
|
||||
year: options.segments?.year, // Akan diproses secara kondisional nanti
|
||||
sequentialDigits: options.segments?.sequentialDigits || 6,
|
||||
includeDate: options.segments?.includeDate ?? false,
|
||||
dateFormat: options.segments?.dateFormat || 'yyyyMMdd',
|
||||
includeTime: options.segments?.includeTime ?? false,
|
||||
|
@ -423,108 +439,124 @@ export function generateId(
|
|||
maxRetries: options.maxRetries || 10,
|
||||
};
|
||||
|
||||
// Static counter for sequential IDs (module-level)
|
||||
// Initialize global counter if not exists
|
||||
if (typeof globalThis.__idCounter === 'undefined') {
|
||||
globalThis.__idCounter = 0;
|
||||
}
|
||||
|
||||
// Get current date and time with high precision
|
||||
const now = new Date();
|
||||
|
||||
// Format date based on selected format
|
||||
// Generate date string if needed
|
||||
let dateString = '';
|
||||
if (config.segments.includeDate) {
|
||||
dateString = format(now, config.segments.dateFormat);
|
||||
}
|
||||
|
||||
// Format time if included (with higher precision)
|
||||
// Generate time string if needed
|
||||
let timeString = '';
|
||||
if (config.segments.includeTime) {
|
||||
timeString = format(now, 'HHmmss');
|
||||
|
||||
// Add milliseconds for even more uniqueness
|
||||
if (config.segments.includeMilliseconds) {
|
||||
timeString += now.getMilliseconds().toString().padStart(3, '0');
|
||||
}
|
||||
}
|
||||
|
||||
// Generate sequence based on strategy
|
||||
// Generate sequential number based on uniqueness strategy
|
||||
let sequentialNum: string;
|
||||
|
||||
switch (config.uniquenessStrategy) {
|
||||
case 'uuid':
|
||||
// Use first part of UUID for high uniqueness
|
||||
sequentialNum = uuidv4().split('-')[0];
|
||||
break;
|
||||
|
||||
case 'timestamp':
|
||||
// Use high-precision timestamp
|
||||
sequentialNum = `${now.getTime()}${Math.floor(Math.random() * 1000)}`;
|
||||
sequentialNum = sequentialNum.slice(-config.segments.sequentialDigits);
|
||||
break;
|
||||
|
||||
case 'counter':
|
||||
// Use an incrementing counter
|
||||
sequentialNum = (++globalThis.__idCounter)
|
||||
.toString()
|
||||
.padStart(config.segments.sequentialDigits, '0');
|
||||
break;
|
||||
|
||||
case 'hash':
|
||||
// Create a hash from the current time and options
|
||||
const hashSource = `${now.getTime()}-${JSON.stringify(options)}-${Math.random()}`;
|
||||
const hash = crypto.createHash('sha256').update(hashSource).digest('hex');
|
||||
sequentialNum = hash.substring(0, config.segments.sequentialDigits);
|
||||
break;
|
||||
|
||||
default:
|
||||
// Standard random sequence with improved randomness
|
||||
if (config.randomSequence) {
|
||||
const randomBytes = crypto.randomBytes(4);
|
||||
const randomNum = parseInt(randomBytes.toString('hex'), 16);
|
||||
sequentialNum = (
|
||||
randomNum % Math.pow(10, config.segments.sequentialDigits)
|
||||
)
|
||||
.toString()
|
||||
.padStart(config.segments.sequentialDigits, '0');
|
||||
} else {
|
||||
try {
|
||||
switch (config.uniquenessStrategy) {
|
||||
case 'uuid':
|
||||
sequentialNum = uuidv4().split('-')[0];
|
||||
break;
|
||||
case 'timestamp':
|
||||
sequentialNum = `${now.getTime()}${Math.floor(Math.random() * 1000)}`;
|
||||
sequentialNum = sequentialNum.slice(-config.segments.sequentialDigits);
|
||||
break;
|
||||
case 'counter':
|
||||
sequentialNum = (++globalThis.__idCounter)
|
||||
.toString()
|
||||
.padStart(config.segments.sequentialDigits, '0');
|
||||
}
|
||||
break;
|
||||
case 'hash':
|
||||
const hashSource = `${now.getTime()}-${JSON.stringify(options)}-${Math.random()}`;
|
||||
const hash = crypto
|
||||
.createHash('sha256')
|
||||
.update(hashSource)
|
||||
.digest('hex');
|
||||
sequentialNum = hash.substring(0, config.segments.sequentialDigits);
|
||||
break;
|
||||
default:
|
||||
if (config.randomSequence) {
|
||||
const randomBytes = crypto.randomBytes(4);
|
||||
const randomNum = parseInt(randomBytes.toString('hex'), 16);
|
||||
sequentialNum = (
|
||||
randomNum % Math.pow(10, config.segments.sequentialDigits)
|
||||
)
|
||||
.toString()
|
||||
.padStart(config.segments.sequentialDigits, '0');
|
||||
} else {
|
||||
sequentialNum = (++globalThis.__idCounter)
|
||||
.toString()
|
||||
.padStart(config.segments.sequentialDigits, '0');
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error generating sequential number:', error);
|
||||
// Fallback to timestamp strategy if other methods fail
|
||||
sequentialNum = `${now.getTime()}`.slice(-config.segments.sequentialDigits);
|
||||
}
|
||||
|
||||
// Prepare all components
|
||||
// Determine if year should be included and what value to use
|
||||
let yearValue = null;
|
||||
if (config.segments.year !== undefined || config.segments.year != false) {
|
||||
if (typeof config.segments.year === 'number') {
|
||||
yearValue = String(config.segments.year);
|
||||
} else if (config.segments.year === true) {
|
||||
yearValue = format(now, 'yyyy');
|
||||
}
|
||||
// if year is false, yearValue remains null and won't be included
|
||||
} else {
|
||||
// Default behavior (backward compatibility)
|
||||
yearValue = format(now, 'yyyy');
|
||||
}
|
||||
|
||||
// Prepare components for ID assembly
|
||||
const components = {
|
||||
prefix: config.prefix,
|
||||
codes: config.segments.codes.join(config.separator),
|
||||
year: config.segments.year || format(now, 'yyyy'),
|
||||
codes:
|
||||
config.segments.codes.length > 0
|
||||
? config.segments.codes.join(config.separator)
|
||||
: '',
|
||||
// year: yearValue,
|
||||
sequence: sequentialNum,
|
||||
date: dateString,
|
||||
time: timeString,
|
||||
};
|
||||
|
||||
// Build the ID based on custom format if provided
|
||||
let result: string;
|
||||
|
||||
// Use custom format if provided
|
||||
if (config.format) {
|
||||
let customID = config.format;
|
||||
for (const [key, value] of Object.entries(components)) {
|
||||
if (value) {
|
||||
const placeholder = `{${key}}`;
|
||||
customID = customID.replace(placeholder, String(value));
|
||||
customID = customID.replace(
|
||||
new RegExp(placeholder, 'g'),
|
||||
String(value)
|
||||
);
|
||||
}
|
||||
}
|
||||
// Clean up any unused placeholders
|
||||
// Remove unused placeholders
|
||||
customID = customID.replace(/{[^}]+}/g, '');
|
||||
// Clean up any consecutive separators
|
||||
|
||||
// Clean up separators
|
||||
const escapedSeparator = config.separator.replace(
|
||||
/[-\/\\^$*+?.()|[\]{}]/g,
|
||||
'\\$&'
|
||||
);
|
||||
const separatorRegex = new RegExp(`${escapedSeparator}+`, 'g');
|
||||
customID = customID.replace(separatorRegex, config.separator);
|
||||
// Remove leading/trailing separators
|
||||
customID = customID.replace(
|
||||
new RegExp(`^${escapedSeparator}|${escapedSeparator}$`, 'g'),
|
||||
''
|
||||
|
@ -532,30 +564,34 @@ export function generateId(
|
|||
|
||||
result = config.upperCase ? customID.toUpperCase() : customID;
|
||||
} else {
|
||||
// Default structured build if no format specified
|
||||
// Assemble ID from parts
|
||||
const parts = [];
|
||||
|
||||
if (components.prefix) parts.push(components.prefix);
|
||||
if (components.codes) parts.push(components.codes);
|
||||
if (components.year) parts.push(components.year);
|
||||
if (components.sequence) parts.push(components.sequence);
|
||||
// if (components.year) parts.push(components.year);
|
||||
if (components.date) parts.push(components.date);
|
||||
if (components.time) parts.push(components.time);
|
||||
if (components.sequence) parts.push(components.sequence);
|
||||
|
||||
result = parts.join(config.separator);
|
||||
if (config.upperCase) result = result.toUpperCase();
|
||||
}
|
||||
|
||||
// Check for collisions and retry if necessary
|
||||
// Handle collisions if required
|
||||
if (config.retryOnCollision) {
|
||||
let retryCount = 0;
|
||||
let originalResult = result;
|
||||
|
||||
while (usedIdRegistry.has(result) && retryCount < config.maxRetries) {
|
||||
retryCount++;
|
||||
// Try adding a unique suffix
|
||||
const suffix = crypto.randomBytes(2).toString('hex');
|
||||
result = `${originalResult}${config.separator}${suffix}`;
|
||||
try {
|
||||
const suffix = crypto.randomBytes(2).toString('hex');
|
||||
result = `${originalResult}${config.separator}${suffix}`;
|
||||
} catch (error) {
|
||||
console.error('Error generating collision suffix:', error);
|
||||
// Simple fallback if crypto fails
|
||||
result = `${originalResult}${config.separator}${Date.now().toString(36)}`;
|
||||
}
|
||||
}
|
||||
|
||||
if (retryCount >= config.maxRetries) {
|
||||
|
@ -565,20 +601,16 @@ export function generateId(
|
|||
}
|
||||
}
|
||||
|
||||
// Register this ID to prevent future duplicates
|
||||
// Register the ID and maintain registry size
|
||||
usedIdRegistry.add(result);
|
||||
|
||||
// Periodically clean up the registry to prevent memory leaks (optional)
|
||||
if (usedIdRegistry.size > 10000) {
|
||||
// This is a simple implementation - in production you might want a more sophisticated strategy
|
||||
const entriesToKeep = Array.from(usedIdRegistry).slice(-5000);
|
||||
usedIdRegistry.clear();
|
||||
entriesToKeep.forEach((id) => usedIdRegistry.add(id));
|
||||
}
|
||||
|
||||
return result;
|
||||
return result.trim();
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the last ID from a specified table and column.
|
||||
* @param tableName - The name of the table to query.
|
||||
|
|
|
@ -22,37 +22,40 @@ export const updateSession = async (request: NextRequest) => {
|
|||
},
|
||||
setAll(cookiesToSet) {
|
||||
cookiesToSet.forEach(({ name, value }) =>
|
||||
request.cookies.set(name, value),
|
||||
request.cookies.set(name, value)
|
||||
);
|
||||
response = NextResponse.next({
|
||||
request,
|
||||
});
|
||||
cookiesToSet.forEach(({ name, value, options }) =>
|
||||
response.cookies.set(name, value, options),
|
||||
response.cookies.set(name, value, options)
|
||||
);
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
// This will refresh session if expired - required for Server Components
|
||||
// https://supabase.com/docs/guides/auth/server-side/nextjs
|
||||
const user = await supabase.auth.getUser();
|
||||
|
||||
if (request.nextUrl.pathname === "/" && user.error) {
|
||||
return NextResponse.redirect(new URL("/sign-in", request.url));
|
||||
// console.log('user', user);
|
||||
console.log('api', process.env.NEXT_PUBLIC_SUPABASE_URL);
|
||||
console.log('anon', process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY);
|
||||
|
||||
if (request.nextUrl.pathname === '/' && user.error) {
|
||||
return NextResponse.redirect(new URL('/sign-in', request.url));
|
||||
}
|
||||
|
||||
// protected routes
|
||||
if (request.nextUrl.pathname.startsWith("/dashboard") && user.error) {
|
||||
return NextResponse.redirect(new URL("/sign-in", request.url));
|
||||
if (request.nextUrl.pathname.startsWith('/dashboard') && user.error) {
|
||||
return NextResponse.redirect(new URL('/sign-in', request.url));
|
||||
}
|
||||
|
||||
if (request.nextUrl.pathname === "/" && !user.error) {
|
||||
return NextResponse.redirect(new URL("/dashboard", request.url));
|
||||
if (request.nextUrl.pathname === '/' && !user.error) {
|
||||
return NextResponse.redirect(new URL('/dashboard', request.url));
|
||||
}
|
||||
|
||||
|
||||
return response;
|
||||
} catch (e) {
|
||||
// If you are here, a Supabase client could not be created!
|
||||
|
|
|
@ -38,6 +38,8 @@
|
|||
"autoprefixer": "10.4.20",
|
||||
"class-variance-authority": "^0.7.0",
|
||||
"clsx": "^2.1.1",
|
||||
"csv-parse": "^5.6.0",
|
||||
"csv-parser": "^3.2.0",
|
||||
"date-fns": "^3.6.0",
|
||||
"embla-carousel-react": "^8.5.2",
|
||||
"input-otp": "^1.4.2",
|
||||
|
@ -8278,6 +8280,24 @@
|
|||
"integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/csv-parse": {
|
||||
"version": "5.6.0",
|
||||
"resolved": "https://registry.npmjs.org/csv-parse/-/csv-parse-5.6.0.tgz",
|
||||
"integrity": "sha512-l3nz3euub2QMg5ouu5U09Ew9Wf6/wQ8I++ch1loQ0ljmzhmfZYrH9fflS22i/PQEvsPvxCwxgz5q7UB8K1JO4Q==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/csv-parser": {
|
||||
"version": "3.2.0",
|
||||
"resolved": "https://registry.npmjs.org/csv-parser/-/csv-parser-3.2.0.tgz",
|
||||
"integrity": "sha512-fgKbp+AJbn1h2dcAHKIdKNSSjfp43BZZykXsCjzALjKy80VXQNHPFJ6T9Afwdzoj24aMkq8GwDS7KGcDPpejrA==",
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"csv-parser": "bin/csv-parser"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
}
|
||||
},
|
||||
"node_modules/d3-array": {
|
||||
"version": "1.2.4",
|
||||
"resolved": "https://registry.npmjs.org/d3-array/-/d3-array-1.2.4.tgz",
|
||||
|
|
|
@ -44,6 +44,8 @@
|
|||
"autoprefixer": "10.4.20",
|
||||
"class-variance-authority": "^0.7.0",
|
||||
"clsx": "^2.1.1",
|
||||
"csv-parse": "^5.6.0",
|
||||
"csv-parser": "^3.2.0",
|
||||
"date-fns": "^3.6.0",
|
||||
"embla-carousel-react": "^8.5.2",
|
||||
"input-otp": "^1.4.2",
|
||||
|
|
|
@ -1,270 +0,0 @@
|
|||
export const crimeCategoriesData = [
|
||||
{
|
||||
name: "Terhadap Ketertiban Umum",
|
||||
description: "Kejahatan yang mengganggu ketertiban umum seperti unjuk rasa ilegal atau kerusuhan."
|
||||
},
|
||||
{
|
||||
name: "Membahayakan Kam Umum",
|
||||
description: "Tindakan yang membahayakan keamanan umum, termasuk penggunaan bahan peledak secara ilegal."
|
||||
},
|
||||
{
|
||||
name: "Pembakaran",
|
||||
description: "Tindakan pembakaran yang disengaja terhadap properti atau bangunan."
|
||||
},
|
||||
{
|
||||
name: "Kebakaran / Meletus",
|
||||
description: "Kejadian kebakaran atau ledakan yang menimbulkan kerusakan atau korban."
|
||||
},
|
||||
{
|
||||
name: "Member Suap",
|
||||
description: "Memberikan suap kepada pejabat publik atau pihak lain untuk keuntungan pribadi."
|
||||
},
|
||||
{
|
||||
name: "Sumpah Palsu",
|
||||
description: "Memberikan keterangan tidak benar di bawah sumpah dalam proses hukum."
|
||||
},
|
||||
{
|
||||
name: "Pemalsuan Materai",
|
||||
description: "Pembuatan atau penggunaan materai palsu untuk dokumen resmi."
|
||||
},
|
||||
{
|
||||
name: "Pemalsuan Surat",
|
||||
description: "Pemalsuan dokumen atau surat dengan tujuan menipu."
|
||||
},
|
||||
{
|
||||
name: "Perzinahan",
|
||||
description: "Hubungan seksual antara orang yang salah satunya sudah terikat pernikahan dengan orang lain."
|
||||
},
|
||||
{
|
||||
name: "Perkosaan",
|
||||
description: "Pemaksaan hubungan seksual tanpa persetujuan korban."
|
||||
},
|
||||
{
|
||||
name: "Perjudian",
|
||||
description: "Kegiatan taruhan yang dilarang oleh hukum."
|
||||
},
|
||||
{
|
||||
name: "Penghinaan",
|
||||
description: "Tindakan menghina atau merendahkan martabat orang lain secara lisan atau tulisan."
|
||||
},
|
||||
{
|
||||
name: "Penculikan",
|
||||
description: "Pengambilan seseorang secara paksa atau tanpa izin untuk tujuan tertentu."
|
||||
},
|
||||
{
|
||||
name: "Perbuatan Tidak Menyenangkan",
|
||||
description: "Tindakan yang menyebabkan ketidaknyamanan atau ketakutan pada orang lain."
|
||||
},
|
||||
{
|
||||
name: "Pembunuhan",
|
||||
description: "Tindakan menghilangkan nyawa orang lain secara sengaja."
|
||||
},
|
||||
{
|
||||
name: "Penganiayaan Ringan",
|
||||
description: "Tindakan kekerasan fisik ringan yang tidak menyebabkan luka berat."
|
||||
},
|
||||
{
|
||||
name: "Penganiayaan Berat",
|
||||
description: "Kekerasan fisik yang menyebabkan luka berat pada korban."
|
||||
},
|
||||
{
|
||||
name: "Kelalaian Akibatkan Orang Mati",
|
||||
description: "Kelalaian yang menyebabkan kematian seseorang."
|
||||
},
|
||||
{
|
||||
name: "Kelalaian Akibatkan Orang Luka",
|
||||
description: "Kelalaian yang menyebabkan seseorang terluka."
|
||||
},
|
||||
{
|
||||
name: "Pencurian Biasa",
|
||||
description: "Pencurian yang dilakukan tanpa kekerasan atau perencanaan khusus."
|
||||
},
|
||||
{
|
||||
name: "Curat",
|
||||
description: "Pencurian dengan pemberatan seperti membobol rumah atau bangunan."
|
||||
},
|
||||
{
|
||||
name: "Curingan",
|
||||
description: "Pencurian ringan terhadap barang-barang bernilai kecil."
|
||||
},
|
||||
{
|
||||
name: "Curas",
|
||||
description: "Pencurian dengan kekerasan atau ancaman kekerasan."
|
||||
},
|
||||
{
|
||||
name: "Curanmor",
|
||||
description: "Pencurian kendaraan bermotor."
|
||||
},
|
||||
{
|
||||
name: "Pengeroyokan",
|
||||
description: "Tindakan kekerasan oleh beberapa orang terhadap satu atau lebih korban."
|
||||
},
|
||||
{
|
||||
name: "Premanisme",
|
||||
description: "Tindakan intimidasi atau kekerasan oleh kelompok preman."
|
||||
},
|
||||
{
|
||||
name: "Pemerasan Dan Pengancaman",
|
||||
description: "Memaksa orang lain menyerahkan sesuatu melalui ancaman."
|
||||
},
|
||||
{
|
||||
name: "Penggelapan",
|
||||
description: "Penguasaan barang milik orang lain yang dipercayakan, namun tidak dikembalikan."
|
||||
},
|
||||
{
|
||||
name: "Penipuan",
|
||||
description: "Tindakan menipu untuk mendapatkan keuntungan pribadi."
|
||||
},
|
||||
{
|
||||
name: "Pengrusakan",
|
||||
description: "Merusak barang milik orang lain secara sengaja."
|
||||
},
|
||||
{
|
||||
name: "Kenakalan Remaja",
|
||||
description: "Perilaku menyimpang dari norma oleh anak remaja seperti tawuran atau balap liar."
|
||||
},
|
||||
{
|
||||
name: "Menerima Suap",
|
||||
description: "Menerima imbalan untuk mempengaruhi keputusan atau tindakan."
|
||||
},
|
||||
{
|
||||
name: "Penadahan",
|
||||
description: "Membeli, menyimpan, atau menjual barang hasil kejahatan."
|
||||
},
|
||||
{
|
||||
name: "Pekerjakan Anak",
|
||||
description: "Mempekerjakan anak di bawah umur dalam pekerjaan yang dilarang oleh hukum."
|
||||
},
|
||||
{
|
||||
name: "Agraria",
|
||||
description: "Sengketa dan kejahatan terkait kepemilikan dan penggunaan lahan."
|
||||
},
|
||||
{
|
||||
name: "Peradilan Anak",
|
||||
description: "Proses hukum yang melibatkan anak sebagai pelaku tindak pidana."
|
||||
},
|
||||
{
|
||||
name: "Perlindungan Anak",
|
||||
description: "Upaya perlindungan anak dari kekerasan, eksploitasi, dan penelantaran."
|
||||
},
|
||||
{
|
||||
name: "PKDRT",
|
||||
description: "Tindak kekerasan dalam rumah tangga baik fisik maupun psikis."
|
||||
},
|
||||
{
|
||||
name: "Perlindungan TKI",
|
||||
description: "Perlindungan hukum terhadap Tenaga Kerja Indonesia di luar negeri."
|
||||
},
|
||||
{
|
||||
name: "Perlindungan Saksi – Korban",
|
||||
description: "Perlindungan bagi saksi atau korban kejahatan dalam proses hukum."
|
||||
},
|
||||
{
|
||||
name: "PTPPO",
|
||||
description: "Perdagangan orang, termasuk eksploitasi tenaga kerja dan seksual."
|
||||
},
|
||||
{
|
||||
name: "Pornografi",
|
||||
description: "Produksi, distribusi, atau kepemilikan materi pornografi yang melanggar hukum."
|
||||
},
|
||||
{
|
||||
name: "Sistem Peradilan Anak",
|
||||
description: "Kerangka hukum dan institusi yang menangani kejahatan oleh anak."
|
||||
},
|
||||
{
|
||||
name: "Penyelenggaraan Pemilu",
|
||||
description: "Kejahatan yang berkaitan dengan pelaksanaan pemilihan umum."
|
||||
},
|
||||
{
|
||||
name: "Pemerintah Daerah",
|
||||
description: "Tindak pidana yang dilakukan atau melibatkan pejabat pemerintah daerah."
|
||||
},
|
||||
{
|
||||
name: "Keimigrasian",
|
||||
description: "Kejahatan yang berkaitan dengan dokumen atau proses imigrasi."
|
||||
},
|
||||
{
|
||||
name: "Ekstradisi",
|
||||
description: "Permintaan penyerahan pelaku kejahatan antar negara."
|
||||
},
|
||||
{
|
||||
name: "Lahgun Senpi/Handak/Sajam",
|
||||
description: "Penyalahgunaan senjata api, bahan peledak, atau senjata tajam."
|
||||
},
|
||||
{
|
||||
name: "Pidum Lainnya",
|
||||
description: "Tindak pidana umum lainnya yang tidak termasuk dalam kategori tertentu."
|
||||
},
|
||||
{
|
||||
name: "Money Loudering",
|
||||
description: "Pencucian uang hasil kejahatan agar tampak legal."
|
||||
},
|
||||
{
|
||||
name: "Trafficking In Person",
|
||||
description: "Perdagangan manusia untuk eksploitasi tenaga kerja atau seksual."
|
||||
},
|
||||
{
|
||||
name: "Selundup Senpi",
|
||||
description: "Penyelundupan senjata api secara ilegal."
|
||||
},
|
||||
{
|
||||
name: "Trans Ekonomi Crime",
|
||||
description: "Kejahatan ekonomi lintas negara atau lintas batas hukum nasional."
|
||||
},
|
||||
{
|
||||
name: "Illegal Logging",
|
||||
description: "Penebangan hutan secara ilegal tanpa izin resmi."
|
||||
},
|
||||
{
|
||||
name: "Illegal Mining",
|
||||
description: "Penambangan tanpa izin yang melanggar hukum."
|
||||
},
|
||||
{
|
||||
name: "Illegal Fishing",
|
||||
description: "Penangkapan ikan secara ilegal tanpa izin atau merusak lingkungan."
|
||||
},
|
||||
{
|
||||
name: "BBM Illegal",
|
||||
description: "Distribusi bahan bakar minyak tanpa izin atau bersubsidi secara ilegal."
|
||||
},
|
||||
{
|
||||
name: "Niaga Pupuk",
|
||||
description: "Penyalahgunaan distribusi atau niaga pupuk bersubsidi."
|
||||
},
|
||||
{
|
||||
name: "ITE",
|
||||
description: "Kejahatan yang dilakukan melalui sistem elektronik dan internet."
|
||||
},
|
||||
{
|
||||
name: "Satwa",
|
||||
description: "Tindak kejahatan terhadap satwa dilindungi dan perdagangan ilegal hewan."
|
||||
},
|
||||
{
|
||||
name: "Upal",
|
||||
description: "Pemalsuan dan peredaran uang palsu."
|
||||
},
|
||||
{
|
||||
name: "Fidusia",
|
||||
description: "Kejahatan terkait jaminan fidusia, seperti penggelapan barang fidusia."
|
||||
},
|
||||
{
|
||||
name: "Perlindungan Konsumen",
|
||||
description: "Pelanggaran hak konsumen atau penipuan dalam transaksi perdagangan."
|
||||
},
|
||||
{
|
||||
name: "Pidter Lainnya",
|
||||
description: "Tindak pidana tertentu lainnya yang tidak diklasifikasikan secara spesifik."
|
||||
},
|
||||
{
|
||||
name: "Korupsi",
|
||||
description: "Penyalahgunaan kekuasaan publik untuk keuntungan pribadi."
|
||||
},
|
||||
{
|
||||
name: "Konflik Etnis",
|
||||
description: "Pertikaian antar kelompok etnis yang memicu kekerasan atau kerusuhan."
|
||||
},
|
||||
{
|
||||
name: "Separatisme",
|
||||
description: "Gerakan pemisahan wilayah dari negara untuk membentuk pemerintahan sendiri."
|
||||
}
|
||||
]
|
Binary file not shown.
Binary file not shown.
|
@ -0,0 +1,33 @@
|
|||
Nama Unit,Alamat,Telepon,lat,long
|
||||
Polres Jember,"Jl. R.A. Kartini No.17, Sawahan Cantian, Kepatihan, Kec. Patrang, Kabupaten Jember, Jawa Timur 68137",331-484285,-8.17092173103588,113.70548051820701
|
||||
Polsek Kaliwates,"Jl. Hayam Wuruk No.153 Kelurahan Sempusari Kecamatan Kaliwates Kabupaten Jember, Jawa Timur",0331-484026,-8.186524646936217,113.67250084656763
|
||||
Polsek Sumbersari,"Jl. MT Haryono, Sumbersari, Kabupaten Jember, Jawa Timur 68124",0331-330647,-8.183296304066134,113.74236702752034
|
||||
Polsek Patrang,"Jl. Slamet Riyadi No.48, Patrang, Jember, Jawa Timur 68111",0331-422569,-8.149057988295857,113.72303569447361
|
||||
Polsek Arjasa,"Jl. Supriadi No.101, Krajan Selatan, Patemon, Kec. Pakusari, Kabupaten Jember, Jawa Timur 68191",0331-540116,-8.122196807958975,113.74731612680222
|
||||
Polsek Jelbuk,"Leces II, Sukojember, Kec. Jelbuk, Kabupaten Jember, Jawa Timur 68192",0331-540110,-8.068867091094502,113.76484762708661
|
||||
Polsek Kalisat,"Jl. DR. Wahidin, Krajan II, Kalisat, Kec. Kalisat, Kabupaten Jember, Jawa Timur 68161",0331-591110,-8.128279710551354,113.81238843263388
|
||||
Polsek Sukowono,"Jl. Chairil Anwar, Krajan, Cumedak, Jember, Kabupaten Jember, Jawa Timur 64194",0331-566210,-8.051079803763889,113.83506285032118
|
||||
Polsek Sempolan,"Krajan, Sumberjati, Kec. Silo, Kabupaten Jember, Jawa Timur 68184",0331-521010,-8.186214080516393,113.87653656806364
|
||||
Polsek Sumber Jambe,"Jl. PB. Sudirman No.81, Pasar, Sumberjambe, Kec. Sumberjambe, Kabupaten Jember, Jawa Timur 68195",0331-566268,-8.067557248871932,113.9001121479762
|
||||
Polsek Ledokombo,"Jl. Bungur Ledokombo No.114, Pasar, Ledokombo, Kec. Ledokombo, Kabupaten Jember, Jawa Timur 68196",0331-591011,-8.135526826985,113.87370140934554
|
||||
Polsek Pakusari,"Jl. Prambanan, Krajan, Kertosari, Kec. Pakusari, Kabupaten Jember, Jawa Timur 68181",0331-4436043,-8.164534108294927,113.76611131781641
|
||||
Polsek Jenggawah,"Jl. Kawi No. 23 Jenggawah, Kab. Jember, Propinsi Jawa Timur 68171",0331-757330,-8.256791120691595,113.6522206707149
|
||||
Polsek Mayang,"Jl. Banyuwangi, Mayang, Majang, Jawa Timur 68182",0331-591512,-8.177356153143444,113.79956203127406
|
||||
Polsek Mumbulsari,"Jl. Budi Utomo No.16, Mumbulsari, Kabupaten Jember, Jawa Timur 68174",0331-793262,-8.252854089767183,113.74221255216453
|
||||
Polsek Tempurejo,"Jl. KH. Abdurrahman, Tempurejo, Jember, Jawa Timur 68173",0331-757410,-8.300592195854021,113.68858041964509
|
||||
Polsek Rambipuji,"Jl. Dharmawangsa 47 Rambipuji, Curahancar, Rambipuji, Kec. Rambipuji, Kabupaten Jember, Jawa Timur 68152",0331-711430,-8.20428708115188,113.61328234847777
|
||||
Polsek Sukorambi,"Jl. Mujahir No. 5, Sukorambi, Jember Lor, Patrang, Jember, Jawa Timur 68118",0331-489523,-8.169930311064727,113.6605586889699
|
||||
Polsek Panti,"Jalan Panglima Besar Sudirman 19 Desa Panti Kecamatan Panti, Jember Jawa Timur 68153",0331-711330,-8.17194959166762,113.62022456331853
|
||||
Polsek Bangsalsari,"Jl. Jenderal Ahmad Yani No.16, Kalisatan, Bangsalsari, Kec. Bangsalsari, Kabupaten Jember, Jawa Timur 68154",0331-711401,-8.200901080105067,113.5338098619667
|
||||
Polsek Balung,"Jl. Rambipuji-Balung, Jember, Jawa Timur 68152",0331-621210,-8.269718539539422,113.54065466193254
|
||||
Polsek Ambulu,"Jl. Raya Suyitman, Ambulu, Jember, Jawa Timur 68172",0336-881007,-8.344079809223068,113.60731912704142
|
||||
Polsek Wuluhan,"Jl. Ambulu, Wuluhan, Kabupaten Jember, Jawa Timur 68162",0336-881003,-8.338356022692308,113.55182787728441
|
||||
Polsek Puger,"Jl. Achmad Yani 57 Puger, Umbulsari, Kabupaten Jember, Jawa Timur 68164",0336-721119,-8.366494602192043,113.47296865214837
|
||||
Polsek Gumukmas,"Jl. Achmad Yani 89 Gumukmas, Gumukmas, Jember, Jawa Timur 68165",0336-321391,-8.315087051246818,113.41296867361133
|
||||
Polsek Kencong,"Jl. Diponegoro, No. 35, Kencong, Jember 68167",0336-321210,-8.279794453467883,113.37664036194629
|
||||
Polsek Tanggul,"Jl. Urip Sumoharjo N0.50 Tanggul 68155, Tanggul Wetan, Tanggul, Jawa Timur 68155",0336-441110,-8.16605008923455,113.46115044482765
|
||||
Polsek Sumberbaru,"Jl. Panglima Besar Sudirman, No. 3, Sumberbaru, Jember, Jawa Timur 68173",0336-324210,-8.119255727431387,113.39383676380444
|
||||
Polsek Semboro,"Jl. Telomoyo Dusun Semboro pasar, Desa Semboro, Jember, Jawa Timur 68157",0336-444200,-8.206242110979067,113.43480704846874
|
||||
Polsek Umbulsari,"Jl. Ahmad Yani No.44, Umbulsari, Jember, Jawa Timur 68166",0336-321191,-8.263912399533561,113.44837139079566
|
||||
Polsek Jombang,"Jl. KH. Dewantara No. 88 Jombang, Jember, Jawa Timur 68168",0336-321100,-8.245938093985266,113.32178564847065
|
||||
Polsek Ajung,"Ajung Kulon, Ajung, Kec. Ajung, Kabupaten Jember, Jawa Timur 68175",,-8.215016859569301,113.66807277175127
|
|
Binary file not shown.
|
@ -0,0 +1,34 @@
|
|||
KESATUAN,JAN,,FEB,,MAR,,APR,,MEI,,JUN,,JUL,,AGT,,SEP,,OKT,,NOV,,DES,,JUMLAH,
|
||||
,CT,CC,CT,CC,CT,CC,CT,CC,CT,CC,CT,CC,CT,CC,CT,CC,CT,CC,CT,CC,CT,CC,CT,CC,CT,CC
|
||||
RESKRIM ,58,39,40,26,46,35,45,29,33,22,46,32,45,32,36,27,42,28,44,35,45,33,27,23,507,361
|
||||
SEK ARJASA,3,3,1,1,5,4,3,1,4,2,2,1,1,0,3,3,4,3,2,1,0,0,1,1,29,20
|
||||
SEK PAKUSARI,7,7,5,4,3,2,2,1,5,4,1,1,4,1,6,4,3,2,1,1,2,2,0,0,39,29
|
||||
SEK KALISAT,2,2,3,2,4,3,8,4,7,3,5,1,12,4,14,9,14,11,6,3,7,4,2,1,84,47
|
||||
SEK SUKOWONO,3,3,4,4,2,2,2,1,5,3,5,1,7,3,3,3,2,3,2,1,1,1,2,2,38,27
|
||||
SEK LEDOKOMBO,1,1,2,1,2,2,1,0,1,0,0,0,2,1,2,1,5,2,3,2,2,1,1,1,22,12
|
||||
SEK SUMBERJAMBE,7,7,4,4,4,4,7,4,7,6,3,1,4,3,6,4,3,3,4,4,5,5,1,1,55,46
|
||||
SEK MAYANG,0,0,1,0,1,1,0,0,2,2,1,1,2,1,1,1,1,0,1,0,0,0,1,0,11,6
|
||||
SEK MUMBULSARI,3,3,3,3,2,2,4,4,2,2,1,1,2,2,1,1,2,2,5,5,3,2,0,0,28,27
|
||||
SEK TEMPUREJO,3,3,0,0,1,0,1,1,5,2,1,1,4,4,0,0,0,0,1,1,3,2,0,0,19,14
|
||||
SEK SEMPOLAN,1,1,0,0,3,1,4,2,5,2,2,1,2,1,2,2,1,1,3,3,2,0,1,1,26,15
|
||||
SEK RAMBIPUJI,4,4,6,5,6,3,8,7,8,6,4,3,4,4,12,8,16,9,9,1,15,5,4,2,96,57
|
||||
SEK PANTI,3,1,1,1,3,3,3,1,9,5,4,3,7,3,4,2,5,4,9,2,4,2,4,3,56,30
|
||||
SEK KALIWATES,3,3,2,1,10,7,9,7,6,4,5,3,5,3,3,2,2,1,2,1,8,5,1,0,56,37
|
||||
SEK JENGGAWAH,3,3,6,5,4,4,9,8,4,4,2,2,7,7,6,5,4,4,2,2,4,4,4,3,55,51
|
||||
SEK BALUNG,3,3,3,2,5,4,3,2,8,8,5,4,4,3,4,3,6,5,7,7,5,4,3,2,56,47
|
||||
SEK AMBULU,1,1,1,0,1,1,3,3,2,2,4,3,6,3,3,2,4,3,0,0,5,3,1,1,31,22
|
||||
SEK WULUHAN,1,1,4,3,4,3,5,5,4,4,7,6,3,3,4,3,4,2,4,3,6,6,2,2,48,41
|
||||
SEK TANGGUL,4,2,4,3,4,2,7,3,5,4,4,3,7,5,4,3,7,7,8,7,6,4,1,0,61,43
|
||||
SEK BANGSALSARI,4,4,4,4,6,5,3,2,5,5,3,3,5,4,2,2,4,4,3,3,2,2,0,0,41,38
|
||||
SEK SUMBERBARU,7,5,4,2,3,1,4,1,3,1,5,3,6,6,4,3,3,3,6,5,6,5,0,0,51,35
|
||||
SEK KENCONG,4,3,1,0,1,1,0,0,0,0,0,0,1,1,2,2,0,0,2,2,0,0,0,0,11,9
|
||||
SEK GUMUKMAS,6,6,1,1,1,1,3,3,0,0,1,1,0,0,2,2,0,0,1,1,0,0,1,0,16,15
|
||||
SEK UMBULSARI,1,1,2,2,1,1,4,4,8,6,1,1,2,3,1,0,3,2,2,0,1,1,2,2,28,23
|
||||
SEK PUGER,4,4,6,6,4,4,5,5,2,2,5,5,4,4,0,0,3,3,7,7,2,2,1,1,43,43
|
||||
SEK SUMBERSARI,3,3,2,2,4,4,4,4,3,2,5,3,3,1,3,0,4,2,5,5,1,1,1,1,38,28
|
||||
SEK PATRANG,13,10,5,4,5,4,4,2,4,3,2,2,5,3,1,0,3,3,1,1,3,1,0,0,46,33
|
||||
SEK JELBUK,6,5,1,1,3,3,3,2,2,2,2,1,3,4,2,1,1,1,4,5,3,3,2,1,32,29
|
||||
SEK SUKORAMBI,0,0,1,1,1,0,4,4,2,2,1,1,2,2,1,1,0,0,0,0,2,2,0,0,14,13
|
||||
SEK SEMBORO,3,3,2,2,2,1,3,3,2,2,1,1,4,4,3,3,3,3,3,3,5,4,3,3,34,32
|
||||
SEK JOMBANG,0,0,3,3,1,1,1,1,1,1,0,0,3,2,1,0,4,4,1,0,0,0,1,2,16,14
|
||||
SEK AJUNG,,,,,,,,,,,,,,,,,,,,,,,,,0,0
|
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,156 @@
|
|||
district_id,district_name,number_of_crime,level,score,method,year
|
||||
350901,Jombang,16,low,86,kmeans,2020
|
||||
350901,Jombang,32,low,71,kmeans,2021
|
||||
350901,Jombang,23,low,79,kmeans,2022
|
||||
350901,Jombang,26,low,77,kmeans,2023
|
||||
350901,Jombang,21,low,81,kmeans,2024
|
||||
350902,Kencong,11,low,90,kmeans,2020
|
||||
350902,Kencong,18,low,84,kmeans,2021
|
||||
350902,Kencong,17,low,85,kmeans,2022
|
||||
350902,Kencong,21,low,81,kmeans,2023
|
||||
350902,Kencong,24,low,78,kmeans,2024
|
||||
350903,Sumberbaru,51,high,54,kmeans,2020
|
||||
350903,Sumberbaru,37,high,67,kmeans,2021
|
||||
350903,Sumberbaru,23,high,79,kmeans,2022
|
||||
350903,Sumberbaru,23,high,79,kmeans,2023
|
||||
350903,Sumberbaru,23,high,79,kmeans,2024
|
||||
350904,Gumukmas,16,low,86,kmeans,2020
|
||||
350904,Gumukmas,27,low,76,kmeans,2021
|
||||
350904,Gumukmas,18,low,84,kmeans,2022
|
||||
350904,Gumukmas,10,low,91,kmeans,2023
|
||||
350904,Gumukmas,20,low,82,kmeans,2024
|
||||
350905,Umbulsari,28,high,75,kmeans,2020
|
||||
350905,Umbulsari,26,low,77,kmeans,2021
|
||||
350905,Umbulsari,24,low,78,kmeans,2022
|
||||
350905,Umbulsari,21,low,81,kmeans,2023
|
||||
350905,Umbulsari,16,low,86,kmeans,2024
|
||||
350906,Tanggul,61,high,45,kmeans,2020
|
||||
350906,Tanggul,58,high,47,kmeans,2021
|
||||
350906,Tanggul,61,high,45,kmeans,2022
|
||||
350906,Tanggul,55,high,50,kmeans,2023
|
||||
350906,Tanggul,31,high,72,kmeans,2024
|
||||
350907,Semboro,34,low,69,kmeans,2020
|
||||
350907,Semboro,23,low,79,kmeans,2021
|
||||
350907,Semboro,19,low,83,kmeans,2022
|
||||
350907,Semboro,9,low,92,kmeans,2023
|
||||
350907,Semboro,9,low,92,kmeans,2024
|
||||
350908,Puger,43,high,61,kmeans,2020
|
||||
350908,Puger,57,high,48,kmeans,2021
|
||||
350908,Puger,33,high,70,kmeans,2022
|
||||
350908,Puger,26,high,77,kmeans,2023
|
||||
350908,Puger,21,high,81,kmeans,2024
|
||||
350909,Bangsalsari,41,high,63,kmeans,2020
|
||||
350909,Bangsalsari,41,high,63,kmeans,2021
|
||||
350909,Bangsalsari,34,high,69,kmeans,2022
|
||||
350909,Bangsalsari,13,low,89,kmeans,2023
|
||||
350909,Bangsalsari,25,high,78,kmeans,2024
|
||||
350910,Balung,56,high,49,kmeans,2020
|
||||
350910,Balung,74,high,33,kmeans,2021
|
||||
350910,Balung,54,high,51,kmeans,2022
|
||||
350910,Balung,55,high,50,kmeans,2023
|
||||
350910,Balung,39,high,65,kmeans,2024
|
||||
350911,Wuluhan,48,high,56,kmeans,2020
|
||||
350911,Wuluhan,70,high,36,kmeans,2021
|
||||
350911,Wuluhan,45,high,59,kmeans,2022
|
||||
350911,Wuluhan,26,high,77,kmeans,2023
|
||||
350911,Wuluhan,27,high,76,kmeans,2024
|
||||
350912,Ambulu,31,high,72,kmeans,2020
|
||||
350912,Ambulu,39,high,65,kmeans,2021
|
||||
350912,Ambulu,26,high,77,kmeans,2022
|
||||
350912,Ambulu,31,high,72,kmeans,2023
|
||||
350912,Ambulu,30,high,73,kmeans,2024
|
||||
350913,Rambipuji,96,high,12,kmeans,2020
|
||||
350913,Rambipuji,109,high,0,kmeans,2021
|
||||
350913,Rambipuji,32,low,71,kmeans,2022
|
||||
350913,Rambipuji,20,low,82,kmeans,2023
|
||||
350913,Rambipuji,21,low,81,kmeans,2024
|
||||
350914,Panti,56,high,49,kmeans,2020
|
||||
350914,Panti,28,low,75,kmeans,2021
|
||||
350914,Panti,22,low,80,kmeans,2022
|
||||
350914,Panti,24,low,78,kmeans,2023
|
||||
350914,Panti,9,low,92,kmeans,2024
|
||||
350915,Sukorambi,14,low,88,kmeans,2020
|
||||
350915,Sukorambi,27,low,76,kmeans,2021
|
||||
350915,Sukorambi,14,low,88,kmeans,2022
|
||||
350915,Sukorambi,18,low,84,kmeans,2023
|
||||
350915,Sukorambi,4,low,97,kmeans,2024
|
||||
350916,Jenggawah,55,high,50,kmeans,2020
|
||||
350916,Jenggawah,51,high,54,kmeans,2021
|
||||
350916,Jenggawah,34,low,69,kmeans,2022
|
||||
350916,Jenggawah,57,high,48,kmeans,2023
|
||||
350916,Jenggawah,38,high,66,kmeans,2024
|
||||
350917,Ajung,0,low,100,kmeans,2020
|
||||
350917,Ajung,16,low,86,kmeans,2021
|
||||
350917,Ajung,23,low,79,kmeans,2022
|
||||
350917,Ajung,24,low,78,kmeans,2023
|
||||
350917,Ajung,25,low,78,kmeans,2024
|
||||
350918,Tempurejo,19,low,83,kmeans,2020
|
||||
350918,Tempurejo,14,low,88,kmeans,2021
|
||||
350918,Tempurejo,18,low,84,kmeans,2022
|
||||
350918,Tempurejo,10,low,91,kmeans,2023
|
||||
350918,Tempurejo,13,low,89,kmeans,2024
|
||||
350919,Kaliwates,56,medium,49,kmeans,2020
|
||||
350919,Kaliwates,63,medium,43,kmeans,2021
|
||||
350919,Kaliwates,36,medium,67,kmeans,2022
|
||||
350919,Kaliwates,23,medium,79,kmeans,2023
|
||||
350919,Kaliwates,16,medium,86,kmeans,2024
|
||||
350920,Patrang,46,medium,58,kmeans,2020
|
||||
350920,Patrang,88,medium,20,kmeans,2021
|
||||
350920,Patrang,42,medium,62,kmeans,2022
|
||||
350920,Patrang,16,medium,86,kmeans,2023
|
||||
350920,Patrang,10,medium,91,kmeans,2024
|
||||
350921,Sumbersari,38,high,66,kmeans,2020
|
||||
350921,Sumbersari,52,medium,53,kmeans,2021
|
||||
350921,Sumbersari,59,medium,46,kmeans,2022
|
||||
350921,Sumbersari,35,medium,68,kmeans,2023
|
||||
350921,Sumbersari,33,medium,70,kmeans,2024
|
||||
350922,Arjasa,29,low,74,kmeans,2020
|
||||
350922,Arjasa,47,low,57,kmeans,2021
|
||||
350922,Arjasa,19,low,83,kmeans,2022
|
||||
350922,Arjasa,10,low,91,kmeans,2023
|
||||
350922,Arjasa,11,low,90,kmeans,2024
|
||||
350923,Mumbulsari,28,high,75,kmeans,2020
|
||||
350923,Mumbulsari,27,low,76,kmeans,2021
|
||||
350923,Mumbulsari,23,low,79,kmeans,2022
|
||||
350923,Mumbulsari,11,low,90,kmeans,2023
|
||||
350923,Mumbulsari,10,low,91,kmeans,2024
|
||||
350924,Pakusari,39,low,65,kmeans,2020
|
||||
350924,Pakusari,52,low,53,kmeans,2021
|
||||
350924,Pakusari,34,low,69,kmeans,2022
|
||||
350924,Pakusari,12,low,89,kmeans,2023
|
||||
350924,Pakusari,15,low,87,kmeans,2024
|
||||
350925,Jelbuk,32,low,71,kmeans,2020
|
||||
350925,Jelbuk,55,low,50,kmeans,2021
|
||||
350925,Jelbuk,16,low,86,kmeans,2022
|
||||
350925,Jelbuk,16,low,86,kmeans,2023
|
||||
350925,Jelbuk,13,low,89,kmeans,2024
|
||||
350926,Mayang,11,low,90,kmeans,2020
|
||||
350926,Mayang,35,low,68,kmeans,2021
|
||||
350926,Mayang,20,low,82,kmeans,2022
|
||||
350926,Mayang,13,low,89,kmeans,2023
|
||||
350926,Mayang,10,low,91,kmeans,2024
|
||||
350927,Kalisat,84,high,23,kmeans,2020
|
||||
350927,Kalisat,95,high,13,kmeans,2021
|
||||
350927,Kalisat,68,high,38,kmeans,2022
|
||||
350927,Kalisat,13,low,89,kmeans,2023
|
||||
350927,Kalisat,10,low,91,kmeans,2024
|
||||
350928,Ledokombo,22,low,80,kmeans,2020
|
||||
350928,Ledokombo,45,low,59,kmeans,2021
|
||||
350928,Ledokombo,17,low,85,kmeans,2022
|
||||
350928,Ledokombo,13,low,89,kmeans,2023
|
||||
350928,Ledokombo,6,low,95,kmeans,2024
|
||||
350929,Sukowono,38,high,66,kmeans,2020
|
||||
350929,Sukowono,46,low,58,kmeans,2021
|
||||
350929,Sukowono,34,low,69,kmeans,2022
|
||||
350929,Sukowono,25,low,78,kmeans,2023
|
||||
350929,Sukowono,28,low,75,kmeans,2024
|
||||
350930,Silo,26,high,77,kmeans,2020
|
||||
350930,Silo,39,high,65,kmeans,2021
|
||||
350930,Silo,28,high,75,kmeans,2022
|
||||
350930,Silo,29,high,74,kmeans,2023
|
||||
350930,Silo,21,low,81,kmeans,2024
|
||||
350931,Sumberjambe,55,high,50,kmeans,2020
|
||||
350931,Sumberjambe,20,low,82,kmeans,2021
|
||||
350931,Sumberjambe,12,low,89,kmeans,2022
|
||||
350931,Sumberjambe,16,low,86,kmeans,2023
|
||||
350931,Sumberjambe,6,low,95,kmeans,2024
|
|
Binary file not shown.
|
@ -0,0 +1,311 @@
|
|||
export const crimeCategoriesData = [
|
||||
{
|
||||
name: 'Terhadap Ketertiban Umum',
|
||||
description:
|
||||
'Kejahatan yang mengganggu ketertiban umum seperti unjuk rasa ilegal atau kerusuhan.',
|
||||
},
|
||||
{
|
||||
name: 'Membahayakan Kam Umum',
|
||||
description:
|
||||
'Tindakan yang membahayakan keamanan umum, termasuk penggunaan bahan peledak secara ilegal.',
|
||||
},
|
||||
{
|
||||
name: 'Pembakaran',
|
||||
description:
|
||||
'Tindakan pembakaran yang disengaja terhadap properti atau bangunan.',
|
||||
},
|
||||
{
|
||||
name: 'Kebakaran / Meletus',
|
||||
description:
|
||||
'Kejadian kebakaran atau ledakan yang menimbulkan kerusakan atau korban.',
|
||||
},
|
||||
{
|
||||
name: 'Member Suap',
|
||||
description:
|
||||
'Memberikan suap kepada pejabat publik atau pihak lain untuk keuntungan pribadi.',
|
||||
},
|
||||
{
|
||||
name: 'Sumpah Palsu',
|
||||
description:
|
||||
'Memberikan keterangan tidak benar di bawah sumpah dalam proses hukum.',
|
||||
},
|
||||
{
|
||||
name: 'Pemalsuan Materai',
|
||||
description: 'Pembuatan atau penggunaan materai palsu untuk dokumen resmi.',
|
||||
},
|
||||
{
|
||||
name: 'Pemalsuan Surat',
|
||||
description: 'Pemalsuan dokumen atau surat dengan tujuan menipu.',
|
||||
},
|
||||
{
|
||||
name: 'Perzinahan',
|
||||
description:
|
||||
'Hubungan seksual antara orang yang salah satunya sudah terikat pernikahan dengan orang lain.',
|
||||
},
|
||||
{
|
||||
name: 'Perkosaan',
|
||||
description: 'Pemaksaan hubungan seksual tanpa persetujuan korban.',
|
||||
},
|
||||
{
|
||||
name: 'Perjudian',
|
||||
description: 'Kegiatan taruhan yang dilarang oleh hukum.',
|
||||
},
|
||||
{
|
||||
name: 'Penghinaan',
|
||||
description:
|
||||
'Tindakan menghina atau merendahkan martabat orang lain secara lisan atau tulisan.',
|
||||
},
|
||||
{
|
||||
name: 'Penculikan',
|
||||
description:
|
||||
'Pengambilan seseorang secara paksa atau tanpa izin untuk tujuan tertentu.',
|
||||
},
|
||||
{
|
||||
name: 'Perbuatan Tidak Menyenangkan',
|
||||
description:
|
||||
'Tindakan yang menyebabkan ketidaknyamanan atau ketakutan pada orang lain.',
|
||||
},
|
||||
{
|
||||
name: 'Pembunuhan',
|
||||
description: 'Tindakan menghilangkan nyawa orang lain secara sengaja.',
|
||||
},
|
||||
{
|
||||
name: 'Penganiayaan Ringan',
|
||||
description:
|
||||
'Tindakan kekerasan fisik ringan yang tidak menyebabkan luka berat.',
|
||||
},
|
||||
{
|
||||
name: 'Penganiayaan Berat',
|
||||
description: 'Kekerasan fisik yang menyebabkan luka berat pada korban.',
|
||||
},
|
||||
{
|
||||
name: 'Kelalaian Akibatkan Orang Mati',
|
||||
description: 'Kelalaian yang menyebabkan kematian seseorang.',
|
||||
},
|
||||
{
|
||||
name: 'Kelalaian Akibatkan Orang Luka',
|
||||
description: 'Kelalaian yang menyebabkan seseorang terluka.',
|
||||
},
|
||||
{
|
||||
name: 'Pencurian Biasa',
|
||||
description:
|
||||
'Pencurian yang dilakukan tanpa kekerasan atau perencanaan khusus.',
|
||||
},
|
||||
{
|
||||
name: 'Curat',
|
||||
description:
|
||||
'Pencurian dengan pemberatan seperti membobol rumah atau bangunan.',
|
||||
},
|
||||
{
|
||||
name: 'Curingan',
|
||||
description: 'Pencurian ringan terhadap barang-barang bernilai kecil.',
|
||||
},
|
||||
{
|
||||
name: 'Curas',
|
||||
description: 'Pencurian dengan kekerasan atau ancaman kekerasan.',
|
||||
},
|
||||
{
|
||||
name: 'Curanmor',
|
||||
description: 'Pencurian kendaraan bermotor.',
|
||||
},
|
||||
{
|
||||
name: 'Pengeroyokan',
|
||||
description:
|
||||
'Tindakan kekerasan oleh beberapa orang terhadap satu atau lebih korban.',
|
||||
},
|
||||
{
|
||||
name: 'Premanisme',
|
||||
description: 'Tindakan intimidasi atau kekerasan oleh kelompok preman.',
|
||||
},
|
||||
{
|
||||
name: 'Pemerasan Dan Pengancaman',
|
||||
description: 'Memaksa orang lain menyerahkan sesuatu melalui ancaman.',
|
||||
},
|
||||
{
|
||||
name: 'Penggelapan',
|
||||
description:
|
||||
'Penguasaan barang milik orang lain yang dipercayakan, namun tidak dikembalikan.',
|
||||
},
|
||||
{
|
||||
name: 'Penipuan',
|
||||
description: 'Tindakan menipu untuk mendapatkan keuntungan pribadi.',
|
||||
},
|
||||
{
|
||||
name: 'Pengrusakan',
|
||||
description: 'Merusak barang milik orang lain secara sengaja.',
|
||||
},
|
||||
{
|
||||
name: 'Kenakalan Remaja',
|
||||
description:
|
||||
'Perilaku menyimpang dari norma oleh anak remaja seperti tawuran atau balap liar.',
|
||||
},
|
||||
{
|
||||
name: 'Menerima Suap',
|
||||
description: 'Menerima imbalan untuk mempengaruhi keputusan atau tindakan.',
|
||||
},
|
||||
{
|
||||
name: 'Penadahan',
|
||||
description: 'Membeli, menyimpan, atau menjual barang hasil kejahatan.',
|
||||
},
|
||||
{
|
||||
name: 'Pekerjakan Anak',
|
||||
description:
|
||||
'Mempekerjakan anak di bawah umur dalam pekerjaan yang dilarang oleh hukum.',
|
||||
},
|
||||
{
|
||||
name: 'Agraria',
|
||||
description:
|
||||
'Sengketa dan kejahatan terkait kepemilikan dan penggunaan lahan.',
|
||||
},
|
||||
{
|
||||
name: 'Peradilan Anak',
|
||||
description:
|
||||
'Proses hukum yang melibatkan anak sebagai pelaku tindak pidana.',
|
||||
},
|
||||
{
|
||||
name: 'Perlindungan Anak',
|
||||
description:
|
||||
'Upaya perlindungan anak dari kekerasan, eksploitasi, dan penelantaran.',
|
||||
},
|
||||
{
|
||||
name: 'PKDRT',
|
||||
description:
|
||||
'Tindak kekerasan dalam rumah tangga baik fisik maupun psikis.',
|
||||
},
|
||||
{
|
||||
name: 'Perlindungan TKI',
|
||||
description:
|
||||
'Perlindungan hukum terhadap Tenaga Kerja Indonesia di luar negeri.',
|
||||
},
|
||||
{
|
||||
name: 'Perlindungan Saksi – Korban',
|
||||
description:
|
||||
'Perlindungan bagi saksi atau korban kejahatan dalam proses hukum.',
|
||||
},
|
||||
{
|
||||
name: 'PTPPO',
|
||||
description:
|
||||
'Perdagangan orang, termasuk eksploitasi tenaga kerja dan seksual.',
|
||||
},
|
||||
{
|
||||
name: 'Pornografi',
|
||||
description:
|
||||
'Produksi, distribusi, atau kepemilikan materi pornografi yang melanggar hukum.',
|
||||
},
|
||||
{
|
||||
name: 'Sistem Peradilan Anak',
|
||||
description:
|
||||
'Kerangka hukum dan institusi yang menangani kejahatan oleh anak.',
|
||||
},
|
||||
{
|
||||
name: 'Penyelenggaraan Pemilu',
|
||||
description: 'Kejahatan yang berkaitan dengan pelaksanaan pemilihan umum.',
|
||||
},
|
||||
{
|
||||
name: 'Pemerintah Daerah',
|
||||
description:
|
||||
'Tindak pidana yang dilakukan atau melibatkan pejabat pemerintah daerah.',
|
||||
},
|
||||
{
|
||||
name: 'Keimigrasian',
|
||||
description:
|
||||
'Kejahatan yang berkaitan dengan dokumen atau proses imigrasi.',
|
||||
},
|
||||
{
|
||||
name: 'Ekstradisi',
|
||||
description: 'Permintaan penyerahan pelaku kejahatan antar negara.',
|
||||
},
|
||||
{
|
||||
name: 'Lahgun Senpi/Handak/Sajam',
|
||||
description:
|
||||
'Penyalahgunaan senjata api, bahan peledak, atau senjata tajam.',
|
||||
},
|
||||
{
|
||||
name: 'Pidum Lainnya',
|
||||
description:
|
||||
'Tindak pidana umum lainnya yang tidak termasuk dalam kategori tertentu.',
|
||||
},
|
||||
{
|
||||
name: 'Money Loudering',
|
||||
description: 'Pencucian uang hasil kejahatan agar tampak legal.',
|
||||
},
|
||||
{
|
||||
name: 'Trafficking In Person',
|
||||
description:
|
||||
'Perdagangan manusia untuk eksploitasi tenaga kerja atau seksual.',
|
||||
},
|
||||
{
|
||||
name: 'Selundup Senpi',
|
||||
description: 'Penyelundupan senjata api secara ilegal.',
|
||||
},
|
||||
{
|
||||
name: 'Trans Ekonomi Crime',
|
||||
description:
|
||||
'Kejahatan ekonomi lintas negara atau lintas batas hukum nasional.',
|
||||
},
|
||||
{
|
||||
name: 'Illegal Logging',
|
||||
description: 'Penebangan hutan secara ilegal tanpa izin resmi.',
|
||||
},
|
||||
{
|
||||
name: 'Illegal Mining',
|
||||
description: 'Penambangan tanpa izin yang melanggar hukum.',
|
||||
},
|
||||
{
|
||||
name: 'Illegal Fishing',
|
||||
description:
|
||||
'Penangkapan ikan secara ilegal tanpa izin atau merusak lingkungan.',
|
||||
},
|
||||
{
|
||||
name: 'BBM Illegal',
|
||||
description:
|
||||
'Distribusi bahan bakar minyak tanpa izin atau bersubsidi secara ilegal.',
|
||||
},
|
||||
{
|
||||
name: 'Niaga Pupuk',
|
||||
description: 'Penyalahgunaan distribusi atau niaga pupuk bersubsidi.',
|
||||
},
|
||||
{
|
||||
name: 'ITE',
|
||||
description:
|
||||
'Kejahatan yang dilakukan melalui sistem elektronik dan internet.',
|
||||
},
|
||||
{
|
||||
name: 'Satwa',
|
||||
description:
|
||||
'Tindak kejahatan terhadap satwa dilindungi dan perdagangan ilegal hewan.',
|
||||
},
|
||||
{
|
||||
name: 'Upal',
|
||||
description: 'Pemalsuan dan peredaran uang palsu.',
|
||||
},
|
||||
{
|
||||
name: 'Fidusia',
|
||||
description:
|
||||
'Kejahatan terkait jaminan fidusia, seperti penggelapan barang fidusia.',
|
||||
},
|
||||
{
|
||||
name: 'Perlindungan Konsumen',
|
||||
description:
|
||||
'Pelanggaran hak konsumen atau penipuan dalam transaksi perdagangan.',
|
||||
},
|
||||
{
|
||||
name: 'Pidter Lainnya',
|
||||
description:
|
||||
'Tindak pidana tertentu lainnya yang tidak diklasifikasikan secara spesifik.',
|
||||
},
|
||||
{
|
||||
name: 'Korupsi',
|
||||
description: 'Penyalahgunaan kekuasaan publik untuk keuntungan pribadi.',
|
||||
},
|
||||
{
|
||||
name: 'Konflik Etnis',
|
||||
description:
|
||||
'Pertikaian antar kelompok etnis yang memicu kekerasan atau kerusuhan.',
|
||||
},
|
||||
{
|
||||
name: 'Separatisme',
|
||||
description:
|
||||
'Gerakan pemisahan wilayah dari negara untuk membentuk pemerintahan sendiri.',
|
||||
},
|
||||
];
|
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,32 @@
|
|||
[
|
||||
{
|
||||
"year": 2020,
|
||||
"crime_total": 1721,
|
||||
"crime_cleared": 1419,
|
||||
"clearance_rate": 82.45
|
||||
},
|
||||
{
|
||||
"year": 2021,
|
||||
"crime_total": 1975,
|
||||
"crime_cleared": 1725,
|
||||
"clearance_rate": 87.34
|
||||
},
|
||||
{
|
||||
"year": 2022,
|
||||
"crime_total": 2928,
|
||||
"crime_cleared": 2646,
|
||||
"clearance_rate": 90.37
|
||||
},
|
||||
{
|
||||
"year": 2023,
|
||||
"crime_total": 2302,
|
||||
"crime_cleared": 1988,
|
||||
"clearance_rate": 86.36
|
||||
},
|
||||
{
|
||||
"year": 2024,
|
||||
"crime_total": 2366,
|
||||
"crime_cleared": 1962,
|
||||
"clearance_rate": 82.92
|
||||
}
|
||||
]
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue