Skip to content

Commit 5406ef3

Browse files
committed
feat(api): camera, alert logs
1 parent 409caad commit 5406ef3

File tree

10 files changed

+209
-35
lines changed

10 files changed

+209
-35
lines changed

client/my-app/src/app/components/Alerts/Details/Notes.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -371,7 +371,7 @@ export default function Notes({ alrId }: { alrId: number }) {
371371
</AlertDialog>
372372
</div>
373373

374-
{/* Table (เอากรอบตารางออก) */}
374+
{/* Table */}
375375
<Table className="w-full table-auto">
376376
<TableHeader>
377377
<TableRow className="border-b border-[var(--color-primary)]">
@@ -422,7 +422,7 @@ export default function Notes({ alrId }: { alrId: number }) {
422422
<TableCell className="py-3 font-medium text-black">{n.note_id}</TableCell>
423423

424424
<TableCell className="px-2 py-3 align-top text-left text-black">
425-
<BadgeUser username={userLabel || undefined} />
425+
<BadgeUser username={userLabel || undefined} role={n.creator_role} />
426426
</TableCell>
427427

428428
<TableCell className="px-2 py-3 align-top text-left whitespace-pre-wrap break-words max-w-[48rem] text-black">
@@ -459,7 +459,7 @@ export default function Notes({ alrId }: { alrId: number }) {
459459
</TableBody>
460460
</Table>
461461

462-
{/* Edit Note (AlertDialog, ไม่มีปุ่ม X) */}
462+
{/* Edit Note */}
463463
<AlertDialog open={openEdit} onOpenChange={setOpenEdit}>
464464
<AlertDialogContent className="sm:max-w-md [&>button:last-child]:hidden">
465465
<AlertDialogHeader>
Lines changed: 63 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,42 +1,90 @@
11
"use client";
22

3-
import { User as UserIcon } from "lucide-react";
3+
import {
4+
User as UserIcon,
5+
MonitorCog,
6+
ShieldAlert,
7+
ShieldCheck,
8+
Wrench,
9+
} from "lucide-react";
410
import React from "react";
511

612
type UserBadgeProps = {
713
username?: string | null;
14+
role?: string | null;
815
className?: string;
916
};
1017

1118
/**
12-
* แสดง badge ชื่อผู้ใช้พร้อมไอคอนด้านหน้า
13-
* ถ้า username เป็น "system" หรือ "System" จะใช้โทนสีม่วง
19+
* แสดง badge ชื่อผู้ใช้พร้อมไอคอนและสีตาม role
20+
* - System → ม่วง + MonitorCog
21+
* - Admin → แดง + ShieldAlert
22+
* - Security Team → เหลือง + ShieldCheck
23+
* - Staff → น้ำเงิน + Wrench
24+
* - อื่น ๆ → เขียว (ดีฟอลต์) + User
1425
*/
15-
export default function UserBadge({ username, className = "" }: UserBadgeProps) {
26+
export default function UserBadge({
27+
username,
28+
role,
29+
className = "",
30+
}: UserBadgeProps) {
1631
const name = (username ?? "").trim() || "unknown";
17-
const isSystem = name.toLowerCase() === "system";
32+
const roleName = (role ?? "").toLowerCase();
1833

19-
const palette = isSystem
20-
? {
34+
let palette = {
35+
border: "border-emerald-300",
36+
text: "text-emerald-700",
37+
bg: "bg-emerald-50",
38+
icon: "text-emerald-700",
39+
iconComponent: <UserIcon className="h-4 w-4 text-emerald-700" />,
40+
};
41+
42+
switch (roleName) {
43+
case "system":
44+
palette = {
2145
border: "border-purple-300",
2246
text: "text-purple-700",
2347
bg: "bg-purple-50",
2448
icon: "text-purple-700",
25-
}
26-
: {
27-
border: "border-emerald-300",
28-
text: "text-emerald-700",
29-
bg: "bg-emerald-50",
30-
icon: "text-emerald-700",
49+
iconComponent: <MonitorCog className="h-4 w-4 text-purple-700" />,
50+
};
51+
break;
52+
case "admin":
53+
palette = {
54+
border: "border-red-300",
55+
text: "text-red-700",
56+
bg: "bg-red-50",
57+
icon: "text-red-700",
58+
iconComponent: <ShieldAlert className="h-4 w-4 text-red-700" />,
59+
};
60+
break;
61+
case "security team":
62+
palette = {
63+
border: "border-amber-300",
64+
text: "text-amber-700",
65+
bg: "bg-amber-50",
66+
icon: "text-amber-700",
67+
iconComponent: <ShieldCheck className="h-4 w-4 text-amber-700" />,
68+
};
69+
break;
70+
case "staff":
71+
palette = {
72+
border: "border-blue-300",
73+
text: "text-blue-700",
74+
bg: "bg-blue-50",
75+
icon: "text-blue-700",
76+
iconComponent: <Wrench className="h-4 w-4 text-blue-700" />,
3177
};
78+
break;
79+
}
3280

3381
return (
3482
<span
3583
className={`inline-flex items-center gap-2 rounded-full px-3 py-1 text-sm font-medium
3684
border ${palette.border} ${palette.text} ${palette.bg} ${className}`}
3785
>
38-
<UserIcon className={`h-4 w-4 ${palette.icon}`} />
86+
{palette.iconComponent}
3987
<span className="opacity-80 text-xs">{name}</span>
4088
</span>
4189
);
42-
}
90+
}

client/my-app/src/app/components/History/CameraLogs.tsx

Whitespace-only changes.

client/my-app/src/app/components/Utilities/DropdownMenu.tsx

Lines changed: 29 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -15,17 +15,20 @@ import {
1515
export default function DropdownMenuDemo({ children }: { children: React.ReactNode }) {
1616
const [loading, setLoading] = useState(false);
1717

18-
// ---- detect platform เพื่อโชว์ปุ่มให้ตรง OS ----
18+
// ---- Detect platform เพื่อโชว์ปุ่มให้ตรง OS ----
1919
const isMac = typeof navigator !== "undefined"
2020
? /Mac|iPhone|iPad|iPod/.test(navigator.platform || "") ||
2121
/Mac OS/.test(navigator.userAgent || "")
2222
: false;
23-
const shortcutLabel = isMac ? "⌘B" : "Ctrl+B";
2423

25-
function handleAccount() {
24+
const shortcutSettings = isMac ? "⌘E" : "Ctrl+E";
25+
const shortcutHelp = isMac ? "⌘H" : "Ctrl+H";
26+
const shortcutLogout = isMac ? "⇧⌘Q" : "Ctrl+Shift+Q";
27+
28+
function handleSettings() {
2629
window.location.href = "/settings";
2730
}
28-
function handleSetting() {}
31+
2932
async function handleLogout() {
3033
if (loading) return;
3134
setLoading(true);
@@ -39,7 +42,8 @@ export default function DropdownMenuDemo({ children }: { children: React.ReactNo
3942
}
4043
}
4144

42-
// ---- คีย์ลัด: ⌘B (Mac) / Ctrl+B (Win/Linux) ----
45+
// ---- คีย์ลัด: ⌘B (Mac) / Ctrl+B → เปิด Settings ----
46+
// ---- คีย์ลัด: ⇧⌘Q (Mac) / Ctrl+Shift+Q → Logout ----
4347
useEffect(() => {
4448
const isEditable = (el: EventTarget | null) => {
4549
if (!(el instanceof HTMLElement)) return false;
@@ -53,41 +57,49 @@ export default function DropdownMenuDemo({ children }: { children: React.ReactNo
5357
};
5458

5559
const onKeyDown = (e: KeyboardEvent) => {
56-
// ข้ามถ้ากำลังพิมพ์ในช่องข้อความ
5760
if (isEditable(e.target)) return;
5861

5962
const key = e.key.toLowerCase();
6063
const metaOrCtrl = isMac ? e.metaKey : e.ctrlKey;
6164

62-
if (metaOrCtrl && key === "b") {
65+
// เปิดหน้า Settings (Settings)
66+
if (metaOrCtrl && !e.shiftKey && key === "e") {
67+
e.preventDefault();
68+
handleSettings();
69+
}
70+
71+
// Logout
72+
if (metaOrCtrl && e.shiftKey && key === "q") {
6373
e.preventDefault();
64-
handleAccount();
74+
handleLogout();
6575
}
6676
};
6777

6878
window.addEventListener("keydown", onKeyDown);
6979
return () => window.removeEventListener("keydown", onKeyDown);
70-
}, [isMac]);
80+
}, [isMac, loading]);
7181

7282
return (
7383
<DropdownMenu>
7484
<DropdownMenuTrigger asChild>{children}</DropdownMenuTrigger>
7585
<DropdownMenuContent className="w-56" align="end" sideOffset={8}>
76-
<DropdownMenuLabel>My Account</DropdownMenuLabel>
86+
<DropdownMenuLabel>Menu</DropdownMenuLabel>
7787
<DropdownMenuGroup>
78-
<DropdownMenuItem onClick={handleAccount}>
79-
Account
80-
<DropdownMenuShortcut>{shortcutLabel}</DropdownMenuShortcut>
81-
</DropdownMenuItem>
82-
<DropdownMenuItem onClick={handleSetting} disabled>
88+
<DropdownMenuItem onClick={handleSettings}>
8389
Settings
84-
{/* <DropdownMenuShortcut>{isMac ? "⌘S" : "Ctrl+S"}</DropdownMenuShortcut> */}
90+
<DropdownMenuShortcut>{shortcutSettings}</DropdownMenuShortcut>
91+
</DropdownMenuItem>
92+
</DropdownMenuGroup>
93+
<DropdownMenuGroup>
94+
<DropdownMenuItem disabled>
95+
Help
96+
<DropdownMenuShortcut>{shortcutHelp}</DropdownMenuShortcut>
8597
</DropdownMenuItem>
8698
</DropdownMenuGroup>
8799
<DropdownMenuSeparator />
88100
<DropdownMenuItem onClick={handleLogout}>
89101
Log out
90-
{/* <DropdownMenuShortcut>{isMac ? "⇧⌘Q" : "Ctrl+Shift+Q"}</DropdownMenuShortcut> */}
102+
<DropdownMenuShortcut>{shortcutLogout}</DropdownMenuShortcut>
91103
</DropdownMenuItem>
92104
</DropdownMenuContent>
93105
</DropdownMenu>
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
import { Request, Response, NextFunction } from "express";
2+
import * as LogService from '../services/logs.service';
3+
4+
// 2025-10-31
5+
export async function getCameraLogs(req: Request, res: Response, next: NextFunction) {
6+
try {
7+
const list = await LogService.getCameraLogs();
8+
return res.status(200).json({ message: 'Fetched successfully', data: list });
9+
} catch (err) {
10+
next(err);
11+
}
12+
};
13+
14+
// 2025-10-31
15+
export async function getAlertLogs(req: Request, res: Response, next: NextFunction) {
16+
try {
17+
const list = await LogService.getAlertLogs();
18+
return res.status(200).json({ message: 'Fetched successfully', data: list });
19+
} catch (err) {
20+
next(err);
21+
}
22+
};
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
import * as Model from "../../models/logs.model"
2+
import { splitDateTime } from "./timeDate.map"
3+
4+
export const mapCameraLogsToSaveResponse = (row: any): Model.Camera => {
5+
6+
const createdAt = splitDateTime(row.clg_created_at);
7+
8+
return {
9+
log_id: row.clg_id,
10+
user_id: row.clg_usr_id,
11+
camera_id: row.clg_cam_id,
12+
log_action: row.clg_action,
13+
log_created_at: createdAt.date + ' ' + createdAt.time
14+
}
15+
};
16+
17+
export const mapAlertLogsToSaveResponse = (row: any): Model.Alert => {
18+
19+
const createdAt = splitDateTime(row.alg_created_at);
20+
21+
return {
22+
log_id: row.alg_id,
23+
user_id: row.alg_usr_id,
24+
alert_id: row.alg_alr_id,
25+
log_action: row.alg_action,
26+
log_created_at: createdAt.date + ' ' + createdAt.time
27+
}
28+
};

server/src/models/logs.model.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
export interface Camera {
2+
log_id: number;
3+
user_id: number;
4+
camera_id: number;
5+
log_action: string;
6+
log_created_at: string;
7+
}
8+
9+
export interface Alert {
10+
log_id: number;
11+
user_id: number;
12+
alert_id: number;
13+
log_action: string;
14+
log_created_at: string;
15+
}

server/src/routes/index.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
* - /api/cameras → การจัดการกล้อง (Cameras)
1212
* - /api/alerts → การจัดการการแจ้งเตือน (Alerts)
1313
* - /api/events → การจัดการเหตุการณ์ (Events)
14+
* - /api/logs → ดูประวัติต่างๆในระบบ (History)
1415
*
1516
* ## Others
1617
* - /api/locations → การจัดการสถานที่ (Locations)
@@ -25,6 +26,7 @@
2526
* @requires ./register.routes
2627
* @requires ./location.route
2728
* @requires ./users.route
29+
* @requires ./logs.route
2830
*
2931
* @author Wanasart
3032
* @created 2025-08-16
@@ -37,6 +39,7 @@ import { Router } from "express";
3739
import cameras from "./cameras.routes";
3840
import alerts from "./alerts.routes";
3941
import events from "./events.routes";
42+
import logs from "./logs.route";
4043

4144
// Authentication modules
4245
import login from "./login.routes";
@@ -59,6 +62,7 @@ router.use("/register", register);
5962
router.use("/cameras", cameras);
6063
router.use("/alerts", alerts);
6164
router.use("/events", events);
65+
router.use("/logs", logs);
6266

6367
/* ============================= Others ============================= */
6468
router.use("/locations", locations);

server/src/routes/logs.route.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
import { Router } from 'express';
2+
import * as ctrl from '../controllers/logs.controller';
3+
4+
const router = Router();
5+
6+
/* ========================== Logs ========================== */
7+
router.get('/camera', ctrl.getCameraLogs);
8+
router.get('/alert', ctrl.getAlertLogs);
9+
// router.get('/user', ctrl.getUserLogs);
10+
11+
export default router;
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
import { pool } from "../config/db";
2+
import * as Mapping from "../models/Mapping/logs.map";
3+
4+
// 2025-10-31
5+
export async function getCameraLogs(){
6+
const { rows } = await pool.query(`
7+
SELECT
8+
clg_id,
9+
clg_usr_id,
10+
clg_cam_id,
11+
clg_action,
12+
clg_created_at
13+
FROM camera_logs
14+
ORDER BY clg_created_at DESC;
15+
`);
16+
17+
return rows.map(Mapping.mapCameraLogsToSaveResponse);
18+
}
19+
20+
// 2025-10-31
21+
export async function getAlertLogs(){
22+
const { rows } = await pool.query(`
23+
SELECT
24+
alg_id,
25+
alg_usr_id,
26+
alg_alr_id,
27+
alg_action,
28+
alg_created_at
29+
FROM alert_logs
30+
ORDER BY alg_created_at DESC;
31+
`);
32+
33+
return rows.map(Mapping.mapAlertLogsToSaveResponse);
34+
}

0 commit comments

Comments
 (0)