Compare commits

..

24 Commits

Author SHA1 Message Date
victorAnumudu 0a31a25162 added border radius 2024-09-05 17:25:51 +01:00
victor.ebuka 50e2385b50 Merge branch 'height-adjust' of DigiFi/digifi-bko into master 2024-09-04 20:17:20 +00:00
victorAnumudu 7117539fa5 adjusted height 2024-09-04 21:15:13 +01:00
ameye 8384b5fd09 Merge branch 'admin-list-api' of DigiFi/digifi-bko into master 2024-09-04 17:14:37 +00:00
victorAnumudu 2a7c25d160 admin list api added 2024-09-04 18:02:07 +01:00
ameye 237be24476 Merge branch 'dashboard-update' of DigiFi/digifi-bko into master 2024-09-03 21:02:12 +00:00
victorAnumudu cffaa9f379 dashboard update added 2024-09-03 21:57:40 +01:00
ameye 00aa5e57fa Merge branch 'customer-list' of DigiFi/digifi-bko into master 2024-07-18 00:34:30 +00:00
victorAnumudu 1d14205656 customer API added 2024-07-17 20:01:27 +01:00
ameye d98624574f Merge branch 'loan-details-display' of DigiFi/digifi-bko into master 2024-07-15 17:16:37 +00:00
victorAnumudu fa46cae1cc made all data to show on details column 2024-07-15 18:13:28 +01:00
ameye 704681c32d Merge branch 'loan-details-update' of DigiFi/digifi-bko into master 2024-07-15 17:00:34 +00:00
victorAnumudu 251fe95a6b updated loan details page 2024-07-15 17:57:01 +01:00
victorAnumudu ca7db4b0aa updated loan details page 2024-07-15 17:38:11 +01:00
ameye 5fe90c3ead Merge branch 'loan-process-page' of DigiFi/digifi-bko into master 2024-07-15 16:14:17 +00:00
victorAnumudu 827d0a1c30 added loan details API 2024-07-15 17:11:14 +01:00
victorAnumudu 0fc549f1b5 added loan details API 2024-07-15 17:09:06 +01:00
ameye ee1fe4a5b6 Merge branch 'process-page' of DigiFi/digifi-bko into master 2024-07-12 09:51:06 +00:00
victorAnumudu 4ac97537cd process page added 2024-07-12 10:22:05 +01:00
ameye fc214b4bad Merge branch 'side-menu-adjust' of DigiFi/digifi-bko into master 2024-07-11 16:52:59 +00:00
victorAnumudu 450ae649c3 side bar update 2024-07-11 17:31:34 +01:00
ameye 2eb12129bb Merge branch 'edit-signatory-update' of DigiFi/digifi-bko into master 2024-06-20 11:11:44 +00:00
victorAnumudu a26b9f5d11 edit signatory submit button renamed 2024-06-20 12:09:27 +01:00
ameye 35dcc853b8 Merge branch 'edit-signatory-modal' of DigiFi/digifi-bko into master 2024-06-19 23:16:31 +00:00
91 changed files with 3873 additions and 160 deletions
Binary file not shown.

After

Width:  |  Height:  |  Size: 602 KiB

@@ -2,9 +2,12 @@ const QUERIES = {
USERS_LIST: 'users-list',
STARTED_LIST: 'started-list',
READY_LIST: 'ready-list',
VERIFIED_LIST: 'verified-list',
PENDING_LIST: 'pending-list',
APPROVED_LIST: 'approved-list',
REJECTED_LIST: 'rejected-list',
CUSTOMERS_LIST: 'customers-list',
ADMIN_USERS_LIST: 'admin-users-list',
EMPLOYERS_LIST: 'employers-list',
SIGNATORY_LIST: 'signatory-list',
}
+26
View File
@@ -4,3 +4,29 @@ export const formatNumbers = (number: string | undefined): string | null => {
}
return number.replace(/\B(?=(\d{3})+(?!\d))/g, ',');
};
// FUNCTION TO RETURN AMOUNT TO TWO DECIMAL PLACES
export const FormatAmount = (
amount = "00",
) => {
// Convert the number to a string
let numStr = String(amount);
// Split the string into integer and decimal parts
let parts = numStr.split(".");
let integerPart = parts[0] || "";
let decimalPart = parts[1] || "";
// Add thousands separators to the integer part
let formattedInteger = integerPart.replace(/\B(?=(\d{3})+(?!\d))/g, ',');
// Truncate or pad the decimal part to two decimal points
let formattedDecimal = decimalPart.slice(0, 2).padEnd(2, "0");
// Combine the formatted integer and decimal parts
let formattedNumber = '₦ ' + formattedInteger + '.' + formattedDecimal;
// return formattedNumber;
return formattedNumber;
};
@@ -89,11 +89,7 @@ export default function RecentBVNList({
<div className="w-full h-100 d-flex gap-4 justify-content-center align-items-end">
<button
onClick={handlePrev}
className={`text-sm md:text-lg d-flex justify-content-center align-items-center border-1 transition-all duration-300 ${
currentPage == 0
? "text-slate-400 border-slate-400 dark:text-slate-400 dark:border-slate-400 pe-none"
: "text-slate-600 border-slate-600 dark:text-white dark:border-white"
}`}
className={`btn btn-primary `}
disabled={currentPage == 0}
// style={{width:'30px', height:'30px'}}
>
@@ -123,11 +119,7 @@ export default function RecentBVNList({
<button
onClick={handleNext}
className={`text-sm md:text-lg d-flex justify-content-center align-items-center border-1 transition-all duration-300 ${
currentPage + numberOfSelection >= data.length
? "text-slate-400 border-slate-400 dark:text-slate-400 dark:border-slate-400 pe-none"
: "text-slate-600 border-slate-600 dark:text-white dark:border-white"
}`}
className={`btn btn-primary`}
disabled={currentPage + numberOfSelection >= data.length}
// style={{width:'30px', height:'30px'}}
>
@@ -10,6 +10,7 @@ type Props = {
icon?: string
fontIcon?: string
hasBullet?: boolean
menuIsOpen?: boolean
}
const SidebarMenuItemWithSub: React.FC<Props & WithChildren> = ({
@@ -19,9 +20,10 @@ const SidebarMenuItemWithSub: React.FC<Props & WithChildren> = ({
icon,
fontIcon,
hasBullet,
menuIsOpen=false
}) => {
const {pathname} = useLocation()
const isActive = checkIsActive(pathname, to)
const isActive = checkIsActive(pathname, to) || menuIsOpen
const {config} = useLayout()
const {app} = config
@@ -15,18 +15,19 @@ const SidebarMenuMain = () => {
fontIcon='bi-app-indicator'
/>
{/*<SidebarMenuItem to='/builder' icon='switch' title='Layout Builder' fontIcon='bi-layers' />*/}
<div className='menu-item'>
{/* <div className='menu-item'>
<div className='menu-content pt-8 pb-2'>
<span className='menu-section text-muted text-uppercase fs-8 ls-1'>Loan</span>
</div>
</div>
</div> */}
<SidebarMenuItemWithSub
to='/loan/pages'
title='Process'
fontIcon='bi-archive'
icon='element-plus'
menuIsOpen={true}
>
<SidebarMenuItemWithSub to='/loan/pages/process' title='Loan' hasBullet={true}>
<SidebarMenuItemWithSub to='/loan/pages/process' title='Loan' hasBullet={true} menuIsOpen={true}>
<SidebarMenuItem to='/loan/pages/process/started' title='Started' hasBullet={true} />
<SidebarMenuItem to='/loan/pages/process/pending' title='Pending' hasBullet={true} />
<SidebarMenuItem
@@ -34,6 +35,7 @@ const SidebarMenuMain = () => {
title='Ready'
hasBullet={true}
/>
<SidebarMenuItem to='/loan/pages/process/verified' title='Verified' hasBullet={true} />
<SidebarMenuItem
to='/loan/pages/process/approved'
title='Approved'
@@ -103,9 +105,9 @@ const SidebarMenuMain = () => {
</SidebarMenuItemWithSub> */}
<SidebarMenuItem
to='/tools/user-management/users'
to='/tools/user-management/customers'
icon='abstract-28'
title='User management'
title='Customers'
fontIcon='bi-layers'
/>
@@ -139,6 +141,18 @@ const SidebarMenuMain = () => {
{/* <span className='menu-title'>Changelog {import.meta.env.VITE_APP_VERSION}</span>*/}
{/* </a>*/}
{/*</div>*/}
<div className='menu-item'>
<div className='menu-content pt-8 pb-2'>
<span className='menu-section text-muted text-uppercase fs-8 ls-1'>Admin</span>
</div>
</div>
<SidebarMenuItem
to='/tools/user-management/users'
icon='abstract-28'
title='Users'
fontIcon='bi-layers'
/>
</>
)
}
@@ -3,12 +3,15 @@ import {FC, useEffect, useRef} from 'react'
import {KTIcon} from '../../../../helpers'
import {getCSSVariableValue} from '../../../../assets/ts/_utils'
import {useThemeMode} from '../../../layout/theme-mode/ThemeModeProvider'
import { DashDataProps } from '../../../../../app/pages/dashboard/model'
import { FormatAmount } from '../../../../helpers/formatNumbers'
type Props = {
className: string
chartSize?: number
chartLine?: number
chartRotate?: number
dashData?: DashDataProps
}
const CardsWidget17: FC<Props> = ({
@@ -16,6 +19,7 @@ const CardsWidget17: FC<Props> = ({
chartSize = 70,
chartLine = 11,
chartRotate = 145,
dashData
}) => {
const chartRef = useRef<HTMLDivElement | null>(null)
const {mode} = useThemeMode()
@@ -39,15 +43,15 @@ const CardsWidget17: FC<Props> = ({
<div className='card-header pt-5'>
<div className='card-title d-flex flex-column'>
<div className='d-flex align-items-center'>
<span className='fs-4 fw-semibold text-gray-500 me-1 align-self-start'>$</span>
{/* <span className='fs-4 fw-semibold text-gray-500 me-1 align-self-start'>$</span> */}
<span className='fs-2hx fw-bold text-gray-900 me-2 lh-1 ls-n2'>69,700</span>
<span className='fs-2hx fw-bold text-gray-900 me-2 lh-1 ls-n2'>{FormatAmount('69,700')}</span>
<span className='badge badge-light-success fs-base'>
<KTIcon iconName='arrow-up' className='fs-5 text-success ms-n1' /> 2.2%
</span>
</div>
<span className='text-gray-500 pt-1 fw-semibold fs-6'>Projects Earnings in April</span>
<span className='text-gray-500 pt-1 fw-semibold fs-6'>Application in {dashData?.loading ? 'Loading...': dashData?.data?.dash_data?.curr_month}</span>
</div>
</div>
@@ -65,21 +69,21 @@ const CardsWidget17: FC<Props> = ({
<div className='d-flex flex-column content-justify-center flex-row-fluid'>
<div className='d-flex fw-semibold align-items-center'>
<div className='bullet w-8px h-3px rounded-2 bg-success me-3'></div>
<div className='text-gray-500 flex-grow-1 me-4'>Leaf CRM</div>
<div className='fw-bolder text-gray-700 text-xxl-end'>$7,660</div>
<div className='text-gray-500 flex-grow-1 me-4'>Ready</div>
<div className='fw-bolder text-gray-700 text-xxl-end'>{dashData?.loading ? 'Loading...': FormatAmount(dashData?.data?.dash_data?.ready_loans)}</div>
</div>
<div className='d-flex fw-semibold align-items-center my-3'>
<div className='bullet w-8px h-3px rounded-2 bg-primary me-3'></div>
<div className='text-gray-500 flex-grow-1 me-4'>Mivy App</div>
<div className='fw-bolder text-gray-700 text-xxl-end'>$2,820</div>
<div className='text-gray-500 flex-grow-1 me-4'>Verified</div>
<div className='fw-bolder text-gray-700 text-xxl-end'>{dashData?.loading ? 'Loading...': FormatAmount(dashData?.data?.dash_data?.verified_loans)}</div>
</div>
<div className='d-flex fw-semibold align-items-center'>
<div
className='bullet w-8px h-3px rounded-2 me-3'
style={{backgroundColor: '#E4E6EF'}}
></div>
<div className='text-gray-500 flex-grow-1 me-4'>Others</div>
<div className=' fw-bolder text-gray-700 text-xxl-end'>$45,257</div>
<div className='text-gray-500 flex-grow-1 me-4'>Approved</div>
<div className=' fw-bolder text-gray-700 text-xxl-end'>{dashData?.loading ? 'Loading...': FormatAmount(dashData?.data?.dash_data?.approved_loans)}</div>
</div>
</div>
</div>
@@ -1,11 +1,14 @@
import { DashDataProps } from "../../../../../app/pages/dashboard/model"
type Props = {
className: string
description: string
color: string
img: string
dashData?: DashDataProps
}
const CardsWidget20 = ({className, description, color, img}: Props) => (
const CardsWidget20 = ({className, description, color, img, dashData}: Props) => (
<div
className={`card card-flush bgi-no-repeat bgi-size-contain bgi-position-x-end ${className}`}
style={{
@@ -15,7 +18,7 @@ const CardsWidget20 = ({className, description, color, img}: Props) => (
>
<div className='card-header pt-5'>
<div className='card-title d-flex flex-column'>
<span className='fs-2hx fw-bold text-white me-2 lh-1 ls-n2'>69</span>
<span className='fs-2hx fw-bold text-white me-2 lh-1 ls-n2'> {dashData?.loading ? 'Loading...': dashData?.data?.dash_data?.active_loans}</span>
<span className='text-white opacity-75 pt-1 fw-semibold fs-6'>{description}</span>
</div>
@@ -1,6 +1,7 @@
import clsx from 'clsx'
import {toAbsoluteUrl} from '../../../../helpers'
import { DashDataProps } from '../../../../../app/pages/dashboard/model'
type Props = {
className: string
@@ -9,6 +10,7 @@ type Props = {
stats: number
labelColor: string
textColor: string
dashData?: DashDataProps
}
const items: Array<{
@@ -25,18 +27,18 @@ const items: Array<{
{name: 'Barry Walter', src: toAbsoluteUrl('media/avatars/300-12.jpg')},
]
const CardsWidget7 = ({className, description, stats, labelColor, textColor}: Props) => (
const CardsWidget7 = ({className, description, stats, labelColor, textColor, dashData}: Props) => (
<div className={`card card-flush ${className}`}>
<div className='card-header pt-5'>
<div className='card-title d-flex flex-column'>
<div className='card-title d-flex flex-column'>
<span className='fs-2hx fw-bold text-gray-900 me-2 lh-1 ls-n2'>{stats}</span>
<span className='fs-2hx fw-bold text-gray-900 me-2 lh-1 ls-n2'>{dashData?.loading ? 'Loading...': dashData?.data?.dash_data?.applications}</span>
<span className='text-gray-500 pt-1 fw-semibold fs-6'>{description}</span>
</div>
</div>
</div>
<div className='card-body d-flex flex-column justify-content-end pe-0'>
<span className='fs-6 fw-bolder text-gray-800 d-block mb-2'>Todays Heroes</span>
<span className='fs-6 fw-bolder text-gray-800 d-block mb-2'>Recent Applications</span>
<div className='symbol-group symbol-hover flex-nowrap'>
{items.map((item, index) => (
<div
@@ -18,26 +18,27 @@ const EngageWidget10 = ({className}: Props) => (
<div className='mb-10'>
<div className='fs-2hx fw-bold text-gray-800 text-center mb-13'>
<span className='me-2'>
Try our all new Enviroment with
Need more help to manage the platform
<br />
<span className='position-relative d-inline-block text-danger'>
<Link
to='/crafted/pages/profile/overview'
to='/help'
// target='_blank'
className='text-danger
opacity-75-hover'
>
Pro Plan
Use our help
</Link>
<span className='position-absolute opacity-15 bottom-0 start-0 border-4 border-danger border-bottom w-100'></span>
</span>
</span>
for Free
{/* for Free */}
</div>
<div className='text-center'>
{/* <div className='text-center'>
<a href='#'>Upgrade Now</a>
</div>
</div> */}
</div>
<img
className='mx-auto h-150px h-lg-200px theme-light-show'
@@ -1,30 +1,31 @@
import {Fragment} from 'react'
import {KTIcon} from '../../../../helpers'
import { Link } from 'react-router-dom'
type Props = {
className: string
}
const rows: Array<{description: string}> = [
{description: 'Avg. Client Rating'},
{description: 'Instagram Followers'},
{description: 'Google Ads CPC'},
const rows: Array<{description: string, link: string}> = [
{description: 'Verified Loans', link: '/loan/pages/process/verified'},
{description: 'Approved Loans', link: '/loan/pages/process/approved'},
{description: 'Rejected Loans', link: '/loan/pages/process/rejected'},
]
const ListsWidget26 = ({className}: Props) => (
<div className={`card card-flush ${className}`}>
<div className='card-header pt-5'>
<h3 className='card-title text-gray-800 fw-bold'>External Links</h3>
<h3 className='card-title text-gray-800 fw-bold'>Other Links</h3>
<div className='card-toolbar'></div>
</div>
<div className='card-body pt-5'>
{rows.map((row, index) => (
<Fragment key={`lw26-rows-${index}`}>
<div className='d-flex flex-stack'>
<a href='#' className='text-primary fw-semibold fs-6 me-2'>
<Link to={row.link} className='text-primary fw-semibold fs-6 me-2'>
{row.description}
</a>
</Link>
<button
type='button'
className='btn btn-icon btn-sm h-auto btn-color-gray-500 btn-active-color-primary justify-content-end'
@@ -108,37 +108,38 @@ const TablesWidget10: FC<Props> = ({className, dashData}) => {
<td className='text-end'>
<div className='d-flex flex-column w-100 me-2'>
<div className='d-flex flex-stack mb-2'>
<span className='text-muted me-2 fs-7 fw-semibold'>50%</span>
{/* <span className='text-muted me-2 fs-7 fw-semibold'>50%</span> */}
<span className='text-muted me-2 fs-7 fw-semibold'>{(Number(item?.status)/5)*100}%</span>
</div>
<div className='progress h-6px w-100'>
<div
className='progress-bar bg-primary'
role='progressbar'
style={{width: '50%'}}
style={{width: `${(Number(item?.status)/5)*100}%`}}
></div>
</div>
</div>
</td>
<td>
<div className='d-flex justify-content-end flex-shrink-0'>
<a
{/* <a
href='#'
className='btn btn-icon btn-bg-light btn-active-color-primary btn-sm me-1'
>
<KTIcon iconName='switch' className='fs-3' />
</a>
</a> */}
<a
href='#'
className='btn btn-icon btn-bg-light btn-active-color-primary btn-sm me-1'
>
<KTIcon iconName='pencil' className='fs-3' />
</a>
<a
{/* <a
href='#'
className='btn btn-icon btn-bg-light btn-active-color-primary btn-sm'
>
<KTIcon iconName='trash' className='fs-3' />
</a>
</a> */}
</div>
</td>
</tr>
+6 -2
View File
@@ -1,5 +1,5 @@
import {Suspense} from 'react'
import {Outlet} from 'react-router-dom'
import {Suspense, useEffect} from 'react'
import {Outlet, useLocation} from 'react-router-dom'
import {I18nProvider} from '../_digifi/i18n/i18nProvider'
import {LayoutProvider, LayoutSplashScreen} from '../_digifi/layout/core'
import {MasterInit} from '../_digifi/layout/MasterInit'
@@ -8,6 +8,10 @@ import {ThemeModeProvider} from '../_digifi/partials'
import { CustomModalProvider } from '../context/CustomModal'
const App = () => {
const {pathname}= useLocation()
useEffect(()=>{
window.scrollTo(0,0)
},[pathname])
return (
<Suspense fallback={<LayoutSplashScreen />}>
<I18nProvider>
@@ -1,6 +1,7 @@
import {Route, Routes, Outlet, Navigate} from 'react-router-dom'
import {Route, Routes, Outlet, Navigate } from 'react-router-dom'
import {PageLink, PageTitle} from '../../../../_digifi/layout/core'
import {UsersListWrapper} from './users-list/UsersList'
import { CustomersListWrapper } from './customers-list/UsersList'
const usersBreadcrumbs: Array<PageLink> = [
{
@@ -18,6 +19,7 @@ const usersBreadcrumbs: Array<PageLink> = [
]
const UsersPage = () => {
return (
<Routes>
<Route element={<Outlet />}>
@@ -30,6 +32,15 @@ const UsersPage = () => {
</>
}
/>
<Route
path='customers'
element={
<>
<PageTitle breadcrumbs={usersBreadcrumbs}>Customers list</PageTitle>
<CustomersListWrapper />
</>
}
/>
</Route>
<Route index element={<Navigate to='/tools/user-management/users' />} />
</Routes>
@@ -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 CustomersListWrapper = () => (
<QueryRequestProvider>
<QueryResponseProvider>
<ListViewProvider>
<ToolbarWrapper />
<Content>
<UsersList />
</Content>
</ListViewProvider>
</QueryResponseProvider>
</QueryRequestProvider>
)
export {CustomersListWrapper}
@@ -0,0 +1,32 @@
import {KTIcon} from '../../../../../../../_digifi/helpers'
import {useListView} from '../../core/ListViewProvider'
import {UsersListFilter} from './UsersListFilter'
const UsersListToolbar = () => {
const {setItemIdForUpdate} = useListView()
const openAddUserModal = () => {
setItemIdForUpdate(null)
}
return (
<div className='d-flex justify-content-end' data-kt-user-table-toolbar='base'>
<UsersListFilter />
{/* begin::Export */}
{/* <button type='button' className='btn btn-light-primary me-3'>
<KTIcon iconName='exit-up' className='fs-2' />
Export
</button> */}
{/* end::Export */}
{/* begin::Add user */}
{/* <button type='button' className='btn btn-primary' onClick={openAddUserModal}>
<KTIcon iconName='plus' className='fs-2' />
Add User
</button> */}
{/* end::Add user */}
</div>
)
}
export {UsersListToolbar}
@@ -0,0 +1,133 @@
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,22 @@
import {useListView} from '../../core/ListViewProvider'
import {UsersListToolbar} from './UserListToolbar'
import {UsersListGrouping} from './UsersListGrouping'
import {UsersListSearchComponent} from './UsersListSearchComponent'
const UsersListHeader = () => {
const {selected} = useListView()
return (
<div className='card-header border-0 pt-6'>
<UsersListSearchComponent />
{/* begin::Card toolbar */}
<div className='card-toolbar'>
{/* begin::Group actions */}
{selected.length > 0 ? <UsersListGrouping /> : <UsersListToolbar />}
{/* end::Group actions */}
</div>
{/* end::Card toolbar */}
</div>
)
}
export {UsersListHeader}
@@ -0,0 +1,45 @@
/* 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,18 @@
const UsersListLoading = () => {
const styles = {
borderRadius: '0.475rem',
boxShadow: '0 0 50px 0 rgb(82 63 105 / 15%)',
backgroundColor: '#fff',
color: '#7e8299',
fontWeight: '500',
margin: '0',
width: 'auto',
padding: '1rem 2rem',
top: 'calc(50% - 2rem)',
left: 'calc(50% - 4rem)',
}
return <div style={{...styles, position: 'absolute', textAlign: 'center'}}>Processing...</div>
}
export {UsersListLoading}
@@ -0,0 +1,156 @@
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 === '&laquo; Previous') {
return 'Previous'
}
if (label === 'Next &raquo;') {
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,51 @@
/* 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,28 @@
/* 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,85 @@
/* 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 {getCustomerList} from './_requests'
import {User} from './_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.CUSTOMERS_LIST}-${query}`,
() => {
return getCustomerList(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,43 @@
import {ID, Response} from '../../../../../../_digifi/helpers'
export type User = {
id?: ID
name?: string
avatar?: string
// email?: string
position?: string
role?: string
last_login?: string
two_steps?: boolean
joined_day?: string
online?: boolean
initials?: {
label: string
state: string
}
firstname?: string,
lastname?: string
uid?: string
loan_amount?: string
payment_month?: string
sales_agent?: string
gender?: string | null
marital_status?: string
email?: string
address?: string
state?: string
country?: string
status?: string
added?: string
updated?: string
bvn?: string
}
export type UsersQueryResponse = Response<Array<User>>
export const initialUser: User = {
avatar: 'avatars/300-6.jpg',
position: 'Art Director',
role: 'Administrator',
name: '',
email: '',
}
@@ -0,0 +1,59 @@
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 getCustomerList = (query: string): Promise<UsersQueryResponse> => { // FUNCTION TO GET USERS THAT HAVE STARTED LOAN APPLICATION
return axios
.get(`${NEW_USER_ENDPOINT}/customers`)
.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 {
getCustomerList,
deleteUser,
deleteSelectedUsers,
getUserById,
createUser,
updateUser,
};
@@ -0,0 +1,62 @@
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,12 @@
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}
@@ -0,0 +1,15 @@
import {FC} from 'react'
import {ColumnInstance} from 'react-table'
import {User} from '../../core/_models'
type Props = {
column: ColumnInstance<User>
}
const CustomHeaderColumn: FC<Props> = ({column}) => (
<>
{column.Header && typeof column.Header === 'string' ? <th {...column.getHeaderProps()}>{column.render('Header')}</th> : column.render('Header')}
</>
)
export {CustomHeaderColumn}
@@ -0,0 +1,25 @@
import clsx from 'clsx'
import {FC} from 'react'
import {Row} from 'react-table'
import {User} from '../../core/_models'
type Props = {
row: Row<User>
}
const CustomRow: FC<Props> = ({row}) => (
<tr {...row.getRowProps()}>
{row.cells.map((cell) => {
return (
<td
{...cell.getCellProps()}
className={clsx({'text-end min-w-100px': cell.column.id === 'actions'})}
>
{cell.render('Cell')}
</td>
)
})}
</tr>
)
export {CustomRow}
@@ -0,0 +1,11 @@
import {FC} from 'react'
type Props = {
status?: string
}
const Status: FC<Props> = ({status}) => (
<> {status && <div className='badge badge-light-success fw-bolder'>{status}</div>}</>
)
export {Status}
@@ -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-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,42 @@
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,11 @@
import {FC} from 'react'
type Props = {
payment_month?: string
}
const PaymentMonthCell: FC<Props> = ({payment_month}) => (
<div className='badge badge-light fw-bolder'>{payment_month}</div>
)
export {PaymentMonthCell}
@@ -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}
@@ -0,0 +1,28 @@
import {FC, PropsWithChildren} from 'react'
import {HeaderProps} from 'react-table'
import {useListView} from '../../core/ListViewProvider'
import {User} from '../../core/_models'
type Props = {
tableProps: PropsWithChildren<HeaderProps<User>>
}
const UserSelectionHeader: FC<Props> = ({tableProps}) => {
const {isAllSelected, onSelectAll} = useListView()
return (
<th {...tableProps.column.getHeaderProps()} className='w-10px pe-2'>
<div className='form-check form-check-sm form-check-custom form-check-solid me-3'>
<input
className='form-check-input'
type='checkbox'
data-kt-check={isAllSelected}
data-kt-check-target='#kt_table_users .form-check-input'
checked={isAllSelected}
onChange={onSelectAll}
/>
</div>
</th>
)
}
export {UserSelectionHeader}
@@ -0,0 +1,57 @@
import {Column} from 'react-table'
import {UserInfoCell} from './UserInfoCell'
import { PaymentMonthCell } from './UserLastLoginCell'
import {Status} from './Status'
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 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='BVN' className='min-w-125px' />,
accessor: 'bvn',
},
// {
// 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='Status' className='min-w-125px' />
),
id: 'status',
Cell: ({...props}) => <Status status={props.data[props.row.index].status} />,
},
{
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,44 @@
import {useEffect} from 'react'
import {UserEditModalHeader} from './UserEditModalHeader'
import {UserEditModalFormWrapper} from './UserEditModalFormWrapper'
const UserEditModal = () => {
useEffect(() => {
document.body.classList.add('modal-open')
return () => {
document.body.classList.remove('modal-open')
}
}, [])
return (
<>
<div
className='modal fade show d-block'
id='kt_modal_add_user'
role='dialog'
tabIndex={-1}
aria-modal='true'
>
{/* begin::Modal dialog */}
<div className='modal-dialog modal-dialog-centered mw-650px'>
{/* begin::Modal content */}
<div className='modal-content'>
<UserEditModalHeader />
{/* begin::Modal body */}
<div className='modal-body scroll-y mx-5 mx-xl-15 my-7'>
<UserEditModalFormWrapper />
</div>
{/* end::Modal body */}
</div>
{/* end::Modal content */}
</div>
{/* end::Modal dialog */}
</div>
{/* begin::Modal Backdrop */}
<div className='modal-backdrop fade show'></div>
{/* end::Modal Backdrop */}
</>
)
}
export {UserEditModal}
@@ -0,0 +1,407 @@
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,40 @@
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}
@@ -11,7 +11,7 @@ import {
stringifyRequestQuery,
WithChildren,
} from '../../../../../../_digifi/helpers'
import {getStartedUsers} from './_requests'
import {getAdminUserList} from './_requests'
import {User} from './_models'
import {useQueryRequest} from './QueryRequestProvider'
@@ -32,9 +32,9 @@ const QueryResponseProvider: FC<WithChildren> = ({children}) => {
refetch,
data: response,
} = useQuery(
`${QUERIES.USERS_LIST}-${query}`,
`${QUERIES.ADMIN_USERS_LIST}-${query}`,
() => {
return getStartedUsers(query)
return getAdminUserList(query)
},
{cacheTime: 0, keepPreviousData: true, refetchOnWindowFocus: false}
)
@@ -29,6 +29,8 @@ export type User = {
status?: string
added?: string
updated?: string
bvn?: string
username?: string
}
export type UsersQueryResponse = Response<Array<User>>
@@ -13,9 +13,15 @@ const NEW_USER_ENDPOINT = import.meta.env.VITE_APP_USER_ENDPOINT
// .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
const getCustomerList = (query: string): Promise<UsersQueryResponse> => { // FUNCTION TO GET USERS THAT HAVE STARTED LOAN APPLICATION
return axios
.get(`${NEW_USER_ENDPOINT}/loan/started`)
.get(`${NEW_USER_ENDPOINT}/customers`)
.then((d: AxiosResponse<UsersQueryResponse>) => d.data);
};
const getAdminUserList = (query: string): Promise<UsersQueryResponse> => { // FUNCTION TO GET USERS THAT HAVE STARTED LOAN APPLICATION
return axios
.get(`${NEW_USER_ENDPOINT}/users`)
.then((d: AxiosResponse<UsersQueryResponse>) => d.data);
};
@@ -50,10 +56,11 @@ const deleteSelectedUsers = (userIds: Array<ID>): Promise<void> => {
};
export {
getStartedUsers,
getCustomerList,
deleteUser,
deleteSelectedUsers,
getUserById,
getAdminUserList,
createUser,
updateUser,
};
@@ -0,0 +1,11 @@
import {FC} from 'react'
type Props = {
status?: string
}
const Status: FC<Props> = ({status}) => (
<> {status && <div className='badge badge-light-success fw-bolder'>{status}</div>}</>
)
export {Status}
@@ -1,7 +1,7 @@
import {Column} from 'react-table'
import {UserInfoCell} from './UserInfoCell'
import { PaymentMonthCell } from './UserLastLoginCell'
import {AgentCell} from './AgentCell'
import {Status} from './Status'
import {UserActionsCell} from './UserActionsCell'
import {UserSelectionCell} from './UserSelectionCell'
import {UserCustomHeader} from './UserCustomHeader'
@@ -20,23 +20,12 @@ const usersColumns: ReadonlyArray<Column<User>> = [
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',
},
{
Header: (props) => (
<UserCustomHeader tableProps={props} title='Payment Terms' className='min-w-125px' />
<UserCustomHeader tableProps={props} title='Status' 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} />,
id: 'status',
Cell: ({...props}) => <Status status={props.data[props.row.index].status} />,
},
{
Header: (props) => (
+23 -54
View File
@@ -16,70 +16,39 @@ const AuthLayout = () => {
}, []);
return (
<div className="d-flex flex-column flex-lg-row flex-column-fluid h-100">
<div className="d-flex flex-column flex-lg-row flex-column-fluid"
style={{backgroundImage: 'url(../../../../public/media/auth/digifi_bko_home.jpg)', backgroundRepeat: 'none', backgroundSize: 'cover'}}
>
{/* begin::Body */}
<div className="d-flex flex-column flex-lg-row-fluid w-lg-50 p-10 order-2 order-lg-1">
<div className="h-100 flex flex-column align-items-center w-lg-50 p-10">
{/* begin::Form */}
<div className="d-flex flex-center flex-column flex-lg-row-fluid">
<div
// className="d-flex flex-center flex-column flex-lg-row-fluid"
className="d-flex h-100 align-items-center"
>
{/* begin::Wrapper */}
<div className="w-lg-500px p-10">
<div className="w-lg-500px p-10 bg-white shadow-sm rounded">
{/* begin::Title */}
<h1 className="text-black fs-2qx fw-bolder text-center mb-7">
{/* begin::Logo */}
<Link to="/" className="mb-12">
<img
alt="Logo"
src={toAbsoluteUrl("media/logos/custom-1.png")}
className="h-75px"
/>
</Link>
{/* end::Logo */}
BackOffice
</h1>
{/* end::Title */}
<Outlet />
</div>
{/* end::Wrapper */}
</div>
{/* end::Form */}
{/* begin::Footer */}
<div className="d-flex flex-center flex-wrap px-5">
{/* begin::Links */}
<div className="d-flex fw-semibold text-primary fs-base">
<a href="#" className="px-5" target="_blank">
Terms
</a>
<a href="#" className="px-5" target="_blank">
Contact Us
</a>
</div>
{/* end::Links */}
</div>
{/* end::Footer */}
</div>
{/* end::Body */}
{/* begin::Aside */}
<div
className="d-flex flex-lg-row-fluid w-lg-50 bgi-size-cover bgi-position-center order-1 order-lg-2"
>
{/* begin::Content */}
<div className="d-flex flex-column flex-center py-15 px-5 px-md-15 w-100">
{/* begin::Logo */}
<Link to="/" className="mb-12">
<img
alt="Logo"
src={toAbsoluteUrl("media/logos/custom-1.png")}
className="h-75px"
/>
</Link>
{/* end::Logo */}
{/* begin::Image */}
<img
className="mx-auto w-275px w-md-50 w-xl-500px mb-10 mb-lg-20"
src={toAbsoluteUrl("media/misc/agents-auth-screens.png")}
alt=""
/>
{/* end::Image */}
{/* begin::Title */}
<h1 className="text-black fs-2qx fw-bolder text-center mb-7">
digiFi BackOffice
</h1>
{/* end::Title */}
</div>
{/* end::Content */}
</div>
{/* end::Aside */}
</div>
);
};
@@ -317,7 +317,7 @@ const ModalForm: FC<Props> = ({ user, isUserLoading }) => {
!formik.touched
}
>
<span className="indicator-label">Add</span>
<span className="indicator-label">Update</span>
{(formik.isSubmitting || isUserLoading) && (
<span className="indicator-progress">
Please wait...{" "}
@@ -14,7 +14,7 @@ const ModalHeader = () => {
return (
<div className='modal-header'>
{/* begin::Modal title */}
<h2 className='fw-bolder'>Add Signatory - {showCustomModal?.data?.employer_name}</h2>
<h2 className='fw-bolder'>Edit Signatory - {showCustomModal?.data?.employer_name}</h2>
{/* end::Modal title */}
{/* begin::Close */}
+8
View File
@@ -0,0 +1,8 @@
export default function HelpPage() {
return (
<div className="w-100 h-100">
<h1 className="fs-2hx fw-bold text-gray-800">Help</h1>
</div>
)
}
+10
View File
@@ -11,6 +11,7 @@ import { UserPendingList } from './components/UserPendingList'
import { UserReadyList } from './components/UserReadyList'
import { UserApprovedList } from './components/UserApprovedList'
import { UserRejectedList } from './components/UserRejectedList'
import { UserVerifiedList } from './components/UserVerifiedList'
const processBreadCrumbs: Array<PageLink> = [
{
@@ -64,6 +65,15 @@ const ProcessPage = () => (
</>
}
/>
<Route
path='verified'
element={
<>
<PageTitle breadcrumbs={processBreadCrumbs}>Verified</PageTitle>
<UserVerifiedList />
</>
}
/>
<Route
path='approved'
element={
@@ -0,0 +1,132 @@
import React, { useEffect, useState } from 'react'
import {useLocation, useNavigate} from 'react-router-dom'
import { Content } from '../../../../_digifi/layout/components/content'
import { ToolbarWrapper } from '../../../../_digifi/layout/components/toolbar'
import { CustomLoading, UsersListLoading } from '../user-started/components/loading/UsersListLoading'
import { getVerifiedLoanDetailsByUID } from '../core/_requests'
export default function ApproveRejectPage() {
const {state:{selectedUser}} = useLocation()
const navigate = useNavigate()
const [requestStatus, setRequestStatus] = useState<any>({loading:false, status:false, data:null})
const [loanDetails, setLoanDetails] = useState<any>({loading:true, data:null})
const handleSubmit = ():any => {
setRequestStatus({loading:true, status:false, data:null})
setTimeout(()=>{
setRequestStatus({loading:false, status:false, data:null})
},2000)
}
useEffect(()=>{
if(!selectedUser){
return navigate('/', {replace:true})
}
getVerifiedLoanDetailsByUID(selectedUser?.uid).then(res => {
setLoanDetails({loading:false, data:res?.data})
}).catch(err => {
console.log(err)
setLoanDetails({loading:false, data:null})
})
},[])
return (
<>
{/* <ToolbarWrapper /> */}
<Content>
{loanDetails.loading ?
<div className='position-relative w-100 vh-100'>
<CustomLoading />
</div>
:
<>
<div className='w-100'>
<h3 className='py-3 py-xl-5 card-title text-gray-800 fw-bold'>Processing: {selectedUser?.uid}</h3>
</div>
{/* begin::Row */}
<div className="row g-5 g-xl-10 mb-5 mb-xl-10">
{/* begin::Col */}
<div className="col-xl-6 mb-md-5 mb-xl-10">
<div className="card card-flash flex flex-col justify-content-between p-4 h-md-50 mb-5 mb-xl-10 bg-secondary">
<h3 className='card-title text-gray-800 fw-bold'>Process Loan</h3>
<div className='w-100 d-flex justify-content-between'>
<button
className='btn btn-light btn-active-light-secondary text-success btn-lg'
onClick={()=>navigate('/loan/pages/process/verified', {replace:true})}
>
Return
</button>
<button
className='btn btn-light btn-active-light-secondary text-danger btn-lg'
onClick={handleSubmit}
>
Reject
</button>
<button
className='btn btn-light btn-active-light-secondary text-primary btn-lg'
onClick={handleSubmit}
>
Approve
</button>
</div>
</div>
<div className="card card-flash flex flex-col p-4 h-md-50 mb-5 mb-xl-10 bg-secondary overflow-scroll">
<h3 className='card-title text-gray-800 fw-bold mb-5'>Verification details</h3>
<div className='w-100'>
{loanDetails?.data?.verification.length > 0 ?
<table className='w-100'>
{loanDetails?.data?.verification?.map((item:any) => (
<tbody key={item.uid || item.id}>
{Object.keys(item).map(key => (
<tr>
<th className='text-uppercase py-3'>{key}</th>
<td>{item[key]}</td>
</tr>
))}
</tbody>
))}
</table>
:
null
}
</div>
</div>
</div>
{/* end::Col */}
{/* begin::Col */}
<div className="col-xl-6">
<div className="card card-flash flex flex-col p-4 h-md-100 bg-secondary">
<h3 className='card-title text-gray-800 fw-bold mb-5'>Loan Details</h3>
<div className='w-100'>
{loanDetails?.data?.application.length > 0 ?
<table className='w-100'>
{loanDetails?.data?.application?.map((item:any) => (
<tbody key={item.uid || item.id}>
{Object.keys(item).map(key => (
<tr>
<th className='text-uppercase py-3'>{key}</th>
<td className='flex text-break'>{item[key]}</td>
</tr>
))}
</tbody>
))}
</table>
:
null
}
</div>
</div>
</div>
{/* end::Col */}
</div>
{/* end::Row */}
</>
}
</Content>
{requestStatus.loading && <UsersListLoading />}
</>
)
}
@@ -0,0 +1,45 @@
import { Navigate, Routes, Route, Outlet } from "react-router-dom";
import { PageLink, PageTitle } from "../../../../_digifi/layout/core";
import ApproveRejectPage from "./ApproveRejectPage";
const processBreadCrumbs: Array<PageLink> = [
{
title: "Loan",
path: "/loan/pages/process/verified",
isSeparator: false,
isActive: false,
},
{
title: "",
path: "",
isSeparator: true,
isActive: false,
},
];
const ApproveRejectRoutes = () => (
<Routes>
<Route
element={
<>
{/* <ProcessHeader /> */}
<Outlet />
</>
}
>
<Route
path="process"
element={
<>
<PageTitle breadcrumbs={processBreadCrumbs}>Verified</PageTitle>
<ApproveRejectPage />
</>
}
/>
<Route index element={<Navigate to="/loan/verified/process" />} />
</Route>
</Routes>
);
export default ApproveRejectRoutes;
@@ -0,0 +1,5 @@
import { UsersListWrapper } from "../user-verified/UsersList";
const UserVerifiedList = () => <UsersListWrapper />;
export { UserVerifiedList };
+49
View File
@@ -33,8 +33,57 @@ export type User = {
employer_name?: string
}
export type VerifiedLoanDetails = {
application_uid?: string
application?: Array<{[index: string]: string}>
// application?: [
// {
// "id": "21",
// "uid": "006915e9-cb0d-42e9-b4af-c047da51e2ac",
// "customer_uid": "9cb678e0-0697-4cc9-9bf0-3f40a3c989fb",
// "loan_amount": "2220",
// "payment_month": "18",
// "sales_agent": "866969",
// "gender": null,
// "marital_status": "single",
// "email": "ameye+update@chiefsoft.com",
// "address": "4201 Defoors Farm Trail",
// "state": "abia",
// "country": "NG",
// "loan_detail": "{\"customer_uid\":\"9cb678e0-0697-4cc9-9bf0-3f40a3c989fb\",\"loan_amount\":\"2220\",\"payment_month\":\"18\",\"sales_agent\":\"866969\",\"gender\":\"female\",\"address\":\"4201 Defoors Farm Trail\",\"marital_status\":\"single\",\"state\":\"abia\",\"email\":\"ameye+update@chiefsoft.com\",\"country\":\"NG\",\"employer_uid\":\"3a9ec95a-090c-4c98-bc01-e96d76b93952\",\"employment\":\"[object Object]\",\"loan_reference\":\"[object Object],[object Object]\",\"disbursement\":\"[object Object]\"}",
// "status": "4",
// "added": "2024-07-10 21:28:13.404726",
// "updated": "2024-07-10 21:28:13.404726",
// "employer_uid": "3a9ec95a-090c-4c98-bc01-e96d76b93952"
// }
// ],
verification?: Array<{[index: string]: string}>
// "verification": [
// {
// "id": "56",
// "uid": "e041279c-875d-4973-b042-48efec05fecf",
// "employer_uid": "3a9ec95a-090c-4c98-bc01-e96d76b93952",
// "status": "1",
// "added": "2024-07-11 16:03:25.343553",
// "updated": "2024-07-11 16:03:25.343553",
// "username": "ameye+update@chiefsoft.com",
// "password": "5769407ab4409037161a51692c6bb617",
// "signatory_uid": "db444b87-ec0f-483b-a8cf-eea9e8466f10",
// "application_uid": "006915e9-cb0d-42e9-b4af-c047da51e2ac",
// "education": "b.sc",
// "grade": "Test 001",
// "applicant_date": "2024-07-10 00:00:00",
// "ippis_number": "",
// "employers_name": "bshshsjss",
// "designation": "jdjdjddk"
// }
// ]
}
export type UsersQueryResponse = Response<Array<User>>
export type VerifiedLoanDetailsResponse = Response<VerifiedLoanDetails>
export const initialUser: User = {
avatar: 'avatars/300-6.jpg',
position: 'Art Director',
+15 -2
View File
@@ -1,7 +1,7 @@
import axios, { AxiosResponse } from "axios";
import { ID, Response } from "../../../../_digifi/helpers"
import { User, UsersQueryResponse } from "./_models";
import { postAuxEnd } from "../../auth/core/AxiosCallHelper";
import { User, UsersQueryResponse, VerifiedLoanDetailsResponse } from "./_models";
import { postAuxEnd, getAuxEnd } from "../../auth/core/AxiosCallHelper";
const API_URL = import.meta.env.VITE_APP_THEME_API_URL;
const USER_URL = `${API_URL}/user`;
@@ -38,6 +38,12 @@ const getReadyUsers = (query: string): Promise<UsersQueryResponse> => { // FUNCT
.then((d: AxiosResponse<UsersQueryResponse>) => d.data);
};
const getVerifiedUsers = (query: string): Promise<UsersQueryResponse> => { // FUNCTION TO GET USERS THAT HAVE ARE VERIFIED
return axios
.get(`${NEW_USER_ENDPOINT}/loan/verified`)
.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`)
@@ -48,6 +54,10 @@ const employersVerify = (uid: ID): Promise<UsersQueryResponse> => { // FUNCTION
return postAuxEnd('/employers/verify', {application_uid:uid})
};
const getVerifiedLoanDetailsByUID = (uid: string): Promise<VerifiedLoanDetailsResponse> => { // FUNCTION TO GET VERIFIED LOAN DETAILS IN ORDER TO PROCESS
return getAuxEnd(`/loan/process/${uid}`)
};
const getUserById = (id: ID): Promise<User | undefined> => {
return axios
.get(`${USER_URL}/${id}`)
@@ -83,8 +93,11 @@ export {
getRejectedUsers,
getPendingUsers,
getReadyUsers,
getVerifiedUsers,
getApprovedUsers,
employersVerify,
getVerifiedLoanDetailsByUID,
deleteUser,
deleteSelectedUsers,
getUserById,
@@ -15,4 +15,21 @@ const UsersListLoading = () => {
return <div style={{...styles, position: 'absolute', textAlign: 'center'}}>Processing...</div>
}
export {UsersListLoading}
const CustomLoading = () => {
const styles = {
borderRadius: '0.475rem',
boxShadow: '0 0 50px 0 rgb(82 63 105 / 15%)',
backgroundColor: '#fff',
color: '#7e8299',
fontWeight: '500',
margin: '0',
width: 'auto',
padding: '1rem 2rem',
top: 'calc(50% - 2rem)',
left: 'calc(50% - 4rem)',
}
return <div style={{...styles, position: 'absolute', textAlign: 'center'}}>Loading...</div>
}
export {UsersListLoading, CustomLoading}
@@ -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 { EditLoanModal } from "./edit-loan-modal/EditLoanModal";
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 && <EditLoanModal />}
</>
);
};
const UsersListWrapper = () => (
<QueryRequestProvider>
<QueryResponseProvider>
<ListViewProvider>
<ToolbarWrapper />
<Content>
<UsersList />
</Content>
</ListViewProvider>
</QueryResponseProvider>
</QueryRequestProvider>
);
export { UsersListWrapper };
@@ -0,0 +1,35 @@
import { KTIcon } from "../../../../../../_digifi/helpers";
import { useListView } from "../../core/ListViewProvider";
import { UsersListFilter } from "./UsersListFilter";
const UsersListToolbar = () => {
const { setItemIdForUpdate } = useListView();
const openAddUserModal = () => {
setItemIdForUpdate(null);
};
return (
<div
className="d-flex justify-content-end"
data-kt-user-table-toolbar="base"
>
<UsersListFilter />
{/* begin::Export */}
{/* <button type='button' className='btn btn-light-primary me-3'>
<KTIcon iconName='exit-up' className='fs-2' />
Export
</button> */}
{/* end::Export */}
{/* begin::Add user */}
{/* <button type='button' className='btn btn-primary' onClick={openAddUserModal}>
<KTIcon iconName='plus' className='fs-2' />
Add User
</button> */}
{/* end::Add user */}
</div>
);
};
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,22 @@
import {useListView} from '../../core/ListViewProvider'
import {UsersListToolbar} from './UserListToolbar'
import {UsersListGrouping} from './UsersListGrouping'
import {UsersListSearchComponent} from './UsersListSearchComponent'
const UsersListHeader = () => {
const {selected} = useListView()
return (
<div className='card-header border-0 pt-6'>
<UsersListSearchComponent />
{/* begin::Card toolbar */}
<div className='card-toolbar'>
{/* begin::Group actions */}
{selected.length > 0 ? <UsersListGrouping /> : <UsersListToolbar />}
{/* end::Group actions */}
</div>
{/* end::Card toolbar */}
</div>
)
}
export {UsersListHeader}
@@ -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,18 @@
const UsersListLoading = () => {
const styles = {
borderRadius: '0.475rem',
boxShadow: '0 0 50px 0 rgb(82 63 105 / 15%)',
backgroundColor: '#fff',
color: '#7e8299',
fontWeight: '500',
margin: '0',
width: 'auto',
padding: '1rem 2rem',
top: 'calc(50% - 2rem)',
left: 'calc(50% - 4rem)',
}
return <div style={{...styles, position: 'absolute', textAlign: 'center'}}>Processing...</div>
}
export {UsersListLoading}
@@ -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 === "&laquo; Previous") {
return "Previous";
}
if (label === "Next &raquo;") {
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 { getVerifiedUsers } 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.VERIFIED_LIST}-${query}`,
() => {
return getVerifiedUsers(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,44 @@
import {useEffect} from 'react'
import {UserEditModalHeader} from './UserEditModalHeader'
import {UserEditModalFormWrapper} from './UserEditModalFormWrapper'
const EditLoanModal = () => {
useEffect(() => {
document.body.classList.add('modal-open')
return () => {
document.body.classList.remove('modal-open')
}
}, [])
return (
<>
<div
className='modal fade show d-block'
id='kt_modal_add_user'
role='dialog'
tabIndex={-1}
aria-modal='true'
>
{/* begin::Modal dialog */}
<div className='modal-dialog modal-dialog-centered mw-650px'>
{/* begin::Modal content */}
<div className='modal-content'>
<UserEditModalHeader />
{/* begin::Modal body */}
<div className='modal-body scroll-y mx-5 mx-xl-15 my-7'>
<UserEditModalFormWrapper />
</div>
{/* end::Modal body */}
</div>
{/* end::Modal content */}
</div>
{/* end::Modal dialog */}
</div>
{/* begin::Modal Backdrop */}
<div className='modal-backdrop fade show'></div>
{/* end::Modal Backdrop */}
</>
)
}
export {EditLoanModal}
@@ -0,0 +1,434 @@
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"
className="d-none 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-danger me-3"
data-kt-users-modal-action="cancel"
disabled={formik.isSubmitting || isUserLoading}
>
Cancel
</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,46 @@
import { useQuery } from "react-query";
import { UserEditModalForm } from "./UserEditModalForm";
import { isNotEmpty, QUERIES } from "../../../../../_digifi/helpers";
import { useListView } from "../core/ListViewProvider";
import { getUserById, getApprovedUsers } from "../../core/_requests";
const UserEditModalFormWrapper = () => {
const { itemIdForUpdate, setItemIdForUpdate } = useListView();
const enabledQuery: boolean = isNotEmpty(itemIdForUpdate);
const {
isLoading,
data: user,
error,
} = useQuery(
`${QUERIES.READY_LIST}-user-${itemIdForUpdate}`,
() => {
// return getUserById(itemIdForUpdate);
return getApprovedUsers('');
},
{
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} />;
// REMOVE LATER AND ALLOW UP ALONE
let newUser:any = user?.records
return <UserEditModalForm isUserLoading={isLoading} user={newUser} />;
}
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">Edit Loan</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,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('users44', 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 };
@@ -0,0 +1,15 @@
import {FC} from 'react'
import {ColumnInstance} from 'react-table'
import {User} from '../../../core/_models'
type Props = {
column: ColumnInstance<User>
}
const CustomHeaderColumn: FC<Props> = ({column}) => (
<>
{column.Header && typeof column.Header === 'string' ? <th {...column.getHeaderProps()}>{column.render('Header')}</th> : column.render('Header')}
</>
)
export {CustomHeaderColumn}
@@ -0,0 +1,25 @@
import clsx from 'clsx'
import {FC} from 'react'
import {Row} from 'react-table'
import {User} from '../../../core/_models'
type Props = {
row: Row<User>
}
const CustomRow: FC<Props> = ({row}) => (
<tr {...row.getRowProps()}>
{row.cells.map((cell) => {
return (
<td
{...cell.getCellProps()}
className={clsx({'text-end min-w-100px': cell.column.id === 'actions'})}
>
{cell.render('Cell')}
</td>
)
})}
</tr>
)
export {CustomRow}
@@ -0,0 +1,11 @@
import {FC} from 'react'
type Props = {
employer_name?: string
}
const EmployerCell: FC<Props> = ({employer_name}) => (
<div className='badge badge-light fw-bolder'>{employer_name}</div>
)
export {EmployerCell}
@@ -0,0 +1,56 @@
import { FC, useEffect } from "react";
import { 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 { Link } from "react-router-dom";
import { User } from "../../../core/_models";
type Props = {
id: ID;
data: Array<User> | any
};
const UserActionsCell: FC<Props> = ({ id, data }) => {
// const { setItemIdForUpdate } = useListView();
// const { query } = useQueryResponse();
// const queryClient = useQueryClient();
let selectedUser = data?.filter((item:User) => item.uid == id)[0]
useEffect(() => {
MenuComponent.reinitialization();
}, []);
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">
<Link state={{selectedUser}} to='/loan/verified/process' className="menu-link px-3">
Process
</Link>
</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,11 @@
import {FC} from 'react'
type Props = {
payment_month?: string
}
const PaymentMonthCell: FC<Props> = ({payment_month}) => (
<div className='badge badge-light fw-bolder'>{payment_month}</div>
)
export {PaymentMonthCell}
@@ -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 };
@@ -0,0 +1,28 @@
import {FC, PropsWithChildren} from 'react'
import {HeaderProps} from 'react-table'
import {useListView} from '../../core/ListViewProvider'
import {User} from '../../../core/_models'
type Props = {
tableProps: PropsWithChildren<HeaderProps<User>>
}
const UserSelectionHeader: FC<Props> = ({tableProps}) => {
const {isAllSelected, onSelectAll} = useListView()
return (
<th {...tableProps.column.getHeaderProps()} className='w-10px pe-2'>
<div className='form-check form-check-sm form-check-custom form-check-solid me-3'>
<input
className='form-check-input'
type='checkbox'
data-kt-check={isAllSelected}
data-kt-check-target='#kt_table_users .form-check-input'
checked={isAllSelected}
onChange={onSelectAll}
/>
</div>
</th>
)
}
export {UserSelectionHeader}
@@ -0,0 +1,65 @@
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'
import { EmployerCell } from './EmployerCell'
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='Employer' className='min-w-125px' />
),
id: 'employer_name',
Cell: ({...props}) => <EmployerCell employer_name={props.data[props.row.index].employer_name} />,
},
{
Header: (props) => <UserCustomHeader tableProps={props} title='Amount' className='min-w-125px' />,
accessor: 'loan_amount',
},
{
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} data={props.data} />,
},
]
export {usersColumns}
+5 -3
View File
@@ -43,24 +43,26 @@ const DashboardPage: FC = () => {
<div className="col-md-6 col-lg-6 col-xl-6 col-xxl-3 mb-md-5 mb-xl-10">
<CardsWidget20
className="h-md-50 mb-5 mb-xl-10"
description="Active Projects"
description="Active Applications"
color="#F1416C"
img={toAbsoluteUrl('media/patterns/vector-1.png')}
dashData={dashDetails}
/>
<CardsWidget7
className="h-md-50 mb-5 mb-xl-10"
description="Professionals"
description="Todays Applications"
icon={false}
stats={357}
labelColor="dark"
textColor="gray-300"
dashData={dashDetails}
/>
</div>
{/* end::Col */}
{/* begin::Col */}
<div className="col-md-6 col-lg-6 col-xl-6 col-xxl-3 mb-md-5 mb-xl-10">
<CardsWidget17 className="h-md-50 mb-5 mb-xl-10" />
<CardsWidget17 dashData={dashDetails} className="h-md-50 mb-5 mb-xl-10" />
<ListsWidget26 className="h-lg-50" />
</div>
{/* end::Col */}
+15
View File
@@ -32,6 +32,20 @@ export type RecentBVNProps = {
nationality?: string | null
}[]
export type DataProps = {
active_loans?: string
applications?: string
today_application?: string | number
curr_month?: string
curr_application_amount?: string | number
curr_application_percentage?: string
curr_application_direction?: string
recent_applications?: Array<{[index: string]: string}>,
ready_loans?: string
verified_loans?: string
approved_loans?: string
}
export type DashDataProps = {
loading: boolean,
@@ -39,5 +53,6 @@ export type DashDataProps = {
call_return?: string
recent_applications? : RecentApplicationsProps
recent_bvn?: RecentBVNProps
dash_data? : DataProps
}
}
+2
View File
@@ -11,6 +11,7 @@ import {PrivateRoutes} from './PrivateRoutes'
import {ErrorsPage} from '../modules/errors/ErrorsPage'
import {Logout, AuthPage, useAuth} from '../modules/auth'
import {App} from '../App'
import HelpPage from '../modules/help/HelpPage'
/**
* Base URL of the website.
@@ -26,6 +27,7 @@ const AppRoutes: FC = () => {
<Routes>
<Route element={<App />}>
<Route path='error/*' element={<ErrorsPage />} />
<Route path='/help' element={<HelpPage />} />
<Route path='logout' element={<Logout />} />
{currentUser ? (
<>
+40 -25
View File
@@ -1,11 +1,11 @@
import {lazy, FC, Suspense} from 'react'
import {Route, Routes, Navigate} from 'react-router-dom'
import {MasterLayout} from '../../_digifi/layout/MasterLayout'
import TopBarProgress from 'react-topbar-progress-indicator'
import {DashboardWrapper} from '../pages/dashboard/DashboardWrapper'
import { lazy, FC, Suspense } from "react";
import { Route, Routes, Navigate } from "react-router-dom";
import { MasterLayout } from "../../_digifi/layout/MasterLayout";
import TopBarProgress from "react-topbar-progress-indicator";
import { DashboardWrapper } from "../pages/dashboard/DashboardWrapper";
// import {MenuTestPage} from '../pages/MenuTestPage'
import {getCSSVariableValue} from '../../_digifi/assets/ts/_utils'
import {WithChildren} from '../../_digifi/helpers'
import { getCSSVariableValue } from "../../_digifi/assets/ts/_utils";
import { WithChildren } from "../../_digifi/helpers";
// import BuilderPageWrapper from '../pages/layout-builder/BuilderPageWrapper'
const PrivateRoutes = () => {
@@ -13,28 +13,43 @@ const PrivateRoutes = () => {
// const AccountPage = lazy(() => import('../modules/accounts/AccountPage'))
// const WidgetsPage = lazy(() => import('../modules/widgets/WidgetsPage'))
// const ChatPage = lazy(() => import('../modules/apps/chat/ChatPage'))
const ProcessPage = lazy(() => import('../modules/process/ProcessPage'))
const UsersPage = lazy(() => import('../modules/apps/user-management/UsersPage'))
const EmployersPage =lazy(() => import('../modules/employers/employers-list/UsersPage'))
const ProcessPage = lazy(() => import("../modules/process/ProcessPage"));
const UsersPage = lazy(
() => import("../modules/apps/user-management/UsersPage")
);
const EmployersPage = lazy(
() => import("../modules/employers/employers-list/UsersPage")
);
const ApproveRejectRoutes = lazy(
() => import("../modules/process/approve-reject-page/ApproveRejectRoutes")
);
return (
<Routes>
<Route element={<MasterLayout />}>
{/* Redirect to Dashboard after success login/registartion */}
<Route path='auth/*' element={<Navigate to='/dashboard' />} />
<Route path="auth/*" element={<Navigate to="/dashboard" />} />
{/* Pages */}
<Route path='dashboard' element={<DashboardWrapper />} />
<Route path="dashboard" element={<DashboardWrapper />} />
{/* <Route path='builder' element={<BuilderPageWrapper />} /> */}
{/* <Route path='menu-test' element={<MenuTestPage />} /> */}
{/* Lazy Modules */}
<Route
path='loan/pages/process/*'
path="loan/pages/process/*"
element={
<SuspensedView>
<ProcessPage />
</SuspensedView>
}
/>
<Route
path="loan/verified/*"
element={
<SuspensedView>
<ApproveRejectRoutes />
</SuspensedView>
}
/>
{/* <Route
path='crafted/pages/wizards/*'
element={
@@ -68,7 +83,7 @@ const PrivateRoutes = () => {
}
/> */}
<Route
path='tools/user-management/*'
path="tools/user-management/*"
element={
<SuspensedView>
<UsersPage />
@@ -76,7 +91,7 @@ const PrivateRoutes = () => {
}
/>
<Route
path='/employers/*'
path="/employers/*"
element={
<SuspensedView>
<EmployersPage />
@@ -84,22 +99,22 @@ const PrivateRoutes = () => {
}
/>
{/* Page Not Found */}
<Route path='*' element={<Navigate to='/error/404' />} />
<Route path="*" element={<Navigate to="/error/404" />} />
</Route>
</Routes>
)
}
);
};
const SuspensedView: FC<WithChildren> = ({children}) => {
const baseColor = getCSSVariableValue('--bs-primary')
const SuspensedView: FC<WithChildren> = ({ children }) => {
const baseColor = getCSSVariableValue("--bs-primary");
TopBarProgress.config({
barColors: {
'0': baseColor,
"0": baseColor,
},
barThickness: 1,
shadowBlur: 5,
})
return <Suspense fallback={<TopBarProgress />}>{children}</Suspense>
}
});
return <Suspense fallback={<TopBarProgress />}>{children}</Suspense>;
};
export {PrivateRoutes}
export { PrivateRoutes };