Skip to content

Commit 3a914c6

Browse files
committed
Merge branch 'module/settings' into develop
2 parents 0b20b56 + 65fc6d4 commit 3a914c6

File tree

4 files changed

+338
-1
lines changed

4 files changed

+338
-1
lines changed
Lines changed: 291 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,291 @@
1+
"use client";
2+
3+
import { useEffect, useState } from "react";
4+
import { Button } from "@/components/ui/button";
5+
import {
6+
Select,
7+
SelectTrigger,
8+
SelectValue,
9+
SelectContent,
10+
SelectItem,
11+
} from "@/components/ui/select";
12+
import { Switch } from "@/components/ui/switch";
13+
import {
14+
Table,
15+
TableBody,
16+
TableCell,
17+
TableHead,
18+
TableHeader,
19+
TableRow,
20+
} from "@/components/ui/table";
21+
22+
// ---------- type (ประกาศไว้นอก component) ----------
23+
// ให้ตรงกับ JSON ของ /api/events/global
24+
type Event = {
25+
event_id: number; // ⬅ จาก backend เป็นเลข (1, 2, 3 ...)
26+
event_name: string;
27+
icon_name: string;
28+
description: string;
29+
status: boolean;
30+
created_at: string;
31+
sensitivity: string;
32+
priority: string;
33+
is_use: boolean;
34+
};
35+
36+
// กล้อง 1 ตัว ที่อยู่ใน event นั้น ๆ
37+
// ให้ event_id เป็น number ให้ตรงกับ DB
38+
type CameraInEvent = {
39+
event_id: number;
40+
camera_id: number;
41+
camera_name: string;
42+
camera_status: boolean;
43+
};
44+
45+
// แถวที่ใช้แสดงในตาราง = event + กล้องที่เกี่ยวข้อง
46+
type RowData = Event & {
47+
cameras: CameraInEvent[];
48+
};
49+
50+
// ---------- URL API ----------
51+
const EVENT_API = "/api/events/global";
52+
const EVENT_IN_CAMERAS_API = (evtId: number) =>
53+
`/api/events/${evtId}/event-in-cameras`;
54+
55+
const MAX_CAMERAS_DISPLAY = 7; // แสดงกล้องกี่ตัวก่อน แล้วค่อยเป็น ...
56+
57+
export default function DefaultEvent() {
58+
const [rows, setRows] = useState<RowData[]>([]);
59+
const [loading, setLoading] = useState(true);
60+
const [error, setError] = useState<string | null>(null);
61+
const [expandedEventId, setExpandedEventId] = useState<number | null>(null);
62+
63+
// --------- ดึง Event แล้วดึงกล้องของแต่ละ Event ---------
64+
useEffect(() => {
65+
const fetchData = async () => {
66+
try {
67+
setLoading(true);
68+
69+
70+
const eventRes = await fetch(EVENT_API);
71+
const eventJson = await eventRes.json();
72+
73+
if (!eventRes.ok) {
74+
throw new Error("โหลดข้อมูล Event ไม่สำเร็จ");
75+
}
76+
77+
78+
const events: Event[] = eventJson.data;
79+
80+
81+
const merged: RowData[] = await Promise.all(
82+
events.map(async (ev) => {
83+
try {
84+
const camRes = await fetch(EVENT_IN_CAMERAS_API(ev.event_id));
85+
const camJson = await camRes.json();
86+
87+
// สมมติ backend ส่ง { message, data: [...] }
88+
const cameras: CameraInEvent[] = camJson.data ?? [];
89+
90+
return { ...ev, cameras };
91+
} catch (e) {
92+
console.error("โหลดกล้องล้มเหลว event:", ev.event_id, e);
93+
return { ...ev, cameras: [] };
94+
}
95+
})
96+
);
97+
98+
setRows(merged);
99+
} catch (err: any) {
100+
console.error(err);
101+
setError(err.message || "เกิดข้อผิดพลาด");
102+
} finally {
103+
setLoading(false);
104+
}
105+
};
106+
107+
fetchData();
108+
}, []);
109+
110+
// --------- อัปเดตค่า sensitivity / priority / is_use กลับไปที่ API ---------
111+
const handleUpdate = async (
112+
eventId: number,
113+
data: Partial<Pick<Event, "sensitivity" | "priority" | "is_use">>
114+
) => {
115+
116+
setRows((prev) =>
117+
prev.map((row) => (row.event_id === eventId ? { ...row, ...data } : row))
118+
);
119+
120+
try {
121+
await fetch(`${EVENT_API}/${eventId}`, {
122+
method: "PATCH",
123+
headers: { "Content-Type": "application/json" },
124+
body: JSON.stringify(data),
125+
});
126+
} catch (err) {
127+
console.error("Update failed", err);
128+
129+
}
130+
};
131+
132+
if (loading) return <div className="p-4">กำลังโหลดข้อมูล...</div>;
133+
if (error)
134+
return <div className="p-4 text-red-500">เกิดข้อผิดพลาด: {error}</div>;
135+
136+
return (
137+
<div className="rounded-2xl border bg-white p-4 shadow-sm">
138+
<h2 className="mb-3 text-lg font-semibold text-blue-500">Default Event</h2>
139+
140+
<Table>
141+
<TableHeader>
142+
<TableRow className="border-b border-blue-400">
143+
<TableHead className="px-4 py-2 text-sm font-semibold text-blue-500 relative pr-3 after:absolute after:right-0 after:top-1/2 after:-translate-y-1/2 after:content-[''] after:w-px after:h-4 after:bg-blue-500">
144+
Event ID
145+
</TableHead>
146+
<TableHead className="text-sm font-semibold text-blue-500 relative pr-3 after:absolute after:right-0 after:top-1/2 after:-translate-y-1/2 after:content-[''] after:w-px after:h-4 after:bg-blue-500">
147+
Event Name
148+
</TableHead>
149+
<TableHead className="text-sm font-semibold text-blue-500 relative pr-3 after:absolute after:right-0 after:top-1/2 after:-translate-y-1/2 after:content-[''] after:w-px after:h-4 after:bg-blue-500">
150+
Created at
151+
</TableHead>
152+
<TableHead className="text-sm font-semibold text-blue-500 relative pr-3 after:absolute after:right-0 after:top-1/2 after:-translate-y-1/2 after:content-[''] after:w-px after:h-4 after:bg-blue-500">
153+
Sensitivity
154+
</TableHead>
155+
<TableHead className="text-sm font-semibold text-blue-500 relative pr-3 after:absolute after:right-0 after:top-1/2 after:-translate-y-1/2 after:content-[''] after:w-px after:h-4 after:bg-blue-500">
156+
Priority
157+
</TableHead>
158+
<TableHead className="px-4 py-2 text-sm font-semibold text-blue-500 relative pr-3 after:absolute after:right-0 after:top-1/2 after:-translate-y-1/2 after:content-[''] after:w-px after:h-4 after:bg-blue-500">
159+
Status
160+
</TableHead>
161+
<TableHead className="text-sm font-semibold text-blue-500 ">
162+
Active Event in Cameras
163+
</TableHead>
164+
</TableRow>
165+
</TableHeader>
166+
167+
<TableBody>
168+
{rows.map((row) => {
169+
const isExpanded = expandedEventId === row.event_id;
170+
const visibleCams = isExpanded
171+
? row.cameras
172+
: row.cameras.slice(0, MAX_CAMERAS_DISPLAY);
173+
174+
const remaining = row.cameras.length - visibleCams.length;
175+
return (
176+
<TableRow
177+
key={row.event_id}
178+
className="border-b border-gray-100 hover:bg-gray-50/60"
179+
>
180+
{/* Event ID */}
181+
<TableCell className="px-4 py-3 text-sm font-medium ">
182+
{row.event_id}
183+
</TableCell>
184+
185+
{/* Event Name */}
186+
<TableCell className="text-sm">{row.event_name}</TableCell>
187+
188+
{/* Created at */}
189+
<TableCell className="text-sm text-gray-500 ">
190+
{row.created_at}
191+
</TableCell>
192+
193+
{/* Sensitivity (Select) */}
194+
<TableCell>
195+
<Select
196+
value={row.sensitivity}
197+
onValueChange={(value) =>
198+
handleUpdate(row.event_id, { sensitivity: value })
199+
}
200+
>
201+
<SelectTrigger className="h-8 w-[110px] rounded-full border-1 border-red-500 bg-red-100 text-xs font-medium text-red-600 px-3">
202+
<SelectValue />
203+
</SelectTrigger>
204+
<SelectContent>
205+
<SelectItem value="critical">Critical</SelectItem>
206+
<SelectItem value="high">High</SelectItem>
207+
<SelectItem value="medium">Medium</SelectItem>
208+
<SelectItem value="low">Low</SelectItem>
209+
</SelectContent>
210+
</Select>
211+
</TableCell>
212+
213+
{/* Priority (Select) */}
214+
<TableCell>
215+
<Select
216+
value={row.priority}
217+
onValueChange={(value) =>
218+
handleUpdate(row.event_id, { priority: value })
219+
}
220+
>
221+
<SelectTrigger className="h-8 w-[110px] rounded-full border-1 border-yellow-400 bg-yellow-50 text-xs font-medium text-yellow-700 px-3">
222+
<SelectValue />
223+
</SelectTrigger>
224+
<SelectContent>
225+
<SelectItem value="high">High</SelectItem>
226+
<SelectItem value="medium">Medium</SelectItem>
227+
<SelectItem value="low">Low</SelectItem>
228+
</SelectContent>
229+
</Select>
230+
</TableCell>
231+
232+
{/* Status (Switch) */}
233+
<TableCell className="px-4 py-3 text-sm font-medium">
234+
<Switch
235+
checked={row.is_use}
236+
onCheckedChange={(checked) =>
237+
handleUpdate(row.event_id, { is_use: checked })
238+
}
239+
/>
240+
</TableCell>
241+
242+
{/* Active Event in Cameras */}
243+
<TableCell>
244+
<div className="flex flex-wrap gap-1">
245+
{visibleCams.map((cam) => (
246+
<Button
247+
key={cam.camera_id}
248+
variant="outline"
249+
size="sm"
250+
className={
251+
"h-6 rounded-full border px-2 text-[10px] " +
252+
(cam.camera_status
253+
? "border-blue-300 text-blue-500 hover:bg-transparent hover:text-blue-500"
254+
: "border-red-300 bg-red-50 text-red-500 hover:bg-red-50 hover:text-red-500")
255+
}
256+
>
257+
{cam.camera_name}
258+
</Button>
259+
))}
260+
{remaining > 0 && !isExpanded && (
261+
<Button
262+
variant="outline"
263+
size="sm"
264+
className="h-6 rounded-full border-blue-200 px-2 text-[10px] text-blue-400"
265+
onClick={() => setExpandedEventId(row.event_id)}
266+
>
267+
...
268+
</Button>
269+
)}
270+
271+
{isExpanded && row.cameras.length > MAX_CAMERAS_DISPLAY && (
272+
<Button
273+
variant="outline"
274+
size="sm"
275+
className="h-6 rounded-full border-blue-200 px-2 text-[10px] text-blue-400"
276+
onClick={() => setExpandedEventId(null)}
277+
>
278+
hide
279+
</Button>
280+
)}
281+
282+
</div>
283+
</TableCell>
284+
</TableRow>
285+
);
286+
})}
287+
</TableBody>
288+
</Table>
289+
</div>
290+
);
291+
}

server/src/controllers/events.controller.ts

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -201,7 +201,7 @@ export async function getGlobalEventById(req: Request, res: Response, next: Next
201201
} catch (err) {
202202
next(err);
203203
}
204-
};
204+
}
205205

206206
/**
207207
* อัปเดตการตั้งค่าการตรวจจับระดับ Global (GDS) ของเหตุการณ์ที่ระบุ
@@ -235,5 +235,27 @@ export async function updateGlobalEvent(req: Request, res: Response, next: NextF
235235
} catch (err) {
236236
next(err);
237237
}
238+
}
238239

240+
/**
241+
* ดึงรายการกล้องทั้งหมดที่ตรวจพบเหตุการณ์ตามรหัสที่ระบุ
242+
*
243+
* @param {Request} req - Express Request ที่มี evt_id ใน params
244+
* @param {Response} res - Express Response
245+
* @param {NextFunction} next - ฟังก์ชันส่งต่อข้อผิดพลาดให้ middleware ถัดไป
246+
* @returns {Promise<Response>} ข้อมูลรายการกล้องที่ตรวจพบเหตุการณ์
247+
* @throws {Error} หากเกิดข้อผิดพลาดในการดึงข้อมูล
248+
*
249+
* @author Wongsakon
250+
* @lastModified 2025-12-04
251+
*/
252+
export async function getEventInCamerasById(req: Request, res: Response, next: NextFunction) {
253+
try {
254+
const event_id = Number(req.params.evt_id);
255+
256+
const event = await EventService.getEventInCamerasById(event_id);
257+
return res.status(200).json({ message: 'Fetched successfully', data: event });
258+
} catch (err) {
259+
next(err);
260+
}
239261
}

server/src/routes/events.routes.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ router.put("/:evt_id/global", ctrl.updateGlobalEvent);
3737
/* ========================== Events ========================== */
3838
router.get("/", ctrl.getEvents);
3939
router.post("/", ctrl.createEvent);
40+
router.get('/:evt_id/event-in-cameras', ctrl.getEventInCamerasById);
4041
router.get("/:evt_id", ctrl.getEventById);
4142
router.put("/:evt_id", ctrl.updateEvent);
4243
router.patch("/:evt_id", ctrl.softDeleteEvent);

server/src/services/events.service.ts

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -348,4 +348,27 @@ export async function updateGlobalEvent(
348348
);
349349

350350
return Mapping.mapGlobalEventsToSaveResponse(rows[0]);
351+
}
352+
353+
/**
354+
* ดึงรายการข้อมูลกล้องทั้งหมดที่ตรวจพบเหตุการณ์ (event_id) ที่ระบุ
355+
* ข้อมูลที่ได้รวมรายละเอียดของเหตุการณ์และกล้องที่ตรวจพบเข้าไว้ด้วยกัน
356+
*
357+
* @param {number} event_id - รหัสเหตุการณ์ที่ต้องการดึง
358+
* @returns {Promise<Model.EventInCameras>} รายการกล้องที่ตรวจพบเหตุการณ์ที่กำหนด (หรือรายการว่างหากไม่พบ)
359+
* @throws {Error} หากเกิดข้อผิดพลาดระหว่างการดึงข้อมูลจากฐานข้อมูล
360+
*
361+
* @author Wongsakon
362+
* @lastModified 2025-12-04
363+
*/
364+
export async function getEventInCamerasById(event_id: number) {
365+
const { rows } = await pool.query(
366+
`
367+
SELECT * FROM v_events_in_cameras
368+
WHERE event_id = $1;
369+
`,
370+
[event_id]
371+
);
372+
373+
return rows;
351374
}

0 commit comments

Comments
 (0)