From e23d9b9ec2e948a22781c52ddcf5a887d9472501 Mon Sep 17 00:00:00 2001 From: rgrgogu Date: Thu, 7 May 2026 12:36:55 +0800 Subject: [PATCH] AdjusteD --- src/components/generic/Table/DataTable.jsx | 1 + src/contexts/AdminUserContext.jsx | 77 +++++++++++++++---- .../admin/components/ArchiveUserDialog.jsx | 29 +++++-- .../admin/components/ArchiveUserTable.jsx | 39 +++++++--- .../admin/components/RestoreUserDialog.jsx | 28 +++++-- src/modules/admin/components/UserTable.jsx | 35 ++++++--- .../admin/config/archive/selection.config.jsx | 24 +++++- src/modules/admin/config/selection.config.jsx | 9 ++- 8 files changed, 188 insertions(+), 54 deletions(-) diff --git a/src/components/generic/Table/DataTable.jsx b/src/components/generic/Table/DataTable.jsx index 6a7b290..109dd27 100644 --- a/src/components/generic/Table/DataTable.jsx +++ b/src/components/generic/Table/DataTable.jsx @@ -54,6 +54,7 @@ export default function DataTable({ onRefsReady?.({ getFilters: () => filtersRef.current, getSort: () => sortRef.current, + resetSelection: () => table.resetRowSelection(), // ← expose this }); onFetch({ page: 1, limit: pagination.limit, filters: [], sort: [] }); diff --git a/src/contexts/AdminUserContext.jsx b/src/contexts/AdminUserContext.jsx index b7bd755..e0c2f7b 100644 --- a/src/contexts/AdminUserContext.jsx +++ b/src/contexts/AdminUserContext.jsx @@ -97,21 +97,6 @@ export const UserProvider = ({ children }) => { [request] ); - // ─── DELETE /api/admin/users/:id (soft delete) ──────────────────────────── - const deactivateUser = useCallback( - (userId) => - request(async () => { - const res = await api.delete(`${BASE}/users/${userId}`); - - setUsers((prev) => - prev.map((u) => (u.user_id === userId ? { ...u, is_active: false } : u)) - ); - - return res.data; - }), - [request] - ); - // ─── GET /api/admin/users/archived ──────────────────────────────────────────── const fetchArchivedUsers = useCallback( ({ page = 1, limit = 10, filters = [], sort = [] } = {}) => @@ -135,6 +120,44 @@ export const UserProvider = ({ children }) => { [request] ); + // ─── DELETE /api/admin/users/:id (soft delete) ──────────────────────────── + const deactivateUser = useCallback( + (userId) => + request(async () => { + const res = await api.delete(`${BASE}/users/${userId}`); + + setUsers((prev) => + prev.map((u) => (u.user_id === userId ? { ...u, is_active: false } : u)) + ); + + return res.data; + }), + [request] + ); + + // ─── DELETE /api/admin/users/bulk ───────────────────────────────────────────── + const deactivateUsers = useCallback( + ({ ids }) => + request(async () => { + const res = await api.delete(`${BASE}/users/bulk`, { data: { ids } }); + + const { deactivated_ids } = res.data?.data ?? {}; // ← unwrap nested data + + if (deactivated_ids?.length) { + setUsers((prev) => + prev.map((u) => + deactivated_ids.includes(u.user_id) + ? { ...u, is_active: false } + : u + ) + ); + } + + return res.data; + }), + [request] + ); + // ─── POST /api/admin/users/:id/restore ──────────────────────────────────── const restoreUser = useCallback( (userId) => @@ -150,6 +173,28 @@ export const UserProvider = ({ children }) => { [request] ); + const restoreUsers = useCallback( + ({ ids }) => + request(async () => { + const res = await api.post(`${BASE}/users/bulk/restore`, { ids }); + + const { restored_ids } = res.data?.data ?? {}; // ← same unwrap + + if (restored_ids?.length) { + setUsers((prev) => + prev.map((u) => + restored_ids.includes(u.user_id) + ? { ...u, is_active: true } + : u + ) + ); + } + + return res.data; + }), + [request] + ); + // ─── GET /api/admin/users/:id/sessions ──────────────────────────────────── const fetchUserSessions = useCallback( (userId) => @@ -209,7 +254,9 @@ export const UserProvider = ({ children }) => { fetchUser, updateUser, deactivateUser, + deactivateUsers, restoreUser, + restoreUsers, fetchUserSessions, terminateSession, }} diff --git a/src/modules/admin/components/ArchiveUserDialog.jsx b/src/modules/admin/components/ArchiveUserDialog.jsx index 52fe355..7512d34 100644 --- a/src/modules/admin/components/ArchiveUserDialog.jsx +++ b/src/modules/admin/components/ArchiveUserDialog.jsx @@ -1,4 +1,3 @@ -// ─── components/ArchiveUserDialog.jsx ──────────────────────────────────────── import { AlertDialog, AlertDialogAction, @@ -12,11 +11,23 @@ import { import { Spinner } from "@/components/ui/spinner"; import { useUsers } from "@/contexts/AdminUserContext"; -export function ArchiveUserDialog({ open, onOpenChange, user, onSuccess }) { - const { deactivateUser, loading } = useUsers(); +/** + * Unified archive dialog — works for both single and bulk. + * + * Single: + * Bulk: + */ +export function ArchiveUserDialog({ open, onOpenChange, user, ids, onSuccess }) { + const { deactivateUser, deactivateUsers, loading } = useUsers(); + + const isBulk = Array.isArray(ids) && ids.length > 0; + const count = isBulk ? ids.length : 1; const handleArchive = async () => { - const res = await deactivateUser(user?.user_id); + const res = isBulk + ? await deactivateUsers({ ids }) + : await deactivateUser(user?.user_id); + if (res) { onOpenChange(false); onSuccess?.(); @@ -27,11 +38,15 @@ export function ArchiveUserDialog({ open, onOpenChange, user, onSuccess }) { - Archive User + + Archive {isBulk ? `${count} Users` : "User"} + Are you sure you want to archive{" "} - {user?.personal_info?.name?.full_name ?? user?.email} + {isBulk + ? `${count} selected user${count !== 1 ? "s" : ""}` + : (user?.personal_info?.name?.full_name ?? user?.email)} ? They will be deactivated and lose access immediately. @@ -44,7 +59,7 @@ export function ArchiveUserDialog({ open, onOpenChange, user, onSuccess }) { className="bg-destructive text-destructive-foreground hover:bg-destructive/90" > {loading ? : null} - Archive + Archive{isBulk ? ` ${count} User${count !== 1 ? "s" : ""}` : ""} diff --git a/src/modules/admin/components/ArchiveUserTable.jsx b/src/modules/admin/components/ArchiveUserTable.jsx index 6d02fd7..8aceaff 100644 --- a/src/modules/admin/components/ArchiveUserTable.jsx +++ b/src/modules/admin/components/ArchiveUserTable.jsx @@ -13,9 +13,10 @@ import { buildRowActions } from "../config/archive/rowActions.config"; import { getTimestamp } from "@/utils/timestamp.util"; -export default function UsersTable() { - const [restoreTarget, setRestoreTarget] = useState(null); - const tableRefsRef = useRef({ getFilters: () => [], getSort: () => [] }); +export default function ArchiveUsersTable() { + const [restoreTarget, setRestoreTarget] = useState(null); // single: row object + const [restoreIds, setRestoreIds] = useState(null); // bulk: array of ids + const tableRefsRef = useRef({ getFilters: () => [], getSort: () => [], resetSelection: () => {} }); const navigate = useNavigate(); const { @@ -26,9 +27,8 @@ export default function UsersTable() { loading, fetchUserFieldValues, fetchArchivedUsers, - restoreUser } = useUsers(); - + // Shared export config — passed into toolbar + selection configs const exportConfig = { allData: users, @@ -37,17 +37,26 @@ export default function UsersTable() { sheetName: "ArchivedUsers", }; - console.log(users) - const rowActions = buildRowActions({ navigate, onRestore: (row) => setRestoreTarget(row), }); const toolbarActions = buildToolbarActions({ fetchArchivedUsers, pagination, exportConfig, navigate, getFilters: () => tableRefsRef.current.getFilters(), getSort: () => tableRefsRef.current.getSort(), }); - const selectionActions = buildSelectionActions({ exportConfig, restoreUser }); + const selectionActions = buildSelectionActions({ + exportConfig, + restoreUser: (row) => setRestoreTarget(row), // single — open dialog with row + restoreUsers: (ids) => setRestoreIds(ids), // bulk — open dialog with ids + }); const columns = useMemo(() => buildUserColumns(attributes, rowActions), [attributes]); + const handleRestoreSuccess = () => { + setRestoreTarget(null); + setRestoreIds(null); + tableRefsRef.current.resetSelection?.(); // ← clear selection + fetchArchivedUsers({ page: 1, limit: pagination.limit }); + }; + return ( <> + {/* Single restore */} !v && setRestoreTarget(null)} user={restoreTarget} - onSuccess={() => { - setRestoreTarget(null); - fetchArchivedUsers({ page: 1, limit: pagination.limit }); - }} + onSuccess={handleRestoreSuccess} + /> + + {/* Bulk restore */} + !v && setRestoreIds(null)} + ids={restoreIds ?? []} + onSuccess={handleRestoreSuccess} /> ); diff --git a/src/modules/admin/components/RestoreUserDialog.jsx b/src/modules/admin/components/RestoreUserDialog.jsx index 0ec356b..f09647f 100644 --- a/src/modules/admin/components/RestoreUserDialog.jsx +++ b/src/modules/admin/components/RestoreUserDialog.jsx @@ -12,11 +12,23 @@ import { import { Spinner } from "@/components/ui/spinner"; import { useUsers } from "@/contexts/AdminUserContext"; -export function RestoreUserDialog({ open, onOpenChange, user, onSuccess }) { - const { restoreUser, loading } = useUsers(); +/** + * Unified restore dialog — works for both single and bulk. + * + * Single: + * Bulk: + */ +export function RestoreUserDialog({ open, onOpenChange, user, ids, onSuccess }) { + const { restoreUser, restoreUsers, loading } = useUsers(); + + const isBulk = Array.isArray(ids) && ids.length > 0; + const count = isBulk ? ids.length : 1; const handleRestore = async () => { - const res = await restoreUser(user?.user_id); + const res = isBulk + ? await restoreUsers({ ids }) + : await restoreUser(user?.user_id); + if (res) { onOpenChange(false); onSuccess?.(); @@ -27,13 +39,15 @@ export function RestoreUserDialog({ open, onOpenChange, user, onSuccess }) { - Restore User + Restore {isBulk ? `${count} Users` : "User"} Are you sure you want to restore{" "} - {user?.personal_info?.name?.full_name ?? user?.email} + {isBulk + ? `${count} selected user${count !== 1 ? "s" : ""}` + : (user?.personal_info?.name?.full_name ?? user?.email)} - ? They will regain access to their account immediately. + ? They will regain access to their account{isBulk && count !== 1 ? "s" : ""} immediately. @@ -44,7 +58,7 @@ export function RestoreUserDialog({ open, onOpenChange, user, onSuccess }) { className="bg-emerald-600 text-white hover:bg-emerald-700" > {loading ? : null} - Restore + Restore{isBulk ? ` ${count} User${count !== 1 ? "s" : ""}` : ""} diff --git a/src/modules/admin/components/UserTable.jsx b/src/modules/admin/components/UserTable.jsx index 2cdc44c..05f59a8 100644 --- a/src/modules/admin/components/UserTable.jsx +++ b/src/modules/admin/components/UserTable.jsx @@ -14,10 +14,11 @@ import { buildRowActions } from "../config/rowActions.config"; import { getTimestamp } from "@/utils/timestamp.util"; export default function UsersTable() { - const [archiveTarget, setArchiveTarget] = useState(null); - const tableRefsRef = useRef({ getFilters: () => [], getSort: () => [] }); + const [archiveTarget, setArchiveTarget] = useState(null); // single: row object + const [archiveIds, setArchiveIds] = useState(null); // bulk: array of ids + const tableRefsRef = useRef({ getFilters: () => [], getSort: () => [], resetSelection: () => {} }); const navigate = useNavigate(); - + const { users, attributes, @@ -26,7 +27,6 @@ export default function UsersTable() { loading, fetchUsers, fetchUserFieldValues, - deactivateUser } = useUsers(); // Shared export config — passed into toolbar + selection configs @@ -43,9 +43,20 @@ export default function UsersTable() { getFilters: () => tableRefsRef.current.getFilters(), getSort: () => tableRefsRef.current.getSort(), }); - const selectionActions = buildSelectionActions({ exportConfig, deactivateUser }); + const selectionActions = buildSelectionActions({ + exportConfig, + archiveUser: (row) => setArchiveTarget(row), // single + archiveUsers: (ids) => setArchiveIds(ids), // bulk + }); const columns = useMemo(() => buildUserColumns(attributes, rowActions), [attributes]); + const handleArchiveSuccess = () => { + setArchiveTarget(null); + setArchiveIds(null); + tableRefsRef.current.resetSelection?.(); // ← clear selection + fetchUsers({ page: 1, limit: pagination.limit }); + }; + return ( <> + {/* Single archive */} !v && setArchiveTarget(null)} user={archiveTarget} - onSuccess={() => { - setArchiveTarget(null); - fetchUsers({ page: 1, limit: pagination.limit }); - }} + onSuccess={handleArchiveSuccess} + /> + + {/* Bulk archive */} + !v && setArchiveIds(null)} + ids={archiveIds ?? []} + onSuccess={handleArchiveSuccess} /> ); diff --git a/src/modules/admin/config/archive/selection.config.jsx b/src/modules/admin/config/archive/selection.config.jsx index db825c1..cfbe652 100644 --- a/src/modules/admin/config/archive/selection.config.jsx +++ b/src/modules/admin/config/archive/selection.config.jsx @@ -1,5 +1,5 @@ // config/selection.config.jsx -import { Download, Archive, Trash2 } from "lucide-react"; +import { Download, ArchiveRestore } from "lucide-react"; import { exportTableToExcel } from "@/utils/excel.util"; /** @@ -8,7 +8,27 @@ import { exportTableToExcel } from "@/utils/excel.util"; * @param {Function} deps.archiveUser Archive handler from useManagement * @param {Function} deps.deleteUser Delete handler from useManagement */ -export function buildSelectionActions({ exportConfig, archiveUser, deleteUser }) { +export function buildSelectionActions({ exportConfig, restoreUser, restoreUsers }) { return [ + { + key: "export-selected", + label: "Export", + icon: , + onClick: (rows, table) => + exportTableToExcel({ ...exportConfig, selectedRows: rows, tableInstance: table }), + }, + { + key: "restore-selected", + label: "Restore", + icon: , + className: + "text-emerald-600 border-emerald-600/40 hover:bg-emerald-600/10 hover:text-emerald-700", + onClick: (rows) => { + const ids = rows.map((r) => r.user_id); + ids.length === 1 + ? restoreUser(rows[0]) // opens single dialog + : restoreUsers(ids); // opens bulk dialog + }, + }, ]; } \ No newline at end of file diff --git a/src/modules/admin/config/selection.config.jsx b/src/modules/admin/config/selection.config.jsx index f39e7b1..3b7f7ce 100644 --- a/src/modules/admin/config/selection.config.jsx +++ b/src/modules/admin/config/selection.config.jsx @@ -8,7 +8,7 @@ import { exportTableToExcel } from "@/utils/excel.util"; * @param {Function} deps.archiveUser Archive handler from useManagement * @param {Function} deps.deleteUser Delete handler from useManagement */ -export function buildSelectionActions({ exportConfig, archiveUser }) { +export function buildSelectionActions({ exportConfig, archiveUser, archiveUsers }) { return [ { key: "export-selected", @@ -22,7 +22,12 @@ export function buildSelectionActions({ exportConfig, archiveUser }) { label: "Archive", icon: , className: "text-destructive border-destructive/40 hover:bg-destructive/10 hover:text-destructive", - onClick: (rows) => archiveUser({ ids: rows.map((r) => r.id) }), + onClick: (rows) => { + const ids = rows.map((r) => r.user_id); + ids.length === 1 + ? archiveUser(rows[0]) // opens single dialog + : archiveUsers(ids); // opens bulk dialog + }, hidden: (rows) => rows.every((r) => r.status === "archived"), }, ];