1+ import { useState } from 'react' ;
2+ import { Button } from './ui/button' ;
3+ import { Textarea } from './ui/textarea' ;
4+ import { Badge } from './ui/badge' ;
5+ import { Card , CardContent , CardHeader , CardTitle } from './ui/card' ;
6+ import { Select , SelectContent , SelectItem , SelectTrigger , SelectValue } from './ui/select' ;
7+ import { Table , TableBody , TableCell , TableHead , TableHeader , TableRow } from './ui/table' ;
8+ import { AlertTriangle , Trash2 , BarChart3 , Bot } from 'lucide-react' ;
9+ import { Ticket } from './ReporterDashboard' ;
10+
11+ export function AdminPanel ( ) {
12+ const [ selectedTicket , setSelectedTicket ] = useState < Ticket | null > ( null ) ;
13+ const [ reviewComment , setReviewComment ] = useState ( '' ) ;
14+
15+ const [ tickets , setTickets ] = useState < Ticket [ ] > ( [
16+ {
17+ id : 'TCK-001' ,
18+ title : 'Misleading Health Claims in Advertisement' ,
19+ description : 'Found an ad making unverified health claims about supplements.' ,
20+ status : 'Resolved' ,
21+ lastUpdated : '2024-01-15' ,
22+ timeline : [
23+ { status : 'Received' , timestamp : '2024-01-10 09:30' , message : 'Report submitted successfully' , type : 'system' } ,
24+ { status : 'In Review' , timestamp : '2024-01-12 14:20' , message : 'Report assigned to reviewer team' , type : 'system' } ,
25+ { status : 'Resolved' , timestamp : '2024-01-15 16:45' , message : 'Advertisement removed and advertiser warned' , type : 'human' }
26+ ]
27+ } ,
28+ {
29+ id : 'TCK-002' ,
30+ title : 'Inappropriate Content Targeting' ,
31+ description : 'Adult content being shown to minors in gaming app.' ,
32+ status : 'In Review' ,
33+ lastUpdated : '2024-01-14' ,
34+ timeline : [
35+ { status : 'Received' , timestamp : '2024-01-13 16:20' , message : 'Report submitted successfully' , type : 'system' } ,
36+ { status : 'In Review' , timestamp : '2024-01-14 10:00' , message : 'Report under active investigation' , type : 'system' }
37+ ]
38+ } ,
39+ {
40+ id : 'TCK-003' ,
41+ title : 'Fraudulent Investment Scheme' ,
42+ description : 'Suspicious investment ad promising unrealistic returns.' ,
43+ status : 'Received' ,
44+ lastUpdated : '2024-01-14' ,
45+ timeline : [
46+ { status : 'Received' , timestamp : '2024-01-14 13:45' , message : 'Report submitted successfully' , type : 'system' }
47+ ]
48+ } ,
49+ {
50+ id : 'TCK-004' ,
51+ title : 'Fake Product Reviews' ,
52+ description : 'Multiple suspicious 5-star reviews posted on the same day.' ,
53+ status : 'Received' ,
54+ lastUpdated : '2024-01-13' ,
55+ timeline : [
56+ { status : 'Received' , timestamp : '2024-01-13 11:20' , message : 'Report submitted successfully' , type : 'system' }
57+ ]
58+ }
59+ ] ) ;
60+
61+ const getStatusColor = ( status : string ) => {
62+ switch ( status ) {
63+ case 'Resolved' : return 'bg-green-100 text-green-800 border-green-200' ;
64+ case 'In Review' : return 'bg-orange-100 text-orange-800 border-orange-200' ;
65+ case 'Received' : return 'bg-gray-100 text-gray-800 border-gray-200' ;
66+ default : return 'bg-gray-100 text-gray-800 border-gray-200' ;
67+ }
68+ } ;
69+
70+ const updateTicketStatus = ( status : string ) => {
71+ if ( ! selectedTicket ) return ;
72+
73+ const updatedTickets = tickets . map ( ticket => {
74+ if ( ticket . id === selectedTicket . id ) {
75+ const newTimeline = [ ...( ticket . timeline || [ ] ) ] ;
76+ newTimeline . push ( {
77+ status,
78+ timestamp : new Date ( ) . toISOString ( ) . replace ( 'T' , ' ' ) . slice ( 0 , 16 ) ,
79+ message : `Status updated to ${ status } ` ,
80+ type : 'system'
81+ } ) ;
82+
83+ return {
84+ ...ticket ,
85+ status : status as 'Received' | 'In Review' | 'Resolved' ,
86+ lastUpdated : new Date ( ) . toISOString ( ) . split ( 'T' ) [ 0 ] ,
87+ timeline : newTimeline
88+ } ;
89+ }
90+ return ticket ;
91+ } ) ;
92+
93+ setTickets ( updatedTickets ) ;
94+ setSelectedTicket ( updatedTickets . find ( t => t . id === selectedTicket . id ) || null ) ;
95+ } ;
96+
97+ const addReviewComment = ( ) => {
98+ if ( ! selectedTicket || ! reviewComment . trim ( ) ) return ;
99+
100+ const updatedTickets = tickets . map ( ticket => {
101+ if ( ticket . id === selectedTicket . id ) {
102+ const newTimeline = [ ...( ticket . timeline || [ ] ) ] ;
103+ newTimeline . push ( {
104+ status : ticket . status ,
105+ timestamp : new Date ( ) . toISOString ( ) . replace ( 'T' , ' ' ) . slice ( 0 , 16 ) ,
106+ message : reviewComment ,
107+ type : 'human'
108+ } ) ;
109+
110+ return {
111+ ...ticket ,
112+ timeline : newTimeline
113+ } ;
114+ }
115+ return ticket ;
116+ } ) ;
117+
118+ setTickets ( updatedTickets ) ;
119+ setSelectedTicket ( updatedTickets . find ( t => t . id === selectedTicket . id ) || null ) ;
120+ setReviewComment ( '' ) ;
121+ } ;
122+
123+ const generateAutoFeedback = ( ) => {
124+ const autoMessages = [
125+ "Automated review completed. No policy violations detected." ,
126+ "Content flagged for manual review due to sensitive keywords." ,
127+ "Similar reports consolidated. Pattern analysis in progress." ,
128+ "Advertiser notification sent. Awaiting response within 48 hours."
129+ ] ;
130+
131+ const randomMessage = autoMessages [ Math . floor ( Math . random ( ) * autoMessages . length ) ] ;
132+ setReviewComment ( randomMessage ) ;
133+ } ;
134+
135+ const triggerImpactEvent = ( impactType : string ) => {
136+ if ( ! selectedTicket ) return ;
137+
138+ // In a real app, this would trigger an actual impact event
139+ // For now, we'll just add it to the timeline
140+ const impactMessages = {
141+ 'ad-removed' : 'Advertisement removed from platform' ,
142+ 'advertiser-warned' : 'Advertiser received formal warning' ,
143+ 'account-suspended' : 'Advertiser account temporarily suspended'
144+ } ;
145+
146+ const message = impactMessages [ impactType as keyof typeof impactMessages ] || 'Impact action triggered' ;
147+
148+ const updatedTickets = tickets . map ( ticket => {
149+ if ( ticket . id === selectedTicket . id ) {
150+ const newTimeline = [ ...( ticket . timeline || [ ] ) ] ;
151+ newTimeline . push ( {
152+ status : ticket . status ,
153+ timestamp : new Date ( ) . toISOString ( ) . replace ( 'T' , ' ' ) . slice ( 0 , 16 ) ,
154+ message,
155+ type : 'system'
156+ } ) ;
157+
158+ return {
159+ ...ticket ,
160+ timeline : newTimeline
161+ } ;
162+ }
163+ return ticket ;
164+ } ) ;
165+
166+ setTickets ( updatedTickets ) ;
167+ setSelectedTicket ( updatedTickets . find ( t => t . id === selectedTicket . id ) || null ) ;
168+ } ;
169+
170+ const pendingTickets = tickets . filter ( t => t . status !== 'Resolved' ) ;
171+
172+ return (
173+ < div className = "flex-1" >
174+ { /* Header */ }
175+ < header className = "border-b bg-background p-6" >
176+ < h1 className = "text-2xl" > Admin Panel</ h1 >
177+ < p className = "text-muted-foreground" > Review and manage reported content</ p >
178+ </ header >
179+
180+ < div className = "flex h-full" >
181+ { /* Left Panel - Tickets List */ }
182+ < div className = "w-1/2 p-6 border-r" >
183+ < Card >
184+ < CardHeader >
185+ < CardTitle > Tickets Awaiting Review ({ pendingTickets . length } )</ CardTitle >
186+ </ CardHeader >
187+ < CardContent >
188+ < Table >
189+ < TableHeader >
190+ < TableRow >
191+ < TableHead > ID</ TableHead >
192+ < TableHead > Title</ TableHead >
193+ < TableHead > Status</ TableHead >
194+ < TableHead > Updated</ TableHead >
195+ </ TableRow >
196+ </ TableHeader >
197+ < TableBody >
198+ { pendingTickets . map ( ( ticket ) => (
199+ < TableRow
200+ key = { ticket . id }
201+ className = { `cursor-pointer hover:bg-muted/50 ${
202+ selectedTicket ?. id === ticket . id ? 'bg-muted' : ''
203+ } `}
204+ onClick = { ( ) => setSelectedTicket ( ticket ) }
205+ >
206+ < TableCell className = "font-mono text-sm" > { ticket . id } </ TableCell >
207+ < TableCell className = "max-w-xs truncate" > { ticket . title } </ TableCell >
208+ < TableCell >
209+ < Badge className = { getStatusColor ( ticket . status ) } >
210+ { ticket . status }
211+ </ Badge >
212+ </ TableCell >
213+ < TableCell className = "text-muted-foreground text-sm" > { ticket . lastUpdated } </ TableCell >
214+ </ TableRow >
215+ ) ) }
216+ </ TableBody >
217+ </ Table >
218+ </ CardContent >
219+ </ Card >
220+ </ div >
221+
222+ { /* Right Panel - Ticket Details & Actions */ }
223+ < div className = "w-1/2 p-6" >
224+ { selectedTicket ? (
225+ < div className = "space-y-6" >
226+ { /* Ticket Info */ }
227+ < Card >
228+ < CardHeader >
229+ < CardTitle className = "flex items-center justify-between" >
230+ { selectedTicket . title }
231+ < Badge className = { getStatusColor ( selectedTicket . status ) } >
232+ { selectedTicket . status }
233+ </ Badge >
234+ </ CardTitle >
235+ </ CardHeader >
236+ < CardContent >
237+ < p className = "text-sm text-muted-foreground mb-2" > ID: { selectedTicket . id } </ p >
238+ < p > { selectedTicket . description } </ p >
239+ </ CardContent >
240+ </ Card >
241+
242+ { /* Status Update */ }
243+ < Card >
244+ < CardHeader >
245+ < CardTitle > Update Status</ CardTitle >
246+ </ CardHeader >
247+ < CardContent className = "space-y-4" >
248+ < Select onValueChange = { updateTicketStatus } >
249+ < SelectTrigger >
250+ < SelectValue placeholder = "Select new status" />
251+ </ SelectTrigger >
252+ < SelectContent >
253+ < SelectItem value = "Received" > Received</ SelectItem >
254+ < SelectItem value = "In Review" > In Review</ SelectItem >
255+ < SelectItem value = "Resolved" > Resolved</ SelectItem >
256+ </ SelectContent >
257+ </ Select >
258+ </ CardContent >
259+ </ Card >
260+
261+ { /* Review Comment */ }
262+ < Card >
263+ < CardHeader >
264+ < CardTitle > Add Review Comment</ CardTitle >
265+ </ CardHeader >
266+ < CardContent className = "space-y-4" >
267+ < Textarea
268+ value = { reviewComment }
269+ onChange = { ( e ) => setReviewComment ( e . target . value ) }
270+ placeholder = "Add feedback or notes about this report"
271+ rows = { 3 }
272+ />
273+ < div className = "flex gap-2" >
274+ < Button onClick = { addReviewComment } disabled = { ! reviewComment . trim ( ) } >
275+ Add Comment
276+ </ Button >
277+ < Button variant = "outline" onClick = { generateAutoFeedback } >
278+ < Bot className = "mr-2 h-4 w-4" />
279+ Generate Auto Feedback
280+ </ Button >
281+ </ div >
282+ </ CardContent >
283+ </ Card >
284+
285+ { /* Impact Actions */ }
286+ < Card >
287+ < CardHeader >
288+ < CardTitle > Trigger Impact Event</ CardTitle >
289+ </ CardHeader >
290+ < CardContent >
291+ < div className = "grid grid-cols-2 gap-2" >
292+ < Button
293+ variant = "outline"
294+ onClick = { ( ) => triggerImpactEvent ( 'advertiser-warned' ) }
295+ className = "flex items-center justify-center"
296+ >
297+ < AlertTriangle className = "mr-2 h-4 w-4" />
298+ Warn Advertiser
299+ </ Button >
300+ < Button
301+ variant = "outline"
302+ onClick = { ( ) => triggerImpactEvent ( 'ad-removed' ) }
303+ className = "flex items-center justify-center"
304+ >
305+ < Trash2 className = "mr-2 h-4 w-4" />
306+ Remove Ad
307+ </ Button >
308+ < Button
309+ variant = "outline"
310+ onClick = { ( ) => triggerImpactEvent ( 'account-suspended' ) }
311+ className = "flex items-center justify-center col-span-2"
312+ >
313+ < BarChart3 className = "mr-2 h-4 w-4" />
314+ Suspend Account
315+ </ Button >
316+ </ div >
317+ </ CardContent >
318+ </ Card >
319+
320+ { /* Timeline */ }
321+ { selectedTicket . timeline && selectedTicket . timeline . length > 0 && (
322+ < Card >
323+ < CardHeader >
324+ < CardTitle > Timeline</ CardTitle >
325+ </ CardHeader >
326+ < CardContent >
327+ < div className = "space-y-3" >
328+ { selectedTicket . timeline . map ( ( event , index ) => (
329+ < div key = { index } className = "flex gap-3 p-3 bg-muted/30 rounded-lg" >
330+ < div className = "flex-1" >
331+ < p className = "text-sm" > { event . message } </ p >
332+ < p className = "text-xs text-muted-foreground mt-1" >
333+ { event . timestamp } • { event . type === 'system' ? 'System' : 'Human' }
334+ </ p >
335+ </ div >
336+ </ div >
337+ ) ) }
338+ </ div >
339+ </ CardContent >
340+ </ Card >
341+ ) }
342+ </ div >
343+ ) : (
344+ < Card >
345+ < CardContent className = "flex items-center justify-center h-64" >
346+ < p className = "text-muted-foreground" > Select a ticket to view details and actions</ p >
347+ </ CardContent >
348+ </ Card >
349+ ) }
350+ </ div >
351+ </ div >
352+ </ div >
353+ ) ;
354+ }
0 commit comments