1+ "use client" ;
2+ import { useState , useEffect } from "react" ;
3+ import {
4+ AlertDialog ,
5+ AlertDialogContent ,
6+ AlertDialogHeader ,
7+ AlertDialogTitle ,
8+ AlertDialogDescription ,
9+ AlertDialogFooter ,
10+ AlertDialogCancel ,
11+ } from "@/components/ui/alert-dialog" ;
12+ import { Button } from "@/components/ui/button" ;
13+ import { Camera } from "@/app/models/cameras.model" ;
14+
15+ /* ✅ ใช้ shadcn Select เพื่อให้สไตล์เดียวกัน */
16+ import {
17+ Select ,
18+ SelectTrigger ,
19+ SelectValue ,
20+ SelectContent ,
21+ SelectItem ,
22+ } from "@/components/ui/select" ;
23+
24+ /* ✅ ไอคอน lucide แบบไดนามิก */
25+ import * as Lucide from "lucide-react" ;
26+
27+ type Props = {
28+ camera : Camera ;
29+ open : boolean ;
30+ setOpen : React . Dispatch < React . SetStateAction < boolean > > ;
31+ } ;
32+
33+ type EventItem = { id : number ; name : string ; icon ?: string } ;
34+
35+ type AlertForm = {
36+ severity : "Critical" | "High" | "Medium" | "Low" ;
37+ eventId : string ;
38+ description : string ;
39+ } ;
40+
41+ export default function EditCameraModal ( { camera, open, setOpen } : Props ) {
42+ const [ events , setEvents ] = useState < EventItem [ ] > ( [ ] ) ;
43+ const [ loadingEvents , setLoadingEvents ] = useState ( false ) ;
44+ const [ errMsg , setErrMsg ] = useState < string | null > ( null ) ;
45+
46+ const [ form , setForm ] = useState < AlertForm > ( {
47+ severity : "High" ,
48+ eventId : "" ,
49+ description : "" ,
50+ } ) ;
51+
52+ // ช่วย normalize ชื่อไอคอน lucide
53+ function normalizeIconName ( name ?: string ) {
54+ if ( ! name || typeof name !== "string" ) return undefined ;
55+ const cleaned = name
56+ . replace ( / [ - _ ] + ( \w ) / g, ( _ , c ) => ( c ? c . toUpperCase ( ) : "" ) )
57+ . replace ( / ^ [ a - z ] / , ( c ) => c . toUpperCase ( ) ) ;
58+ const alias : Record < string , string > = {
59+ TriangleAlert : "AlertTriangle" ,
60+ Alert : "AlertCircle" ,
61+ Cam : "Camera" ,
62+ } ;
63+ return alias [ cleaned ] ?? cleaned ;
64+ }
65+
66+ useEffect ( ( ) => {
67+ if ( ! open ) return ;
68+ setLoadingEvents ( true ) ;
69+ setErrMsg ( null ) ;
70+
71+ fetch ( "/api/events" )
72+ . then ( ( res ) => {
73+ if ( ! res . ok ) throw new Error ( `HTTP ${ res . status } ` ) ;
74+ return res . json ( ) ;
75+ } )
76+ . then ( ( data : any [ ] ) => {
77+ const normalized : EventItem [ ] = ( Array . isArray ( data ) ? data : [ ] )
78+ . map ( ( e : any ) => ( {
79+ id : e ?. id ?? e ?. evt_id ?? e ?. event_id ,
80+ name : e ?. name ?? e ?. evt_name ?? e ?. event_name ,
81+ icon : e ?. icon ?? e ?. evt_icon ?? e ?. event_icon ,
82+ } ) )
83+ . filter ( ( x : EventItem ) => Number . isFinite ( x . id ) && ! ! x . name ) ;
84+
85+ const uniq = [ ...new Map ( normalized . map ( ( v ) => [ v . id , v ] ) ) . values ( ) ] ;
86+ setEvents ( uniq ) ;
87+ } )
88+ . catch ( ( err ) => {
89+ console . error ( "Load events failed:" , err ) ;
90+ setErrMsg ( "Cannot load events." ) ;
91+ } )
92+ . finally ( ( ) => setLoadingEvents ( false ) ) ;
93+ } , [ open ] ) ;
94+
95+ const camCode = `CAM${ String ( camera . id ) . padStart ( 3 , "0" ) } ` ;
96+
97+ function onChange < K extends keyof AlertForm > ( key : K , value : AlertForm [ K ] ) {
98+ setForm ( ( prev ) => ( { ...prev , [ key ] : value } ) ) ;
99+ }
100+
101+ async function handleSubmit ( e : React . FormEvent ) {
102+ e . preventDefault ( ) ;
103+ setErrMsg ( null ) ;
104+
105+ if ( ! form . eventId ) {
106+ setErrMsg ( "Please choose an event." ) ;
107+ return ;
108+ }
109+
110+ const payload = {
111+ severity : form . severity ,
112+ camera_id : Number ( camera . id ) ,
113+ footage_id : 1 ,
114+ event_id : Number ( form . eventId ) ,
115+ description : form . description ?. trim ( ) ?? "" ,
116+ } ;
117+
118+ try {
119+ const res = await fetch ( `/api/alerts` , {
120+ method : "POST" ,
121+ headers : { "Content-Type" : "application/json" } ,
122+ body : JSON . stringify ( payload ) ,
123+ } ) ;
124+
125+ if ( ! res . ok ) {
126+ const data = await res . json ( ) . catch ( ( ) => ( { } ) ) ;
127+ throw new Error ( data ?. message || `Create alert failed (${ res . status } )` ) ;
128+ }
129+
130+ setOpen ( false ) ;
131+ window . location . href = "/alerts" ;
132+ } catch ( err : any ) {
133+ console . error ( err ) ;
134+ setErrMsg ( err . message || "Create alert failed." ) ;
135+ }
136+ }
137+
138+ return (
139+ < AlertDialog open = { open } onOpenChange = { setOpen } >
140+ < AlertDialogContent className = "!top-[40%] !-translate-y-[40%]" >
141+ < AlertDialogHeader >
142+ < AlertDialogTitle className = "text-[var(--color-primary)]" >
143+ New Alert — { camera ?. name } ({ camCode } )
144+ </ AlertDialogTitle >
145+ < AlertDialogDescription >
146+ Linking to #{ camCode } . Set severity, category, and note, then Save.
147+ </ AlertDialogDescription >
148+ </ AlertDialogHeader >
149+
150+ < form onSubmit = { handleSubmit } className = "space-y-4 mt-4" >
151+ { /* Readonly camera info */ }
152+ < div className = "grid grid-cols-2 gap-2" >
153+ < div className = "grid gap-1" >
154+ < label className = "text-sm font-medium" > Camera Name</ label >
155+ < input
156+ value = { camera ?. name ?? "" }
157+ className = "font-light w-full rounded-md border px-3 py-2 outline-none border-[var(--color-primary)] text-[var(--color-primary)] bg-[var(--color-primary-bg)]"
158+ disabled
159+ />
160+ </ div >
161+ < div className = "grid gap-1" >
162+ < label className = "text-sm font-medium" > Location</ label >
163+ < input
164+ value = { ( camera as any ) ?. location ?. name ?? "" }
165+ className = "font-light w-full rounded-md border px-3 py-2 outline-none border-[var(--color-primary)] text-[var(--color-primary)] bg-[var(--color-primary-bg)]"
166+ disabled
167+ />
168+ </ div >
169+ </ div >
170+
171+ { /* ✅ Severity ใช้ shadcn Select (สไตล์เดียวกับ Event Type) */ }
172+ < div className = "grid grid-cols-3 gap-2" >
173+ < div className = "grid gap-1" >
174+ < label className = "text-sm font-medium" htmlFor = "severity" >
175+ Severity
176+ </ label >
177+ < Select
178+ value = { form . severity }
179+ onValueChange = { ( val ) =>
180+ onChange ( "severity" , val as AlertForm [ "severity" ] )
181+ }
182+ >
183+ < SelectTrigger className = "w-full" >
184+ < SelectValue placeholder = "Choose Severity" />
185+ </ SelectTrigger >
186+ < SelectContent >
187+ < SelectItem value = "Critical" > Critical</ SelectItem >
188+ < SelectItem value = "High" > High</ SelectItem >
189+ < SelectItem value = "Medium" > Medium</ SelectItem >
190+ < SelectItem value = "Low" > Low</ SelectItem >
191+ </ SelectContent >
192+ </ Select >
193+ </ div >
194+
195+ { /* ✅ Event Type + ไอคอนสี var(--color-primary) */ }
196+ < div className = "grid col-span-2 gap-1" >
197+ < label className = "text-sm font-medium" htmlFor = "event" >
198+ Event Type
199+ </ label >
200+ < Select
201+ value = { form . eventId }
202+ onValueChange = { ( val ) => onChange ( "eventId" , val ) }
203+ >
204+ < SelectTrigger className = "w-full overflow-hidden text-ellipsis whitespace-nowrap" >
205+ < SelectValue placeholder = { loadingEvents ? "Loading..." : "Choose Event Type" } />
206+ </ SelectTrigger >
207+
208+ < SelectContent className = "w-[var(--radix-select-trigger-width)] max-h-64" >
209+ { loadingEvents ? (
210+ < SelectItem value = "__loading" disabled >
211+ Loading...
212+ </ SelectItem >
213+ ) : events . length === 0 ? (
214+ < SelectItem value = "__empty" disabled >
215+ No events
216+ </ SelectItem >
217+ ) : (
218+ events . map ( ( evt ) => {
219+ const iconName = normalizeIconName ( evt . icon ) ;
220+ // @ts -ignore
221+ const IconComp = ( iconName && ( Lucide as any ) [ iconName ] ) || Lucide . Dot ;
222+ return (
223+ < SelectItem key = { evt . id } value = { String ( evt . id ) } textValue = { evt . name } >
224+ < div className = "flex min-w-0 items-center gap-2" >
225+ < IconComp className = "h-4 w-4 shrink-0 text-[var(--color-primary)]" />
226+ < span className = "truncate" title = { evt . name } > { evt . name } </ span >
227+ </ div >
228+ </ SelectItem >
229+ ) ;
230+ } )
231+ ) }
232+ </ SelectContent >
233+ </ Select >
234+ </ div >
235+ </ div >
236+
237+ { /* Description */ }
238+ < div className = "grid gap-1" >
239+ < label className = "text-sm font-medium" htmlFor = "description" >
240+ Description
241+ </ label >
242+ < textarea
243+ name = "description"
244+ placeholder = "Enter your description"
245+ className = "font-light w-full rounded-md border px-3 py-3 outline-none focus-within:ring focus-within:ring-[var(--color-primary)]"
246+ value = { form . description }
247+ onChange = { ( e ) => onChange ( "description" , e . target . value ) }
248+ />
249+ </ div >
250+
251+ { errMsg && < div className = "text-sm text-red-600 mt-2" > { errMsg } </ div > }
252+
253+ < AlertDialogFooter >
254+ < AlertDialogCancel className = "border-gray-300 hover:bg-gray-50" >
255+ Cancel
256+ </ AlertDialogCancel >
257+ < Button
258+ type = "submit"
259+ disabled = { ! form . eventId }
260+ className = "bg-[var(--color-primary)] text-white hover:bg-[var(--color-secondary)] px-4 py-2 rounded-md disabled:opacity-50"
261+ >
262+ Save
263+ </ Button >
264+ </ AlertDialogFooter >
265+ </ form >
266+ </ AlertDialogContent >
267+ </ AlertDialog >
268+ ) ;
269+ }
0 commit comments