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"),
},
];