@@ -9,14 +9,33 @@ import { Badge } from "@/components/ui/badge";
99import { Input } from "@/components/ui/input" ;
1010import { Skeleton } from "@/components/ui/skeleton" ;
1111import { ThemeSwitcher } from "@/components/theme-switcher" ;
12- import { Shield , LogOut , Users , Settings , Activity , Clock , AlertTriangle , TrendingUp , CheckCircle2 , XCircle , Search , Wrench , MapPin } from "lucide-react" ;
12+ import { Shield , LogOut , Users , Settings , Activity , Clock , AlertTriangle , TrendingUp , CheckCircle2 , XCircle , Search , Wrench , MapPin , Bell , Loader2 } from "lucide-react" ;
13+ import {
14+ Dialog ,
15+ DialogContent ,
16+ DialogDescription ,
17+ DialogFooter ,
18+ DialogHeader ,
19+ DialogTitle ,
20+ } from "@/components/ui/dialog" ;
21+ import {
22+ Select ,
23+ SelectContent ,
24+ SelectItem ,
25+ SelectTrigger ,
26+ SelectValue ,
27+ } from "@/components/ui/select" ;
28+ import { Label } from "@/components/ui/label" ;
1329import {
1430 getAdminStats ,
1531 getRecentCrises ,
1632 getRecentUsers ,
33+ listUsers ,
34+ triggerTestAlert ,
1735 type AdminStats ,
1836 type RecentCrisis ,
19- type RecentUser
37+ type RecentUser ,
38+ type User
2039} from "@/lib/admin-api-client" ;
2140import { formatActivityTime } from "@/lib/utils" ;
2241import { LoadingSpinner } from "@/components/loading-spinner" ;
@@ -37,8 +56,67 @@ export default function AdminDashboard() {
3756 const [ recentCrises , setRecentCrises ] = useState < RecentCrisis [ ] > ( [ ] ) ;
3857 const [ recentUsers , setRecentUsers ] = useState < RecentUser [ ] > ( [ ] ) ;
3958 const [ searchQuery , setSearchQuery ] = useState ( "" ) ;
59+ const [ alertDialogOpen , setAlertDialogOpen ] = useState ( false ) ;
60+ const [ sendingAlert , setSendingAlert ] = useState ( false ) ;
61+ const [ allUsers , setAllUsers ] = useState < User [ ] > ( [ ] ) ;
62+ const [ selectedUserId , setSelectedUserId ] = useState < string > ( "" ) ;
63+ const [ alertForm , setAlertForm ] = useState ( {
64+ disaster_type : "earthquake" ,
65+ severity : 4 ,
66+ description : "" ,
67+ } ) ;
68+ const [ alertSuccess , setAlertSuccess ] = useState < string | null > ( null ) ;
69+ const [ alertError , setAlertError ] = useState < string | null > ( null ) ;
4070 const router = useRouter ( ) ;
4171
72+ const DISASTER_TYPES = [
73+ { value : "earthquake" , label : "Earthquake" } ,
74+ { value : "flood" , label : "Flood" } ,
75+ { value : "wildfire" , label : "Wildfire" } ,
76+ { value : "hurricane" , label : "Hurricane" } ,
77+ { value : "tornado" , label : "Tornado" } ,
78+ { value : "tsunami" , label : "Tsunami" } ,
79+ { value : "volcano" , label : "Volcanic Activity" } ,
80+ ] ;
81+
82+ const RANDOM_DESCRIPTIONS : Record < string , string [ ] > = {
83+ earthquake : [
84+ "Seismic activity detected. Residents advised to take shelter." ,
85+ "Earthquake warning issued. Secure loose objects and stay away from windows." ,
86+ "Ground tremors reported. Emergency services on standby." ,
87+ ] ,
88+ flood : [
89+ "Flash flood warning in effect. Move to higher ground immediately." ,
90+ "Rising water levels detected. Evacuations may be necessary." ,
91+ "Heavy rainfall causing flooding in low-lying areas." ,
92+ ] ,
93+ wildfire : [
94+ "Wildfire spreading rapidly. Evacuation orders in effect." ,
95+ "Brush fire reported. Air quality advisory issued." ,
96+ "Fire danger extreme. Avoid outdoor burning." ,
97+ ] ,
98+ hurricane : [
99+ "Hurricane approaching. Secure property and prepare emergency supplies." ,
100+ "Tropical storm intensifying. Coastal areas should prepare for impact." ,
101+ "Hurricane warning issued. Evacuate if in flood-prone areas." ,
102+ ] ,
103+ tornado : [
104+ "Tornado warning issued. Seek shelter immediately in interior room." ,
105+ "Severe thunderstorm with tornado potential. Stay alert." ,
106+ "Funnel cloud spotted. Take cover now." ,
107+ ] ,
108+ tsunami : [
109+ "Tsunami warning. Move to high ground immediately." ,
110+ "Coastal evacuation ordered due to tsunami threat." ,
111+ "Seismic event may trigger tsunami. Stay away from beaches." ,
112+ ] ,
113+ volcano : [
114+ "Volcanic activity increasing. Ash fall possible." ,
115+ "Eruption imminent. Evacuate danger zone immediately." ,
116+ "Volcanic alert level raised. Monitor official channels." ,
117+ ] ,
118+ } ;
119+
42120 useEffect ( ( ) => {
43121 const checkAuth = async ( ) => {
44122 const token = localStorage . getItem ( 'admin_token' ) || sessionStorage . getItem ( 'admin_token' ) ;
@@ -124,6 +202,74 @@ export default function AdminDashboard() {
124202 router . push ( '/admin/login' ) ;
125203 } ;
126204
205+ const openAlertDialog = async ( ) => {
206+ setAlertDialogOpen ( true ) ;
207+ setAlertError ( null ) ;
208+ setAlertSuccess ( null ) ;
209+
210+ try {
211+ const response = await listUsers ( { page_size : 100 } ) ;
212+ setAllUsers ( response . users ) ;
213+ if ( response . users . length > 0 ) {
214+ setSelectedUserId ( response . users [ 0 ] . id ) ;
215+ }
216+ } catch ( err ) {
217+ console . error ( "Failed to fetch users:" , err ) ;
218+ }
219+
220+ const randomType = DISASTER_TYPES [ Math . floor ( Math . random ( ) * DISASTER_TYPES . length ) ] . value ;
221+ const descriptions = RANDOM_DESCRIPTIONS [ randomType ] ;
222+ const randomDesc = descriptions [ Math . floor ( Math . random ( ) * descriptions . length ) ] ;
223+
224+ setAlertForm ( {
225+ disaster_type : randomType ,
226+ severity : Math . floor ( Math . random ( ) * 2 ) + 4 ,
227+ description : randomDesc ,
228+ } ) ;
229+ } ;
230+
231+ const handleSendTestAlert = async ( ) => {
232+ if ( ! selectedUserId ) {
233+ setAlertError ( "Please select a user" ) ;
234+ return ;
235+ }
236+
237+ setSendingAlert ( true ) ;
238+ setAlertError ( null ) ;
239+
240+ try {
241+ const result = await triggerTestAlert ( {
242+ user_id : selectedUserId ,
243+ disaster_type : alertForm . disaster_type ,
244+ severity : alertForm . severity ,
245+ description : alertForm . description ,
246+ send_email : true ,
247+ } ) ;
248+
249+ setAlertSuccess ( `Alert sent to ${ result . user_email } at ${ result . location } ` ) ;
250+ setTimeout ( ( ) => {
251+ setAlertDialogOpen ( false ) ;
252+ setAlertSuccess ( null ) ;
253+ } , 2000 ) ;
254+ } catch ( err ) {
255+ setAlertError ( err instanceof Error ? err . message : "Failed to send alert" ) ;
256+ } finally {
257+ setSendingAlert ( false ) ;
258+ }
259+ } ;
260+
261+ const randomizeAlert = ( ) => {
262+ const randomType = DISASTER_TYPES [ Math . floor ( Math . random ( ) * DISASTER_TYPES . length ) ] . value ;
263+ const descriptions = RANDOM_DESCRIPTIONS [ randomType ] ;
264+ const randomDesc = descriptions [ Math . floor ( Math . random ( ) * descriptions . length ) ] ;
265+
266+ setAlertForm ( {
267+ disaster_type : randomType ,
268+ severity : Math . floor ( Math . random ( ) * 2 ) + 4 ,
269+ description : randomDesc ,
270+ } ) ;
271+ } ;
272+
127273 const formatLastLogin = ( timeStr : string | null ) => {
128274 if ( ! timeStr ) return "Never" ;
129275 return formatActivityTime ( timeStr ) ;
@@ -575,17 +721,6 @@ export default function AdminDashboard() {
575721 < div className = "font-medium" > Manage Users</ div >
576722 < div className = "text-xs text-muted-foreground" > View and manage all users</ div >
577723 </ div >
578- </ Button >
579- < Button
580- className = "justify-start h-auto py-3"
581- variant = "outline"
582- onClick = { ( ) => router . push ( '/admin' ) }
583- >
584- < Settings className = "mr-2 h-4 w-4" />
585- < div className = "text-left" >
586- < div className = "font-medium" > Admin Settings</ div >
587- < div className = "text-xs text-muted-foreground" > Configure system settings</ div >
588- </ div >
589724 </ Button >
590725 < Button
591726 className = "justify-start h-auto py-3"
@@ -609,10 +744,152 @@ export default function AdminDashboard() {
609744 < div className = "text-xs text-muted-foreground" > Developer tools and utilities</ div >
610745 </ div >
611746 </ Button >
747+ < Button
748+ className = "justify-start h-auto py-3 border-amber-500/50 hover:bg-amber-500/10"
749+ variant = "outline"
750+ onClick = { openAlertDialog }
751+ >
752+ < Bell className = "mr-2 h-4 w-4 text-amber-600" />
753+ < div className = "text-left" >
754+ < div className = "font-medium" > Send Test Alert</ div >
755+ < div className = "text-xs text-muted-foreground" > Trigger a test alert for a user</ div >
756+ </ div >
757+ </ Button >
612758 </ div >
613759 </ CardContent >
614760 </ Card >
615761 </ div >
762+
763+ { alertSuccess && (
764+ < div className = "fixed bottom-4 right-4 p-4 bg-green-500/10 border border-green-500/20 rounded-lg flex items-center gap-2 text-green-700 dark:text-green-400 shadow-lg z-50" >
765+ < CheckCircle2 className = "h-4 w-4" />
766+ < span > { alertSuccess } </ span >
767+ </ div >
768+ ) }
769+
770+ < Dialog open = { alertDialogOpen } onOpenChange = { setAlertDialogOpen } >
771+ < DialogContent className = "sm:max-w-[500px]" >
772+ < DialogHeader >
773+ < DialogTitle className = "flex items-center gap-2" >
774+ < Bell className = "h-5 w-5 text-amber-600" />
775+ Send Test Alert
776+ </ DialogTitle >
777+ < DialogDescription >
778+ Create a test disaster and send an alert notification to a user.
779+ </ DialogDescription >
780+ </ DialogHeader >
781+
782+ { alertError && (
783+ < div className = "p-3 bg-destructive/10 border border-destructive/20 rounded-lg text-destructive text-sm" >
784+ { alertError }
785+ </ div >
786+ ) }
787+
788+ { alertSuccess ? (
789+ < div className = "p-4 bg-green-500/10 border border-green-500/20 rounded-lg text-green-700 dark:text-green-400 text-center" >
790+ < CheckCircle2 className = "h-8 w-8 mx-auto mb-2" />
791+ < p className = "font-medium" > { alertSuccess } </ p >
792+ </ div >
793+ ) : (
794+ < div className = "space-y-4" >
795+ < div >
796+ < Label > Select User</ Label >
797+ < Select value = { selectedUserId } onValueChange = { setSelectedUserId } >
798+ < SelectTrigger >
799+ < SelectValue placeholder = "Select a user..." />
800+ </ SelectTrigger >
801+ < SelectContent >
802+ { allUsers . map ( ( user ) => (
803+ < SelectItem key = { user . id } value = { user . id } >
804+ { user . email } { user . location ? `(${ user . location } )` : "(No location)" }
805+ </ SelectItem >
806+ ) ) }
807+ </ SelectContent >
808+ </ Select >
809+ </ div >
810+
811+ < div className = "grid grid-cols-2 gap-4" >
812+ < div >
813+ < Label > Disaster Type</ Label >
814+ < Select
815+ value = { alertForm . disaster_type }
816+ onValueChange = { ( v ) => {
817+ const descriptions = RANDOM_DESCRIPTIONS [ v ] ;
818+ const randomDesc = descriptions [ Math . floor ( Math . random ( ) * descriptions . length ) ] ;
819+ setAlertForm ( { ...alertForm , disaster_type : v , description : randomDesc } ) ;
820+ } }
821+ >
822+ < SelectTrigger >
823+ < SelectValue />
824+ </ SelectTrigger >
825+ < SelectContent >
826+ { DISASTER_TYPES . map ( ( type ) => (
827+ < SelectItem key = { type . value } value = { type . value } >
828+ { type . label }
829+ </ SelectItem >
830+ ) ) }
831+ </ SelectContent >
832+ </ Select >
833+ </ div >
834+ < div >
835+ < Label > Severity</ Label >
836+ < Select
837+ value = { alertForm . severity . toString ( ) }
838+ onValueChange = { ( v ) => setAlertForm ( { ...alertForm , severity : parseInt ( v ) } ) }
839+ >
840+ < SelectTrigger >
841+ < SelectValue />
842+ </ SelectTrigger >
843+ < SelectContent >
844+ < SelectItem value = "3" > 3 - Significant</ SelectItem >
845+ < SelectItem value = "4" > 4 - Severe</ SelectItem >
846+ < SelectItem value = "5" > 5 - Critical</ SelectItem >
847+ </ SelectContent >
848+ </ Select >
849+ </ div >
850+ </ div >
851+
852+ < div >
853+ < Label > Description</ Label >
854+ < Input
855+ value = { alertForm . description }
856+ onChange = { ( e ) => setAlertForm ( { ...alertForm , description : e . target . value } ) }
857+ placeholder = "Alert description..."
858+ />
859+ </ div >
860+
861+ < Button variant = "ghost" size = "sm" onClick = { randomizeAlert } className = "w-full" >
862+ 🎲 Randomize Alert
863+ </ Button >
864+ </ div >
865+ ) }
866+
867+ < DialogFooter >
868+ < Button variant = "outline" onClick = { ( ) => setAlertDialogOpen ( false ) } disabled = { sendingAlert } >
869+ Cancel
870+ </ Button >
871+ { ! alertSuccess && (
872+ < Button
873+ onClick = { handleSendTestAlert }
874+ disabled = { sendingAlert || ! selectedUserId }
875+ className = "bg-amber-600 hover:bg-amber-700"
876+ >
877+ { sendingAlert ? (
878+ < >
879+ < Loader2 className = "mr-2 h-4 w-4 animate-spin" />
880+ Sending...
881+ </ >
882+ ) : (
883+ < >
884+ < Bell className = "mr-2 h-4 w-4" />
885+ Send Alert
886+ </ >
887+ ) }
888+ </ Button >
889+ ) }
890+ </ DialogFooter >
891+ </ DialogContent >
892+ </ Dialog >
616893 </ main >
617894 </ div >
618895 ) ;
0 commit comments