mirror of
https://github.com/rgrgogu/new_starr.git
synced 2026-09-27 00:12:54 +08:00
AdjusteD
This commit is contained in:
@@ -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: [] });
|
||||
|
||||
@@ -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,
|
||||
}}
|
||||
|
||||
@@ -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: <ArchiveUserDialog user={rowObject} ... />
|
||||
* Bulk: <ArchiveUserDialog ids={[1, 2, 3]} ... />
|
||||
*/
|
||||
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 }) {
|
||||
<AlertDialog open={open} onOpenChange={onOpenChange}>
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>Archive User</AlertDialogTitle>
|
||||
<AlertDialogTitle>
|
||||
Archive {isBulk ? `${count} Users` : "User"}
|
||||
</AlertDialogTitle>
|
||||
<AlertDialogDescription>
|
||||
Are you sure you want to archive{" "}
|
||||
<span className="font-medium text-foreground">
|
||||
{user?.personal_info?.name?.full_name ?? user?.email}
|
||||
{isBulk
|
||||
? `${count} selected user${count !== 1 ? "s" : ""}`
|
||||
: (user?.personal_info?.name?.full_name ?? user?.email)}
|
||||
</span>
|
||||
? They will be deactivated and lose access immediately.
|
||||
</AlertDialogDescription>
|
||||
@@ -44,7 +59,7 @@ export function ArchiveUserDialog({ open, onOpenChange, user, onSuccess }) {
|
||||
className="bg-destructive text-destructive-foreground hover:bg-destructive/90"
|
||||
>
|
||||
{loading ? <Spinner className="size-4 mr-2" /> : null}
|
||||
Archive
|
||||
Archive{isBulk ? ` ${count} User${count !== 1 ? "s" : ""}` : ""}
|
||||
</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
|
||||
@@ -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 (
|
||||
<>
|
||||
<DataTable
|
||||
@@ -77,14 +86,20 @@ export default function UsersTable() {
|
||||
recordLabel="user"
|
||||
emptyMessage="No users match the current filters."
|
||||
/>
|
||||
{/* Single restore */}
|
||||
<RestoreUserDialog
|
||||
open={!!restoreTarget}
|
||||
onOpenChange={(v) => !v && setRestoreTarget(null)}
|
||||
user={restoreTarget}
|
||||
onSuccess={() => {
|
||||
setRestoreTarget(null);
|
||||
fetchArchivedUsers({ page: 1, limit: pagination.limit });
|
||||
}}
|
||||
onSuccess={handleRestoreSuccess}
|
||||
/>
|
||||
|
||||
{/* Bulk restore */}
|
||||
<RestoreUserDialog
|
||||
open={!!restoreIds}
|
||||
onOpenChange={(v) => !v && setRestoreIds(null)}
|
||||
ids={restoreIds ?? []}
|
||||
onSuccess={handleRestoreSuccess}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -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: <RestoreUserDialog user={rowObject} ... />
|
||||
* Bulk: <RestoreUserDialog ids={[1, 2, 3]} ... />
|
||||
*/
|
||||
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 }) {
|
||||
<AlertDialog open={open} onOpenChange={onOpenChange}>
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>Restore User</AlertDialogTitle>
|
||||
<AlertDialogTitle>Restore {isBulk ? `${count} Users` : "User"}</AlertDialogTitle>
|
||||
<AlertDialogDescription>
|
||||
Are you sure you want to restore{" "}
|
||||
<span className="font-medium text-foreground">
|
||||
{user?.personal_info?.name?.full_name ?? user?.email}
|
||||
{isBulk
|
||||
? `${count} selected user${count !== 1 ? "s" : ""}`
|
||||
: (user?.personal_info?.name?.full_name ?? user?.email)}
|
||||
</span>
|
||||
? They will regain access to their account immediately.
|
||||
? They will regain access to their account{isBulk && count !== 1 ? "s" : ""} immediately.
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
@@ -44,7 +58,7 @@ export function RestoreUserDialog({ open, onOpenChange, user, onSuccess }) {
|
||||
className="bg-emerald-600 text-white hover:bg-emerald-700"
|
||||
>
|
||||
{loading ? <Spinner className="size-4 mr-2" /> : null}
|
||||
Restore
|
||||
Restore{isBulk ? ` ${count} User${count !== 1 ? "s" : ""}` : ""}
|
||||
</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
|
||||
@@ -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 (
|
||||
<>
|
||||
<DataTable
|
||||
@@ -75,14 +86,20 @@ export default function UsersTable() {
|
||||
recordLabel="user"
|
||||
emptyMessage="No users match the current filters."
|
||||
/>
|
||||
{/* Single archive */}
|
||||
<ArchiveUserDialog
|
||||
open={!!archiveTarget}
|
||||
onOpenChange={(v) => !v && setArchiveTarget(null)}
|
||||
user={archiveTarget}
|
||||
onSuccess={() => {
|
||||
setArchiveTarget(null);
|
||||
fetchUsers({ page: 1, limit: pagination.limit });
|
||||
}}
|
||||
onSuccess={handleArchiveSuccess}
|
||||
/>
|
||||
|
||||
{/* Bulk archive */}
|
||||
<ArchiveUserDialog
|
||||
open={!!archiveIds}
|
||||
onOpenChange={(v) => !v && setArchiveIds(null)}
|
||||
ids={archiveIds ?? []}
|
||||
onSuccess={handleArchiveSuccess}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -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: <Download className="h-3.5 w-3.5" />,
|
||||
onClick: (rows, table) =>
|
||||
exportTableToExcel({ ...exportConfig, selectedRows: rows, tableInstance: table }),
|
||||
},
|
||||
{
|
||||
key: "restore-selected",
|
||||
label: "Restore",
|
||||
icon: <ArchiveRestore className="h-3.5 w-3.5" />,
|
||||
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
|
||||
},
|
||||
},
|
||||
];
|
||||
}
|
||||
@@ -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: <Archive className="h-3.5 w-3.5" />,
|
||||
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"),
|
||||
},
|
||||
];
|
||||
|
||||
Reference in New Issue
Block a user