Skip to content

Commit 1cab016

Browse files
committed
feat: เชื่อม edit, delete ให้ user management
1 parent 455bd29 commit 1cab016

File tree

5 files changed

+585
-149
lines changed

5 files changed

+585
-149
lines changed

aods_dev_v3.sql

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,7 @@ CREATE TABLE IF NOT EXISTS users (
6060
usr_username VARCHAR(32) NOT NULL UNIQUE,
6161
usr_email VARCHAR(64) NOT NULL UNIQUE,
6262
usr_password VARCHAR(255) NOT NULL,
63-
usr_name VARCHAR(32),
63+
usr_name VARCHAR(64),
6464
usr_phone VARCHAR(10),
6565
usr_profile VARCHAR(255),
6666

client/my-app/src/app/(with-layout)/test/page.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import React from "react";
44
import AlertDocumentPreview from "@/components/documents/AlertDocumentPreview";
55
import ModalsGallery from "@/components/modals/AlertsModal";
66
import { showAlert } from "@/components/modals/StatusAlert";
7+
import { StatusMessage } from "@/components/utilities/StatusMessage";
78

89
export default function AlertPreviewPage() {
910
const handleSave = () => {
@@ -19,6 +20,7 @@ export default function AlertPreviewPage() {
1920
<div className="rounded-lg bg-[var(--color-white)] shadow-md p-6">
2021
{/* <AlertDocumentPreview /> */}
2122
<ModalsGallery />
23+
<StatusMessage message={'test message'} status={'info'} />
2224
<button onClick={handleSave}>Save</button>
2325
</div>
2426
);

client/my-app/src/components/features/settings/UserFilters.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -52,14 +52,14 @@ export default function UserFilters() {
5252
className="w-full rounded-md border border-[var(--color-primary)]
5353
text-[var(--color-primary)]
5454
focus:ring-[var(--color-primary)] focus:border-[var(--color-primary)]
55-
px-2 py-1.5 text-xs sm:px-3 sm:py-2 sm:text-sm"
55+
px-2 py-1.5 text-xs sm:px-3 sm:py-2 sm:text-sm capitalize"
5656
>
5757
<SelectValue placeholder="All Roles" />
5858
</SelectTrigger>
5959
<SelectContent className="border-[var(--color-primary)]">
6060
<SelectItem value="All">All Roles</SelectItem>
6161
{roles.map((r) => (
62-
<SelectItem key={r} value={r}>{r}</SelectItem>
62+
<SelectItem key={r} value={r} className="capitalize">{r}</SelectItem>
6363
))}
6464
</SelectContent>
6565
</Select>

client/my-app/src/components/features/settings/UserTable.tsx

Lines changed: 133 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,13 @@ import * as Icons from "lucide-react";
1010
import {
1111
Tooltip, TooltipContent, TooltipProvider, TooltipTrigger,
1212
} from "@/components/ui/tooltip";
13-
import { SaveConfirmModal } from "@/components/modals/AlertsModal";
13+
import EditUserModal from "@/components/forms/users/EditUserForm";
1414

15+
import { SaveConfirmModal, DeleteConfirmModal } from "@/components/modals/AlertsModal";
16+
import { showAlert } from "@/components/modals/StatusAlert";
17+
import { useMe } from "@/hooks/useMe";
18+
19+
import { DeleteUserDialog } from "@/components/forms/users/DeleteUserForm";
1520
/* ------------------------------ ROLE COLORS ------------------------------ */
1621
const ROLE_STYLES = {
1722
admin: "bg-emerald-50 text-emerald-700 ring-emerald-200",
@@ -42,6 +47,14 @@ function IconAction({
4247
variant = "primary",
4348
onClick,
4449
className = "",
50+
disabled,
51+
}: {
52+
label: string;
53+
children: React.ReactNode;
54+
variant?: "primary" | "danger" | "info" | "success";
55+
onClick?: () => void;
56+
className?: string;
57+
disabled?: boolean;
4558
}) {
4659
const palette =
4760
variant === "danger"
@@ -79,6 +92,7 @@ function IconAction({
7992
<button
8093
onClick={onClick}
8194
aria-label={label}
95+
disabled={disabled}
8296
className={[
8397
"inline-flex items-center justify-center h-8 w-8 rounded-full border",
8498
palette.border,
@@ -87,6 +101,7 @@ function IconAction({
87101
"hover:text-white hover:border-transparent",
88102
"transition focus:outline-none focus:ring-2 focus:ring-offset-2",
89103
palette.focusRing,
104+
"disabled:opacity-50 disabled:cursor-not-allowed",
90105
className,
91106
].join(" ")}
92107
>
@@ -122,7 +137,7 @@ export default function UserTable({ users }: { users: User[] }) {
122137
useEffect(() => setRows(users), [users]);
123138

124139
// Pagination
125-
const PAGE_SIZE = 25;
140+
const PAGE_SIZE = 10;
126141
const [page, setPage] = useState(1);
127142

128143
// Sorting
@@ -190,7 +205,12 @@ export default function UserTable({ users }: { users: User[] }) {
190205
return;
191206
}
192207

193-
alert("Password has been reset.");
208+
showAlert({
209+
title: "Success",
210+
message: "Password has been reset successfully.",
211+
variant: "success",
212+
duration: 3000,
213+
});
194214
} catch (err) {
195215
alert("Error resetting password.");
196216
} finally {
@@ -200,14 +220,91 @@ export default function UserTable({ users }: { users: User[] }) {
200220
};
201221

202222
/* ----------------------------- ACTIONS ----------------------------- */
203-
const onResetPassword = (id: number) => {
204-
setResetUserId(id);
205-
setConfirmOpen(true);
223+
const [editingUserId, setEditingUserId] = useState<number | null>(null);
224+
const [editingOpen, setEditingOpen] = useState(false);
225+
226+
const handleOpenEdit = (id: number) => {
227+
setEditingUserId(id);
228+
setEditingOpen(true);
206229
};
230+
const handleUserUpdated = (updated: User) => {
231+
setRows((prev) => prev.map((r) => (r.usr_id === updated.usr_id ? { ...r, ...updated } : r)));
232+
};
233+
234+
// --- new delete-related state ---
235+
const { me, loading: loadingMe, error: meError } = useMe();
236+
const [busyId, setBusyId] = useState<number | null>(null);
237+
const [deleteOpen, setDeleteOpen] = useState(false);
238+
const [deleteTarget, setDeleteTarget] = useState<{ id: number; name?: string } | null>(null);
207239

208-
// ฟังก์ชันสำหรับปุ่ม Action พร้อมให้เชื่อมต่อ logic จริง เช่น แก้ไขหรือ ลบผู้ใช้
209-
const onEdit = (id: number) => console.log("Edit user:", id);
210-
const onDelete = (id: number) => console.log("Delete user:", id);
240+
function openDelete(u: User) {
241+
setDeleteTarget({ id: u.usr_id, name: u.usr_username });
242+
setDeleteOpen(true);
243+
}
244+
245+
// onConfirmDelete signature follows DeleteConfirmModal -> receives object { input?, note? }
246+
async function onConfirmDelete(_: { input?: string; note?: string }) {
247+
if (!deleteTarget) return;
248+
249+
// get user id who performs the action
250+
const user_id = Number((me as any)?.usr_id);
251+
if (!Number.isFinite(user_id) || user_id <= 0) {
252+
// fallback: show alert and keep modal open
253+
showAlert({
254+
title: "Error",
255+
message: meError || "Cannot resolve user_id from current user.",
256+
variant: "destructive",
257+
duration: 4000,
258+
});
259+
return;
260+
}
261+
262+
try {
263+
setBusyId(deleteTarget.id);
264+
265+
// call backend to deactivate/delete user
266+
const res = await fetch(`/api/users/${deleteTarget.id}/deactivate`, {
267+
method: "PATCH",
268+
credentials: "include",
269+
cache: "no-store",
270+
headers: { "Content-Type": "application/json" },
271+
body: JSON.stringify({ user_id }),
272+
});
273+
274+
const json = await res.json().catch(() => ({}));
275+
if (!res.ok) throw new Error(json?.message || "Delete failed");
276+
277+
// remove from local rows
278+
setRows((prev) => prev.filter((r) => r.usr_id !== deleteTarget.id));
279+
280+
// show status alert
281+
showAlert({
282+
title: "Deleted",
283+
message: `User "${deleteTarget.name ?? deleteTarget.id}" has been deactivated.`,
284+
variant: "success",
285+
duration: 3000,
286+
});
287+
288+
setDeleteOpen(false);
289+
setDeleteTarget(null);
290+
} catch (e: any) {
291+
// show status alert error
292+
showAlert({
293+
title: "Error",
294+
message: e?.message || "Delete failed",
295+
variant: "destructive",
296+
duration: 4000,
297+
});
298+
} finally {
299+
setBusyId(null);
300+
}
301+
}
302+
303+
const onDeleteClick = (id: number) => {
304+
// kept for compatibility or if you want to pass to child props
305+
const u = rows.find((r) => r.usr_id === id);
306+
if (u) openDelete(u);
307+
};
211308

212309
if (!pagedUsers.length) {
213310
return <div className="text-sm text-gray-500">No users to display.</div>;
@@ -249,7 +346,7 @@ export default function UserTable({ users }: { users: User[] }) {
249346
<TableBody>
250347
{pagedUsers.map((usr) => (
251348
<TableRow key={usr.usr_id}>
252-
<TableCell>{`USR${String(usr.usr_id).padStart(4, "0")}`}</TableCell>
349+
<TableCell>{`USR${String(usr.usr_id).padStart(3, "0")}`}</TableCell>
253350
<TableCell>{usr.usr_name}</TableCell>
254351
<TableCell><RoleBadge value={usr.usr_role} /></TableCell>
255352
<TableCell>{usr.usr_username}</TableCell>
@@ -262,23 +359,24 @@ export default function UserTable({ users }: { users: User[] }) {
262359
<IconAction
263360
label="Reset Password"
264361
variant="primary"
265-
onClick={() => onResetPassword(usr.usr_id)}
362+
onClick={() => { setResetUserId(usr.usr_id); setConfirmOpen(true); }}
266363
>
267364
<KeyRound className="w-4 h-4" />
268365
</IconAction>
269366

270367
<IconAction
271368
label="Edit"
272369
variant="primary"
273-
onClick={() => onEdit(usr.usr_id)}
370+
onClick={() => handleOpenEdit(usr.usr_id)}
274371
>
275372
<Pencil className="w-4 h-4" />
276373
</IconAction>
277374

278375
<IconAction
279376
label="Delete"
280377
variant="danger"
281-
onClick={() => onDelete(usr.usr_id)}
378+
onClick={() => openDelete(usr)}
379+
disabled={busyId === usr.usr_id}
282380
>
283381
<Trash2 className="w-4 h-4" />
284382
</IconAction>
@@ -335,6 +433,27 @@ export default function UserTable({ users }: { users: User[] }) {
335433
onConfirm={handleConfirmReset}
336434
icon={<KeyRound className="h-7 w-7 text-blue-600" />}
337435
/>
436+
437+
{/* edit modal */}
438+
<EditUserModal
439+
usrId={editingUserId}
440+
open={editingOpen}
441+
setOpen={setEditingOpen}
442+
onUpdated={handleUserUpdated}
443+
/>
444+
445+
{/* Delete confirm modal — no typing required (just Cancel / Delete) */}
446+
<DeleteConfirmModal
447+
open={deleteOpen}
448+
onOpenChange={(v) => {
449+
if (!v) setDeleteTarget(null);
450+
setDeleteOpen(v);
451+
}}
452+
title="Delete User?"
453+
description={`This will deactivate "${deleteTarget?.name ?? "this user"}" (ID: ${deleteTarget?.id ?? "—"}) and related data. This action cannot be undone.`}
454+
confirmText="Delete"
455+
onConfirm={onConfirmDelete}
456+
/>
338457
</div>
339458
);
340-
}
459+
}

0 commit comments

Comments
 (0)