Compare commits
18 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 63090f0b74 | |||
| afa81eacd8 | |||
| 19198b09ba | |||
| 22894a62d8 | |||
| 5ad9413dc1 | |||
| 7449dea29c | |||
| 886c7dd8b3 | |||
| fb6dc18073 | |||
| 7d955381c7 | |||
| 8df8fa45db | |||
| 4e60059290 | |||
| c6fe8c7d5a | |||
| 5eb07b2057 | |||
| 3a35a34266 | |||
| de5cb74241 | |||
| c711e000b3 | |||
| 0a28d478d8 | |||
| 0d9318ddd9 |
@@ -60,13 +60,9 @@ const Navbar = () => {
|
|||||||
data-kt-menu-attach="parent"
|
data-kt-menu-attach="parent"
|
||||||
data-kt-menu-placement="bottom-end"
|
data-kt-menu-placement="bottom-end"
|
||||||
>
|
>
|
||||||
<img
|
<img src={toAbsoluteUrl('media/avatars/300-3.jpg')} alt="" />
|
||||||
src={toAbsoluteUrl('media/avatars/300-3.jpg')}
|
|
||||||
alt=""
|
|
||||||
style={{ cursor: 'auto' }}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
{/* <HeaderUserMenu /> */}
|
<HeaderUserMenu />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{config.app?.header?.default?.menu?.display && (
|
{config.app?.header?.default?.menu?.display && (
|
||||||
|
|||||||
@@ -108,6 +108,19 @@ const SidebarMenuMain = () => {
|
|||||||
title='User management'
|
title='User management'
|
||||||
fontIcon='bi-layers'
|
fontIcon='bi-layers'
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
|
||||||
|
<div className='menu-item'>
|
||||||
|
<div className='menu-content pt-8 pb-2'>
|
||||||
|
<span className='menu-section text-muted text-uppercase fs-8 ls-1'>Employers</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<SidebarMenuItem
|
||||||
|
to='apps/employers-list/employers'
|
||||||
|
icon='abstract-28'
|
||||||
|
title='List'
|
||||||
|
fontIcon='bi-layers'
|
||||||
|
/>
|
||||||
{/*<div className='menu-item'>*/}
|
{/*<div className='menu-item'>*/}
|
||||||
{/* <a*/}
|
{/* <a*/}
|
||||||
{/* target='_blank'*/}
|
{/* target='_blank'*/}
|
||||||
|
|||||||
@@ -1,64 +1,99 @@
|
|||||||
|
import { FC, useEffect } from 'react';
|
||||||
import {FC} from 'react'
|
import { Link } from 'react-router-dom';
|
||||||
import {Link} from 'react-router-dom'
|
import { useAuth } from '../../../../app/modules/auth';
|
||||||
import {useAuth} from '../../../../app/modules/auth'
|
import { Languages } from './Languages';
|
||||||
import {Languages} from './Languages'
|
import { toAbsoluteUrl } from '../../../helpers';
|
||||||
import {toAbsoluteUrl} from '../../../helpers'
|
|
||||||
|
|
||||||
const HeaderUserMenu: FC = () => {
|
const HeaderUserMenu: FC = () => {
|
||||||
const {currentUser, logout} = useAuth()
|
const { currentUser, logout } = useAuth();
|
||||||
|
|
||||||
|
// Listen for user activity and trigger logout
|
||||||
|
useEffect(() => {
|
||||||
|
let timeout: number;
|
||||||
|
const inactiveTime: number = 7 * 60 * 1000; //default inactive period (milliseconds)
|
||||||
|
// Logout user after inactiveTime minutes of inactivity
|
||||||
|
const resetTimeout = () => {
|
||||||
|
clearTimeout(timeout);
|
||||||
|
// Set logout timeout
|
||||||
|
timeout = window.setTimeout(() => {
|
||||||
|
logout();
|
||||||
|
}, inactiveTime);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleUserActivity: any = () => {
|
||||||
|
resetTimeout(); // reset session on user activity
|
||||||
|
};
|
||||||
|
|
||||||
|
document.addEventListener('mousemove', handleUserActivity);
|
||||||
|
document.addEventListener('keydown', handleUserActivity);
|
||||||
|
document.addEventListener('click', handleUserActivity);
|
||||||
|
document.addEventListener('focus', handleUserActivity);
|
||||||
|
|
||||||
|
// Initialize timeout
|
||||||
|
resetTimeout();
|
||||||
|
|
||||||
|
// Remove event listeners on unmount
|
||||||
|
return () => {
|
||||||
|
clearTimeout(timeout);
|
||||||
|
document.removeEventListener('mousemove', handleUserActivity);
|
||||||
|
document.removeEventListener('keydown', handleUserActivity);
|
||||||
|
document.removeEventListener('click', handleUserActivity);
|
||||||
|
document.removeEventListener('focus', handleUserActivity);
|
||||||
|
};
|
||||||
|
}, [logout]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className='menu menu-sub menu-sub-dropdown menu-column menu-rounded menu-gray-600 menu-state-bg menu-state-primary fw-bold py-4 fs-6 w-275px'
|
className="menu menu-sub menu-sub-dropdown menu-column menu-rounded menu-gray-600 menu-state-bg menu-state-primary fw-bold py-4 fs-6 w-275px"
|
||||||
data-kt-menu='true'
|
data-kt-menu="true"
|
||||||
>
|
>
|
||||||
<div className='menu-item px-3'>
|
<div className="menu-item px-3">
|
||||||
<div className='menu-content d-flex align-items-center px-3'>
|
<div className="menu-content d-flex align-items-center px-3">
|
||||||
<div className='symbol symbol-50px me-5'>
|
<div className="symbol symbol-50px me-5">
|
||||||
<img alt='Logo' src={toAbsoluteUrl('media/avatars/300-3.jpg')} />
|
<img alt="Logo" src={toAbsoluteUrl('media/avatars/300-3.jpg')} />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className='d-flex flex-column'>
|
<div className="d-flex flex-column">
|
||||||
<div className='fw-bolder d-flex align-items-center fs-5'>
|
<div className="fw-bolder d-flex align-items-center fs-5">
|
||||||
{currentUser?.first_name} {currentUser?.first_name}
|
{currentUser?.first_name} {currentUser?.first_name}
|
||||||
{/*<span className='badge badge-light-success fw-bolder fs-8 px-2 py-1 ms-2'>Pro</span>*/}
|
{/*<span className='badge badge-light-success fw-bolder fs-8 px-2 py-1 ms-2'>Pro</span>*/}
|
||||||
</div>
|
</div>
|
||||||
<a href='#' className='fw-bold text-muted text-hover-primary fs-7'>
|
<a href="#" className="fw-bold text-muted text-hover-primary fs-7">
|
||||||
{currentUser?.email}
|
{currentUser?.email}
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className='separator my-2'></div>
|
{/* <div className="separator my-2"></div> */}
|
||||||
|
|
||||||
<div className='menu-item px-5'>
|
{/* <div className='menu-item px-5'>
|
||||||
<Link to={'/crafted/pages/profile'} className='menu-link px-5'>
|
<Link to={'/crafted/pages/profile'} className='menu-link px-5'>
|
||||||
My Profile
|
My Profile
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div> */}
|
||||||
|
|
||||||
<div className='menu-item px-5'>
|
{/* <div className='menu-item px-5'>
|
||||||
<a href='#' className='menu-link px-5'>
|
<a href='#' className='menu-link px-5'>
|
||||||
<span className='menu-text'>My Projects</span>
|
<span className='menu-text'>My Projects</span>
|
||||||
<span className='menu-badge'>
|
<span className='menu-badge'>
|
||||||
<span className='badge badge-light-danger badge-circle fw-bolder fs-7'>3</span>
|
<span className='badge badge-light-danger badge-circle fw-bolder fs-7'>3</span>
|
||||||
</span>
|
</span>
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div> */}
|
||||||
|
|
||||||
<div
|
{/* <div
|
||||||
className='menu-item px-5'
|
className="menu-item px-5"
|
||||||
data-kt-menu-trigger='hover'
|
data-kt-menu-trigger="hover"
|
||||||
data-kt-menu-placement='left-start'
|
data-kt-menu-placement="left-start"
|
||||||
data-kt-menu-flip='bottom'
|
data-kt-menu-flip="bottom"
|
||||||
>
|
>
|
||||||
<a href='#' className='menu-link px-5'>
|
<a href='#' className='menu-link px-5'>
|
||||||
<span className='menu-title'>My Subscription</span>
|
<span className='menu-title'>My Subscription</span>
|
||||||
<span className='menu-arrow'></span>
|
<span className='menu-arrow'></span>
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
<div className='menu-sub menu-sub-dropdown w-175px py-4'>
|
<div className="menu-sub menu-sub-dropdown w-175px py-4">
|
||||||
<div className='menu-item px-3'>
|
<div className='menu-item px-3'>
|
||||||
<a href='#' className='menu-link px-5'>
|
<a href='#' className='menu-link px-5'>
|
||||||
Referrals
|
Referrals
|
||||||
@@ -88,7 +123,7 @@ const HeaderUserMenu: FC = () => {
|
|||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className='separator my-2'></div>
|
<div className="separator my-2"></div>
|
||||||
|
|
||||||
<div className='menu-item px-3'>
|
<div className='menu-item px-3'>
|
||||||
<div className='menu-content px-3'>
|
<div className='menu-content px-3'>
|
||||||
@@ -105,31 +140,31 @@ const HeaderUserMenu: FC = () => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div> */}
|
||||||
|
|
||||||
<div className='menu-item px-5'>
|
{/* <div className='menu-item px-5'>
|
||||||
<a href='#' className='menu-link px-5'>
|
<a href='#' className='menu-link px-5'>
|
||||||
My Statements
|
My Statements
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div> */}
|
||||||
|
|
||||||
<div className='separator my-2'></div>
|
<div className="separator my-2"></div>
|
||||||
|
|
||||||
<Languages />
|
{/* <Languages /> */}
|
||||||
|
|
||||||
<div className='menu-item px-5 my-1'>
|
{/* <div className='menu-item px-5 my-1'>
|
||||||
<Link to='/crafted/account/settings' className='menu-link px-5'>
|
<Link to='/crafted/account/settings' className='menu-link px-5'>
|
||||||
Account Settings
|
Account Settings
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div> */}
|
||||||
|
|
||||||
<div className='menu-item px-5'>
|
<div className="menu-item px-5">
|
||||||
<a onClick={logout} className='menu-link px-5'>
|
<a onClick={logout} className="menu-link px-5">
|
||||||
Sign Out
|
Sign Out
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
export {HeaderUserMenu}
|
export { HeaderUserMenu };
|
||||||
|
|||||||
+7
-7
@@ -4,8 +4,8 @@ import {UsersListWrapper} from './users-list/UsersList'
|
|||||||
|
|
||||||
const usersBreadcrumbs: Array<PageLink> = [
|
const usersBreadcrumbs: Array<PageLink> = [
|
||||||
{
|
{
|
||||||
title: 'User Management',
|
title: 'Employer Management',
|
||||||
path: '/apps/user-management/users',
|
path: '/apps/employers-list/employers',
|
||||||
isSeparator: false,
|
isSeparator: false,
|
||||||
isActive: false,
|
isActive: false,
|
||||||
},
|
},
|
||||||
@@ -17,23 +17,23 @@ const usersBreadcrumbs: Array<PageLink> = [
|
|||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
const UsersPage = () => {
|
const EmployersPage = () => {
|
||||||
return (
|
return (
|
||||||
<Routes>
|
<Routes>
|
||||||
<Route element={<Outlet />}>
|
<Route element={<Outlet />}>
|
||||||
<Route
|
<Route
|
||||||
path='users'
|
path='employers'
|
||||||
element={
|
element={
|
||||||
<>
|
<>
|
||||||
<PageTitle breadcrumbs={usersBreadcrumbs}>Users list</PageTitle>
|
<PageTitle breadcrumbs={usersBreadcrumbs}>Employers list</PageTitle>
|
||||||
<UsersListWrapper />
|
<UsersListWrapper />
|
||||||
</>
|
</>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
</Route>
|
</Route>
|
||||||
<Route index element={<Navigate to='/apps/user-management/users' />} />
|
<Route index element={<Navigate to='/apps/employers-list/employers' />} />
|
||||||
</Routes>
|
</Routes>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export default UsersPage
|
export default EmployersPage
|
||||||
@@ -1,37 +1,5 @@
|
|||||||
import { KTCard } from "../../../../_digifi/helpers"
|
import { UsersListWrapper } from "../user-approved/UsersList";
|
||||||
import { Content } from "../../../../_digifi/layout/components/content"
|
|
||||||
import { ToolbarWrapper } from "../../../../_digifi/layout/components/toolbar"
|
|
||||||
import { UsersListHeader } from "../user-approved/users-list/components/header/UsersListHeader"
|
|
||||||
import { ListViewProvider, useListView } from "../user-approved/users-list/core/ListViewProvider"
|
|
||||||
import { QueryRequestProvider } from "../user-approved/users-list/core/QueryRequestProvider"
|
|
||||||
import { QueryResponseProvider } from "../user-approved/users-list/core/QueryResponseProvider"
|
|
||||||
import { UsersTable } from "../user-approved/users-list/table/UsersTable"
|
|
||||||
import { UserEditModal } from "../user-approved/users-list/user-edit-modal/UserEditModal"
|
|
||||||
|
|
||||||
const UsersList = () => {
|
const UserApprovedList = () => <UsersListWrapper />;
|
||||||
const {itemIdForUpdate} = useListView()
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<KTCard>
|
|
||||||
<UsersListHeader />
|
|
||||||
<UsersTable />
|
|
||||||
</KTCard>
|
|
||||||
{itemIdForUpdate !== undefined && <UserEditModal />}
|
|
||||||
</>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
const UserApprovedList = () => (
|
export { UserApprovedList };
|
||||||
<QueryRequestProvider>
|
|
||||||
<QueryResponseProvider>
|
|
||||||
<ListViewProvider>
|
|
||||||
<ToolbarWrapper />
|
|
||||||
<Content>
|
|
||||||
<UsersList />
|
|
||||||
</Content>
|
|
||||||
</ListViewProvider>
|
|
||||||
</QueryResponseProvider>
|
|
||||||
</QueryRequestProvider>
|
|
||||||
)
|
|
||||||
|
|
||||||
export {UserApprovedList}
|
|
||||||
|
|||||||
@@ -1,37 +1,5 @@
|
|||||||
import { KTCard } from "../../../../_digifi/helpers"
|
import { UsersListWrapper } from "../user-pending/UsersList";
|
||||||
import { Content } from "../../../../_digifi/layout/components/content"
|
|
||||||
import { ToolbarWrapper } from "../../../../_digifi/layout/components/toolbar"
|
|
||||||
import { UsersListHeader } from "../user-pending/users-list/components/header/UsersListHeader"
|
|
||||||
import { ListViewProvider, useListView } from "../user-pending/users-list/core/ListViewProvider"
|
|
||||||
import { QueryRequestProvider } from "../user-pending/users-list/core/QueryRequestProvider"
|
|
||||||
import { QueryResponseProvider } from "../user-pending/users-list/core/QueryResponseProvider"
|
|
||||||
import { UsersTable } from "../user-pending/users-list/table/UsersTable"
|
|
||||||
import { UserEditModal } from "../user-pending/users-list/user-edit-modal/UserEditModal"
|
|
||||||
|
|
||||||
const UsersList = () => {
|
const UserPendingList = () => <UsersListWrapper />;
|
||||||
const {itemIdForUpdate} = useListView()
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<KTCard>
|
|
||||||
<UsersListHeader />
|
|
||||||
<UsersTable />
|
|
||||||
</KTCard>
|
|
||||||
{itemIdForUpdate !== undefined && <UserEditModal />}
|
|
||||||
</>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
const UserPendingList = () => (
|
export { UserPendingList };
|
||||||
<QueryRequestProvider>
|
|
||||||
<QueryResponseProvider>
|
|
||||||
<ListViewProvider>
|
|
||||||
<ToolbarWrapper />
|
|
||||||
<Content>
|
|
||||||
<UsersList />
|
|
||||||
</Content>
|
|
||||||
</ListViewProvider>
|
|
||||||
</QueryResponseProvider>
|
|
||||||
</QueryRequestProvider>
|
|
||||||
)
|
|
||||||
|
|
||||||
export {UserPendingList}
|
|
||||||
|
|||||||
@@ -1,37 +1,5 @@
|
|||||||
import { KTCard } from "../../../../_digifi/helpers"
|
import { UsersListWrapper } from "../user-ready/UsersList";
|
||||||
import { Content } from "../../../../_digifi/layout/components/content"
|
|
||||||
import { ToolbarWrapper } from "../../../../_digifi/layout/components/toolbar"
|
|
||||||
import { UsersListHeader } from "../user-ready/users-list/components/header/UsersListHeader"
|
|
||||||
import { ListViewProvider, useListView } from "../user-ready/users-list/core/ListViewProvider"
|
|
||||||
import { QueryRequestProvider } from "../user-ready/users-list/core/QueryRequestProvider"
|
|
||||||
import { QueryResponseProvider } from "../user-ready/users-list/core/QueryResponseProvider"
|
|
||||||
import { UsersTable } from "../user-ready/users-list/table/UsersTable"
|
|
||||||
import { UserEditModal } from "../user-ready/users-list/user-edit-modal/UserEditModal"
|
|
||||||
|
|
||||||
const UsersList = () => {
|
const UserReadyList = () => <UsersListWrapper />;
|
||||||
const {itemIdForUpdate} = useListView()
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<KTCard>
|
|
||||||
<UsersListHeader />
|
|
||||||
<UsersTable />
|
|
||||||
</KTCard>
|
|
||||||
{itemIdForUpdate !== undefined && <UserEditModal />}
|
|
||||||
</>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
const UserReadyList = () => (
|
export { UserReadyList };
|
||||||
<QueryRequestProvider>
|
|
||||||
<QueryResponseProvider>
|
|
||||||
<ListViewProvider>
|
|
||||||
<ToolbarWrapper />
|
|
||||||
<Content>
|
|
||||||
<UsersList />
|
|
||||||
</Content>
|
|
||||||
</ListViewProvider>
|
|
||||||
</QueryResponseProvider>
|
|
||||||
</QueryRequestProvider>
|
|
||||||
)
|
|
||||||
|
|
||||||
export {UserReadyList}
|
|
||||||
|
|||||||
@@ -1,37 +1,5 @@
|
|||||||
import { KTCard } from "../../../../_digifi/helpers"
|
import { UsersListWrapper } from "../user-rejected/UsersList";
|
||||||
import { Content } from "../../../../_digifi/layout/components/content"
|
|
||||||
import { ToolbarWrapper } from "../../../../_digifi/layout/components/toolbar"
|
|
||||||
import { UsersListHeader } from "../user-rejected/users-list/components/header/UsersListHeader"
|
|
||||||
import { ListViewProvider, useListView } from "../user-rejected/users-list/core/ListViewProvider"
|
|
||||||
import { QueryRequestProvider } from "../user-rejected/users-list/core/QueryRequestProvider"
|
|
||||||
import { QueryResponseProvider } from "../user-rejected/users-list/core/QueryResponseProvider"
|
|
||||||
import { UsersTable } from "../user-rejected/users-list/table/UsersTable"
|
|
||||||
import { UserEditModal } from "../user-rejected/users-list/user-edit-modal/UserEditModal"
|
|
||||||
|
|
||||||
const UsersList = () => {
|
const UserRejectedList = () => <UsersListWrapper />;
|
||||||
const {itemIdForUpdate} = useListView()
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<KTCard>
|
|
||||||
<UsersListHeader />
|
|
||||||
<UsersTable />
|
|
||||||
</KTCard>
|
|
||||||
{itemIdForUpdate !== undefined && <UserEditModal />}
|
|
||||||
</>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
const UserRejectedList = () => (
|
export { UserRejectedList };
|
||||||
<QueryRequestProvider>
|
|
||||||
<QueryResponseProvider>
|
|
||||||
<ListViewProvider>
|
|
||||||
<ToolbarWrapper />
|
|
||||||
<Content>
|
|
||||||
<UsersList />
|
|
||||||
</Content>
|
|
||||||
</ListViewProvider>
|
|
||||||
</QueryResponseProvider>
|
|
||||||
</QueryRequestProvider>
|
|
||||||
)
|
|
||||||
|
|
||||||
export {UserRejectedList}
|
|
||||||
|
|||||||
@@ -1,37 +1,5 @@
|
|||||||
import { KTCard } from "../../../../_digifi/helpers"
|
import { UsersListWrapper } from "../user-started/UsersList";
|
||||||
import { Content } from "../../../../_digifi/layout/components/content"
|
|
||||||
import { ToolbarWrapper } from "../../../../_digifi/layout/components/toolbar"
|
|
||||||
import { UsersListHeader } from "../user-started/users-list/components/header/UsersListHeader"
|
|
||||||
import { ListViewProvider, useListView } from "../user-started/users-list/core/ListViewProvider"
|
|
||||||
import { QueryRequestProvider } from "../user-started/users-list/core/QueryRequestProvider"
|
|
||||||
import { QueryResponseProvider } from "../user-started/users-list/core/QueryResponseProvider"
|
|
||||||
import { UsersTable } from "../user-started/users-list/table/UsersTable"
|
|
||||||
import { UserEditModal } from "../user-started/users-list/user-edit-modal/UserEditModal"
|
|
||||||
|
|
||||||
const UsersList = () => {
|
const UserStartedList = () => <UsersListWrapper />;
|
||||||
const {itemIdForUpdate} = useListView()
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<KTCard>
|
|
||||||
<UsersListHeader />
|
|
||||||
<UsersTable />
|
|
||||||
</KTCard>
|
|
||||||
{itemIdForUpdate !== undefined && <UserEditModal />}
|
|
||||||
</>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
const UserStartedList = () => (
|
export { UserStartedList };
|
||||||
<QueryRequestProvider>
|
|
||||||
<QueryResponseProvider>
|
|
||||||
<ListViewProvider>
|
|
||||||
<ToolbarWrapper />
|
|
||||||
<Content>
|
|
||||||
<UsersList />
|
|
||||||
</Content>
|
|
||||||
</ListViewProvider>
|
|
||||||
</QueryResponseProvider>
|
|
||||||
</QueryRequestProvider>
|
|
||||||
)
|
|
||||||
|
|
||||||
export {UserStartedList}
|
|
||||||
|
|||||||
+2
-1
@@ -1,4 +1,5 @@
|
|||||||
import {ID, Response} from '../../../../../../_digifi/helpers'
|
import { ID, Response } from "../../../../_digifi/helpers"
|
||||||
|
|
||||||
export type User = {
|
export type User = {
|
||||||
id?: ID
|
id?: ID
|
||||||
name?: string
|
name?: string
|
||||||
+29
-1
@@ -1,5 +1,5 @@
|
|||||||
import axios, { AxiosResponse } from "axios";
|
import axios, { AxiosResponse } from "axios";
|
||||||
import { ID, Response } from "../../../../../../_digifi/helpers";
|
import { ID, Response } from "../../../../_digifi/helpers"
|
||||||
import { User, UsersQueryResponse } from "./_models";
|
import { User, UsersQueryResponse } from "./_models";
|
||||||
|
|
||||||
const API_URL = import.meta.env.VITE_APP_THEME_API_URL;
|
const API_URL = import.meta.env.VITE_APP_THEME_API_URL;
|
||||||
@@ -19,6 +19,30 @@ const getStartedUsers = (query: string): Promise<UsersQueryResponse> => { // FUN
|
|||||||
.then((d: AxiosResponse<UsersQueryResponse>) => d.data);
|
.then((d: AxiosResponse<UsersQueryResponse>) => d.data);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const getRejectedUsers = (query: string): Promise<UsersQueryResponse> => { // FUNCTION TO GET USERS THAT HAVE REJECTED LOAN APPLICATION
|
||||||
|
return axios
|
||||||
|
.get(`${NEW_USER_ENDPOINT}/loan/rejected`)
|
||||||
|
.then((d: AxiosResponse<UsersQueryResponse>) => d.data);
|
||||||
|
};
|
||||||
|
|
||||||
|
const getPendingUsers = (query: string): Promise<UsersQueryResponse> => { // FUNCTION TO GET USERS THAT HAVE PENDING LOAN APPLICATION
|
||||||
|
return axios
|
||||||
|
.get(`${NEW_USER_ENDPOINT}/loan/pending`)
|
||||||
|
.then((d: AxiosResponse<UsersQueryResponse>) => d.data);
|
||||||
|
};
|
||||||
|
|
||||||
|
const getReadyUsers = (query: string): Promise<UsersQueryResponse> => { // FUNCTION TO GET USERS THAT HAVE READY LOAN APPLICATION
|
||||||
|
return axios
|
||||||
|
.get(`${NEW_USER_ENDPOINT}/loan/ready`)
|
||||||
|
.then((d: AxiosResponse<UsersQueryResponse>) => d.data);
|
||||||
|
};
|
||||||
|
|
||||||
|
const getApprovedUsers = (query: string): Promise<UsersQueryResponse> => { // FUNCTION TO GET USERS THAT HAVE APPROVED LOAN APPLICATION
|
||||||
|
return axios
|
||||||
|
.get(`${NEW_USER_ENDPOINT}/loan/approved`)
|
||||||
|
.then((d: AxiosResponse<UsersQueryResponse>) => d.data);
|
||||||
|
};
|
||||||
|
|
||||||
const getUserById = (id: ID): Promise<User | undefined> => {
|
const getUserById = (id: ID): Promise<User | undefined> => {
|
||||||
return axios
|
return axios
|
||||||
.get(`${USER_URL}/${id}`)
|
.get(`${USER_URL}/${id}`)
|
||||||
@@ -51,6 +75,10 @@ const deleteSelectedUsers = (userIds: Array<ID>): Promise<void> => {
|
|||||||
|
|
||||||
export {
|
export {
|
||||||
getStartedUsers,
|
getStartedUsers,
|
||||||
|
getRejectedUsers,
|
||||||
|
getPendingUsers,
|
||||||
|
getReadyUsers,
|
||||||
|
getApprovedUsers,
|
||||||
deleteUser,
|
deleteUser,
|
||||||
deleteSelectedUsers,
|
deleteSelectedUsers,
|
||||||
getUserById,
|
getUserById,
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
import { ListViewProvider, useListView } from "./core/ListViewProvider";
|
||||||
|
import { QueryRequestProvider } from "./core/QueryRequestProvider";
|
||||||
|
import { QueryResponseProvider } from "./core/QueryResponseProvider";
|
||||||
|
import { UsersListHeader } from "./components/header/UsersListHeader";
|
||||||
|
import { UsersTable } from "./table/UsersTable";
|
||||||
|
import { UserEditModal } from "./user-edit-modal/UserEditModal";
|
||||||
|
import { KTCard } from "../../../../_digifi/helpers";
|
||||||
|
import { ToolbarWrapper } from "../../../../_digifi/layout/components/toolbar";
|
||||||
|
import { Content } from "../../../../_digifi/layout/components/content";
|
||||||
|
|
||||||
|
const UsersList = () => {
|
||||||
|
const { itemIdForUpdate } = useListView();
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<KTCard>
|
||||||
|
<UsersListHeader />
|
||||||
|
<UsersTable />
|
||||||
|
</KTCard>
|
||||||
|
{itemIdForUpdate !== undefined && <UserEditModal />}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const UsersListWrapper = () => (
|
||||||
|
<QueryRequestProvider>
|
||||||
|
<QueryResponseProvider>
|
||||||
|
<ListViewProvider>
|
||||||
|
<ToolbarWrapper />
|
||||||
|
<Content>
|
||||||
|
<UsersList />
|
||||||
|
</Content>
|
||||||
|
</ListViewProvider>
|
||||||
|
</QueryResponseProvider>
|
||||||
|
</QueryRequestProvider>
|
||||||
|
);
|
||||||
|
|
||||||
|
export { UsersListWrapper };
|
||||||
+13
-10
@@ -1,15 +1,18 @@
|
|||||||
import {KTIcon} from '../../../../../../../_digifi/helpers'
|
import { KTIcon } from "../../../../../../_digifi/helpers";
|
||||||
import {useListView} from '../../core/ListViewProvider'
|
import { useListView } from "../../core/ListViewProvider";
|
||||||
import {UsersListFilter} from './UsersListFilter'
|
import { UsersListFilter } from "./UsersListFilter";
|
||||||
|
|
||||||
const UsersListToolbar = () => {
|
const UsersListToolbar = () => {
|
||||||
const {setItemIdForUpdate} = useListView()
|
const { setItemIdForUpdate } = useListView();
|
||||||
const openAddUserModal = () => {
|
const openAddUserModal = () => {
|
||||||
setItemIdForUpdate(null)
|
setItemIdForUpdate(null);
|
||||||
}
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='d-flex justify-content-end' data-kt-user-table-toolbar='base'>
|
<div
|
||||||
|
className="d-flex justify-content-end"
|
||||||
|
data-kt-user-table-toolbar="base"
|
||||||
|
>
|
||||||
<UsersListFilter />
|
<UsersListFilter />
|
||||||
|
|
||||||
{/* begin::Export */}
|
{/* begin::Export */}
|
||||||
@@ -26,7 +29,7 @@ const UsersListToolbar = () => {
|
|||||||
</button> */}
|
</button> */}
|
||||||
{/* end::Add user */}
|
{/* end::Add user */}
|
||||||
</div>
|
</div>
|
||||||
)
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
export {UsersListToolbar}
|
export { UsersListToolbar };
|
||||||
@@ -0,0 +1,136 @@
|
|||||||
|
import { useEffect, useState } from "react";
|
||||||
|
import { MenuComponent } from "../../../../../../_digifi/assets/ts/components";
|
||||||
|
import { initialQueryState, KTIcon } from "../../../../../../_digifi/helpers";
|
||||||
|
import { useQueryRequest } from "../../core/QueryRequestProvider";
|
||||||
|
import { useQueryResponse } from "../../core/QueryResponseProvider";
|
||||||
|
|
||||||
|
const UsersListFilter = () => {
|
||||||
|
const { updateState } = useQueryRequest();
|
||||||
|
const { isLoading } = useQueryResponse();
|
||||||
|
const [role, setRole] = useState<string | undefined>();
|
||||||
|
const [lastLogin, setLastLogin] = useState<string | undefined>();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
MenuComponent.reinitialization();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const resetData = () => {
|
||||||
|
updateState({ filter: undefined, ...initialQueryState });
|
||||||
|
};
|
||||||
|
|
||||||
|
const filterData = () => {
|
||||||
|
updateState({
|
||||||
|
filter: { role, last_login: lastLogin },
|
||||||
|
...initialQueryState,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{/* begin::Filter Button */}
|
||||||
|
<button
|
||||||
|
disabled={isLoading}
|
||||||
|
type="button"
|
||||||
|
className="btn btn-light-primary me-3"
|
||||||
|
data-kt-menu-trigger="click"
|
||||||
|
data-kt-menu-placement="bottom-end"
|
||||||
|
>
|
||||||
|
<KTIcon iconName="filter" className="fs-2" />
|
||||||
|
Filter
|
||||||
|
</button>
|
||||||
|
{/* end::Filter Button */}
|
||||||
|
{/* begin::SubMenu */}
|
||||||
|
<div
|
||||||
|
className="menu menu-sub menu-sub-dropdown w-300px w-md-325px"
|
||||||
|
data-kt-menu="true"
|
||||||
|
>
|
||||||
|
{/* begin::Header */}
|
||||||
|
<div className="px-7 py-5">
|
||||||
|
<div className="fs-5 text-gray-900 fw-bolder">Filter Options</div>
|
||||||
|
</div>
|
||||||
|
{/* end::Header */}
|
||||||
|
|
||||||
|
{/* begin::Separator */}
|
||||||
|
<div className="separator border-gray-200"></div>
|
||||||
|
{/* end::Separator */}
|
||||||
|
|
||||||
|
{/* begin::Content */}
|
||||||
|
<div className="px-7 py-5" data-kt-user-table-filter="form">
|
||||||
|
{/* begin::Input group */}
|
||||||
|
<div className="mb-10">
|
||||||
|
<label className="form-label fs-6 fw-bold">Role:</label>
|
||||||
|
<select
|
||||||
|
className="form-select form-select-solid fw-bolder"
|
||||||
|
data-kt-select2="true"
|
||||||
|
data-placeholder="Select option"
|
||||||
|
data-allow-clear="true"
|
||||||
|
data-kt-user-table-filter="role"
|
||||||
|
data-hide-search="true"
|
||||||
|
onChange={(e) => setRole(e.target.value)}
|
||||||
|
value={role}
|
||||||
|
>
|
||||||
|
<option value=""></option>
|
||||||
|
<option value="Administrator">Administrator</option>
|
||||||
|
<option value="Analyst">Analyst</option>
|
||||||
|
<option value="Developer">Developer</option>
|
||||||
|
<option value="Support">Support</option>
|
||||||
|
<option value="Trial">Trial</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
{/* end::Input group */}
|
||||||
|
|
||||||
|
{/* begin::Input group */}
|
||||||
|
<div className="mb-10">
|
||||||
|
<label className="form-label fs-6 fw-bold">Last login:</label>
|
||||||
|
<select
|
||||||
|
className="form-select form-select-solid fw-bolder"
|
||||||
|
data-kt-select2="true"
|
||||||
|
data-placeholder="Select option"
|
||||||
|
data-allow-clear="true"
|
||||||
|
data-kt-user-table-filter="two-step"
|
||||||
|
data-hide-search="true"
|
||||||
|
onChange={(e) => setLastLogin(e.target.value)}
|
||||||
|
value={lastLogin}
|
||||||
|
>
|
||||||
|
<option value=""></option>
|
||||||
|
<option value="Yesterday">Yesterday</option>
|
||||||
|
<option value="20 mins ago">20 mins ago</option>
|
||||||
|
<option value="5 hours ago">5 hours ago</option>
|
||||||
|
<option value="2 days ago">2 days ago</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
{/* end::Input group */}
|
||||||
|
|
||||||
|
{/* begin::Actions */}
|
||||||
|
<div className="d-flex justify-content-end">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
disabled={isLoading}
|
||||||
|
onClick={filterData}
|
||||||
|
className="btn btn-light btn-active-light-primary fw-bold me-2 px-6"
|
||||||
|
data-kt-menu-dismiss="true"
|
||||||
|
data-kt-user-table-filter="reset"
|
||||||
|
>
|
||||||
|
Reset
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
disabled={isLoading}
|
||||||
|
type="button"
|
||||||
|
onClick={resetData}
|
||||||
|
className="btn btn-primary fw-bold px-6"
|
||||||
|
data-kt-menu-dismiss="true"
|
||||||
|
data-kt-user-table-filter="filter"
|
||||||
|
>
|
||||||
|
Apply
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
{/* end::Actions */}
|
||||||
|
</div>
|
||||||
|
{/* end::Content */}
|
||||||
|
</div>
|
||||||
|
{/* end::SubMenu */}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export { UsersListFilter };
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
import { useQueryClient, useMutation } from "react-query";
|
||||||
|
import { QUERIES } from "../../../../../../_digifi/helpers";
|
||||||
|
import { useListView } from "../../core/ListViewProvider";
|
||||||
|
import { useQueryResponse } from "../../core/QueryResponseProvider";
|
||||||
|
import { deleteSelectedUsers } from "../../../core/_requests";
|
||||||
|
|
||||||
|
const UsersListGrouping = () => {
|
||||||
|
const { selected, clearSelected } = useListView();
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
const { query } = useQueryResponse();
|
||||||
|
|
||||||
|
const deleteSelectedItems = useMutation(() => deleteSelectedUsers(selected), {
|
||||||
|
// 💡 response of the mutation is passed to onSuccess
|
||||||
|
onSuccess: () => {
|
||||||
|
// ✅ update detail view directly
|
||||||
|
queryClient.invalidateQueries([`${QUERIES.USERS_LIST}-${query}`]);
|
||||||
|
clearSelected();
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="d-flex justify-content-end align-items-center">
|
||||||
|
<div className="fw-bolder me-5">
|
||||||
|
<span className="me-2">{selected.length}</span> Selected
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="btn btn-danger"
|
||||||
|
onClick={async () => await deleteSelectedItems.mutateAsync()}
|
||||||
|
>
|
||||||
|
Delete Selected
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export { UsersListGrouping };
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
/* eslint-disable react-hooks/exhaustive-deps */
|
||||||
|
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
|
import {
|
||||||
|
initialQueryState,
|
||||||
|
KTIcon,
|
||||||
|
useDebounce,
|
||||||
|
} from "../../../../../../_digifi/helpers";
|
||||||
|
import { useQueryRequest } from "../../core/QueryRequestProvider";
|
||||||
|
|
||||||
|
const UsersListSearchComponent = () => {
|
||||||
|
const { updateState } = useQueryRequest();
|
||||||
|
const [searchTerm, setSearchTerm] = useState<string>("");
|
||||||
|
// Debounce search term so that it only gives us latest value ...
|
||||||
|
// ... if searchTerm has not been updated within last 500ms.
|
||||||
|
// The goal is to only have the API call fire when user stops typing ...
|
||||||
|
// ... so that we aren't hitting our API rapidly.
|
||||||
|
const debouncedSearchTerm = useDebounce(searchTerm, 150);
|
||||||
|
// Effect for API call
|
||||||
|
useEffect(
|
||||||
|
() => {
|
||||||
|
if (debouncedSearchTerm !== undefined && searchTerm !== undefined) {
|
||||||
|
updateState({ search: debouncedSearchTerm, ...initialQueryState });
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[debouncedSearchTerm] // Only call effect if debounced search term changes
|
||||||
|
// More details about useDebounce: https://usehooks.com/useDebounce/
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="card-title">
|
||||||
|
{/* begin::Search */}
|
||||||
|
<div className="d-flex align-items-center position-relative my-1">
|
||||||
|
<KTIcon iconName="magnifier" className="fs-1 position-absolute ms-6" />
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
data-kt-user-table-filter="search"
|
||||||
|
className="form-control form-control-solid w-250px ps-14"
|
||||||
|
placeholder="Search user"
|
||||||
|
value={searchTerm}
|
||||||
|
onChange={(e) => setSearchTerm(e.target.value)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
{/* end::Search */}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export { UsersListSearchComponent };
|
||||||
@@ -0,0 +1,179 @@
|
|||||||
|
import clsx from "clsx";
|
||||||
|
import {
|
||||||
|
useQueryResponseLoading,
|
||||||
|
useQueryResponsePagination,
|
||||||
|
} from "../../core/QueryResponseProvider";
|
||||||
|
import { useQueryRequest } from "../../core/QueryRequestProvider";
|
||||||
|
import { PaginationState } from "../../../../../../_digifi/helpers";
|
||||||
|
import { useMemo } from "react";
|
||||||
|
|
||||||
|
const mappedLabel = (label: string): string => {
|
||||||
|
if (label === "« Previous") {
|
||||||
|
return "Previous";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (label === "Next »") {
|
||||||
|
return "Next";
|
||||||
|
}
|
||||||
|
|
||||||
|
return label;
|
||||||
|
};
|
||||||
|
|
||||||
|
const UsersListPagination = () => {
|
||||||
|
const pagination = useQueryResponsePagination();
|
||||||
|
const isLoading = useQueryResponseLoading();
|
||||||
|
const { updateState } = useQueryRequest();
|
||||||
|
const updatePage = (page: number | undefined | null) => {
|
||||||
|
if (!page || isLoading || pagination.page === page) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
updateState({ page, items_per_page: pagination.items_per_page || 10 });
|
||||||
|
};
|
||||||
|
|
||||||
|
const PAGINATION_PAGES_COUNT = 5;
|
||||||
|
const sliceLinks = (pagination?: PaginationState) => {
|
||||||
|
if (!pagination?.links?.length) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
const scopedLinks = [...pagination.links];
|
||||||
|
|
||||||
|
let pageLinks: Array<{
|
||||||
|
label: string;
|
||||||
|
active: boolean;
|
||||||
|
url: string | null;
|
||||||
|
page: number | null;
|
||||||
|
}> = [];
|
||||||
|
const previousLink: {
|
||||||
|
label: string;
|
||||||
|
active: boolean;
|
||||||
|
url: string | null;
|
||||||
|
page: number | null;
|
||||||
|
} = scopedLinks.shift()!;
|
||||||
|
const nextLink: {
|
||||||
|
label: string;
|
||||||
|
active: boolean;
|
||||||
|
url: string | null;
|
||||||
|
page: number | null;
|
||||||
|
} = scopedLinks.pop()!;
|
||||||
|
|
||||||
|
const halfOfPagesCount = Math.floor(PAGINATION_PAGES_COUNT / 2);
|
||||||
|
|
||||||
|
pageLinks.push(previousLink);
|
||||||
|
|
||||||
|
if (
|
||||||
|
pagination.page <= Math.round(PAGINATION_PAGES_COUNT / 2) ||
|
||||||
|
scopedLinks.length <= PAGINATION_PAGES_COUNT
|
||||||
|
) {
|
||||||
|
pageLinks = [
|
||||||
|
...pageLinks,
|
||||||
|
...scopedLinks.slice(0, PAGINATION_PAGES_COUNT),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
pagination.page > scopedLinks.length - halfOfPagesCount &&
|
||||||
|
scopedLinks.length > PAGINATION_PAGES_COUNT
|
||||||
|
) {
|
||||||
|
pageLinks = [
|
||||||
|
...pageLinks,
|
||||||
|
...scopedLinks.slice(
|
||||||
|
scopedLinks.length - PAGINATION_PAGES_COUNT,
|
||||||
|
scopedLinks.length
|
||||||
|
),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
!(
|
||||||
|
pagination.page <= Math.round(PAGINATION_PAGES_COUNT / 2) ||
|
||||||
|
scopedLinks.length <= PAGINATION_PAGES_COUNT
|
||||||
|
) &&
|
||||||
|
!(pagination.page > scopedLinks.length - halfOfPagesCount)
|
||||||
|
) {
|
||||||
|
pageLinks = [
|
||||||
|
...pageLinks,
|
||||||
|
...scopedLinks.slice(
|
||||||
|
pagination.page - 1 - halfOfPagesCount,
|
||||||
|
pagination.page + halfOfPagesCount
|
||||||
|
),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
pageLinks.push(nextLink);
|
||||||
|
|
||||||
|
return pageLinks;
|
||||||
|
};
|
||||||
|
|
||||||
|
const paginationLinks = useMemo(() => sliceLinks(pagination), [pagination]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="row">
|
||||||
|
<div className="col-sm-12 col-md-5 d-flex align-items-center justify-content-center justify-content-md-start"></div>
|
||||||
|
<div className="col-sm-12 col-md-7 d-flex align-items-center justify-content-center justify-content-md-end">
|
||||||
|
<div id="kt_table_users_paginate">
|
||||||
|
<ul className="pagination">
|
||||||
|
<li
|
||||||
|
className={clsx("page-item", {
|
||||||
|
disabled: isLoading || pagination.page === 1,
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
<a
|
||||||
|
onClick={() => updatePage(1)}
|
||||||
|
style={{ cursor: "pointer" }}
|
||||||
|
className="page-link"
|
||||||
|
>
|
||||||
|
First
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
{paginationLinks
|
||||||
|
?.map((link) => {
|
||||||
|
return { ...link, label: mappedLabel(link.label) };
|
||||||
|
})
|
||||||
|
.map((link) => (
|
||||||
|
<li
|
||||||
|
key={link.label}
|
||||||
|
className={clsx("page-item", {
|
||||||
|
active: pagination.page === link.page,
|
||||||
|
disabled: isLoading,
|
||||||
|
previous: link.label === "Previous",
|
||||||
|
next: link.label === "Next",
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
<a
|
||||||
|
className={clsx("page-link", {
|
||||||
|
"page-text":
|
||||||
|
link.label === "Previous" || link.label === "Next",
|
||||||
|
"me-5": link.label === "Previous",
|
||||||
|
})}
|
||||||
|
onClick={() => updatePage(link.page)}
|
||||||
|
style={{ cursor: "pointer" }}
|
||||||
|
>
|
||||||
|
{mappedLabel(link.label)}
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
<li
|
||||||
|
className={clsx("page-item", {
|
||||||
|
disabled:
|
||||||
|
isLoading ||
|
||||||
|
pagination.page === (pagination.links?.length || 3) - 2,
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
<a
|
||||||
|
onClick={() => updatePage((pagination.links?.length || 3) - 2)}
|
||||||
|
style={{ cursor: "pointer" }}
|
||||||
|
className="page-link"
|
||||||
|
>
|
||||||
|
Last
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export { UsersListPagination };
|
||||||
@@ -0,0 +1,62 @@
|
|||||||
|
/* eslint-disable react-refresh/only-export-components */
|
||||||
|
import { FC, useState, createContext, useContext, useMemo } from "react";
|
||||||
|
import {
|
||||||
|
ID,
|
||||||
|
calculatedGroupingIsDisabled,
|
||||||
|
calculateIsAllDataSelected,
|
||||||
|
groupingOnSelect,
|
||||||
|
initialListView,
|
||||||
|
ListViewContextProps,
|
||||||
|
groupingOnSelectAll,
|
||||||
|
WithChildren,
|
||||||
|
} from "../../../../../_digifi/helpers";
|
||||||
|
import {
|
||||||
|
useQueryResponse,
|
||||||
|
useQueryResponseData,
|
||||||
|
} from "./QueryResponseProvider";
|
||||||
|
|
||||||
|
const ListViewContext = createContext<ListViewContextProps>(initialListView);
|
||||||
|
|
||||||
|
const ListViewProvider: FC<WithChildren> = ({ children }) => {
|
||||||
|
const [selected, setSelected] = useState<Array<ID>>(initialListView.selected);
|
||||||
|
const [itemIdForUpdate, setItemIdForUpdate] = useState<ID>(
|
||||||
|
initialListView.itemIdForUpdate
|
||||||
|
);
|
||||||
|
const { isLoading } = useQueryResponse();
|
||||||
|
const data = useQueryResponseData();
|
||||||
|
const disabled = useMemo(
|
||||||
|
() => calculatedGroupingIsDisabled(isLoading, data),
|
||||||
|
[isLoading, data]
|
||||||
|
);
|
||||||
|
const isAllSelected = useMemo(
|
||||||
|
() => calculateIsAllDataSelected(data, selected),
|
||||||
|
[data, selected]
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ListViewContext.Provider
|
||||||
|
value={{
|
||||||
|
selected,
|
||||||
|
itemIdForUpdate,
|
||||||
|
setItemIdForUpdate,
|
||||||
|
disabled,
|
||||||
|
isAllSelected,
|
||||||
|
onSelect: (id: ID) => {
|
||||||
|
groupingOnSelect(id, selected, setSelected);
|
||||||
|
},
|
||||||
|
onSelectAll: () => {
|
||||||
|
groupingOnSelectAll(isAllSelected, setSelected, data);
|
||||||
|
},
|
||||||
|
clearSelected: () => {
|
||||||
|
setSelected([]);
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</ListViewContext.Provider>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const useListView = () => useContext(ListViewContext);
|
||||||
|
|
||||||
|
export { ListViewProvider, useListView };
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
/* eslint-disable react-refresh/only-export-components */
|
||||||
|
import { FC, useState, createContext, useContext } from "react";
|
||||||
|
import {
|
||||||
|
QueryState,
|
||||||
|
QueryRequestContextProps,
|
||||||
|
initialQueryRequest,
|
||||||
|
WithChildren,
|
||||||
|
} from "../../../../../_digifi/helpers";
|
||||||
|
|
||||||
|
const QueryRequestContext =
|
||||||
|
createContext<QueryRequestContextProps>(initialQueryRequest);
|
||||||
|
|
||||||
|
const QueryRequestProvider: FC<WithChildren> = ({ children }) => {
|
||||||
|
const [state, setState] = useState<QueryState>(initialQueryRequest.state);
|
||||||
|
|
||||||
|
const updateState = (updates: Partial<QueryState>) => {
|
||||||
|
const updatedState = { ...state, ...updates } as QueryState;
|
||||||
|
setState(updatedState);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<QueryRequestContext.Provider value={{ state, updateState }}>
|
||||||
|
{children}
|
||||||
|
</QueryRequestContext.Provider>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const useQueryRequest = () => useContext(QueryRequestContext);
|
||||||
|
export { QueryRequestProvider, useQueryRequest };
|
||||||
@@ -0,0 +1,87 @@
|
|||||||
|
/* eslint-disable react-refresh/only-export-components */
|
||||||
|
/* eslint-disable react-hooks/exhaustive-deps */
|
||||||
|
import { FC, useContext, useState, useEffect, useMemo } from "react";
|
||||||
|
import { useQuery } from "react-query";
|
||||||
|
import {
|
||||||
|
createResponseContext,
|
||||||
|
initialQueryResponse,
|
||||||
|
initialQueryState,
|
||||||
|
PaginationState,
|
||||||
|
QUERIES,
|
||||||
|
stringifyRequestQuery,
|
||||||
|
WithChildren,
|
||||||
|
} from "../../../../../_digifi/helpers";
|
||||||
|
import { getApprovedUsers } from "../../core/_requests";
|
||||||
|
import { User } from "../../core/_models";
|
||||||
|
import { useQueryRequest } from "./QueryRequestProvider";
|
||||||
|
|
||||||
|
const QueryResponseContext = createResponseContext<User>(initialQueryResponse);
|
||||||
|
const QueryResponseProvider: FC<WithChildren> = ({ children }) => {
|
||||||
|
const { state } = useQueryRequest();
|
||||||
|
const [query, setQuery] = useState<string>(stringifyRequestQuery(state));
|
||||||
|
const updatedQuery = useMemo(() => stringifyRequestQuery(state), [state]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (query !== updatedQuery) {
|
||||||
|
setQuery(updatedQuery);
|
||||||
|
}
|
||||||
|
}, [updatedQuery]);
|
||||||
|
|
||||||
|
const {
|
||||||
|
isFetching,
|
||||||
|
refetch,
|
||||||
|
data: response,
|
||||||
|
} = useQuery(
|
||||||
|
`${QUERIES.USERS_LIST}-${query}`,
|
||||||
|
() => {
|
||||||
|
return getApprovedUsers(query);
|
||||||
|
},
|
||||||
|
{ cacheTime: 0, keepPreviousData: true, refetchOnWindowFocus: false }
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<QueryResponseContext.Provider
|
||||||
|
value={{ isLoading: isFetching, refetch, response, query }}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</QueryResponseContext.Provider>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const useQueryResponse = () => useContext(QueryResponseContext);
|
||||||
|
|
||||||
|
const useQueryResponseData = () => {
|
||||||
|
const { response } = useQueryResponse();
|
||||||
|
if (!response) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
return response?.records || [];
|
||||||
|
};
|
||||||
|
|
||||||
|
const useQueryResponsePagination = () => {
|
||||||
|
const defaultPaginationState: PaginationState = {
|
||||||
|
links: [],
|
||||||
|
...initialQueryState,
|
||||||
|
};
|
||||||
|
|
||||||
|
const { response } = useQueryResponse();
|
||||||
|
if (!response || !response.payload || !response.payload.pagination) {
|
||||||
|
return defaultPaginationState;
|
||||||
|
}
|
||||||
|
|
||||||
|
return response.payload.pagination;
|
||||||
|
};
|
||||||
|
|
||||||
|
const useQueryResponseLoading = (): boolean => {
|
||||||
|
const { isLoading } = useQueryResponse();
|
||||||
|
return isLoading;
|
||||||
|
};
|
||||||
|
|
||||||
|
export {
|
||||||
|
QueryResponseProvider,
|
||||||
|
useQueryResponse,
|
||||||
|
useQueryResponseData,
|
||||||
|
useQueryResponsePagination,
|
||||||
|
useQueryResponseLoading,
|
||||||
|
};
|
||||||
@@ -0,0 +1,66 @@
|
|||||||
|
import { useMemo } from "react";
|
||||||
|
import { useTable, ColumnInstance, Row } from "react-table";
|
||||||
|
import { CustomHeaderColumn } from "./columns/CustomHeaderColumn";
|
||||||
|
import { CustomRow } from "./columns/CustomRow";
|
||||||
|
import {
|
||||||
|
useQueryResponseData,
|
||||||
|
useQueryResponseLoading,
|
||||||
|
} from "../core/QueryResponseProvider";
|
||||||
|
import { usersColumns } from "./columns/_columns";
|
||||||
|
import { User } from "../../core/_models";
|
||||||
|
import { UsersListLoading } from "../components/loading/UsersListLoading";
|
||||||
|
import { UsersListPagination } from "../components/pagination/UsersListPagination";
|
||||||
|
import { KTCardBody } from "../../../../../_digifi/helpers";
|
||||||
|
|
||||||
|
const UsersTable = () => {
|
||||||
|
const users = useQueryResponseData();
|
||||||
|
// console.log('users', users)
|
||||||
|
const isLoading = useQueryResponseLoading();
|
||||||
|
const data = useMemo(() => users, [users]);
|
||||||
|
const columns = useMemo(() => usersColumns, []);
|
||||||
|
const { getTableProps, getTableBodyProps, headers, rows, prepareRow } =
|
||||||
|
useTable({
|
||||||
|
columns,
|
||||||
|
data,
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<KTCardBody className="py-4">
|
||||||
|
<div className="table-responsive">
|
||||||
|
<table
|
||||||
|
id="kt_table_users"
|
||||||
|
className="table align-middle table-row-dashed fs-6 gy-5 dataTable no-footer"
|
||||||
|
{...getTableProps()}
|
||||||
|
>
|
||||||
|
<thead>
|
||||||
|
<tr className="text-start text-muted fw-bolder fs-7 text-uppercase gs-0">
|
||||||
|
{headers.map((column: ColumnInstance<User>) => (
|
||||||
|
<CustomHeaderColumn key={column.id} column={column} />
|
||||||
|
))}
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody className="text-gray-600 fw-bold" {...getTableBodyProps()}>
|
||||||
|
{rows.length > 0 ? (
|
||||||
|
rows.map((row: Row<User>, i) => {
|
||||||
|
prepareRow(row);
|
||||||
|
return <CustomRow row={row} key={`row-${i}-${row.id}`} />;
|
||||||
|
})
|
||||||
|
) : (
|
||||||
|
<tr>
|
||||||
|
<td colSpan={7}>
|
||||||
|
<div className="d-flex text-center w-100 align-content-center justify-content-center">
|
||||||
|
No matching records found
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
)}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
<UsersListPagination />
|
||||||
|
{isLoading && <UsersListLoading />}
|
||||||
|
</KTCardBody>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export { UsersTable };
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
import { FC } from "react";
|
||||||
|
import { NewDateTimeFormatter } from "../../../../../../_digifi/lib/NewDateTimeFormatter";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
added?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
const AddedCell: FC<Props> = ({ added }) => (
|
||||||
|
<div className="badge badge-light fw-bolder">
|
||||||
|
{NewDateTimeFormatter(added)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
export { AddedCell };
|
||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
import {FC} from 'react'
|
import {FC} from 'react'
|
||||||
import {ColumnInstance} from 'react-table'
|
import {ColumnInstance} from 'react-table'
|
||||||
import {User} from '../../core/_models'
|
import {User} from '../../../core/_models'
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
column: ColumnInstance<User>
|
column: ColumnInstance<User>
|
||||||
+1
-1
@@ -1,7 +1,7 @@
|
|||||||
import clsx from 'clsx'
|
import clsx from 'clsx'
|
||||||
import {FC} from 'react'
|
import {FC} from 'react'
|
||||||
import {Row} from 'react-table'
|
import {Row} from 'react-table'
|
||||||
import {User} from '../../core/_models'
|
import {User} from '../../../core/_models'
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
row: Row<User>
|
row: Row<User>
|
||||||
@@ -0,0 +1,75 @@
|
|||||||
|
import { FC, useEffect } from "react";
|
||||||
|
import { useMutation, useQueryClient } from "react-query";
|
||||||
|
import { MenuComponent } from "../../../../../../_digifi/assets/ts/components";
|
||||||
|
import { ID, KTIcon, QUERIES } from "../../../../../../_digifi/helpers";
|
||||||
|
import { useListView } from "../../core/ListViewProvider";
|
||||||
|
import { useQueryResponse } from "../../core/QueryResponseProvider";
|
||||||
|
import { deleteUser } from "../../../core/_requests";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
id: ID;
|
||||||
|
};
|
||||||
|
|
||||||
|
const UserActionsCell: FC<Props> = ({ id }) => {
|
||||||
|
const { setItemIdForUpdate } = useListView();
|
||||||
|
const { query } = useQueryResponse();
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
MenuComponent.reinitialization();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const openEditModal = () => {
|
||||||
|
setItemIdForUpdate(id);
|
||||||
|
};
|
||||||
|
|
||||||
|
const deleteItem = useMutation(() => deleteUser(id), {
|
||||||
|
// 💡 response of the mutation is passed to onSuccess
|
||||||
|
onSuccess: () => {
|
||||||
|
// ✅ update detail view directly
|
||||||
|
queryClient.invalidateQueries([`${QUERIES.USERS_LIST}-${query}`]);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<a
|
||||||
|
href="#"
|
||||||
|
className="btn btn-light btn-active-light-primary btn-sm"
|
||||||
|
data-kt-menu-trigger="click"
|
||||||
|
data-kt-menu-placement="bottom-end"
|
||||||
|
>
|
||||||
|
Actions
|
||||||
|
<KTIcon iconName="down" className="fs-5 m-0" />
|
||||||
|
</a>
|
||||||
|
{/* begin::Menu */}
|
||||||
|
<div
|
||||||
|
className="menu menu-sub menu-sub-dropdown menu-column menu-rounded menu-gray-600 menu-state-bg-light-primary fw-bold fs-7 w-125px py-4"
|
||||||
|
data-kt-menu="true"
|
||||||
|
>
|
||||||
|
{/* begin::Menu item */}
|
||||||
|
<div className="menu-item px-3">
|
||||||
|
<a className="menu-link px-3" onClick={openEditModal}>
|
||||||
|
Edit
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
{/* end::Menu item */}
|
||||||
|
|
||||||
|
{/* begin::Menu item */}
|
||||||
|
<div className="menu-item px-3">
|
||||||
|
<a
|
||||||
|
className="menu-link px-3"
|
||||||
|
data-kt-users-table-filter="delete_row"
|
||||||
|
onClick={async () => await deleteItem.mutateAsync()}
|
||||||
|
>
|
||||||
|
Delete
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
{/* end::Menu item */}
|
||||||
|
</div>
|
||||||
|
{/* end::Menu */}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export { UserActionsCell };
|
||||||
@@ -0,0 +1,61 @@
|
|||||||
|
import clsx from "clsx";
|
||||||
|
import { FC, PropsWithChildren, useMemo } from "react";
|
||||||
|
import { HeaderProps } from "react-table";
|
||||||
|
import { initialQueryState } from "../../../../../../_digifi/helpers";
|
||||||
|
import { useQueryRequest } from "../../core/QueryRequestProvider";
|
||||||
|
import { User } from "../../../core/_models";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
className?: string;
|
||||||
|
title?: string;
|
||||||
|
tableProps: PropsWithChildren<HeaderProps<User>>;
|
||||||
|
};
|
||||||
|
const UserCustomHeader: FC<Props> = ({ className, title, tableProps }) => {
|
||||||
|
const id = tableProps.column.id;
|
||||||
|
const { state, updateState } = useQueryRequest();
|
||||||
|
|
||||||
|
const isSelectedForSorting = useMemo(() => {
|
||||||
|
return state.sort && state.sort === id;
|
||||||
|
}, [state, id]);
|
||||||
|
const order: "asc" | "desc" | undefined = useMemo(() => state.order, [state]);
|
||||||
|
|
||||||
|
const sortColumn = () => {
|
||||||
|
// avoid sorting for these columns
|
||||||
|
if (id === "actions" || id === "selection") {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isSelectedForSorting) {
|
||||||
|
// enable sort asc
|
||||||
|
updateState({ sort: id, order: "asc", ...initialQueryState });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isSelectedForSorting && order !== undefined) {
|
||||||
|
if (order === "asc") {
|
||||||
|
// enable sort desc
|
||||||
|
updateState({ sort: id, order: "desc", ...initialQueryState });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// disable sort
|
||||||
|
updateState({ sort: undefined, order: undefined, ...initialQueryState });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<th
|
||||||
|
{...tableProps.column.getHeaderProps()}
|
||||||
|
className={clsx(
|
||||||
|
className,
|
||||||
|
isSelectedForSorting && order !== undefined && `table-sort-${order}`
|
||||||
|
)}
|
||||||
|
style={{ cursor: "pointer" }}
|
||||||
|
onClick={sortColumn}
|
||||||
|
>
|
||||||
|
{title}
|
||||||
|
</th>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export { UserCustomHeader };
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
import clsx from "clsx";
|
||||||
|
import { FC } from "react";
|
||||||
|
import { toAbsoluteUrl } from "../../../../../../_digifi/helpers";
|
||||||
|
import { User } from "../../../core/_models";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
user: User;
|
||||||
|
};
|
||||||
|
|
||||||
|
const UserInfoCell: FC<Props> = ({ user }) => (
|
||||||
|
<div className="d-flex align-items-center">
|
||||||
|
{/* begin:: Avatar */}
|
||||||
|
<div className="symbol symbol-circle symbol-50px overflow-hidden me-3">
|
||||||
|
<a href="#">
|
||||||
|
{user.avatar ? (
|
||||||
|
<div className="symbol-label">
|
||||||
|
<img
|
||||||
|
src={toAbsoluteUrl(`media/${user.avatar}`)}
|
||||||
|
alt={user.name}
|
||||||
|
className="w-100"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div
|
||||||
|
className={clsx(
|
||||||
|
"symbol-label fs-3",
|
||||||
|
`bg-light-${user.initials?.state}`,
|
||||||
|
`text-${user.initials?.state}`
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{user.firstname?.substring(0, 1).toUpperCase()}{" "}
|
||||||
|
{user.lastname?.substring(0, 1).toUpperCase()}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<div className="d-flex flex-column">
|
||||||
|
<a href="#" className="text-gray-800 text-hover-primary mb-1">
|
||||||
|
{user.firstname} {user.lastname}
|
||||||
|
</a>
|
||||||
|
<span>{user.email}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
export { UserInfoCell };
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
import { FC, useMemo } from "react";
|
||||||
|
import { ID } from "../../../../../../_digifi/helpers";
|
||||||
|
import { useListView } from "../../core/ListViewProvider";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
id: ID;
|
||||||
|
};
|
||||||
|
|
||||||
|
const UserSelectionCell: FC<Props> = ({ id }) => {
|
||||||
|
const { selected, onSelect } = useListView();
|
||||||
|
const isSelected = useMemo(() => selected.includes(id), [id, selected]);
|
||||||
|
return (
|
||||||
|
<div className="form-check form-check-custom form-check-solid">
|
||||||
|
<input
|
||||||
|
className="form-check-input"
|
||||||
|
type="checkbox"
|
||||||
|
data-kt-check={isSelected}
|
||||||
|
data-kt-check-target="#kt_table_users .form-check-input"
|
||||||
|
checked={isSelected}
|
||||||
|
onChange={() => onSelect(id)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export { UserSelectionCell };
|
||||||
+1
-1
@@ -1,7 +1,7 @@
|
|||||||
import {FC, PropsWithChildren} from 'react'
|
import {FC, PropsWithChildren} from 'react'
|
||||||
import {HeaderProps} from 'react-table'
|
import {HeaderProps} from 'react-table'
|
||||||
import {useListView} from '../../core/ListViewProvider'
|
import {useListView} from '../../core/ListViewProvider'
|
||||||
import {User} from '../../core/_models'
|
import {User} from '../../../core/_models'
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
tableProps: PropsWithChildren<HeaderProps<User>>
|
tableProps: PropsWithChildren<HeaderProps<User>>
|
||||||
+1
-1
@@ -6,7 +6,7 @@ import {UserActionsCell} from './UserActionsCell'
|
|||||||
import {UserSelectionCell} from './UserSelectionCell'
|
import {UserSelectionCell} from './UserSelectionCell'
|
||||||
import {UserCustomHeader} from './UserCustomHeader'
|
import {UserCustomHeader} from './UserCustomHeader'
|
||||||
import {UserSelectionHeader} from './UserSelectionHeader'
|
import {UserSelectionHeader} from './UserSelectionHeader'
|
||||||
import {User} from '../../core/_models'
|
import {User} from '../../../core/_models'
|
||||||
import { AddedCell } from './AddedCell'
|
import { AddedCell } from './AddedCell'
|
||||||
|
|
||||||
const usersColumns: ReadonlyArray<Column<User>> = [
|
const usersColumns: ReadonlyArray<Column<User>> = [
|
||||||
@@ -0,0 +1,433 @@
|
|||||||
|
import { FC, useState } from "react";
|
||||||
|
import * as Yup from "yup";
|
||||||
|
import { useFormik } from "formik";
|
||||||
|
import { isNotEmpty, toAbsoluteUrl } from "../../../../../_digifi/helpers";
|
||||||
|
import { initialUser, User } from "../../core/_models";
|
||||||
|
import clsx from "clsx";
|
||||||
|
import { useListView } from "../core/ListViewProvider";
|
||||||
|
import { UsersListLoading } from "../components/loading/UsersListLoading";
|
||||||
|
import { createUser, updateUser } from "../../core/_requests";
|
||||||
|
import { useQueryResponse } from "../core/QueryResponseProvider";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
isUserLoading: boolean;
|
||||||
|
user: User;
|
||||||
|
};
|
||||||
|
|
||||||
|
const editUserSchema = Yup.object().shape({
|
||||||
|
email: Yup.string()
|
||||||
|
.email("Wrong email format")
|
||||||
|
.min(3, "Minimum 3 symbols")
|
||||||
|
.max(50, "Maximum 50 symbols")
|
||||||
|
.required("Email is required"),
|
||||||
|
name: Yup.string()
|
||||||
|
.min(3, "Minimum 3 symbols")
|
||||||
|
.max(50, "Maximum 50 symbols")
|
||||||
|
.required("Name is required"),
|
||||||
|
});
|
||||||
|
|
||||||
|
const UserEditModalForm: FC<Props> = ({ user, isUserLoading }) => {
|
||||||
|
const { setItemIdForUpdate } = useListView();
|
||||||
|
const { refetch } = useQueryResponse();
|
||||||
|
|
||||||
|
const [userForEdit] = useState<User>({
|
||||||
|
...user,
|
||||||
|
avatar: user.avatar || initialUser.avatar,
|
||||||
|
role: user.role || initialUser.role,
|
||||||
|
position: user.position || initialUser.position,
|
||||||
|
name: user.name || initialUser.name,
|
||||||
|
email: user.email || initialUser.email,
|
||||||
|
});
|
||||||
|
|
||||||
|
const cancel = (withRefresh?: boolean) => {
|
||||||
|
if (withRefresh) {
|
||||||
|
refetch();
|
||||||
|
}
|
||||||
|
setItemIdForUpdate(undefined);
|
||||||
|
};
|
||||||
|
|
||||||
|
const blankImg = toAbsoluteUrl("media/svg/avatars/blank.svg");
|
||||||
|
const userAvatarImg = toAbsoluteUrl(`media/${userForEdit.avatar}`);
|
||||||
|
|
||||||
|
const formik = useFormik({
|
||||||
|
initialValues: userForEdit,
|
||||||
|
validationSchema: editUserSchema,
|
||||||
|
onSubmit: async (values, { setSubmitting }) => {
|
||||||
|
setSubmitting(true);
|
||||||
|
try {
|
||||||
|
if (isNotEmpty(values.id)) {
|
||||||
|
await updateUser(values);
|
||||||
|
} else {
|
||||||
|
await createUser(values);
|
||||||
|
}
|
||||||
|
} catch (ex) {
|
||||||
|
console.error(ex);
|
||||||
|
} finally {
|
||||||
|
setSubmitting(true);
|
||||||
|
cancel(true);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<form
|
||||||
|
id="kt_modal_add_user_form"
|
||||||
|
className="form"
|
||||||
|
onSubmit={formik.handleSubmit}
|
||||||
|
noValidate
|
||||||
|
>
|
||||||
|
{/* begin::Scroll */}
|
||||||
|
<div
|
||||||
|
className="d-flex flex-column scroll-y me-n7 pe-7"
|
||||||
|
id="kt_modal_add_user_scroll"
|
||||||
|
data-kt-scroll="true"
|
||||||
|
data-kt-scroll-activate="{default: false, lg: true}"
|
||||||
|
data-kt-scroll-max-height="auto"
|
||||||
|
data-kt-scroll-dependencies="#kt_modal_add_user_header"
|
||||||
|
data-kt-scroll-wrappers="#kt_modal_add_user_scroll"
|
||||||
|
data-kt-scroll-offset="300px"
|
||||||
|
>
|
||||||
|
{/* begin::Input group */}
|
||||||
|
<div className="fv-row mb-7">
|
||||||
|
{/* begin::Label */}
|
||||||
|
<label className="d-block fw-bold fs-6 mb-5">Avatar</label>
|
||||||
|
{/* end::Label */}
|
||||||
|
|
||||||
|
{/* begin::Image input */}
|
||||||
|
<div
|
||||||
|
className="image-input image-input-outline"
|
||||||
|
data-kt-image-input="true"
|
||||||
|
style={{ backgroundImage: `url('${blankImg}')` }}
|
||||||
|
>
|
||||||
|
{/* begin::Preview existing avatar */}
|
||||||
|
<div
|
||||||
|
className="image-input-wrapper w-125px h-125px"
|
||||||
|
style={{ backgroundImage: `url('${userAvatarImg}')` }}
|
||||||
|
></div>
|
||||||
|
{/* end::Preview existing avatar */}
|
||||||
|
|
||||||
|
{/* begin::Label */}
|
||||||
|
{/* <label
|
||||||
|
className='btn btn-icon btn-circle btn-active-color-primary w-25px h-25px bg-body shadow'
|
||||||
|
data-kt-image-input-action='change'
|
||||||
|
data-bs-toggle='tooltip'
|
||||||
|
title='Change avatar'
|
||||||
|
>
|
||||||
|
<i className='bi bi-pencil-fill fs-7'></i>
|
||||||
|
|
||||||
|
<input type='file' name='avatar' accept='.png, .jpg, .jpeg' />
|
||||||
|
<input type='hidden' name='avatar_remove' />
|
||||||
|
</label> */}
|
||||||
|
{/* end::Label */}
|
||||||
|
|
||||||
|
{/* begin::Cancel */}
|
||||||
|
{/* <span
|
||||||
|
className='btn btn-icon btn-circle btn-active-color-primary w-25px h-25px bg-body shadow'
|
||||||
|
data-kt-image-input-action='cancel'
|
||||||
|
data-bs-toggle='tooltip'
|
||||||
|
title='Cancel avatar'
|
||||||
|
>
|
||||||
|
<i className='bi bi-x fs-2'></i>
|
||||||
|
</span> */}
|
||||||
|
{/* end::Cancel */}
|
||||||
|
|
||||||
|
{/* begin::Remove */}
|
||||||
|
{/* <span
|
||||||
|
className='btn btn-icon btn-circle btn-active-color-primary w-25px h-25px bg-body shadow'
|
||||||
|
data-kt-image-input-action='remove'
|
||||||
|
data-bs-toggle='tooltip'
|
||||||
|
title='Remove avatar'
|
||||||
|
>
|
||||||
|
<i className='bi bi-x fs-2'></i>
|
||||||
|
</span> */}
|
||||||
|
{/* end::Remove */}
|
||||||
|
</div>
|
||||||
|
{/* end::Image input */}
|
||||||
|
|
||||||
|
{/* begin::Hint */}
|
||||||
|
{/* <div className='form-text'>Allowed file types: png, jpg, jpeg.</div> */}
|
||||||
|
{/* end::Hint */}
|
||||||
|
</div>
|
||||||
|
{/* end::Input group */}
|
||||||
|
|
||||||
|
{/* begin::Input group */}
|
||||||
|
<div className="fv-row mb-7">
|
||||||
|
{/* begin::Label */}
|
||||||
|
<label className="required fw-bold fs-6 mb-2">Full Name</label>
|
||||||
|
{/* end::Label */}
|
||||||
|
|
||||||
|
{/* begin::Input */}
|
||||||
|
<input
|
||||||
|
placeholder="Full name"
|
||||||
|
{...formik.getFieldProps("name")}
|
||||||
|
type="text"
|
||||||
|
name="name"
|
||||||
|
className={clsx(
|
||||||
|
"form-control form-control-solid mb-3 mb-lg-0",
|
||||||
|
{ "is-invalid": formik.touched.name && formik.errors.name },
|
||||||
|
{
|
||||||
|
"is-valid": formik.touched.name && !formik.errors.name,
|
||||||
|
}
|
||||||
|
)}
|
||||||
|
autoComplete="off"
|
||||||
|
disabled={formik.isSubmitting || isUserLoading}
|
||||||
|
/>
|
||||||
|
{formik.touched.name && formik.errors.name && (
|
||||||
|
<div className="fv-plugins-message-container">
|
||||||
|
<div className="fv-help-block">
|
||||||
|
<span role="alert">{formik.errors.name}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{/* end::Input */}
|
||||||
|
</div>
|
||||||
|
{/* end::Input group */}
|
||||||
|
|
||||||
|
{/* begin::Input group */}
|
||||||
|
<div className="fv-row mb-7">
|
||||||
|
{/* begin::Label */}
|
||||||
|
<label className="required fw-bold fs-6 mb-2">Email</label>
|
||||||
|
{/* end::Label */}
|
||||||
|
|
||||||
|
{/* begin::Input */}
|
||||||
|
<input
|
||||||
|
placeholder="Email"
|
||||||
|
{...formik.getFieldProps("email")}
|
||||||
|
className={clsx(
|
||||||
|
"form-control form-control-solid mb-3 mb-lg-0",
|
||||||
|
{ "is-invalid": formik.touched.email && formik.errors.email },
|
||||||
|
{
|
||||||
|
"is-valid": formik.touched.email && !formik.errors.email,
|
||||||
|
}
|
||||||
|
)}
|
||||||
|
type="email"
|
||||||
|
name="email"
|
||||||
|
autoComplete="off"
|
||||||
|
disabled={formik.isSubmitting || isUserLoading}
|
||||||
|
/>
|
||||||
|
{/* end::Input */}
|
||||||
|
{formik.touched.email && formik.errors.email && (
|
||||||
|
<div className="fv-plugins-message-container">
|
||||||
|
<span role="alert">{formik.errors.email}</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
{/* end::Input group */}
|
||||||
|
|
||||||
|
{/* begin::Input group */}
|
||||||
|
<div className="mb-7">
|
||||||
|
{/* begin::Label */}
|
||||||
|
<label className="required fw-bold fs-6 mb-5">Role</label>
|
||||||
|
{/* end::Label */}
|
||||||
|
{/* begin::Roles */}
|
||||||
|
{/* begin::Input row */}
|
||||||
|
<div className="d-flex fv-row">
|
||||||
|
{/* begin::Radio */}
|
||||||
|
<div className="form-check form-check-custom form-check-solid">
|
||||||
|
{/* begin::Input */}
|
||||||
|
<input
|
||||||
|
className="form-check-input me-3"
|
||||||
|
{...formik.getFieldProps("role")}
|
||||||
|
name="role"
|
||||||
|
type="radio"
|
||||||
|
value="Administrator"
|
||||||
|
id="kt_modal_update_role_option_0"
|
||||||
|
checked={formik.values.role === "Administrator"}
|
||||||
|
disabled={formik.isSubmitting || isUserLoading}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* end::Input */}
|
||||||
|
{/* begin::Label */}
|
||||||
|
<label
|
||||||
|
className="form-check-label"
|
||||||
|
htmlFor="kt_modal_update_role_option_0"
|
||||||
|
>
|
||||||
|
<div className="fw-bolder text-gray-800">Administrator</div>
|
||||||
|
<div className="text-gray-600">
|
||||||
|
Best for business owners and company administrators
|
||||||
|
</div>
|
||||||
|
</label>
|
||||||
|
{/* end::Label */}
|
||||||
|
</div>
|
||||||
|
{/* end::Radio */}
|
||||||
|
</div>
|
||||||
|
{/* end::Input row */}
|
||||||
|
<div className="separator separator-dashed my-5"></div>
|
||||||
|
{/* begin::Input row */}
|
||||||
|
<div className="d-flex fv-row">
|
||||||
|
{/* begin::Radio */}
|
||||||
|
<div className="form-check form-check-custom form-check-solid">
|
||||||
|
{/* begin::Input */}
|
||||||
|
<input
|
||||||
|
className="form-check-input me-3"
|
||||||
|
{...formik.getFieldProps("role")}
|
||||||
|
name="role"
|
||||||
|
type="radio"
|
||||||
|
value="Developer"
|
||||||
|
id="kt_modal_update_role_option_1"
|
||||||
|
checked={formik.values.role === "Developer"}
|
||||||
|
disabled={formik.isSubmitting || isUserLoading}
|
||||||
|
/>
|
||||||
|
{/* end::Input */}
|
||||||
|
{/* begin::Label */}
|
||||||
|
<label
|
||||||
|
className="form-check-label"
|
||||||
|
htmlFor="kt_modal_update_role_option_1"
|
||||||
|
>
|
||||||
|
<div className="fw-bolder text-gray-800">Developer</div>
|
||||||
|
<div className="text-gray-600">
|
||||||
|
Best for developers or people primarily using the API
|
||||||
|
</div>
|
||||||
|
</label>
|
||||||
|
{/* end::Label */}
|
||||||
|
</div>
|
||||||
|
{/* end::Radio */}
|
||||||
|
</div>
|
||||||
|
{/* end::Input row */}
|
||||||
|
<div className="separator separator-dashed my-5"></div>
|
||||||
|
{/* begin::Input row */}
|
||||||
|
<div className="d-flex fv-row">
|
||||||
|
{/* begin::Radio */}
|
||||||
|
<div className="form-check form-check-custom form-check-solid">
|
||||||
|
{/* begin::Input */}
|
||||||
|
<input
|
||||||
|
className="form-check-input me-3"
|
||||||
|
{...formik.getFieldProps("role")}
|
||||||
|
name="role"
|
||||||
|
type="radio"
|
||||||
|
value="Analyst"
|
||||||
|
id="kt_modal_update_role_option_2"
|
||||||
|
checked={formik.values.role === "Analyst"}
|
||||||
|
disabled={formik.isSubmitting || isUserLoading}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* end::Input */}
|
||||||
|
{/* begin::Label */}
|
||||||
|
<label
|
||||||
|
className="form-check-label"
|
||||||
|
htmlFor="kt_modal_update_role_option_2"
|
||||||
|
>
|
||||||
|
<div className="fw-bolder text-gray-800">Analyst</div>
|
||||||
|
<div className="text-gray-600">
|
||||||
|
Best for people who need full access to analytics data, but
|
||||||
|
don't need to update business settings
|
||||||
|
</div>
|
||||||
|
</label>
|
||||||
|
{/* end::Label */}
|
||||||
|
</div>
|
||||||
|
{/* end::Radio */}
|
||||||
|
</div>
|
||||||
|
{/* end::Input row */}
|
||||||
|
<div className="separator separator-dashed my-5"></div>
|
||||||
|
{/* begin::Input row */}
|
||||||
|
<div className="d-flex fv-row">
|
||||||
|
{/* begin::Radio */}
|
||||||
|
<div className="form-check form-check-custom form-check-solid">
|
||||||
|
{/* begin::Input */}
|
||||||
|
<input
|
||||||
|
className="form-check-input me-3"
|
||||||
|
{...formik.getFieldProps("role")}
|
||||||
|
name="role"
|
||||||
|
type="radio"
|
||||||
|
value="Support"
|
||||||
|
id="kt_modal_update_role_option_3"
|
||||||
|
checked={formik.values.role === "Support"}
|
||||||
|
disabled={formik.isSubmitting || isUserLoading}
|
||||||
|
/>
|
||||||
|
{/* end::Input */}
|
||||||
|
{/* begin::Label */}
|
||||||
|
<label
|
||||||
|
className="form-check-label"
|
||||||
|
htmlFor="kt_modal_update_role_option_3"
|
||||||
|
>
|
||||||
|
<div className="fw-bolder text-gray-800">Support</div>
|
||||||
|
<div className="text-gray-600">
|
||||||
|
Best for employees who regularly refund payments and respond
|
||||||
|
to disputes
|
||||||
|
</div>
|
||||||
|
</label>
|
||||||
|
{/* end::Label */}
|
||||||
|
</div>
|
||||||
|
{/* end::Radio */}
|
||||||
|
</div>
|
||||||
|
{/* end::Input row */}
|
||||||
|
<div className="separator separator-dashed my-5"></div>
|
||||||
|
{/* begin::Input row */}
|
||||||
|
<div className="d-flex fv-row">
|
||||||
|
{/* begin::Radio */}
|
||||||
|
<div className="form-check form-check-custom form-check-solid">
|
||||||
|
{/* begin::Input */}
|
||||||
|
<input
|
||||||
|
className="form-check-input me-3"
|
||||||
|
{...formik.getFieldProps("role")}
|
||||||
|
name="role"
|
||||||
|
type="radio"
|
||||||
|
id="kt_modal_update_role_option_4"
|
||||||
|
value="Trial"
|
||||||
|
checked={formik.values.role === "Trial"}
|
||||||
|
disabled={formik.isSubmitting || isUserLoading}
|
||||||
|
/>
|
||||||
|
{/* end::Input */}
|
||||||
|
{/* begin::Label */}
|
||||||
|
<label
|
||||||
|
className="form-check-label"
|
||||||
|
htmlFor="kt_modal_update_role_option_4"
|
||||||
|
>
|
||||||
|
<div className="fw-bolder text-gray-800">Trial</div>
|
||||||
|
<div className="text-gray-600">
|
||||||
|
Best for people who need to preview content data, but don't
|
||||||
|
need to make any updates
|
||||||
|
</div>
|
||||||
|
</label>
|
||||||
|
{/* end::Label */}
|
||||||
|
</div>
|
||||||
|
{/* end::Radio */}
|
||||||
|
</div>
|
||||||
|
{/* end::Input row */}
|
||||||
|
{/* end::Roles */}
|
||||||
|
</div>
|
||||||
|
{/* end::Input group */}
|
||||||
|
</div>
|
||||||
|
{/* end::Scroll */}
|
||||||
|
|
||||||
|
{/* begin::Actions */}
|
||||||
|
<div className="text-center pt-15">
|
||||||
|
<button
|
||||||
|
type="reset"
|
||||||
|
onClick={() => cancel()}
|
||||||
|
className="btn btn-light me-3"
|
||||||
|
data-kt-users-modal-action="cancel"
|
||||||
|
disabled={formik.isSubmitting || isUserLoading}
|
||||||
|
>
|
||||||
|
Discard
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
className="btn btn-primary"
|
||||||
|
data-kt-users-modal-action="submit"
|
||||||
|
disabled={
|
||||||
|
isUserLoading ||
|
||||||
|
formik.isSubmitting ||
|
||||||
|
!formik.isValid ||
|
||||||
|
!formik.touched
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<span className="indicator-label">Submit</span>
|
||||||
|
{(formik.isSubmitting || isUserLoading) && (
|
||||||
|
<span className="indicator-progress">
|
||||||
|
Please wait...{" "}
|
||||||
|
<span className="spinner-border spinner-border-sm align-middle ms-2"></span>
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
{/* end::Actions */}
|
||||||
|
</form>
|
||||||
|
{(formik.isSubmitting || isUserLoading) && <UsersListLoading />}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export { UserEditModalForm };
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
import { useQuery } from "react-query";
|
||||||
|
import { UserEditModalForm } from "./UserEditModalForm";
|
||||||
|
import { isNotEmpty, QUERIES } from "../../../../../_digifi/helpers";
|
||||||
|
import { useListView } from "../core/ListViewProvider";
|
||||||
|
import { getUserById } from "../../core/_requests";
|
||||||
|
|
||||||
|
const UserEditModalFormWrapper = () => {
|
||||||
|
const { itemIdForUpdate, setItemIdForUpdate } = useListView();
|
||||||
|
const enabledQuery: boolean = isNotEmpty(itemIdForUpdate);
|
||||||
|
const {
|
||||||
|
isLoading,
|
||||||
|
data: user,
|
||||||
|
error,
|
||||||
|
} = useQuery(
|
||||||
|
`${QUERIES.USERS_LIST}-user-${itemIdForUpdate}`,
|
||||||
|
() => {
|
||||||
|
return getUserById(itemIdForUpdate);
|
||||||
|
},
|
||||||
|
{
|
||||||
|
cacheTime: 0,
|
||||||
|
enabled: enabledQuery,
|
||||||
|
onError: (err) => {
|
||||||
|
setItemIdForUpdate(undefined);
|
||||||
|
console.error(err);
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!itemIdForUpdate) {
|
||||||
|
return (
|
||||||
|
<UserEditModalForm isUserLoading={isLoading} user={{ id: undefined }} />
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isLoading && !error && user) {
|
||||||
|
return <UserEditModalForm isUserLoading={isLoading} user={user} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
|
||||||
|
export { UserEditModalFormWrapper };
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
import { KTIcon } from "../../../../../_digifi/helpers";
|
||||||
|
import { useListView } from "../core/ListViewProvider";
|
||||||
|
|
||||||
|
const UserEditModalHeader = () => {
|
||||||
|
const { setItemIdForUpdate } = useListView();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="modal-header">
|
||||||
|
{/* begin::Modal title */}
|
||||||
|
<h2 className="fw-bolder">Add User</h2>
|
||||||
|
{/* end::Modal title */}
|
||||||
|
|
||||||
|
{/* begin::Close */}
|
||||||
|
<div
|
||||||
|
className="btn btn-icon btn-sm btn-active-icon-primary"
|
||||||
|
data-kt-users-modal-action="close"
|
||||||
|
onClick={() => setItemIdForUpdate(undefined)}
|
||||||
|
style={{ cursor: "pointer" }}
|
||||||
|
>
|
||||||
|
<KTIcon iconName="cross" className="fs-1" />
|
||||||
|
</div>
|
||||||
|
{/* end::Close */}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export { UserEditModalHeader };
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
import {
|
||||||
|
ListViewProvider,
|
||||||
|
useListView,
|
||||||
|
} from "./core/ListViewProvider";
|
||||||
|
import { QueryRequestProvider } from "./core/QueryRequestProvider";
|
||||||
|
import { QueryResponseProvider } from "./core/QueryResponseProvider";
|
||||||
|
import { UsersListHeader } from "./components/header/UsersListHeader";
|
||||||
|
import { UsersTable } from "./table/UsersTable";
|
||||||
|
import { UserEditModal } from "./user-edit-modal/UserEditModal";
|
||||||
|
import { KTCard } from "../../../../_digifi/helpers";
|
||||||
|
import { ToolbarWrapper } from "../../../../_digifi/layout/components/toolbar";
|
||||||
|
import { Content } from "../../../../_digifi/layout/components/content";
|
||||||
|
|
||||||
|
const UsersList = () => {
|
||||||
|
const { itemIdForUpdate } = useListView();
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<KTCard>
|
||||||
|
<UsersListHeader />
|
||||||
|
<UsersTable />
|
||||||
|
</KTCard>
|
||||||
|
{itemIdForUpdate !== undefined && <UserEditModal />}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const UsersListWrapper = () => (
|
||||||
|
<QueryRequestProvider>
|
||||||
|
<QueryResponseProvider>
|
||||||
|
<ListViewProvider>
|
||||||
|
<ToolbarWrapper />
|
||||||
|
<Content>
|
||||||
|
<UsersList />
|
||||||
|
</Content>
|
||||||
|
</ListViewProvider>
|
||||||
|
</QueryResponseProvider>
|
||||||
|
</QueryRequestProvider>
|
||||||
|
);
|
||||||
|
|
||||||
|
export { UsersListWrapper };
|
||||||
@@ -1,39 +0,0 @@
|
|||||||
import {Route, Routes, Outlet, Navigate} from 'react-router-dom'
|
|
||||||
import {PageLink, PageTitle} from '../../../../_digifi/layout/core'
|
|
||||||
import {UsersListWrapper} from './users-list/UsersList'
|
|
||||||
|
|
||||||
const usersBreadcrumbs: Array<PageLink> = [
|
|
||||||
{
|
|
||||||
title: 'User Management',
|
|
||||||
path: '/apps/user-management/users',
|
|
||||||
isSeparator: false,
|
|
||||||
isActive: false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: '',
|
|
||||||
path: '',
|
|
||||||
isSeparator: true,
|
|
||||||
isActive: false,
|
|
||||||
},
|
|
||||||
]
|
|
||||||
|
|
||||||
const UsersPage = () => {
|
|
||||||
return (
|
|
||||||
<Routes>
|
|
||||||
<Route element={<Outlet />}>
|
|
||||||
<Route
|
|
||||||
path='users'
|
|
||||||
element={
|
|
||||||
<>
|
|
||||||
<PageTitle breadcrumbs={usersBreadcrumbs}>Users list</PageTitle>
|
|
||||||
<UsersListWrapper />
|
|
||||||
</>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</Route>
|
|
||||||
<Route index element={<Navigate to='/apps/user-management/users' />} />
|
|
||||||
</Routes>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export default UsersPage
|
|
||||||
+14
-11
@@ -1,16 +1,19 @@
|
|||||||
import {KTIcon} from '../../../../../../../_digifi/helpers'
|
import { KTIcon } from '../../../../../../_digifi/helpers';
|
||||||
import {useListView} from '../../core/ListViewProvider'
|
import { useListView } from '../../core/ListViewProvider';
|
||||||
import {UsersListFilter} from './UsersListFilter'
|
import { UsersListFilter } from './UsersListFilter';
|
||||||
|
|
||||||
const UsersListToolbar = () => {
|
const UsersListToolbar = () => {
|
||||||
const {setItemIdForUpdate} = useListView()
|
const { setItemIdForUpdate } = useListView();
|
||||||
const openAddUserModal = () => {
|
const openAddUserModal = () => {
|
||||||
setItemIdForUpdate(null)
|
setItemIdForUpdate(null);
|
||||||
}
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='d-flex justify-content-end' data-kt-user-table-toolbar='base'>
|
<div
|
||||||
<UsersListFilter />
|
className="d-flex justify-content-end"
|
||||||
|
data-kt-user-table-toolbar="base"
|
||||||
|
>
|
||||||
|
{/* <UsersListFilter /> */}
|
||||||
|
|
||||||
{/* begin::Export */}
|
{/* begin::Export */}
|
||||||
{/* <button type='button' className='btn btn-light-primary me-3'>
|
{/* <button type='button' className='btn btn-light-primary me-3'>
|
||||||
@@ -26,7 +29,7 @@ const UsersListToolbar = () => {
|
|||||||
</button> */}
|
</button> */}
|
||||||
{/* end::Add user */}
|
{/* end::Add user */}
|
||||||
</div>
|
</div>
|
||||||
)
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
export {UsersListToolbar}
|
export { UsersListToolbar };
|
||||||
@@ -0,0 +1,136 @@
|
|||||||
|
import { useEffect, useState } from "react";
|
||||||
|
import { MenuComponent } from "../../../../../../_digifi/assets/ts/components";
|
||||||
|
import { initialQueryState, KTIcon } from "../../../../../../_digifi/helpers";
|
||||||
|
import { useQueryRequest } from "../../core/QueryRequestProvider";
|
||||||
|
import { useQueryResponse } from "../../core/QueryResponseProvider";
|
||||||
|
|
||||||
|
const UsersListFilter = () => {
|
||||||
|
const { updateState } = useQueryRequest();
|
||||||
|
const { isLoading } = useQueryResponse();
|
||||||
|
const [role, setRole] = useState<string | undefined>();
|
||||||
|
const [lastLogin, setLastLogin] = useState<string | undefined>();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
MenuComponent.reinitialization();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const resetData = () => {
|
||||||
|
updateState({ filter: undefined, ...initialQueryState });
|
||||||
|
};
|
||||||
|
|
||||||
|
const filterData = () => {
|
||||||
|
updateState({
|
||||||
|
filter: { role, last_login: lastLogin },
|
||||||
|
...initialQueryState,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{/* begin::Filter Button */}
|
||||||
|
<button
|
||||||
|
disabled={isLoading}
|
||||||
|
type="button"
|
||||||
|
className="btn btn-light-primary me-3"
|
||||||
|
data-kt-menu-trigger="click"
|
||||||
|
data-kt-menu-placement="bottom-end"
|
||||||
|
>
|
||||||
|
<KTIcon iconName="filter" className="fs-2" />
|
||||||
|
Filter
|
||||||
|
</button>
|
||||||
|
{/* end::Filter Button */}
|
||||||
|
{/* begin::SubMenu */}
|
||||||
|
<div
|
||||||
|
className="menu menu-sub menu-sub-dropdown w-300px w-md-325px"
|
||||||
|
data-kt-menu="true"
|
||||||
|
>
|
||||||
|
{/* begin::Header */}
|
||||||
|
<div className="px-7 py-5">
|
||||||
|
<div className="fs-5 text-gray-900 fw-bolder">Filter Options</div>
|
||||||
|
</div>
|
||||||
|
{/* end::Header */}
|
||||||
|
|
||||||
|
{/* begin::Separator */}
|
||||||
|
<div className="separator border-gray-200"></div>
|
||||||
|
{/* end::Separator */}
|
||||||
|
|
||||||
|
{/* begin::Content */}
|
||||||
|
<div className="px-7 py-5" data-kt-user-table-filter="form">
|
||||||
|
{/* begin::Input group */}
|
||||||
|
<div className="mb-10">
|
||||||
|
<label className="form-label fs-6 fw-bold">Role:</label>
|
||||||
|
<select
|
||||||
|
className="form-select form-select-solid fw-bolder"
|
||||||
|
data-kt-select2="true"
|
||||||
|
data-placeholder="Select option"
|
||||||
|
data-allow-clear="true"
|
||||||
|
data-kt-user-table-filter="role"
|
||||||
|
data-hide-search="true"
|
||||||
|
onChange={(e) => setRole(e.target.value)}
|
||||||
|
value={role}
|
||||||
|
>
|
||||||
|
<option value=""></option>
|
||||||
|
<option value="Administrator">Administrator</option>
|
||||||
|
<option value="Analyst">Analyst</option>
|
||||||
|
<option value="Developer">Developer</option>
|
||||||
|
<option value="Support">Support</option>
|
||||||
|
<option value="Trial">Trial</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
{/* end::Input group */}
|
||||||
|
|
||||||
|
{/* begin::Input group */}
|
||||||
|
<div className="mb-10">
|
||||||
|
<label className="form-label fs-6 fw-bold">Last login:</label>
|
||||||
|
<select
|
||||||
|
className="form-select form-select-solid fw-bolder"
|
||||||
|
data-kt-select2="true"
|
||||||
|
data-placeholder="Select option"
|
||||||
|
data-allow-clear="true"
|
||||||
|
data-kt-user-table-filter="two-step"
|
||||||
|
data-hide-search="true"
|
||||||
|
onChange={(e) => setLastLogin(e.target.value)}
|
||||||
|
value={lastLogin}
|
||||||
|
>
|
||||||
|
<option value=""></option>
|
||||||
|
<option value="Yesterday">Yesterday</option>
|
||||||
|
<option value="20 mins ago">20 mins ago</option>
|
||||||
|
<option value="5 hours ago">5 hours ago</option>
|
||||||
|
<option value="2 days ago">2 days ago</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
{/* end::Input group */}
|
||||||
|
|
||||||
|
{/* begin::Actions */}
|
||||||
|
<div className="d-flex justify-content-end">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
disabled={isLoading}
|
||||||
|
onClick={filterData}
|
||||||
|
className="btn btn-light btn-active-light-primary fw-bold me-2 px-6"
|
||||||
|
data-kt-menu-dismiss="true"
|
||||||
|
data-kt-user-table-filter="reset"
|
||||||
|
>
|
||||||
|
Reset
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
disabled={isLoading}
|
||||||
|
type="button"
|
||||||
|
onClick={resetData}
|
||||||
|
className="btn btn-primary fw-bold px-6"
|
||||||
|
data-kt-menu-dismiss="true"
|
||||||
|
data-kt-user-table-filter="filter"
|
||||||
|
>
|
||||||
|
Apply
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
{/* end::Actions */}
|
||||||
|
</div>
|
||||||
|
{/* end::Content */}
|
||||||
|
</div>
|
||||||
|
{/* end::SubMenu */}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export { UsersListFilter };
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
import { useQueryClient, useMutation } from "react-query";
|
||||||
|
import { QUERIES } from "../../../../../../_digifi/helpers";
|
||||||
|
import { useListView } from "../../core/ListViewProvider";
|
||||||
|
import { useQueryResponse } from "../../core/QueryResponseProvider";
|
||||||
|
import { deleteSelectedUsers } from "../../../core/_requests";
|
||||||
|
|
||||||
|
const UsersListGrouping = () => {
|
||||||
|
const { selected, clearSelected } = useListView();
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
const { query } = useQueryResponse();
|
||||||
|
|
||||||
|
const deleteSelectedItems = useMutation(() => deleteSelectedUsers(selected), {
|
||||||
|
// 💡 response of the mutation is passed to onSuccess
|
||||||
|
onSuccess: () => {
|
||||||
|
// ✅ update detail view directly
|
||||||
|
queryClient.invalidateQueries([`${QUERIES.USERS_LIST}-${query}`]);
|
||||||
|
clearSelected();
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="d-flex justify-content-end align-items-center">
|
||||||
|
<div className="fw-bolder me-5">
|
||||||
|
<span className="me-2">{selected.length}</span> Selected
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="btn btn-danger"
|
||||||
|
onClick={async () => await deleteSelectedItems.mutateAsync()}
|
||||||
|
>
|
||||||
|
Delete Selected
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export { UsersListGrouping };
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
/* eslint-disable react-hooks/exhaustive-deps */
|
||||||
|
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
|
import {
|
||||||
|
initialQueryState,
|
||||||
|
KTIcon,
|
||||||
|
useDebounce,
|
||||||
|
} from "../../../../../../_digifi/helpers";
|
||||||
|
import { useQueryRequest } from "../../core/QueryRequestProvider";
|
||||||
|
|
||||||
|
const UsersListSearchComponent = () => {
|
||||||
|
const { updateState } = useQueryRequest();
|
||||||
|
const [searchTerm, setSearchTerm] = useState<string>("");
|
||||||
|
// Debounce search term so that it only gives us latest value ...
|
||||||
|
// ... if searchTerm has not been updated within last 500ms.
|
||||||
|
// The goal is to only have the API call fire when user stops typing ...
|
||||||
|
// ... so that we aren't hitting our API rapidly.
|
||||||
|
const debouncedSearchTerm = useDebounce(searchTerm, 150);
|
||||||
|
// Effect for API call
|
||||||
|
useEffect(
|
||||||
|
() => {
|
||||||
|
if (debouncedSearchTerm !== undefined && searchTerm !== undefined) {
|
||||||
|
updateState({ search: debouncedSearchTerm, ...initialQueryState });
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[debouncedSearchTerm] // Only call effect if debounced search term changes
|
||||||
|
// More details about useDebounce: https://usehooks.com/useDebounce/
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="card-title">
|
||||||
|
{/* begin::Search */}
|
||||||
|
<div className="d-flex align-items-center position-relative my-1">
|
||||||
|
<KTIcon iconName="magnifier" className="fs-1 position-absolute ms-6" />
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
data-kt-user-table-filter="search"
|
||||||
|
className="form-control form-control-solid w-250px ps-14"
|
||||||
|
placeholder="Search user"
|
||||||
|
value={searchTerm}
|
||||||
|
onChange={(e) => setSearchTerm(e.target.value)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
{/* end::Search */}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export { UsersListSearchComponent };
|
||||||
@@ -0,0 +1,179 @@
|
|||||||
|
import clsx from "clsx";
|
||||||
|
import {
|
||||||
|
useQueryResponseLoading,
|
||||||
|
useQueryResponsePagination,
|
||||||
|
} from "../../core/QueryResponseProvider";
|
||||||
|
import { useQueryRequest } from "../../core/QueryRequestProvider";
|
||||||
|
import { PaginationState } from "../../../../../../_digifi/helpers";
|
||||||
|
import { useMemo } from "react";
|
||||||
|
|
||||||
|
const mappedLabel = (label: string): string => {
|
||||||
|
if (label === "« Previous") {
|
||||||
|
return "Previous";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (label === "Next »") {
|
||||||
|
return "Next";
|
||||||
|
}
|
||||||
|
|
||||||
|
return label;
|
||||||
|
};
|
||||||
|
|
||||||
|
const UsersListPagination = () => {
|
||||||
|
const pagination = useQueryResponsePagination();
|
||||||
|
const isLoading = useQueryResponseLoading();
|
||||||
|
const { updateState } = useQueryRequest();
|
||||||
|
const updatePage = (page: number | undefined | null) => {
|
||||||
|
if (!page || isLoading || pagination.page === page) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
updateState({ page, items_per_page: pagination.items_per_page || 10 });
|
||||||
|
};
|
||||||
|
|
||||||
|
const PAGINATION_PAGES_COUNT = 5;
|
||||||
|
const sliceLinks = (pagination?: PaginationState) => {
|
||||||
|
if (!pagination?.links?.length) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
const scopedLinks = [...pagination.links];
|
||||||
|
|
||||||
|
let pageLinks: Array<{
|
||||||
|
label: string;
|
||||||
|
active: boolean;
|
||||||
|
url: string | null;
|
||||||
|
page: number | null;
|
||||||
|
}> = [];
|
||||||
|
const previousLink: {
|
||||||
|
label: string;
|
||||||
|
active: boolean;
|
||||||
|
url: string | null;
|
||||||
|
page: number | null;
|
||||||
|
} = scopedLinks.shift()!;
|
||||||
|
const nextLink: {
|
||||||
|
label: string;
|
||||||
|
active: boolean;
|
||||||
|
url: string | null;
|
||||||
|
page: number | null;
|
||||||
|
} = scopedLinks.pop()!;
|
||||||
|
|
||||||
|
const halfOfPagesCount = Math.floor(PAGINATION_PAGES_COUNT / 2);
|
||||||
|
|
||||||
|
pageLinks.push(previousLink);
|
||||||
|
|
||||||
|
if (
|
||||||
|
pagination.page <= Math.round(PAGINATION_PAGES_COUNT / 2) ||
|
||||||
|
scopedLinks.length <= PAGINATION_PAGES_COUNT
|
||||||
|
) {
|
||||||
|
pageLinks = [
|
||||||
|
...pageLinks,
|
||||||
|
...scopedLinks.slice(0, PAGINATION_PAGES_COUNT),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
pagination.page > scopedLinks.length - halfOfPagesCount &&
|
||||||
|
scopedLinks.length > PAGINATION_PAGES_COUNT
|
||||||
|
) {
|
||||||
|
pageLinks = [
|
||||||
|
...pageLinks,
|
||||||
|
...scopedLinks.slice(
|
||||||
|
scopedLinks.length - PAGINATION_PAGES_COUNT,
|
||||||
|
scopedLinks.length
|
||||||
|
),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
!(
|
||||||
|
pagination.page <= Math.round(PAGINATION_PAGES_COUNT / 2) ||
|
||||||
|
scopedLinks.length <= PAGINATION_PAGES_COUNT
|
||||||
|
) &&
|
||||||
|
!(pagination.page > scopedLinks.length - halfOfPagesCount)
|
||||||
|
) {
|
||||||
|
pageLinks = [
|
||||||
|
...pageLinks,
|
||||||
|
...scopedLinks.slice(
|
||||||
|
pagination.page - 1 - halfOfPagesCount,
|
||||||
|
pagination.page + halfOfPagesCount
|
||||||
|
),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
pageLinks.push(nextLink);
|
||||||
|
|
||||||
|
return pageLinks;
|
||||||
|
};
|
||||||
|
|
||||||
|
const paginationLinks = useMemo(() => sliceLinks(pagination), [pagination]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="row">
|
||||||
|
<div className="col-sm-12 col-md-5 d-flex align-items-center justify-content-center justify-content-md-start"></div>
|
||||||
|
<div className="col-sm-12 col-md-7 d-flex align-items-center justify-content-center justify-content-md-end">
|
||||||
|
<div id="kt_table_users_paginate">
|
||||||
|
<ul className="pagination">
|
||||||
|
<li
|
||||||
|
className={clsx("page-item", {
|
||||||
|
disabled: isLoading || pagination.page === 1,
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
<a
|
||||||
|
onClick={() => updatePage(1)}
|
||||||
|
style={{ cursor: "pointer" }}
|
||||||
|
className="page-link"
|
||||||
|
>
|
||||||
|
First
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
{paginationLinks
|
||||||
|
?.map((link) => {
|
||||||
|
return { ...link, label: mappedLabel(link.label) };
|
||||||
|
})
|
||||||
|
.map((link) => (
|
||||||
|
<li
|
||||||
|
key={link.label}
|
||||||
|
className={clsx("page-item", {
|
||||||
|
active: pagination.page === link.page,
|
||||||
|
disabled: isLoading,
|
||||||
|
previous: link.label === "Previous",
|
||||||
|
next: link.label === "Next",
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
<a
|
||||||
|
className={clsx("page-link", {
|
||||||
|
"page-text":
|
||||||
|
link.label === "Previous" || link.label === "Next",
|
||||||
|
"me-5": link.label === "Previous",
|
||||||
|
})}
|
||||||
|
onClick={() => updatePage(link.page)}
|
||||||
|
style={{ cursor: "pointer" }}
|
||||||
|
>
|
||||||
|
{mappedLabel(link.label)}
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
<li
|
||||||
|
className={clsx("page-item", {
|
||||||
|
disabled:
|
||||||
|
isLoading ||
|
||||||
|
pagination.page === (pagination.links?.length || 3) - 2,
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
<a
|
||||||
|
onClick={() => updatePage((pagination.links?.length || 3) - 2)}
|
||||||
|
style={{ cursor: "pointer" }}
|
||||||
|
className="page-link"
|
||||||
|
>
|
||||||
|
Last
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export { UsersListPagination };
|
||||||
@@ -0,0 +1,62 @@
|
|||||||
|
/* eslint-disable react-refresh/only-export-components */
|
||||||
|
import { FC, useState, createContext, useContext, useMemo } from "react";
|
||||||
|
import {
|
||||||
|
ID,
|
||||||
|
calculatedGroupingIsDisabled,
|
||||||
|
calculateIsAllDataSelected,
|
||||||
|
groupingOnSelect,
|
||||||
|
initialListView,
|
||||||
|
ListViewContextProps,
|
||||||
|
groupingOnSelectAll,
|
||||||
|
WithChildren,
|
||||||
|
} from "../../../../../_digifi/helpers";
|
||||||
|
import {
|
||||||
|
useQueryResponse,
|
||||||
|
useQueryResponseData,
|
||||||
|
} from "./QueryResponseProvider";
|
||||||
|
|
||||||
|
const ListViewContext = createContext<ListViewContextProps>(initialListView);
|
||||||
|
|
||||||
|
const ListViewProvider: FC<WithChildren> = ({ children }) => {
|
||||||
|
const [selected, setSelected] = useState<Array<ID>>(initialListView.selected);
|
||||||
|
const [itemIdForUpdate, setItemIdForUpdate] = useState<ID>(
|
||||||
|
initialListView.itemIdForUpdate
|
||||||
|
);
|
||||||
|
const { isLoading } = useQueryResponse();
|
||||||
|
const data = useQueryResponseData();
|
||||||
|
const disabled = useMemo(
|
||||||
|
() => calculatedGroupingIsDisabled(isLoading, data),
|
||||||
|
[isLoading, data]
|
||||||
|
);
|
||||||
|
const isAllSelected = useMemo(
|
||||||
|
() => calculateIsAllDataSelected(data, selected),
|
||||||
|
[data, selected]
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ListViewContext.Provider
|
||||||
|
value={{
|
||||||
|
selected,
|
||||||
|
itemIdForUpdate,
|
||||||
|
setItemIdForUpdate,
|
||||||
|
disabled,
|
||||||
|
isAllSelected,
|
||||||
|
onSelect: (id: ID) => {
|
||||||
|
groupingOnSelect(id, selected, setSelected);
|
||||||
|
},
|
||||||
|
onSelectAll: () => {
|
||||||
|
groupingOnSelectAll(isAllSelected, setSelected, data);
|
||||||
|
},
|
||||||
|
clearSelected: () => {
|
||||||
|
setSelected([]);
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</ListViewContext.Provider>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const useListView = () => useContext(ListViewContext);
|
||||||
|
|
||||||
|
export { ListViewProvider, useListView };
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
/* eslint-disable react-refresh/only-export-components */
|
||||||
|
import { FC, useState, createContext, useContext } from "react";
|
||||||
|
import {
|
||||||
|
QueryState,
|
||||||
|
QueryRequestContextProps,
|
||||||
|
initialQueryRequest,
|
||||||
|
WithChildren,
|
||||||
|
} from "../../../../../_digifi/helpers";
|
||||||
|
|
||||||
|
const QueryRequestContext =
|
||||||
|
createContext<QueryRequestContextProps>(initialQueryRequest);
|
||||||
|
|
||||||
|
const QueryRequestProvider: FC<WithChildren> = ({ children }) => {
|
||||||
|
const [state, setState] = useState<QueryState>(initialQueryRequest.state);
|
||||||
|
|
||||||
|
const updateState = (updates: Partial<QueryState>) => {
|
||||||
|
const updatedState = { ...state, ...updates } as QueryState;
|
||||||
|
setState(updatedState);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<QueryRequestContext.Provider value={{ state, updateState }}>
|
||||||
|
{children}
|
||||||
|
</QueryRequestContext.Provider>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const useQueryRequest = () => useContext(QueryRequestContext);
|
||||||
|
export { QueryRequestProvider, useQueryRequest };
|
||||||
+35
-33
@@ -1,7 +1,7 @@
|
|||||||
/* eslint-disable react-refresh/only-export-components */
|
/* eslint-disable react-refresh/only-export-components */
|
||||||
/* eslint-disable react-hooks/exhaustive-deps */
|
/* eslint-disable react-hooks/exhaustive-deps */
|
||||||
import {FC, useContext, useState, useEffect, useMemo} from 'react'
|
import { FC, useContext, useState, useEffect, useMemo } from "react";
|
||||||
import {useQuery} from 'react-query'
|
import { useQuery } from "react-query";
|
||||||
import {
|
import {
|
||||||
createResponseContext,
|
createResponseContext,
|
||||||
initialQueryResponse,
|
initialQueryResponse,
|
||||||
@@ -10,22 +10,22 @@ import {
|
|||||||
QUERIES,
|
QUERIES,
|
||||||
stringifyRequestQuery,
|
stringifyRequestQuery,
|
||||||
WithChildren,
|
WithChildren,
|
||||||
} from '../../../../../../_digifi/helpers'
|
} from "../../../../../_digifi/helpers";
|
||||||
import {getStartedUsers} from './_requests'
|
import { getPendingUsers } from "../../core/_requests";
|
||||||
import {User} from './_models'
|
import { User } from "../../core/_models";
|
||||||
import {useQueryRequest} from './QueryRequestProvider'
|
import { useQueryRequest } from "./QueryRequestProvider";
|
||||||
|
|
||||||
const QueryResponseContext = createResponseContext<User>(initialQueryResponse)
|
const QueryResponseContext = createResponseContext<User>(initialQueryResponse);
|
||||||
const QueryResponseProvider: FC<WithChildren> = ({children}) => {
|
const QueryResponseProvider: FC<WithChildren> = ({ children }) => {
|
||||||
const {state} = useQueryRequest()
|
const { state } = useQueryRequest();
|
||||||
const [query, setQuery] = useState<string>(stringifyRequestQuery(state))
|
const [query, setQuery] = useState<string>(stringifyRequestQuery(state));
|
||||||
const updatedQuery = useMemo(() => stringifyRequestQuery(state), [state])
|
const updatedQuery = useMemo(() => stringifyRequestQuery(state), [state]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (query !== updatedQuery) {
|
if (query !== updatedQuery) {
|
||||||
setQuery(updatedQuery)
|
setQuery(updatedQuery);
|
||||||
}
|
}
|
||||||
}, [updatedQuery])
|
}, [updatedQuery]);
|
||||||
|
|
||||||
const {
|
const {
|
||||||
isFetching,
|
isFetching,
|
||||||
@@ -34,47 +34,49 @@ const QueryResponseProvider: FC<WithChildren> = ({children}) => {
|
|||||||
} = useQuery(
|
} = useQuery(
|
||||||
`${QUERIES.USERS_LIST}-${query}`,
|
`${QUERIES.USERS_LIST}-${query}`,
|
||||||
() => {
|
() => {
|
||||||
return getStartedUsers(query)
|
return getPendingUsers(query);
|
||||||
},
|
},
|
||||||
{cacheTime: 0, keepPreviousData: true, refetchOnWindowFocus: false}
|
{ cacheTime: 0, keepPreviousData: true, refetchOnWindowFocus: false }
|
||||||
)
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<QueryResponseContext.Provider value={{isLoading: isFetching, refetch, response, query}}>
|
<QueryResponseContext.Provider
|
||||||
|
value={{ isLoading: isFetching, refetch, response, query }}
|
||||||
|
>
|
||||||
{children}
|
{children}
|
||||||
</QueryResponseContext.Provider>
|
</QueryResponseContext.Provider>
|
||||||
)
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
const useQueryResponse = () => useContext(QueryResponseContext)
|
const useQueryResponse = () => useContext(QueryResponseContext);
|
||||||
|
|
||||||
const useQueryResponseData = () => {
|
const useQueryResponseData = () => {
|
||||||
const {response} = useQueryResponse()
|
const { response } = useQueryResponse();
|
||||||
if (!response) {
|
if (!response) {
|
||||||
return []
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
return response?.records || []
|
return response?.records || [];
|
||||||
}
|
};
|
||||||
|
|
||||||
const useQueryResponsePagination = () => {
|
const useQueryResponsePagination = () => {
|
||||||
const defaultPaginationState: PaginationState = {
|
const defaultPaginationState: PaginationState = {
|
||||||
links: [],
|
links: [],
|
||||||
...initialQueryState,
|
...initialQueryState,
|
||||||
}
|
};
|
||||||
|
|
||||||
const {response} = useQueryResponse()
|
const { response } = useQueryResponse();
|
||||||
if (!response || !response.payload || !response.payload.pagination) {
|
if (!response || !response.payload || !response.payload.pagination) {
|
||||||
return defaultPaginationState
|
return defaultPaginationState;
|
||||||
}
|
}
|
||||||
|
|
||||||
return response.payload.pagination
|
return response.payload.pagination;
|
||||||
}
|
};
|
||||||
|
|
||||||
const useQueryResponseLoading = (): boolean => {
|
const useQueryResponseLoading = (): boolean => {
|
||||||
const {isLoading} = useQueryResponse()
|
const { isLoading } = useQueryResponse();
|
||||||
return isLoading
|
return isLoading;
|
||||||
}
|
};
|
||||||
|
|
||||||
export {
|
export {
|
||||||
QueryResponseProvider,
|
QueryResponseProvider,
|
||||||
@@ -82,4 +84,4 @@ export {
|
|||||||
useQueryResponseData,
|
useQueryResponseData,
|
||||||
useQueryResponsePagination,
|
useQueryResponsePagination,
|
||||||
useQueryResponseLoading,
|
useQueryResponseLoading,
|
||||||
}
|
};
|
||||||
@@ -0,0 +1,66 @@
|
|||||||
|
import { useMemo } from "react";
|
||||||
|
import { useTable, ColumnInstance, Row } from "react-table";
|
||||||
|
import { CustomHeaderColumn } from "./columns/CustomHeaderColumn";
|
||||||
|
import { CustomRow } from "./columns/CustomRow";
|
||||||
|
import {
|
||||||
|
useQueryResponseData,
|
||||||
|
useQueryResponseLoading,
|
||||||
|
} from "../core/QueryResponseProvider";
|
||||||
|
import { usersColumns } from "./columns/_columns";
|
||||||
|
import { User } from "../../core/_models";
|
||||||
|
import { UsersListLoading } from "../components/loading/UsersListLoading";
|
||||||
|
import { UsersListPagination } from "../components/pagination/UsersListPagination";
|
||||||
|
import { KTCardBody } from "../../../../../_digifi/helpers";
|
||||||
|
|
||||||
|
const UsersTable = () => {
|
||||||
|
const users = useQueryResponseData();
|
||||||
|
// console.log('users', users)
|
||||||
|
const isLoading = useQueryResponseLoading();
|
||||||
|
const data = useMemo(() => users, [users]);
|
||||||
|
const columns = useMemo(() => usersColumns, []);
|
||||||
|
const { getTableProps, getTableBodyProps, headers, rows, prepareRow } =
|
||||||
|
useTable({
|
||||||
|
columns,
|
||||||
|
data,
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<KTCardBody className="py-4">
|
||||||
|
<div className="table-responsive">
|
||||||
|
<table
|
||||||
|
id="kt_table_users"
|
||||||
|
className="table align-middle table-row-dashed fs-6 gy-5 dataTable no-footer"
|
||||||
|
{...getTableProps()}
|
||||||
|
>
|
||||||
|
<thead>
|
||||||
|
<tr className="text-start text-muted fw-bolder fs-7 text-uppercase gs-0">
|
||||||
|
{headers.map((column: ColumnInstance<User>) => (
|
||||||
|
<CustomHeaderColumn key={column.id} column={column} />
|
||||||
|
))}
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody className="text-gray-600 fw-bold" {...getTableBodyProps()}>
|
||||||
|
{rows.length > 0 ? (
|
||||||
|
rows.map((row: Row<User>, i) => {
|
||||||
|
prepareRow(row);
|
||||||
|
return <CustomRow row={row} key={`row-${i}-${row.id}`} />;
|
||||||
|
})
|
||||||
|
) : (
|
||||||
|
<tr>
|
||||||
|
<td colSpan={7}>
|
||||||
|
<div className="d-flex text-center w-100 align-content-center justify-content-center">
|
||||||
|
No matching records found
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
)}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
<UsersListPagination />
|
||||||
|
{isLoading && <UsersListLoading />}
|
||||||
|
</KTCardBody>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export { UsersTable };
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
import { FC } from "react";
|
||||||
|
import { NewDateTimeFormatter } from "../../../../../../_digifi/lib/NewDateTimeFormatter";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
added?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
const AddedCell: FC<Props> = ({ added }) => (
|
||||||
|
<div className="badge badge-light fw-bolder">
|
||||||
|
{NewDateTimeFormatter(added)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
export { AddedCell };
|
||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
import {FC} from 'react'
|
import {FC} from 'react'
|
||||||
import {ColumnInstance} from 'react-table'
|
import {ColumnInstance} from 'react-table'
|
||||||
import {User} from '../../core/_models'
|
import {User} from '../../../core/_models'
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
column: ColumnInstance<User>
|
column: ColumnInstance<User>
|
||||||
+1
-1
@@ -1,7 +1,7 @@
|
|||||||
import clsx from 'clsx'
|
import clsx from 'clsx'
|
||||||
import {FC} from 'react'
|
import {FC} from 'react'
|
||||||
import {Row} from 'react-table'
|
import {Row} from 'react-table'
|
||||||
import {User} from '../../core/_models'
|
import {User} from '../../../core/_models'
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
row: Row<User>
|
row: Row<User>
|
||||||
@@ -0,0 +1,76 @@
|
|||||||
|
import { FC, useEffect } from "react";
|
||||||
|
import { useMutation, useQueryClient } from "react-query";
|
||||||
|
import { MenuComponent } from "../../../../../../_digifi/assets/ts/components";
|
||||||
|
import { ID, KTIcon, QUERIES } from "../../../../../../_digifi/helpers";
|
||||||
|
import { useListView } from "../../core/ListViewProvider";
|
||||||
|
import { useQueryResponse } from "../../core/QueryResponseProvider";
|
||||||
|
import { deleteUser } from "../../../core/_requests";
|
||||||
|
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
id: ID;
|
||||||
|
};
|
||||||
|
|
||||||
|
const UserActionsCell: FC<Props> = ({ id }) => {
|
||||||
|
const { setItemIdForUpdate } = useListView();
|
||||||
|
const { query } = useQueryResponse();
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
MenuComponent.reinitialization();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const openEditModal = () => {
|
||||||
|
setItemIdForUpdate(id);
|
||||||
|
};
|
||||||
|
|
||||||
|
const deleteItem = useMutation(() => deleteUser(id), {
|
||||||
|
// 💡 response of the mutation is passed to onSuccess
|
||||||
|
onSuccess: () => {
|
||||||
|
// ✅ update detail view directly
|
||||||
|
queryClient.invalidateQueries([`${QUERIES.USERS_LIST}-${query}`]);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<a
|
||||||
|
href="#"
|
||||||
|
className="btn btn-light btn-active-light-primary btn-sm"
|
||||||
|
data-kt-menu-trigger="click"
|
||||||
|
data-kt-menu-placement="bottom-end"
|
||||||
|
>
|
||||||
|
Actions
|
||||||
|
<KTIcon iconName="down" className="fs-5 m-0" />
|
||||||
|
</a>
|
||||||
|
{/* begin::Menu */}
|
||||||
|
<div
|
||||||
|
className="menu menu-sub menu-sub-dropdown menu-column menu-rounded menu-gray-600 menu-state-bg-light-primary fw-bold fs-7 w-175px py-4"
|
||||||
|
data-kt-menu="true"
|
||||||
|
>
|
||||||
|
{/* begin::Menu item */}
|
||||||
|
<div className="menu-item px-3">
|
||||||
|
<a className="menu-link px-3" onClick={openEditModal}>
|
||||||
|
Cancel Application
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
{/* end::Menu item */}
|
||||||
|
|
||||||
|
{/* begin::Menu item */}
|
||||||
|
<div className="menu-item px-3">
|
||||||
|
<a
|
||||||
|
className="menu-link px-3"
|
||||||
|
data-kt-users-table-filter="delete_row"
|
||||||
|
onClick={async () => await deleteItem.mutateAsync()}
|
||||||
|
>
|
||||||
|
Start Processing
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
{/* end::Menu item */}
|
||||||
|
</div>
|
||||||
|
{/* end::Menu */}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export { UserActionsCell };
|
||||||
@@ -0,0 +1,61 @@
|
|||||||
|
import clsx from "clsx";
|
||||||
|
import { FC, PropsWithChildren, useMemo } from "react";
|
||||||
|
import { HeaderProps } from "react-table";
|
||||||
|
import { initialQueryState } from "../../../../../../_digifi/helpers";
|
||||||
|
import { useQueryRequest } from "../../core/QueryRequestProvider";
|
||||||
|
import { User } from "../../../core/_models";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
className?: string;
|
||||||
|
title?: string;
|
||||||
|
tableProps: PropsWithChildren<HeaderProps<User>>;
|
||||||
|
};
|
||||||
|
const UserCustomHeader: FC<Props> = ({ className, title, tableProps }) => {
|
||||||
|
const id = tableProps.column.id;
|
||||||
|
const { state, updateState } = useQueryRequest();
|
||||||
|
|
||||||
|
const isSelectedForSorting = useMemo(() => {
|
||||||
|
return state.sort && state.sort === id;
|
||||||
|
}, [state, id]);
|
||||||
|
const order: "asc" | "desc" | undefined = useMemo(() => state.order, [state]);
|
||||||
|
|
||||||
|
const sortColumn = () => {
|
||||||
|
// avoid sorting for these columns
|
||||||
|
if (id === "actions" || id === "selection") {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isSelectedForSorting) {
|
||||||
|
// enable sort asc
|
||||||
|
updateState({ sort: id, order: "asc", ...initialQueryState });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isSelectedForSorting && order !== undefined) {
|
||||||
|
if (order === "asc") {
|
||||||
|
// enable sort desc
|
||||||
|
updateState({ sort: id, order: "desc", ...initialQueryState });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// disable sort
|
||||||
|
updateState({ sort: undefined, order: undefined, ...initialQueryState });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<th
|
||||||
|
{...tableProps.column.getHeaderProps()}
|
||||||
|
className={clsx(
|
||||||
|
className,
|
||||||
|
isSelectedForSorting && order !== undefined && `table-sort-${order}`
|
||||||
|
)}
|
||||||
|
style={{ cursor: "pointer" }}
|
||||||
|
onClick={sortColumn}
|
||||||
|
>
|
||||||
|
{title}
|
||||||
|
</th>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export { UserCustomHeader };
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
import clsx from "clsx";
|
||||||
|
import { FC } from "react";
|
||||||
|
import { toAbsoluteUrl } from "../../../../../../_digifi/helpers";
|
||||||
|
import { User } from "../../../core/_models";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
user: User;
|
||||||
|
};
|
||||||
|
|
||||||
|
const UserInfoCell: FC<Props> = ({ user }) => (
|
||||||
|
<div className="d-flex align-items-center">
|
||||||
|
{/* begin:: Avatar */}
|
||||||
|
<div className="symbol symbol-circle symbol-50px overflow-hidden me-3">
|
||||||
|
<a href="#">
|
||||||
|
{user.avatar ? (
|
||||||
|
<div className="symbol-label">
|
||||||
|
<img
|
||||||
|
src={toAbsoluteUrl(`media/${user.avatar}`)}
|
||||||
|
alt={user.name}
|
||||||
|
className="w-100"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div
|
||||||
|
className={clsx(
|
||||||
|
"symbol-label fs-3",
|
||||||
|
`bg-light-${user.initials?.state}`,
|
||||||
|
`text-${user.initials?.state}`
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{user.firstname?.substring(0, 1).toUpperCase()}{" "}
|
||||||
|
{user.lastname?.substring(0, 1).toUpperCase()}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<div className="d-flex flex-column">
|
||||||
|
<a href="#" className="text-gray-800 text-hover-primary mb-1">
|
||||||
|
{user.firstname} {user.lastname}
|
||||||
|
</a>
|
||||||
|
<span>{user.email}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
export { UserInfoCell };
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
import { FC, useMemo } from "react";
|
||||||
|
import { ID } from "../../../../../../_digifi/helpers";
|
||||||
|
import { useListView } from "../../core/ListViewProvider";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
id: ID;
|
||||||
|
};
|
||||||
|
|
||||||
|
const UserSelectionCell: FC<Props> = ({ id }) => {
|
||||||
|
const { selected, onSelect } = useListView();
|
||||||
|
const isSelected = useMemo(() => selected.includes(id), [id, selected]);
|
||||||
|
return (
|
||||||
|
<div className="form-check form-check-custom form-check-solid">
|
||||||
|
<input
|
||||||
|
className="form-check-input"
|
||||||
|
type="checkbox"
|
||||||
|
data-kt-check={isSelected}
|
||||||
|
data-kt-check-target="#kt_table_users .form-check-input"
|
||||||
|
checked={isSelected}
|
||||||
|
onChange={() => onSelect(id)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export { UserSelectionCell };
|
||||||
+1
-1
@@ -1,7 +1,7 @@
|
|||||||
import {FC, PropsWithChildren} from 'react'
|
import {FC, PropsWithChildren} from 'react'
|
||||||
import {HeaderProps} from 'react-table'
|
import {HeaderProps} from 'react-table'
|
||||||
import {useListView} from '../../core/ListViewProvider'
|
import {useListView} from '../../core/ListViewProvider'
|
||||||
import {User} from '../../core/_models'
|
import {User} from '../../../core/_models'
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
tableProps: PropsWithChildren<HeaderProps<User>>
|
tableProps: PropsWithChildren<HeaderProps<User>>
|
||||||
@@ -0,0 +1,103 @@
|
|||||||
|
import { Column } from 'react-table';
|
||||||
|
import { UserInfoCell } from './UserInfoCell';
|
||||||
|
import { PaymentMonthCell } from './UserLastLoginCell';
|
||||||
|
import { AgentCell } from './AgentCell';
|
||||||
|
import { UserActionsCell } from './UserActionsCell';
|
||||||
|
import { UserSelectionCell } from './UserSelectionCell';
|
||||||
|
import { UserCustomHeader } from './UserCustomHeader';
|
||||||
|
import { UserSelectionHeader } from './UserSelectionHeader';
|
||||||
|
import { User } from '../../../core/_models';
|
||||||
|
import { AddedCell } from './AddedCell';
|
||||||
|
|
||||||
|
const formatNumberWithCommas = (number: string): string => {
|
||||||
|
return number.replace(/\B(?=(\d{3})+(?!\d))/g, ',');
|
||||||
|
};
|
||||||
|
|
||||||
|
const usersColumns: ReadonlyArray<Column<User>> = [
|
||||||
|
{
|
||||||
|
Header: (props) => <UserSelectionHeader tableProps={props} />,
|
||||||
|
id: 'selection',
|
||||||
|
Cell: ({ ...props }) => (
|
||||||
|
<UserSelectionCell id={props.data[props.row.index].uid} />
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Header: (props) => (
|
||||||
|
<UserCustomHeader
|
||||||
|
tableProps={props}
|
||||||
|
title="Name"
|
||||||
|
className="min-w-125px"
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
id: 'firstname',
|
||||||
|
Cell: ({ ...props }) => <UserInfoCell user={props.data[props.row.index]} />,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Header: (props) => (
|
||||||
|
<UserCustomHeader
|
||||||
|
tableProps={props}
|
||||||
|
title="Amount"
|
||||||
|
className="min-w-125px"
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
accessor: 'loan_amount',
|
||||||
|
Cell: ({ row, data }) =>
|
||||||
|
formatNumberWithCommas(data[row.index].loan_amount || '0'),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Header: (props) => (
|
||||||
|
<UserCustomHeader
|
||||||
|
tableProps={props}
|
||||||
|
title="Payment Terms"
|
||||||
|
className="min-w-125px"
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
id: 'payment_month',
|
||||||
|
Cell: ({ ...props }) => (
|
||||||
|
<PaymentMonthCell
|
||||||
|
payment_month={props.data[props.row.index].payment_month}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Header: (props) => (
|
||||||
|
<UserCustomHeader
|
||||||
|
tableProps={props}
|
||||||
|
title="Agent"
|
||||||
|
className="min-w-125px"
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
id: 'sales_agent',
|
||||||
|
Cell: ({ ...props }) => (
|
||||||
|
<AgentCell agent={props.data[props.row.index].sales_agent} />
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Header: (props) => (
|
||||||
|
<UserCustomHeader
|
||||||
|
tableProps={props}
|
||||||
|
title="Added"
|
||||||
|
className="min-w-125px"
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
id: 'added',
|
||||||
|
Cell: ({ ...props }) => (
|
||||||
|
<AddedCell added={props.data[props.row.index].added} />
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Header: (props) => (
|
||||||
|
<UserCustomHeader
|
||||||
|
tableProps={props}
|
||||||
|
title="Actions"
|
||||||
|
className="text-end min-w-100px"
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
id: 'actions',
|
||||||
|
Cell: ({ ...props }) => (
|
||||||
|
<UserActionsCell id={props.data[props.row.index].uid} />
|
||||||
|
),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
export { usersColumns };
|
||||||
@@ -0,0 +1,433 @@
|
|||||||
|
import { FC, useState } from "react";
|
||||||
|
import * as Yup from "yup";
|
||||||
|
import { useFormik } from "formik";
|
||||||
|
import { isNotEmpty, toAbsoluteUrl } from "../../../../../_digifi/helpers";
|
||||||
|
import { initialUser, User } from "../../core/_models";
|
||||||
|
import clsx from "clsx";
|
||||||
|
import { useListView } from "../core/ListViewProvider";
|
||||||
|
import { UsersListLoading } from "../components/loading/UsersListLoading";
|
||||||
|
import { createUser, updateUser } from "../../core/_requests";
|
||||||
|
import { useQueryResponse } from "../core/QueryResponseProvider";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
isUserLoading: boolean;
|
||||||
|
user: User;
|
||||||
|
};
|
||||||
|
|
||||||
|
const editUserSchema = Yup.object().shape({
|
||||||
|
email: Yup.string()
|
||||||
|
.email("Wrong email format")
|
||||||
|
.min(3, "Minimum 3 symbols")
|
||||||
|
.max(50, "Maximum 50 symbols")
|
||||||
|
.required("Email is required"),
|
||||||
|
name: Yup.string()
|
||||||
|
.min(3, "Minimum 3 symbols")
|
||||||
|
.max(50, "Maximum 50 symbols")
|
||||||
|
.required("Name is required"),
|
||||||
|
});
|
||||||
|
|
||||||
|
const UserEditModalForm: FC<Props> = ({ user, isUserLoading }) => {
|
||||||
|
const { setItemIdForUpdate } = useListView();
|
||||||
|
const { refetch } = useQueryResponse();
|
||||||
|
|
||||||
|
const [userForEdit] = useState<User>({
|
||||||
|
...user,
|
||||||
|
avatar: user.avatar || initialUser.avatar,
|
||||||
|
role: user.role || initialUser.role,
|
||||||
|
position: user.position || initialUser.position,
|
||||||
|
name: user.name || initialUser.name,
|
||||||
|
email: user.email || initialUser.email,
|
||||||
|
});
|
||||||
|
|
||||||
|
const cancel = (withRefresh?: boolean) => {
|
||||||
|
if (withRefresh) {
|
||||||
|
refetch();
|
||||||
|
}
|
||||||
|
setItemIdForUpdate(undefined);
|
||||||
|
};
|
||||||
|
|
||||||
|
const blankImg = toAbsoluteUrl("media/svg/avatars/blank.svg");
|
||||||
|
const userAvatarImg = toAbsoluteUrl(`media/${userForEdit.avatar}`);
|
||||||
|
|
||||||
|
const formik = useFormik({
|
||||||
|
initialValues: userForEdit,
|
||||||
|
validationSchema: editUserSchema,
|
||||||
|
onSubmit: async (values, { setSubmitting }) => {
|
||||||
|
setSubmitting(true);
|
||||||
|
try {
|
||||||
|
if (isNotEmpty(values.id)) {
|
||||||
|
await updateUser(values);
|
||||||
|
} else {
|
||||||
|
await createUser(values);
|
||||||
|
}
|
||||||
|
} catch (ex) {
|
||||||
|
console.error(ex);
|
||||||
|
} finally {
|
||||||
|
setSubmitting(true);
|
||||||
|
cancel(true);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<form
|
||||||
|
id="kt_modal_add_user_form"
|
||||||
|
className="form"
|
||||||
|
onSubmit={formik.handleSubmit}
|
||||||
|
noValidate
|
||||||
|
>
|
||||||
|
{/* begin::Scroll */}
|
||||||
|
<div
|
||||||
|
className="d-flex flex-column scroll-y me-n7 pe-7"
|
||||||
|
id="kt_modal_add_user_scroll"
|
||||||
|
data-kt-scroll="true"
|
||||||
|
data-kt-scroll-activate="{default: false, lg: true}"
|
||||||
|
data-kt-scroll-max-height="auto"
|
||||||
|
data-kt-scroll-dependencies="#kt_modal_add_user_header"
|
||||||
|
data-kt-scroll-wrappers="#kt_modal_add_user_scroll"
|
||||||
|
data-kt-scroll-offset="300px"
|
||||||
|
>
|
||||||
|
{/* begin::Input group */}
|
||||||
|
<div className="fv-row mb-7">
|
||||||
|
{/* begin::Label */}
|
||||||
|
<label className="d-block fw-bold fs-6 mb-5">Avatar</label>
|
||||||
|
{/* end::Label */}
|
||||||
|
|
||||||
|
{/* begin::Image input */}
|
||||||
|
<div
|
||||||
|
className="image-input image-input-outline"
|
||||||
|
data-kt-image-input="true"
|
||||||
|
style={{ backgroundImage: `url('${blankImg}')` }}
|
||||||
|
>
|
||||||
|
{/* begin::Preview existing avatar */}
|
||||||
|
<div
|
||||||
|
className="image-input-wrapper w-125px h-125px"
|
||||||
|
style={{ backgroundImage: `url('${userAvatarImg}')` }}
|
||||||
|
></div>
|
||||||
|
{/* end::Preview existing avatar */}
|
||||||
|
|
||||||
|
{/* begin::Label */}
|
||||||
|
{/* <label
|
||||||
|
className='btn btn-icon btn-circle btn-active-color-primary w-25px h-25px bg-body shadow'
|
||||||
|
data-kt-image-input-action='change'
|
||||||
|
data-bs-toggle='tooltip'
|
||||||
|
title='Change avatar'
|
||||||
|
>
|
||||||
|
<i className='bi bi-pencil-fill fs-7'></i>
|
||||||
|
|
||||||
|
<input type='file' name='avatar' accept='.png, .jpg, .jpeg' />
|
||||||
|
<input type='hidden' name='avatar_remove' />
|
||||||
|
</label> */}
|
||||||
|
{/* end::Label */}
|
||||||
|
|
||||||
|
{/* begin::Cancel */}
|
||||||
|
{/* <span
|
||||||
|
className='btn btn-icon btn-circle btn-active-color-primary w-25px h-25px bg-body shadow'
|
||||||
|
data-kt-image-input-action='cancel'
|
||||||
|
data-bs-toggle='tooltip'
|
||||||
|
title='Cancel avatar'
|
||||||
|
>
|
||||||
|
<i className='bi bi-x fs-2'></i>
|
||||||
|
</span> */}
|
||||||
|
{/* end::Cancel */}
|
||||||
|
|
||||||
|
{/* begin::Remove */}
|
||||||
|
{/* <span
|
||||||
|
className='btn btn-icon btn-circle btn-active-color-primary w-25px h-25px bg-body shadow'
|
||||||
|
data-kt-image-input-action='remove'
|
||||||
|
data-bs-toggle='tooltip'
|
||||||
|
title='Remove avatar'
|
||||||
|
>
|
||||||
|
<i className='bi bi-x fs-2'></i>
|
||||||
|
</span> */}
|
||||||
|
{/* end::Remove */}
|
||||||
|
</div>
|
||||||
|
{/* end::Image input */}
|
||||||
|
|
||||||
|
{/* begin::Hint */}
|
||||||
|
{/* <div className='form-text'>Allowed file types: png, jpg, jpeg.</div> */}
|
||||||
|
{/* end::Hint */}
|
||||||
|
</div>
|
||||||
|
{/* end::Input group */}
|
||||||
|
|
||||||
|
{/* begin::Input group */}
|
||||||
|
<div className="fv-row mb-7">
|
||||||
|
{/* begin::Label */}
|
||||||
|
<label className="required fw-bold fs-6 mb-2">Full Name</label>
|
||||||
|
{/* end::Label */}
|
||||||
|
|
||||||
|
{/* begin::Input */}
|
||||||
|
<input
|
||||||
|
placeholder="Full name"
|
||||||
|
{...formik.getFieldProps("name")}
|
||||||
|
type="text"
|
||||||
|
name="name"
|
||||||
|
className={clsx(
|
||||||
|
"form-control form-control-solid mb-3 mb-lg-0",
|
||||||
|
{ "is-invalid": formik.touched.name && formik.errors.name },
|
||||||
|
{
|
||||||
|
"is-valid": formik.touched.name && !formik.errors.name,
|
||||||
|
}
|
||||||
|
)}
|
||||||
|
autoComplete="off"
|
||||||
|
disabled={formik.isSubmitting || isUserLoading}
|
||||||
|
/>
|
||||||
|
{formik.touched.name && formik.errors.name && (
|
||||||
|
<div className="fv-plugins-message-container">
|
||||||
|
<div className="fv-help-block">
|
||||||
|
<span role="alert">{formik.errors.name}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{/* end::Input */}
|
||||||
|
</div>
|
||||||
|
{/* end::Input group */}
|
||||||
|
|
||||||
|
{/* begin::Input group */}
|
||||||
|
<div className="fv-row mb-7">
|
||||||
|
{/* begin::Label */}
|
||||||
|
<label className="required fw-bold fs-6 mb-2">Email</label>
|
||||||
|
{/* end::Label */}
|
||||||
|
|
||||||
|
{/* begin::Input */}
|
||||||
|
<input
|
||||||
|
placeholder="Email"
|
||||||
|
{...formik.getFieldProps("email")}
|
||||||
|
className={clsx(
|
||||||
|
"form-control form-control-solid mb-3 mb-lg-0",
|
||||||
|
{ "is-invalid": formik.touched.email && formik.errors.email },
|
||||||
|
{
|
||||||
|
"is-valid": formik.touched.email && !formik.errors.email,
|
||||||
|
}
|
||||||
|
)}
|
||||||
|
type="email"
|
||||||
|
name="email"
|
||||||
|
autoComplete="off"
|
||||||
|
disabled={formik.isSubmitting || isUserLoading}
|
||||||
|
/>
|
||||||
|
{/* end::Input */}
|
||||||
|
{formik.touched.email && formik.errors.email && (
|
||||||
|
<div className="fv-plugins-message-container">
|
||||||
|
<span role="alert">{formik.errors.email}</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
{/* end::Input group */}
|
||||||
|
|
||||||
|
{/* begin::Input group */}
|
||||||
|
<div className="mb-7">
|
||||||
|
{/* begin::Label */}
|
||||||
|
<label className="required fw-bold fs-6 mb-5">Role</label>
|
||||||
|
{/* end::Label */}
|
||||||
|
{/* begin::Roles */}
|
||||||
|
{/* begin::Input row */}
|
||||||
|
<div className="d-flex fv-row">
|
||||||
|
{/* begin::Radio */}
|
||||||
|
<div className="form-check form-check-custom form-check-solid">
|
||||||
|
{/* begin::Input */}
|
||||||
|
<input
|
||||||
|
className="form-check-input me-3"
|
||||||
|
{...formik.getFieldProps("role")}
|
||||||
|
name="role"
|
||||||
|
type="radio"
|
||||||
|
value="Administrator"
|
||||||
|
id="kt_modal_update_role_option_0"
|
||||||
|
checked={formik.values.role === "Administrator"}
|
||||||
|
disabled={formik.isSubmitting || isUserLoading}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* end::Input */}
|
||||||
|
{/* begin::Label */}
|
||||||
|
<label
|
||||||
|
className="form-check-label"
|
||||||
|
htmlFor="kt_modal_update_role_option_0"
|
||||||
|
>
|
||||||
|
<div className="fw-bolder text-gray-800">Administrator</div>
|
||||||
|
<div className="text-gray-600">
|
||||||
|
Best for business owners and company administrators
|
||||||
|
</div>
|
||||||
|
</label>
|
||||||
|
{/* end::Label */}
|
||||||
|
</div>
|
||||||
|
{/* end::Radio */}
|
||||||
|
</div>
|
||||||
|
{/* end::Input row */}
|
||||||
|
<div className="separator separator-dashed my-5"></div>
|
||||||
|
{/* begin::Input row */}
|
||||||
|
<div className="d-flex fv-row">
|
||||||
|
{/* begin::Radio */}
|
||||||
|
<div className="form-check form-check-custom form-check-solid">
|
||||||
|
{/* begin::Input */}
|
||||||
|
<input
|
||||||
|
className="form-check-input me-3"
|
||||||
|
{...formik.getFieldProps("role")}
|
||||||
|
name="role"
|
||||||
|
type="radio"
|
||||||
|
value="Developer"
|
||||||
|
id="kt_modal_update_role_option_1"
|
||||||
|
checked={formik.values.role === "Developer"}
|
||||||
|
disabled={formik.isSubmitting || isUserLoading}
|
||||||
|
/>
|
||||||
|
{/* end::Input */}
|
||||||
|
{/* begin::Label */}
|
||||||
|
<label
|
||||||
|
className="form-check-label"
|
||||||
|
htmlFor="kt_modal_update_role_option_1"
|
||||||
|
>
|
||||||
|
<div className="fw-bolder text-gray-800">Developer</div>
|
||||||
|
<div className="text-gray-600">
|
||||||
|
Best for developers or people primarily using the API
|
||||||
|
</div>
|
||||||
|
</label>
|
||||||
|
{/* end::Label */}
|
||||||
|
</div>
|
||||||
|
{/* end::Radio */}
|
||||||
|
</div>
|
||||||
|
{/* end::Input row */}
|
||||||
|
<div className="separator separator-dashed my-5"></div>
|
||||||
|
{/* begin::Input row */}
|
||||||
|
<div className="d-flex fv-row">
|
||||||
|
{/* begin::Radio */}
|
||||||
|
<div className="form-check form-check-custom form-check-solid">
|
||||||
|
{/* begin::Input */}
|
||||||
|
<input
|
||||||
|
className="form-check-input me-3"
|
||||||
|
{...formik.getFieldProps("role")}
|
||||||
|
name="role"
|
||||||
|
type="radio"
|
||||||
|
value="Analyst"
|
||||||
|
id="kt_modal_update_role_option_2"
|
||||||
|
checked={formik.values.role === "Analyst"}
|
||||||
|
disabled={formik.isSubmitting || isUserLoading}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* end::Input */}
|
||||||
|
{/* begin::Label */}
|
||||||
|
<label
|
||||||
|
className="form-check-label"
|
||||||
|
htmlFor="kt_modal_update_role_option_2"
|
||||||
|
>
|
||||||
|
<div className="fw-bolder text-gray-800">Analyst</div>
|
||||||
|
<div className="text-gray-600">
|
||||||
|
Best for people who need full access to analytics data, but
|
||||||
|
don't need to update business settings
|
||||||
|
</div>
|
||||||
|
</label>
|
||||||
|
{/* end::Label */}
|
||||||
|
</div>
|
||||||
|
{/* end::Radio */}
|
||||||
|
</div>
|
||||||
|
{/* end::Input row */}
|
||||||
|
<div className="separator separator-dashed my-5"></div>
|
||||||
|
{/* begin::Input row */}
|
||||||
|
<div className="d-flex fv-row">
|
||||||
|
{/* begin::Radio */}
|
||||||
|
<div className="form-check form-check-custom form-check-solid">
|
||||||
|
{/* begin::Input */}
|
||||||
|
<input
|
||||||
|
className="form-check-input me-3"
|
||||||
|
{...formik.getFieldProps("role")}
|
||||||
|
name="role"
|
||||||
|
type="radio"
|
||||||
|
value="Support"
|
||||||
|
id="kt_modal_update_role_option_3"
|
||||||
|
checked={formik.values.role === "Support"}
|
||||||
|
disabled={formik.isSubmitting || isUserLoading}
|
||||||
|
/>
|
||||||
|
{/* end::Input */}
|
||||||
|
{/* begin::Label */}
|
||||||
|
<label
|
||||||
|
className="form-check-label"
|
||||||
|
htmlFor="kt_modal_update_role_option_3"
|
||||||
|
>
|
||||||
|
<div className="fw-bolder text-gray-800">Support</div>
|
||||||
|
<div className="text-gray-600">
|
||||||
|
Best for employees who regularly refund payments and respond
|
||||||
|
to disputes
|
||||||
|
</div>
|
||||||
|
</label>
|
||||||
|
{/* end::Label */}
|
||||||
|
</div>
|
||||||
|
{/* end::Radio */}
|
||||||
|
</div>
|
||||||
|
{/* end::Input row */}
|
||||||
|
<div className="separator separator-dashed my-5"></div>
|
||||||
|
{/* begin::Input row */}
|
||||||
|
<div className="d-flex fv-row">
|
||||||
|
{/* begin::Radio */}
|
||||||
|
<div className="form-check form-check-custom form-check-solid">
|
||||||
|
{/* begin::Input */}
|
||||||
|
<input
|
||||||
|
className="form-check-input me-3"
|
||||||
|
{...formik.getFieldProps("role")}
|
||||||
|
name="role"
|
||||||
|
type="radio"
|
||||||
|
id="kt_modal_update_role_option_4"
|
||||||
|
value="Trial"
|
||||||
|
checked={formik.values.role === "Trial"}
|
||||||
|
disabled={formik.isSubmitting || isUserLoading}
|
||||||
|
/>
|
||||||
|
{/* end::Input */}
|
||||||
|
{/* begin::Label */}
|
||||||
|
<label
|
||||||
|
className="form-check-label"
|
||||||
|
htmlFor="kt_modal_update_role_option_4"
|
||||||
|
>
|
||||||
|
<div className="fw-bolder text-gray-800">Trial</div>
|
||||||
|
<div className="text-gray-600">
|
||||||
|
Best for people who need to preview content data, but don't
|
||||||
|
need to make any updates
|
||||||
|
</div>
|
||||||
|
</label>
|
||||||
|
{/* end::Label */}
|
||||||
|
</div>
|
||||||
|
{/* end::Radio */}
|
||||||
|
</div>
|
||||||
|
{/* end::Input row */}
|
||||||
|
{/* end::Roles */}
|
||||||
|
</div>
|
||||||
|
{/* end::Input group */}
|
||||||
|
</div>
|
||||||
|
{/* end::Scroll */}
|
||||||
|
|
||||||
|
{/* begin::Actions */}
|
||||||
|
<div className="text-center pt-15">
|
||||||
|
<button
|
||||||
|
type="reset"
|
||||||
|
onClick={() => cancel()}
|
||||||
|
className="btn btn-light me-3"
|
||||||
|
data-kt-users-modal-action="cancel"
|
||||||
|
disabled={formik.isSubmitting || isUserLoading}
|
||||||
|
>
|
||||||
|
Discard
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
className="btn btn-primary"
|
||||||
|
data-kt-users-modal-action="submit"
|
||||||
|
disabled={
|
||||||
|
isUserLoading ||
|
||||||
|
formik.isSubmitting ||
|
||||||
|
!formik.isValid ||
|
||||||
|
!formik.touched
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<span className="indicator-label">Submit</span>
|
||||||
|
{(formik.isSubmitting || isUserLoading) && (
|
||||||
|
<span className="indicator-progress">
|
||||||
|
Please wait...{" "}
|
||||||
|
<span className="spinner-border spinner-border-sm align-middle ms-2"></span>
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
{/* end::Actions */}
|
||||||
|
</form>
|
||||||
|
{(formik.isSubmitting || isUserLoading) && <UsersListLoading />}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export { UserEditModalForm };
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
import { useQuery } from "react-query";
|
||||||
|
import { UserEditModalForm } from "./UserEditModalForm";
|
||||||
|
import { isNotEmpty, QUERIES } from "../../../../../_digifi/helpers";
|
||||||
|
import { useListView } from "../core/ListViewProvider";
|
||||||
|
import { getUserById } from "../../core/_requests";
|
||||||
|
|
||||||
|
const UserEditModalFormWrapper = () => {
|
||||||
|
const { itemIdForUpdate, setItemIdForUpdate } = useListView();
|
||||||
|
const enabledQuery: boolean = isNotEmpty(itemIdForUpdate);
|
||||||
|
const {
|
||||||
|
isLoading,
|
||||||
|
data: user,
|
||||||
|
error,
|
||||||
|
} = useQuery(
|
||||||
|
`${QUERIES.USERS_LIST}-user-${itemIdForUpdate}`,
|
||||||
|
() => {
|
||||||
|
return getUserById(itemIdForUpdate);
|
||||||
|
},
|
||||||
|
{
|
||||||
|
cacheTime: 0,
|
||||||
|
enabled: enabledQuery,
|
||||||
|
onError: (err) => {
|
||||||
|
setItemIdForUpdate(undefined);
|
||||||
|
console.error(err);
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!itemIdForUpdate) {
|
||||||
|
return (
|
||||||
|
<UserEditModalForm isUserLoading={isLoading} user={{ id: undefined }} />
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isLoading && !error && user) {
|
||||||
|
return <UserEditModalForm isUserLoading={isLoading} user={user} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
|
||||||
|
export { UserEditModalFormWrapper };
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
import { KTIcon } from "../../../../../_digifi/helpers";
|
||||||
|
import { useListView } from "../core/ListViewProvider";
|
||||||
|
|
||||||
|
const UserEditModalHeader = () => {
|
||||||
|
const { setItemIdForUpdate } = useListView();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="modal-header">
|
||||||
|
{/* begin::Modal title */}
|
||||||
|
<h2 className="fw-bolder">Add User</h2>
|
||||||
|
{/* end::Modal title */}
|
||||||
|
|
||||||
|
{/* begin::Close */}
|
||||||
|
<div
|
||||||
|
className="btn btn-icon btn-sm btn-active-icon-primary"
|
||||||
|
data-kt-users-modal-action="close"
|
||||||
|
onClick={() => setItemIdForUpdate(undefined)}
|
||||||
|
style={{ cursor: "pointer" }}
|
||||||
|
>
|
||||||
|
<KTIcon iconName="cross" className="fs-1" />
|
||||||
|
</div>
|
||||||
|
{/* end::Close */}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export { UserEditModalHeader };
|
||||||
@@ -1,37 +0,0 @@
|
|||||||
import {ListViewProvider, useListView} from './core/ListViewProvider'
|
|
||||||
import {QueryRequestProvider} from './core/QueryRequestProvider'
|
|
||||||
import {QueryResponseProvider} from './core/QueryResponseProvider'
|
|
||||||
import {UsersListHeader} from './components/header/UsersListHeader'
|
|
||||||
import {UsersTable} from './table/UsersTable'
|
|
||||||
import {UserEditModal} from './user-edit-modal/UserEditModal'
|
|
||||||
import {KTCard} from '../../../../../_digifi/helpers'
|
|
||||||
import { ToolbarWrapper } from '../../../../../_digifi/layout/components/toolbar'
|
|
||||||
import { Content } from '../../../../../_digifi/layout/components/content'
|
|
||||||
|
|
||||||
const UsersList = () => {
|
|
||||||
const {itemIdForUpdate} = useListView()
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<KTCard>
|
|
||||||
<UsersListHeader />
|
|
||||||
<UsersTable />
|
|
||||||
</KTCard>
|
|
||||||
{itemIdForUpdate !== undefined && <UserEditModal />}
|
|
||||||
</>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
const UsersListWrapper = () => (
|
|
||||||
<QueryRequestProvider>
|
|
||||||
<QueryResponseProvider>
|
|
||||||
<ListViewProvider>
|
|
||||||
<ToolbarWrapper />
|
|
||||||
<Content>
|
|
||||||
<UsersList />
|
|
||||||
</Content>
|
|
||||||
</ListViewProvider>
|
|
||||||
</QueryResponseProvider>
|
|
||||||
</QueryRequestProvider>
|
|
||||||
)
|
|
||||||
|
|
||||||
export {UsersListWrapper}
|
|
||||||
-156
@@ -1,156 +0,0 @@
|
|||||||
|
|
||||||
import clsx from 'clsx'
|
|
||||||
import {useQueryResponseLoading, useQueryResponsePagination} from '../../core/QueryResponseProvider'
|
|
||||||
import {useQueryRequest} from '../../core/QueryRequestProvider'
|
|
||||||
import {PaginationState} from '../../../../../../../_digifi/helpers'
|
|
||||||
import {useMemo} from 'react'
|
|
||||||
|
|
||||||
const mappedLabel = (label: string): string => {
|
|
||||||
if (label === '« Previous') {
|
|
||||||
return 'Previous'
|
|
||||||
}
|
|
||||||
|
|
||||||
if (label === 'Next »') {
|
|
||||||
return 'Next'
|
|
||||||
}
|
|
||||||
|
|
||||||
return label
|
|
||||||
}
|
|
||||||
|
|
||||||
const UsersListPagination = () => {
|
|
||||||
const pagination = useQueryResponsePagination()
|
|
||||||
const isLoading = useQueryResponseLoading()
|
|
||||||
const {updateState} = useQueryRequest()
|
|
||||||
const updatePage = (page: number | undefined | null) => {
|
|
||||||
if (!page || isLoading || pagination.page === page) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
updateState({page, items_per_page: pagination.items_per_page || 10})
|
|
||||||
}
|
|
||||||
|
|
||||||
const PAGINATION_PAGES_COUNT = 5
|
|
||||||
const sliceLinks = (pagination?: PaginationState) => {
|
|
||||||
if (!pagination?.links?.length) {
|
|
||||||
return []
|
|
||||||
}
|
|
||||||
|
|
||||||
const scopedLinks = [...pagination.links]
|
|
||||||
|
|
||||||
let pageLinks: Array<{
|
|
||||||
label: string
|
|
||||||
active: boolean
|
|
||||||
url: string | null
|
|
||||||
page: number | null
|
|
||||||
}> = []
|
|
||||||
const previousLink: {label: string; active: boolean; url: string | null; page: number | null} =
|
|
||||||
scopedLinks.shift()!
|
|
||||||
const nextLink: {label: string; active: boolean; url: string | null; page: number | null} =
|
|
||||||
scopedLinks.pop()!
|
|
||||||
|
|
||||||
const halfOfPagesCount = Math.floor(PAGINATION_PAGES_COUNT / 2)
|
|
||||||
|
|
||||||
pageLinks.push(previousLink)
|
|
||||||
|
|
||||||
if (
|
|
||||||
pagination.page <= Math.round(PAGINATION_PAGES_COUNT / 2) ||
|
|
||||||
scopedLinks.length <= PAGINATION_PAGES_COUNT
|
|
||||||
) {
|
|
||||||
pageLinks = [...pageLinks, ...scopedLinks.slice(0, PAGINATION_PAGES_COUNT)]
|
|
||||||
}
|
|
||||||
|
|
||||||
if (
|
|
||||||
pagination.page > scopedLinks.length - halfOfPagesCount &&
|
|
||||||
scopedLinks.length > PAGINATION_PAGES_COUNT
|
|
||||||
) {
|
|
||||||
pageLinks = [
|
|
||||||
...pageLinks,
|
|
||||||
...scopedLinks.slice(scopedLinks.length - PAGINATION_PAGES_COUNT, scopedLinks.length),
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|
||||||
if (
|
|
||||||
!(
|
|
||||||
pagination.page <= Math.round(PAGINATION_PAGES_COUNT / 2) ||
|
|
||||||
scopedLinks.length <= PAGINATION_PAGES_COUNT
|
|
||||||
) &&
|
|
||||||
!(pagination.page > scopedLinks.length - halfOfPagesCount)
|
|
||||||
) {
|
|
||||||
pageLinks = [
|
|
||||||
...pageLinks,
|
|
||||||
...scopedLinks.slice(
|
|
||||||
pagination.page - 1 - halfOfPagesCount,
|
|
||||||
pagination.page + halfOfPagesCount
|
|
||||||
),
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|
||||||
pageLinks.push(nextLink)
|
|
||||||
|
|
||||||
return pageLinks
|
|
||||||
}
|
|
||||||
|
|
||||||
const paginationLinks = useMemo(() => sliceLinks(pagination), [pagination])
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className='row'>
|
|
||||||
<div className='col-sm-12 col-md-5 d-flex align-items-center justify-content-center justify-content-md-start'></div>
|
|
||||||
<div className='col-sm-12 col-md-7 d-flex align-items-center justify-content-center justify-content-md-end'>
|
|
||||||
<div id='kt_table_users_paginate'>
|
|
||||||
<ul className='pagination'>
|
|
||||||
<li
|
|
||||||
className={clsx('page-item', {
|
|
||||||
disabled: isLoading || pagination.page === 1,
|
|
||||||
})}
|
|
||||||
>
|
|
||||||
<a onClick={() => updatePage(1)} style={{cursor: 'pointer'}} className='page-link'>
|
|
||||||
First
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
{paginationLinks
|
|
||||||
?.map((link) => {
|
|
||||||
return {...link, label: mappedLabel(link.label)}
|
|
||||||
})
|
|
||||||
.map((link) => (
|
|
||||||
<li
|
|
||||||
key={link.label}
|
|
||||||
className={clsx('page-item', {
|
|
||||||
active: pagination.page === link.page,
|
|
||||||
disabled: isLoading,
|
|
||||||
previous: link.label === 'Previous',
|
|
||||||
next: link.label === 'Next',
|
|
||||||
})}
|
|
||||||
>
|
|
||||||
<a
|
|
||||||
className={clsx('page-link', {
|
|
||||||
'page-text': link.label === 'Previous' || link.label === 'Next',
|
|
||||||
'me-5': link.label === 'Previous',
|
|
||||||
})}
|
|
||||||
onClick={() => updatePage(link.page)}
|
|
||||||
style={{cursor: 'pointer'}}
|
|
||||||
>
|
|
||||||
{mappedLabel(link.label)}
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
))}
|
|
||||||
<li
|
|
||||||
className={clsx('page-item', {
|
|
||||||
disabled: isLoading || pagination.page === (pagination.links?.length || 3) - 2,
|
|
||||||
})}
|
|
||||||
>
|
|
||||||
<a
|
|
||||||
onClick={() => updatePage((pagination.links?.length || 3) - 2)}
|
|
||||||
style={{cursor: 'pointer'}}
|
|
||||||
className='page-link'
|
|
||||||
>
|
|
||||||
Last
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export {UsersListPagination}
|
|
||||||
@@ -1,51 +0,0 @@
|
|||||||
/* eslint-disable react-refresh/only-export-components */
|
|
||||||
import {FC, useState, createContext, useContext, useMemo} from 'react'
|
|
||||||
import {
|
|
||||||
ID,
|
|
||||||
calculatedGroupingIsDisabled,
|
|
||||||
calculateIsAllDataSelected,
|
|
||||||
groupingOnSelect,
|
|
||||||
initialListView,
|
|
||||||
ListViewContextProps,
|
|
||||||
groupingOnSelectAll,
|
|
||||||
WithChildren,
|
|
||||||
} from '../../../../../../_digifi/helpers'
|
|
||||||
import {useQueryResponse, useQueryResponseData} from './QueryResponseProvider'
|
|
||||||
|
|
||||||
const ListViewContext = createContext<ListViewContextProps>(initialListView)
|
|
||||||
|
|
||||||
const ListViewProvider: FC<WithChildren> = ({children}) => {
|
|
||||||
const [selected, setSelected] = useState<Array<ID>>(initialListView.selected)
|
|
||||||
const [itemIdForUpdate, setItemIdForUpdate] = useState<ID>(initialListView.itemIdForUpdate)
|
|
||||||
const {isLoading} = useQueryResponse()
|
|
||||||
const data = useQueryResponseData()
|
|
||||||
const disabled = useMemo(() => calculatedGroupingIsDisabled(isLoading, data), [isLoading, data])
|
|
||||||
const isAllSelected = useMemo(() => calculateIsAllDataSelected(data, selected), [data, selected])
|
|
||||||
|
|
||||||
return (
|
|
||||||
<ListViewContext.Provider
|
|
||||||
value={{
|
|
||||||
selected,
|
|
||||||
itemIdForUpdate,
|
|
||||||
setItemIdForUpdate,
|
|
||||||
disabled,
|
|
||||||
isAllSelected,
|
|
||||||
onSelect: (id: ID) => {
|
|
||||||
groupingOnSelect(id, selected, setSelected)
|
|
||||||
},
|
|
||||||
onSelectAll: () => {
|
|
||||||
groupingOnSelectAll(isAllSelected, setSelected, data)
|
|
||||||
},
|
|
||||||
clearSelected: () => {
|
|
||||||
setSelected([])
|
|
||||||
},
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{children}
|
|
||||||
</ListViewContext.Provider>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
const useListView = () => useContext(ListViewContext)
|
|
||||||
|
|
||||||
export {ListViewProvider, useListView}
|
|
||||||
@@ -1,28 +0,0 @@
|
|||||||
/* eslint-disable react-refresh/only-export-components */
|
|
||||||
import {FC, useState, createContext, useContext} from 'react'
|
|
||||||
import {
|
|
||||||
QueryState,
|
|
||||||
QueryRequestContextProps,
|
|
||||||
initialQueryRequest,
|
|
||||||
WithChildren,
|
|
||||||
} from '../../../../../../_digifi/helpers'
|
|
||||||
|
|
||||||
const QueryRequestContext = createContext<QueryRequestContextProps>(initialQueryRequest)
|
|
||||||
|
|
||||||
const QueryRequestProvider: FC<WithChildren> = ({children}) => {
|
|
||||||
const [state, setState] = useState<QueryState>(initialQueryRequest.state)
|
|
||||||
|
|
||||||
const updateState = (updates: Partial<QueryState>) => {
|
|
||||||
const updatedState = {...state, ...updates} as QueryState
|
|
||||||
setState(updatedState)
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<QueryRequestContext.Provider value={{state, updateState}}>
|
|
||||||
{children}
|
|
||||||
</QueryRequestContext.Provider>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
const useQueryRequest = () => useContext(QueryRequestContext)
|
|
||||||
export {QueryRequestProvider, useQueryRequest}
|
|
||||||
@@ -1,59 +0,0 @@
|
|||||||
import axios, { AxiosResponse } from "axios";
|
|
||||||
import { ID, Response } from "../../../../../../_digifi/helpers";
|
|
||||||
import { User, UsersQueryResponse } from "./_models";
|
|
||||||
|
|
||||||
const API_URL = import.meta.env.VITE_APP_THEME_API_URL;
|
|
||||||
const USER_URL = `${API_URL}/user`;
|
|
||||||
// const GET_USERS_URL = `${API_URL}/users/query`;
|
|
||||||
|
|
||||||
const NEW_USER_ENDPOINT = import.meta.env.VITE_APP_USER_ENDPOINT
|
|
||||||
|
|
||||||
// const getStartedUsers = (query: string): Promise<UsersQueryResponse> => {
|
|
||||||
// return axios
|
|
||||||
// .get(`${GET_USERS_URL}?${query}`)
|
|
||||||
// .then((d: AxiosResponse<UsersQueryResponse>) => d.data);
|
|
||||||
// };
|
|
||||||
const getStartedUsers = (query: string): Promise<UsersQueryResponse> => { // FUNCTION TO GET USERS THAT HAVE STARTED LOAN APPLICATION
|
|
||||||
return axios
|
|
||||||
.get(`${NEW_USER_ENDPOINT}/loan/started`)
|
|
||||||
.then((d: AxiosResponse<UsersQueryResponse>) => d.data);
|
|
||||||
};
|
|
||||||
|
|
||||||
const getUserById = (id: ID): Promise<User | undefined> => {
|
|
||||||
return axios
|
|
||||||
.get(`${USER_URL}/${id}`)
|
|
||||||
.then((response: AxiosResponse<Response<User>>) => response.data)
|
|
||||||
.then((response: Response<User>) => response.data);
|
|
||||||
};
|
|
||||||
|
|
||||||
const createUser = (user: User): Promise<User | undefined> => {
|
|
||||||
return axios
|
|
||||||
.put(USER_URL, user)
|
|
||||||
.then((response: AxiosResponse<Response<User>>) => response.data)
|
|
||||||
.then((response: Response<User>) => response.data);
|
|
||||||
};
|
|
||||||
|
|
||||||
const updateUser = (user: User): Promise<User | undefined> => {
|
|
||||||
return axios
|
|
||||||
.post(`${USER_URL}/${user.id}`, user)
|
|
||||||
.then((response: AxiosResponse<Response<User>>) => response.data)
|
|
||||||
.then((response: Response<User>) => response.data);
|
|
||||||
};
|
|
||||||
|
|
||||||
const deleteUser = (userId: ID): Promise<void> => {
|
|
||||||
return axios.delete(`${USER_URL}/${userId}`).then(() => {});
|
|
||||||
};
|
|
||||||
|
|
||||||
const deleteSelectedUsers = (userIds: Array<ID>): Promise<void> => {
|
|
||||||
const requests = userIds.map((id) => axios.delete(`${USER_URL}/${id}`));
|
|
||||||
return axios.all(requests).then(() => {});
|
|
||||||
};
|
|
||||||
|
|
||||||
export {
|
|
||||||
getStartedUsers,
|
|
||||||
deleteUser,
|
|
||||||
deleteSelectedUsers,
|
|
||||||
getUserById,
|
|
||||||
createUser,
|
|
||||||
updateUser,
|
|
||||||
};
|
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user