integrate users

This commit is contained in:
rgrgogu
2026-05-06 14:14:54 +08:00
parent 569442bf33
commit f165e8b3bb
35 changed files with 2516 additions and 24 deletions
@@ -0,0 +1,76 @@
import { useMemo, useRef } from "react";
import { useNavigate } from "react-router-dom";
import { useUsers } from "@/contexts/AdminUserContext";
import DataTable from "@/components/generic/Table/DataTable";
import { FilterSheet } from "@/components/generic/Sheet/FilterSheet";
import { buildUserColumns, columnPinning } from "../config/columns.config";
import { buildToolbarActions } from "../config/toolbar.config";
import { buildSelectionActions } from "../config/selection.config";
import { buildRowActions } from "../config/rowActions.config";
import { getTimestamp } from "@/utils/timestamp.util";
export default function UsersTable() {
const navigate = useNavigate();
const tableRefsRef = useRef({ getFilters: () => [], getSort: () => [] });
const {
users,
attributes,
pagination,
setPagination,
loading,
fetchUsers,
fetchUserFieldValues,
deactivateUser,
} = useUsers();
// Shared export config — passed into toolbar + selection configs
const exportConfig = {
allData: users,
attributes,
filename: `${getTimestamp()}_Users`,
sheetName: "Users",
};
const rowActions = buildRowActions({ navigate, deactivateUser });
const toolbarActions = buildToolbarActions({
fetchUsers, pagination, exportConfig, navigate,
getFilters: () => tableRefsRef.current.getFilters(),
getSort: () => tableRefsRef.current.getSort(),
});
const selectionActions = buildSelectionActions({ exportConfig, deactivateUser });
const columns = useMemo(() => buildUserColumns(attributes, rowActions), [attributes]);
return (
<DataTable
title="Users"
data={users}
columns={columns}
attributes={attributes}
pagination={pagination}
setPagination={setPagination}
loading={loading}
onFetch={fetchUsers}
onFetchFilterData={fetchUserFieldValues}
onRefsReady={(refs) => tableRefsRef.current = refs}
renderFilterSheet={({ open, onOpenChange, column, attr, data, loading }) => (
<FilterSheet
open={open}
onOpenChange={onOpenChange}
column={column}
attr={attr}
data={data}
loading={loading}
/>
)}
columnPinning={columnPinning}
toolbarActions={toolbarActions}
selectionActions={selectionActions}
recordLabel="user"
emptyMessage="No users match the current filters."
/>
);
}
@@ -0,0 +1,28 @@
// config/columns.config.jsx
// Column definitions and pinning config for the Users table.
import { buildColumns } from "@/utils/table.util";
import { buildSelectionColumn } from "@/components/generic/Table/buildSelectionColumn";
import { buildRowActionsColumn } from "@/components/generic/Table/buildRowActionsColumn";
export const columnPinning = {
right: ["actions"],
left: [],
};
/**
* Builds the full column array for the Users table.
*
* @param {Array} attributes Field definitions from the server (drives data columns)
* @param {Array} rowActions Row-level kebab action definitions
* @returns {Array} TanStack column definitions
*/
export function buildUserColumns(attributes, rowActions) {
const visibleAttributes = attributes.filter((a) => !a.hidden);
return [
buildSelectionColumn(),
...buildColumns(visibleAttributes),
buildRowActionsColumn(rowActions, { dropdownLabel: "User Actions" }),
];
}
@@ -0,0 +1,39 @@
// config/rowActions.config.jsx
// Per-row kebab menu action definitions for the Users table.
//
// Each onClick receives the row's data object from buildRowActionsColumn.
import { Eye, Pencil, Archive } from "lucide-react";
/**
* @param {Object} deps
* @param {Function} deps.navigate React Router navigate
* @param {Function} deps.archiveUser Archive handler from useManagement
* @returns {Array} rowActions
*/
export function buildRowActions({ navigate, archiveUser }) {
return [
{
key: "view",
label: "View details",
icon: <Eye className="h-3.5 w-3.5" />,
onClick: (row) => navigate(`view/${row.user_id}`),
},
{
key: "edit",
label: "Edit",
icon: <Pencil className="h-3.5 w-3.5" />,
onClick: (row) => navigate(`/users/${row.id}/edit`),
disabled: (row) => row.role === "super_admin",
},
{
key: "archive",
label: "Archive",
className: "text-destructive focus:text-destructive",
icon: <Archive className="h-3.5 w-3.5" />,
onClick: (row) => archiveUser(row.id),
hidden: (row) => row.status === "archived",
separator: true,
},
];
}
@@ -0,0 +1,36 @@
// config/selection.config.jsx
import { Download, Archive, Trash2 } from "lucide-react";
import { exportTableToExcel } from "@/utils/excel.util";
/**
* @param {Object} deps
* @param {Object} deps.exportConfig { allData, attributes, filename, sheetName }
* @param {Function} deps.archiveUser Archive handler from useManagement
* @param {Function} deps.deleteUser Delete handler from useManagement
*/
export function buildSelectionActions({ exportConfig, archiveUser, deleteUser }) {
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: "archive-selected",
label: "Archive",
icon: <Archive className="h-3.5 w-3.5" />,
onClick: (rows) => archiveUser({ ids: rows.map((r) => r.id) }),
hidden: (rows) => rows.every((r) => r.status === "archived"),
},
{
key: "delete-selected",
label: "Delete",
icon: <Trash2 className="h-3.5 w-3.5" />,
className: "text-destructive border-destructive/40 hover:bg-destructive/10 hover:text-destructive",
onClick: (rows) => deleteUser({ ids: rows.map((r) => r.id) }),
disabled: (rows) => rows.some((r) => r.role === "admin" || r.role === "super_admin"),
},
];
}
@@ -0,0 +1,38 @@
// ─── config/toolbar.config.jsx ────────────────────────────────────────────────
import { RefreshCw, Download, UserPlus } from "lucide-react";
import { exportTableToExcel } from "@/utils/excel.util";
/**
* @param {Object} deps
* @param {Function} deps.fetchUsers Refetch handler from useUsers
* @param {Object} deps.pagination Current pagination state
* @param {Object} deps.exportConfig { allData, attributes, filename, sheetName }
* @param {Function} deps.navigate React Router navigate
*/
export function buildToolbarActions({ fetchUsers, refetchUsers, pagination, exportConfig, navigate, getFilters, getSort }) {
return [
{
key: "refresh",
type: "button",
icon: <RefreshCw className="h-3.5 w-3.5" />,
label: "Refresh",
onClick: () => fetchUsers({ page: 1, limit: pagination.limit, filters: getFilters(), sort: getSort()}),
},
{
key: "export",
type: "button",
icon: <Download className="h-3.5 w-3.5" />,
label: "Export",
onClick: (table) => exportTableToExcel({ ...exportConfig, tableInstance: table }),
},
{
key: "add-user",
type: "button",
icon: <UserPlus className="h-3.5 w-3.5" />,
label: "Add User",
variant: "default",
className: "text-primary-foreground",
onClick: () => navigate("add"),
},
];
}
+10 -4
View File
@@ -22,6 +22,8 @@ import AdminSideTabs from "../components/AdminSideTabs"
import UserMenu from "@/components/generic/UserMenu"
import { ROLE_CONFIG } from "@/data/profile.data"
import { AdminProvider } from "@/contexts/provider/AdminProvider" // ← add
const AdminLayout = () => {
const { user, logout } = useAuth();
const navigate = useNavigate();
@@ -81,10 +83,14 @@ const AdminLayout = () => {
<AdminSideTabs />
</div>
</div>
<div id="main-body">
<Outlet />
<Toaster position="bottom-right" richColors />
</div>
{/* ─── All admin contexts live here, scoped to admin routes only ── */}
<AdminProvider>
<div id="main-body">
<Outlet />
<Toaster position="bottom-right" richColors />
</div>
</AdminProvider>
</TooltipProvider>
</section>
)
@@ -0,0 +1,12 @@
import React from 'react'
import { Outlet } from 'react-router-dom'
const UserManagementLayout = () => {
return (
<div>
<Outlet />
</div>
)
}
export default UserManagementLayout
@@ -0,0 +1,11 @@
import React from 'react'
const GroupList = () => {
return (
<div>
GroupList
</div>
)
}
export default GroupList
@@ -0,0 +1,11 @@
import React from 'react'
const ViewGroup = () => {
return (
<div>
ViewGroup
</div>
)
}
export default ViewGroup
+11
View File
@@ -0,0 +1,11 @@
import React from 'react'
const AddUser = () => {
return (
<div>
AddUser
</div>
)
}
export default AddUser
@@ -0,0 +1,6 @@
import { USER_MANAGEMENT } from "@/data/adminTiles.data";
import DashboardGrid from "@/components/generic/DashboardGrid";
export default function UserDashboard() {
return <DashboardGrid sections={USER_MANAGEMENT} />
}
@@ -0,0 +1,25 @@
import { House } from "lucide-react";
import AppBreadcrumb from "@/components/generic/Breadcrumb/AppBreadcrumb";
import UsersTable from "../../components/UserTable";
export default function UserList() {
const items = [
{ label: "Home", icon: <House className="size-4" />, to: `/admin/users` },
{ label: "Users" },
]
return (
<section className="bg-muted/60 h-full">
<div className="lg:container lg:mx-auto lg:px-0 flex flex-col items-start px-4">
<div className="flex flex-col gap-2 my-6 ">
<AppBreadcrumb items={items} />
</div>
<div className="w-full">
<UsersTable />
</div>
</div>
</section>
)
}
@@ -0,0 +1,11 @@
import React from 'react'
const ViewUser = () => {
return (
<div>
ViewUser
</div>
)
}
export default ViewUser
+47 -10
View File
@@ -1,8 +1,25 @@
import { Outlet } from 'react-router-dom'
import ProtectedRoute from '../../../routes/ProtectedRoute'
// Layouts
import AdminLayout from '../layouts/AdminLayout'
import UserManagementLayout from '../layouts/UserManagementLayout'
// Global Pages
import Admin from '../pages/Admin'
import ProfilePage from '@/components/generic/Profile'
// Specific Pages
import UsersDashboard from '../pages/users/UserDashboard'
import UserList from '../pages/users/UserList'
import AddUser from '../pages/users/AddUser'
import ViewUser from '../pages/users/ViewUser'
import GroupList from '../pages/user_groups/GroupList'
import ViewGroup from '../pages/user_groups/ViewGroup'
export const AdminRoutes = {
element: <ProtectedRoute allowedRoles={['admin']} />,
children: [
@@ -12,17 +29,37 @@ export const AdminRoutes = {
children: [
// Admin Management
{ index: true, element: <Admin /> }, // /admin
{ path: 'my-profile', element: <ProfilePage /> },
{ path: 'my-profile', element: <ProfilePage /> },
// Users Management
// {
// path: 'users',
// element: <UsersLayout />, // ← shared header/nav for user pages
// children: [
// { index: true, element: <Users /> }, // /admin/users
// { path: 'add', element: <UsersAdd /> }, // /admin/users/add
// { path: 'edit/:id', element: <UsersEdit /> }, // /admin/users/edit/123
// ]
// },
{
path: 'users',
element: <UserManagementLayout />,
children: [
{ index: true, element: <UsersDashboard /> },
// User View
{
path: 'all',
element: <Outlet />,
children: [
{ index: true, element: <UserList /> },
{ path: 'add', element: <AddUser /> },
{ path: 'view/:userId', element: <ViewUser /> }
]
},
// User Group View
{
path: 'groups',
element: <Outlet />,
children: [
{ index: true, element: <GroupList /> },
{ path: 'view/:groupId', element: <ViewGroup /> }
]
},
]
},
// Add here
]