Skip to content

Commit c4ec399

Browse files
authored
Merge branch 'module/settings' into feature/notification-settings
2 parents 6f4735d + 81971db commit c4ec399

File tree

7 files changed

+554
-2
lines changed

7 files changed

+554
-2
lines changed
Lines changed: 276 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,276 @@
1+
"use client";
2+
3+
import { useEffect, useState } from "react";
4+
import {
5+
Table, TableBody, TableCell, TableHead, TableHeader, TableRow,
6+
} from "@/components/ui/table";
7+
8+
/* ----------------------- Toggle Button ----------------------- */
9+
const Toggle = ({
10+
enabled,
11+
onChange,
12+
}: {
13+
enabled: boolean;
14+
onChange: (v: boolean) => void;
15+
}) => (
16+
<button
17+
type="button"
18+
role="switch"
19+
aria-checked={enabled}
20+
onClick={() => onChange(!enabled)}
21+
className={`relative inline-flex h-5 w-10 items-center rounded-full transition-colors duration-200
22+
${enabled ? "bg-[var(--color-primary)]/90" : "bg-slate-300"}
23+
focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--color-primary)]/50`}
24+
>
25+
<span
26+
className={`inline-block h-4 w-4 transform rounded-full bg-white shadow transition-transform
27+
${enabled ? "translate-x-5" : "translate-x-1"}`}
28+
/>
29+
</button>
30+
);
31+
32+
/* ---------------------- API TYPE ---------------------- */
33+
type CameraPermission = {
34+
camera_id: number;
35+
camera_name: string;
36+
require_authorization: boolean;
37+
restrict_role: boolean;
38+
log_access_attempts: boolean;
39+
};
40+
41+
/* ---------- Map field names for backend ---------- */
42+
function mapToApiFormat(patch: Partial<CameraPermission>) {
43+
return {
44+
require_auth: patch.require_authorization,
45+
restrict: patch.restrict_role,
46+
log: patch.log_access_attempts,
47+
};
48+
}
49+
50+
export default function CameraPermissionsTable() {
51+
const [rows, setRows] = useState<CameraPermission[]>([]);
52+
const [loading, setLoading] = useState(true);
53+
54+
/* -------------------------- FETCH DATA -------------------------- */
55+
useEffect(() => {
56+
const fetchData = async () => {
57+
try {
58+
const res = await fetch("/api/cameras/permission", {
59+
credentials: "include",
60+
cache: "no-store",
61+
});
62+
63+
const json = await res.json().catch(() => ({}));
64+
65+
if (!res.ok) {
66+
console.error(json?.message || "Failed to fetch camera permissions");
67+
return;
68+
}
69+
70+
setRows(json.data || []);
71+
} catch (err) {
72+
console.error("Error fetching camera permissions", err);
73+
} finally {
74+
setLoading(false);
75+
}
76+
};
77+
78+
fetchData();
79+
}, []);
80+
81+
/* ---------- Local state update helper ---------- */
82+
const updateLocal = (id: number, patch: Partial<CameraPermission>) => {
83+
setRows((prev) =>
84+
prev.map((r) =>
85+
r.camera_id === id ? { ...r, ...patch } : r
86+
)
87+
);
88+
};
89+
90+
/* ------------------------ UPDATE PERMISSION ------------------------ */
91+
async function updatePermission(id: number, patch: Partial<CameraPermission>) {
92+
// หาค่าเก่าจาก state
93+
const current = rows.find(r => r.camera_id === id);
94+
if (!current) return;
95+
96+
const payload = {
97+
require_auth:
98+
patch.require_authorization ?? current.require_authorization,
99+
restrict:
100+
patch.restrict_role ?? current.restrict_role,
101+
log:
102+
patch.log_access_attempts ?? current.log_access_attempts,
103+
};
104+
105+
const res = await fetch(`/api/cameras/${id}/permission`, {
106+
method: "PUT",
107+
headers: { "Content-Type": "application/json" },
108+
credentials: "include",
109+
cache: "no-store",
110+
body: JSON.stringify(payload),
111+
});
112+
113+
if (!res.ok) throw new Error("Update failed");
114+
}
115+
116+
/* ----------------------------- PAGINATION ----------------------------- */
117+
const PAGE_SIZE = 5;
118+
const [page, setPage] = useState(1);
119+
120+
const total = rows.length;
121+
const totalPages = Math.max(1, Math.ceil(total / PAGE_SIZE));
122+
123+
const start = (page - 1) * PAGE_SIZE;
124+
const end = Math.min(start + PAGE_SIZE, total);
125+
126+
const pagedRows = rows.slice(start, end);
127+
128+
/* ----------------------------- RENDER ----------------------------- */
129+
130+
if (loading)
131+
return <div className="text-sm text-gray-500">Loading permissions...</div>;
132+
133+
if (!pagedRows.length)
134+
return (
135+
<div className="text-sm text-gray-500">No camera permissions to display.</div>
136+
);
137+
138+
return (
139+
<div className="w-full">
140+
<label className="font-bold text-lg text-[var(--color-primary)]">Camera Permissions</label>
141+
<Table className="table-auto w-full">
142+
<TableHeader>
143+
<TableRow className="border-b border-[var(--color-primary)]">
144+
{[
145+
{ key: "camera_id", label: "Camera ID" },
146+
{ key: "camera_name", label: "Camera Name" },
147+
{ key: "require_authorization", label: "Require Authorization" },
148+
{ key: "restrict_role", label: "Restrict Role" },
149+
{ key: "log_access_attempts", label: "Log Access Attempts" },
150+
].map(({ key, label }, index, arr) => (
151+
<TableHead
152+
key={key}
153+
className="select-none text-[var(--color-primary)] cursor-default"
154+
>
155+
<div
156+
className={
157+
"flex items-center justify-between pr-3 w-full " +
158+
(index !== arr.length - 1
159+
? "border-r border-[var(--color-primary)]"
160+
: "")
161+
}
162+
>
163+
<span>{label}</span>
164+
</div>
165+
</TableHead>
166+
))}
167+
</TableRow>
168+
</TableHeader>
169+
170+
<TableBody>
171+
{pagedRows.map((row) => (
172+
<TableRow key={row.camera_id}>
173+
<TableCell>{`CAM${String(row.camera_id).padStart(4, "0")}`}</TableCell>
174+
<TableCell>{row.camera_name}</TableCell>
175+
176+
{/* ---------- Toggle Require Authorization ---------- */}
177+
<TableCell>
178+
<Toggle
179+
enabled={row.require_authorization}
180+
onChange={async (v) => {
181+
const old = row.require_authorization;
182+
183+
updateLocal(row.camera_id, { require_authorization: v });
184+
185+
try {
186+
await updatePermission(row.camera_id, {
187+
require_authorization: v,
188+
});
189+
} catch {
190+
updateLocal(row.camera_id, { require_authorization: old });
191+
}
192+
}}
193+
/>
194+
</TableCell>
195+
196+
{/* ---------- Toggle Restrict Role ---------- */}
197+
<TableCell>
198+
<Toggle
199+
enabled={row.restrict_role}
200+
onChange={async (v) => {
201+
const old = row.restrict_role;
202+
203+
updateLocal(row.camera_id, { restrict_role: v });
204+
205+
try {
206+
await updatePermission(row.camera_id, {
207+
restrict_role: v,
208+
});
209+
} catch {
210+
updateLocal(row.camera_id, { restrict_role: old });
211+
}
212+
}}
213+
/>
214+
</TableCell>
215+
216+
{/* ---------- Toggle Log Access Attempts ---------- */}
217+
<TableCell>
218+
<Toggle
219+
enabled={row.log_access_attempts}
220+
onChange={async (v) => {
221+
const old = row.log_access_attempts;
222+
223+
updateLocal(row.camera_id, { log_access_attempts: v });
224+
225+
try {
226+
await updatePermission(row.camera_id, {
227+
log_access_attempts: v,
228+
});
229+
} catch {
230+
updateLocal(row.camera_id, { log_access_attempts: old });
231+
}
232+
}}
233+
/>
234+
</TableCell>
235+
</TableRow>
236+
))}
237+
</TableBody>
238+
</Table>
239+
240+
{/* Pagination */}
241+
<div className="mt-3 flex items-center justify-between">
242+
<div className="text-xs text-gray-500">
243+
Showing <span className="font-medium">{start + 1}</span>
244+
<span className="font-medium">{end}</span> of{" "}
245+
<span className="font-medium">{total}</span>
246+
</div>
247+
248+
<div className="flex items-center gap-2">
249+
<button
250+
onClick={() => setPage((p) => Math.max(1, p - 1))}
251+
disabled={page <= 1}
252+
className={`px-3 py-1 rounded-md border text-sm ${page <= 1
253+
? "text-gray-400 border-gray-200"
254+
: "text-gray-700 border-gray-300 hover:bg-gray-50"
255+
}`}
256+
>
257+
Previous
258+
</button>
259+
<div className="text-sm tabular-nums">
260+
{page} / {totalPages}
261+
</div>
262+
<button
263+
onClick={() => setPage((p) => Math.min(totalPages, p + 1))}
264+
disabled={page >= totalPages}
265+
className={`px-3 py-1 rounded-md border text-sm ${page >= totalPages
266+
? "text-gray-400 border-gray-200"
267+
: "text-gray-700 border-gray-300 hover:bg-gray-50"
268+
}`}
269+
>
270+
Next
271+
</button>
272+
</div>
273+
</div>
274+
</div>
275+
);
276+
}

0 commit comments

Comments
 (0)