Compare commits
14 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 2a7c25d160 | |||
| 237be24476 | |||
| cffaa9f379 | |||
| 00aa5e57fa | |||
| 1d14205656 | |||
| d98624574f | |||
| fa46cae1cc | |||
| 704681c32d | |||
| 251fe95a6b | |||
| ca7db4b0aa | |||
| 5fe90c3ead | |||
| 827d0a1c30 | |||
| 0fc549f1b5 | |||
| ee1fe4a5b6 |
Binary file not shown.
|
After Width: | Height: | Size: 602 KiB |
@@ -6,6 +6,8 @@ const QUERIES = {
|
|||||||
PENDING_LIST: 'pending-list',
|
PENDING_LIST: 'pending-list',
|
||||||
APPROVED_LIST: 'approved-list',
|
APPROVED_LIST: 'approved-list',
|
||||||
REJECTED_LIST: 'rejected-list',
|
REJECTED_LIST: 'rejected-list',
|
||||||
|
CUSTOMERS_LIST: 'customers-list',
|
||||||
|
ADMIN_USERS_LIST: 'admin-users-list',
|
||||||
EMPLOYERS_LIST: 'employers-list',
|
EMPLOYERS_LIST: 'employers-list',
|
||||||
SIGNATORY_LIST: 'signatory-list',
|
SIGNATORY_LIST: 'signatory-list',
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,3 +4,29 @@ export const formatNumbers = (number: string | undefined): string | null => {
|
|||||||
}
|
}
|
||||||
return number.replace(/\B(?=(\d{3})+(?!\d))/g, ',');
|
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">
|
<div className="w-full h-100 d-flex gap-4 justify-content-center align-items-end">
|
||||||
<button
|
<button
|
||||||
onClick={handlePrev}
|
onClick={handlePrev}
|
||||||
className={`text-sm md:text-lg d-flex justify-content-center align-items-center border-1 transition-all duration-300 ${
|
className={`btn btn-primary `}
|
||||||
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"
|
|
||||||
}`}
|
|
||||||
disabled={currentPage == 0}
|
disabled={currentPage == 0}
|
||||||
// style={{width:'30px', height:'30px'}}
|
// style={{width:'30px', height:'30px'}}
|
||||||
>
|
>
|
||||||
@@ -123,11 +119,7 @@ export default function RecentBVNList({
|
|||||||
|
|
||||||
<button
|
<button
|
||||||
onClick={handleNext}
|
onClick={handleNext}
|
||||||
className={`text-sm md:text-lg d-flex justify-content-center align-items-center border-1 transition-all duration-300 ${
|
className={`btn btn-primary`}
|
||||||
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"
|
|
||||||
}`}
|
|
||||||
disabled={currentPage + numberOfSelection >= data.length}
|
disabled={currentPage + numberOfSelection >= data.length}
|
||||||
// style={{width:'30px', height:'30px'}}
|
// style={{width:'30px', height:'30px'}}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -105,9 +105,9 @@ const SidebarMenuMain = () => {
|
|||||||
</SidebarMenuItemWithSub> */}
|
</SidebarMenuItemWithSub> */}
|
||||||
|
|
||||||
<SidebarMenuItem
|
<SidebarMenuItem
|
||||||
to='/tools/user-management/users'
|
to='/tools/user-management/customers'
|
||||||
icon='abstract-28'
|
icon='abstract-28'
|
||||||
title='User management'
|
title='Customers'
|
||||||
fontIcon='bi-layers'
|
fontIcon='bi-layers'
|
||||||
/>
|
/>
|
||||||
|
|
||||||
@@ -141,6 +141,18 @@ const SidebarMenuMain = () => {
|
|||||||
{/* <span className='menu-title'>Changelog {import.meta.env.VITE_APP_VERSION}</span>*/}
|
{/* <span className='menu-title'>Changelog {import.meta.env.VITE_APP_VERSION}</span>*/}
|
||||||
{/* </a>*/}
|
{/* </a>*/}
|
||||||
{/*</div>*/}
|
{/*</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 {KTIcon} from '../../../../helpers'
|
||||||
import {getCSSVariableValue} from '../../../../assets/ts/_utils'
|
import {getCSSVariableValue} from '../../../../assets/ts/_utils'
|
||||||
import {useThemeMode} from '../../../layout/theme-mode/ThemeModeProvider'
|
import {useThemeMode} from '../../../layout/theme-mode/ThemeModeProvider'
|
||||||
|
import { DashDataProps } from '../../../../../app/pages/dashboard/model'
|
||||||
|
import { FormatAmount } from '../../../../helpers/formatNumbers'
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
className: string
|
className: string
|
||||||
chartSize?: number
|
chartSize?: number
|
||||||
chartLine?: number
|
chartLine?: number
|
||||||
chartRotate?: number
|
chartRotate?: number
|
||||||
|
dashData?: DashDataProps
|
||||||
}
|
}
|
||||||
|
|
||||||
const CardsWidget17: FC<Props> = ({
|
const CardsWidget17: FC<Props> = ({
|
||||||
@@ -16,6 +19,7 @@ const CardsWidget17: FC<Props> = ({
|
|||||||
chartSize = 70,
|
chartSize = 70,
|
||||||
chartLine = 11,
|
chartLine = 11,
|
||||||
chartRotate = 145,
|
chartRotate = 145,
|
||||||
|
dashData
|
||||||
}) => {
|
}) => {
|
||||||
const chartRef = useRef<HTMLDivElement | null>(null)
|
const chartRef = useRef<HTMLDivElement | null>(null)
|
||||||
const {mode} = useThemeMode()
|
const {mode} = useThemeMode()
|
||||||
@@ -39,15 +43,15 @@ const CardsWidget17: FC<Props> = ({
|
|||||||
<div className='card-header pt-5'>
|
<div className='card-header pt-5'>
|
||||||
<div className='card-title d-flex flex-column'>
|
<div className='card-title d-flex flex-column'>
|
||||||
<div className='d-flex align-items-center'>
|
<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'>
|
<span className='badge badge-light-success fs-base'>
|
||||||
<KTIcon iconName='arrow-up' className='fs-5 text-success ms-n1' /> 2.2%
|
<KTIcon iconName='arrow-up' className='fs-5 text-success ms-n1' /> 2.2%
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</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>
|
||||||
</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 flex-column content-justify-center flex-row-fluid'>
|
||||||
<div className='d-flex fw-semibold align-items-center'>
|
<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='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='text-gray-500 flex-grow-1 me-4'>Ready</div>
|
||||||
<div className='fw-bolder text-gray-700 text-xxl-end'>$7,660</div>
|
<div className='fw-bolder text-gray-700 text-xxl-end'>{dashData?.loading ? 'Loading...': FormatAmount(dashData?.data?.dash_data?.ready_loans)}</div>
|
||||||
</div>
|
</div>
|
||||||
<div className='d-flex fw-semibold align-items-center my-3'>
|
<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='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='text-gray-500 flex-grow-1 me-4'>Verified</div>
|
||||||
<div className='fw-bolder text-gray-700 text-xxl-end'>$2,820</div>
|
<div className='fw-bolder text-gray-700 text-xxl-end'>{dashData?.loading ? 'Loading...': FormatAmount(dashData?.data?.dash_data?.verified_loans)}</div>
|
||||||
</div>
|
</div>
|
||||||
<div className='d-flex fw-semibold align-items-center'>
|
<div className='d-flex fw-semibold align-items-center'>
|
||||||
<div
|
<div
|
||||||
className='bullet w-8px h-3px rounded-2 me-3'
|
className='bullet w-8px h-3px rounded-2 me-3'
|
||||||
style={{backgroundColor: '#E4E6EF'}}
|
style={{backgroundColor: '#E4E6EF'}}
|
||||||
></div>
|
></div>
|
||||||
<div className='text-gray-500 flex-grow-1 me-4'>Others</div>
|
<div className='text-gray-500 flex-grow-1 me-4'>Approved</div>
|
||||||
<div className=' fw-bolder text-gray-700 text-xxl-end'>$45,257</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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,11 +1,14 @@
|
|||||||
|
import { DashDataProps } from "../../../../../app/pages/dashboard/model"
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
className: string
|
className: string
|
||||||
description: string
|
description: string
|
||||||
color: string
|
color: string
|
||||||
img: string
|
img: string
|
||||||
|
dashData?: DashDataProps
|
||||||
}
|
}
|
||||||
|
|
||||||
const CardsWidget20 = ({className, description, color, img}: Props) => (
|
const CardsWidget20 = ({className, description, color, img, dashData}: Props) => (
|
||||||
<div
|
<div
|
||||||
className={`card card-flush bgi-no-repeat bgi-size-contain bgi-position-x-end ${className}`}
|
className={`card card-flush bgi-no-repeat bgi-size-contain bgi-position-x-end ${className}`}
|
||||||
style={{
|
style={{
|
||||||
@@ -15,7 +18,7 @@ const CardsWidget20 = ({className, description, color, img}: Props) => (
|
|||||||
>
|
>
|
||||||
<div className='card-header pt-5'>
|
<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-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>
|
<span className='text-white opacity-75 pt-1 fw-semibold fs-6'>{description}</span>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
|
|
||||||
import clsx from 'clsx'
|
import clsx from 'clsx'
|
||||||
import {toAbsoluteUrl} from '../../../../helpers'
|
import {toAbsoluteUrl} from '../../../../helpers'
|
||||||
|
import { DashDataProps } from '../../../../../app/pages/dashboard/model'
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
className: string
|
className: string
|
||||||
@@ -9,6 +10,7 @@ type Props = {
|
|||||||
stats: number
|
stats: number
|
||||||
labelColor: string
|
labelColor: string
|
||||||
textColor: string
|
textColor: string
|
||||||
|
dashData?: DashDataProps
|
||||||
}
|
}
|
||||||
|
|
||||||
const items: Array<{
|
const items: Array<{
|
||||||
@@ -25,18 +27,18 @@ const items: Array<{
|
|||||||
{name: 'Barry Walter', src: toAbsoluteUrl('media/avatars/300-12.jpg')},
|
{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 card-flush ${className}`}>
|
||||||
<div className='card-header pt-5'>
|
<div className='card-header pt-5'>
|
||||||
<div className='card-title d-flex flex-column'>
|
<div className='card-title d-flex flex-column'>
|
||||||
<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>
|
<span className='text-gray-500 pt-1 fw-semibold fs-6'>{description}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className='card-body d-flex flex-column justify-content-end pe-0'>
|
<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'>Today’s 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'>
|
<div className='symbol-group symbol-hover flex-nowrap'>
|
||||||
{items.map((item, index) => (
|
{items.map((item, index) => (
|
||||||
<div
|
<div
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ const EngageWidget10 = ({className}: Props) => (
|
|||||||
<div className='mb-10'>
|
<div className='mb-10'>
|
||||||
<div className='fs-2hx fw-bold text-gray-800 text-center mb-13'>
|
<div className='fs-2hx fw-bold text-gray-800 text-center mb-13'>
|
||||||
<span className='me-2'>
|
<span className='me-2'>
|
||||||
Try our all new Enviroment with
|
Need more help to manage to the platform
|
||||||
<br />
|
<br />
|
||||||
<span className='position-relative d-inline-block text-danger'>
|
<span className='position-relative d-inline-block text-danger'>
|
||||||
<Link
|
<Link
|
||||||
@@ -26,18 +26,18 @@ const EngageWidget10 = ({className}: Props) => (
|
|||||||
className='text-danger
|
className='text-danger
|
||||||
opacity-75-hover'
|
opacity-75-hover'
|
||||||
>
|
>
|
||||||
Pro Plan
|
Use our help
|
||||||
</Link>
|
</Link>
|
||||||
|
|
||||||
<span className='position-absolute opacity-15 bottom-0 start-0 border-4 border-danger border-bottom w-100'></span>
|
<span className='position-absolute opacity-15 bottom-0 start-0 border-4 border-danger border-bottom w-100'></span>
|
||||||
</span>
|
</span>
|
||||||
</span>
|
</span>
|
||||||
for Free
|
{/* for Free */}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className='text-center'>
|
{/* <div className='text-center'>
|
||||||
<a href='#'>Upgrade Now</a>
|
<a href='#'>Upgrade Now</a>
|
||||||
</div>
|
</div> */}
|
||||||
</div>
|
</div>
|
||||||
<img
|
<img
|
||||||
className='mx-auto h-150px h-lg-200px theme-light-show'
|
className='mx-auto h-150px h-lg-200px theme-light-show'
|
||||||
|
|||||||
@@ -1,30 +1,31 @@
|
|||||||
|
|
||||||
import {Fragment} from 'react'
|
import {Fragment} from 'react'
|
||||||
import {KTIcon} from '../../../../helpers'
|
import {KTIcon} from '../../../../helpers'
|
||||||
|
import { Link } from 'react-router-dom'
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
className: string
|
className: string
|
||||||
}
|
}
|
||||||
|
|
||||||
const rows: Array<{description: string}> = [
|
const rows: Array<{description: string, link: string}> = [
|
||||||
{description: 'Avg. Client Rating'},
|
{description: 'Verified Loans', link: '/loan/pages/process/verified'},
|
||||||
{description: 'Instagram Followers'},
|
{description: 'Approved Loans', link: '/loan/pages/process/approved'},
|
||||||
{description: 'Google Ads CPC'},
|
{description: 'Rejected Loans', link: '/loan/pages/process/rejected'},
|
||||||
]
|
]
|
||||||
|
|
||||||
const ListsWidget26 = ({className}: Props) => (
|
const ListsWidget26 = ({className}: Props) => (
|
||||||
<div className={`card card-flush ${className}`}>
|
<div className={`card card-flush ${className}`}>
|
||||||
<div className='card-header pt-5'>
|
<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 className='card-toolbar'></div>
|
||||||
</div>
|
</div>
|
||||||
<div className='card-body pt-5'>
|
<div className='card-body pt-5'>
|
||||||
{rows.map((row, index) => (
|
{rows.map((row, index) => (
|
||||||
<Fragment key={`lw26-rows-${index}`}>
|
<Fragment key={`lw26-rows-${index}`}>
|
||||||
<div className='d-flex flex-stack'>
|
<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}
|
{row.description}
|
||||||
</a>
|
</Link>
|
||||||
<button
|
<button
|
||||||
type='button'
|
type='button'
|
||||||
className='btn btn-icon btn-sm h-auto btn-color-gray-500 btn-active-color-primary justify-content-end'
|
className='btn btn-icon btn-sm h-auto btn-color-gray-500 btn-active-color-primary justify-content-end'
|
||||||
|
|||||||
@@ -121,24 +121,24 @@ const TablesWidget10: FC<Props> = ({className, dashData}) => {
|
|||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<div className='d-flex justify-content-end flex-shrink-0'>
|
<div className='d-flex justify-content-end flex-shrink-0'>
|
||||||
<a
|
{/* <a
|
||||||
href='#'
|
href='#'
|
||||||
className='btn btn-icon btn-bg-light btn-active-color-primary btn-sm me-1'
|
className='btn btn-icon btn-bg-light btn-active-color-primary btn-sm me-1'
|
||||||
>
|
>
|
||||||
<KTIcon iconName='switch' className='fs-3' />
|
<KTIcon iconName='switch' className='fs-3' />
|
||||||
</a>
|
</a> */}
|
||||||
<a
|
<a
|
||||||
href='#'
|
href='#'
|
||||||
className='btn btn-icon btn-bg-light btn-active-color-primary btn-sm me-1'
|
className='btn btn-icon btn-bg-light btn-active-color-primary btn-sm me-1'
|
||||||
>
|
>
|
||||||
<KTIcon iconName='pencil' className='fs-3' />
|
<KTIcon iconName='pencil' className='fs-3' />
|
||||||
</a>
|
</a>
|
||||||
<a
|
{/* <a
|
||||||
href='#'
|
href='#'
|
||||||
className='btn btn-icon btn-bg-light btn-active-color-primary btn-sm'
|
className='btn btn-icon btn-bg-light btn-active-color-primary btn-sm'
|
||||||
>
|
>
|
||||||
<KTIcon iconName='trash' className='fs-3' />
|
<KTIcon iconName='trash' className='fs-3' />
|
||||||
</a>
|
</a> */}
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
|||||||
+6
-2
@@ -1,5 +1,5 @@
|
|||||||
import {Suspense} from 'react'
|
import {Suspense, useEffect} from 'react'
|
||||||
import {Outlet} from 'react-router-dom'
|
import {Outlet, useLocation} from 'react-router-dom'
|
||||||
import {I18nProvider} from '../_digifi/i18n/i18nProvider'
|
import {I18nProvider} from '../_digifi/i18n/i18nProvider'
|
||||||
import {LayoutProvider, LayoutSplashScreen} from '../_digifi/layout/core'
|
import {LayoutProvider, LayoutSplashScreen} from '../_digifi/layout/core'
|
||||||
import {MasterInit} from '../_digifi/layout/MasterInit'
|
import {MasterInit} from '../_digifi/layout/MasterInit'
|
||||||
@@ -8,6 +8,10 @@ import {ThemeModeProvider} from '../_digifi/partials'
|
|||||||
import { CustomModalProvider } from '../context/CustomModal'
|
import { CustomModalProvider } from '../context/CustomModal'
|
||||||
|
|
||||||
const App = () => {
|
const App = () => {
|
||||||
|
const {pathname}= useLocation()
|
||||||
|
useEffect(()=>{
|
||||||
|
window.scrollTo(0,0)
|
||||||
|
},[pathname])
|
||||||
return (
|
return (
|
||||||
<Suspense fallback={<LayoutSplashScreen />}>
|
<Suspense fallback={<LayoutSplashScreen />}>
|
||||||
<I18nProvider>
|
<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 {PageLink, PageTitle} from '../../../../_digifi/layout/core'
|
||||||
import {UsersListWrapper} from './users-list/UsersList'
|
import {UsersListWrapper} from './users-list/UsersList'
|
||||||
|
import { CustomersListWrapper } from './customers-list/UsersList'
|
||||||
|
|
||||||
const usersBreadcrumbs: Array<PageLink> = [
|
const usersBreadcrumbs: Array<PageLink> = [
|
||||||
{
|
{
|
||||||
@@ -18,6 +19,7 @@ const usersBreadcrumbs: Array<PageLink> = [
|
|||||||
]
|
]
|
||||||
|
|
||||||
const UsersPage = () => {
|
const UsersPage = () => {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Routes>
|
<Routes>
|
||||||
<Route element={<Outlet />}>
|
<Route element={<Outlet />}>
|
||||||
@@ -30,6 +32,15 @@ const UsersPage = () => {
|
|||||||
</>
|
</>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
<Route
|
||||||
|
path='customers'
|
||||||
|
element={
|
||||||
|
<>
|
||||||
|
<PageTitle breadcrumbs={usersBreadcrumbs}>Customers list</PageTitle>
|
||||||
|
<CustomersListWrapper />
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
/>
|
||||||
</Route>
|
</Route>
|
||||||
<Route index element={<Navigate to='/tools/user-management/users' />} />
|
<Route index element={<Navigate to='/tools/user-management/users' />} />
|
||||||
</Routes>
|
</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}
|
||||||
+32
@@ -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}
|
||||||
+133
@@ -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}
|
||||||
+38
@@ -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}
|
||||||
+22
@@ -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}
|
||||||
+45
@@ -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}
|
||||||
+18
@@ -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}
|
||||||
+156
@@ -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 === '« Previous') {
|
||||||
|
return 'Previous'
|
||||||
|
}
|
||||||
|
|
||||||
|
if (label === 'Next »') {
|
||||||
|
return 'Next'
|
||||||
|
}
|
||||||
|
|
||||||
|
return label
|
||||||
|
}
|
||||||
|
|
||||||
|
const UsersListPagination = () => {
|
||||||
|
const pagination = useQueryResponsePagination()
|
||||||
|
const isLoading = useQueryResponseLoading()
|
||||||
|
const {updateState} = useQueryRequest()
|
||||||
|
const updatePage = (page: number | undefined | null) => {
|
||||||
|
if (!page || isLoading || pagination.page === page) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
updateState({page, items_per_page: pagination.items_per_page || 10})
|
||||||
|
}
|
||||||
|
|
||||||
|
const PAGINATION_PAGES_COUNT = 5
|
||||||
|
const sliceLinks = (pagination?: PaginationState) => {
|
||||||
|
if (!pagination?.links?.length) {
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
|
||||||
|
const scopedLinks = [...pagination.links]
|
||||||
|
|
||||||
|
let pageLinks: Array<{
|
||||||
|
label: string
|
||||||
|
active: boolean
|
||||||
|
url: string | null
|
||||||
|
page: number | null
|
||||||
|
}> = []
|
||||||
|
const previousLink: {label: string; active: boolean; url: string | null; page: number | null} =
|
||||||
|
scopedLinks.shift()!
|
||||||
|
const nextLink: {label: string; active: boolean; url: string | null; page: number | null} =
|
||||||
|
scopedLinks.pop()!
|
||||||
|
|
||||||
|
const halfOfPagesCount = Math.floor(PAGINATION_PAGES_COUNT / 2)
|
||||||
|
|
||||||
|
pageLinks.push(previousLink)
|
||||||
|
|
||||||
|
if (
|
||||||
|
pagination.page <= Math.round(PAGINATION_PAGES_COUNT / 2) ||
|
||||||
|
scopedLinks.length <= PAGINATION_PAGES_COUNT
|
||||||
|
) {
|
||||||
|
pageLinks = [...pageLinks, ...scopedLinks.slice(0, PAGINATION_PAGES_COUNT)]
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
pagination.page > scopedLinks.length - halfOfPagesCount &&
|
||||||
|
scopedLinks.length > PAGINATION_PAGES_COUNT
|
||||||
|
) {
|
||||||
|
pageLinks = [
|
||||||
|
...pageLinks,
|
||||||
|
...scopedLinks.slice(scopedLinks.length - PAGINATION_PAGES_COUNT, scopedLinks.length),
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
!(
|
||||||
|
pagination.page <= Math.round(PAGINATION_PAGES_COUNT / 2) ||
|
||||||
|
scopedLinks.length <= PAGINATION_PAGES_COUNT
|
||||||
|
) &&
|
||||||
|
!(pagination.page > scopedLinks.length - halfOfPagesCount)
|
||||||
|
) {
|
||||||
|
pageLinks = [
|
||||||
|
...pageLinks,
|
||||||
|
...scopedLinks.slice(
|
||||||
|
pagination.page - 1 - halfOfPagesCount,
|
||||||
|
pagination.page + halfOfPagesCount
|
||||||
|
),
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
pageLinks.push(nextLink)
|
||||||
|
|
||||||
|
return pageLinks
|
||||||
|
}
|
||||||
|
|
||||||
|
const paginationLinks = useMemo(() => sliceLinks(pagination), [pagination])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className='row'>
|
||||||
|
<div className='col-sm-12 col-md-5 d-flex align-items-center justify-content-center justify-content-md-start'></div>
|
||||||
|
<div className='col-sm-12 col-md-7 d-flex align-items-center justify-content-center justify-content-md-end'>
|
||||||
|
<div id='kt_table_users_paginate'>
|
||||||
|
<ul className='pagination'>
|
||||||
|
<li
|
||||||
|
className={clsx('page-item', {
|
||||||
|
disabled: isLoading || pagination.page === 1,
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
<a onClick={() => updatePage(1)} style={{cursor: 'pointer'}} className='page-link'>
|
||||||
|
First
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
{paginationLinks
|
||||||
|
?.map((link) => {
|
||||||
|
return {...link, label: mappedLabel(link.label)}
|
||||||
|
})
|
||||||
|
.map((link) => (
|
||||||
|
<li
|
||||||
|
key={link.label}
|
||||||
|
className={clsx('page-item', {
|
||||||
|
active: pagination.page === link.page,
|
||||||
|
disabled: isLoading,
|
||||||
|
previous: link.label === 'Previous',
|
||||||
|
next: link.label === 'Next',
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
<a
|
||||||
|
className={clsx('page-link', {
|
||||||
|
'page-text': link.label === 'Previous' || link.label === 'Next',
|
||||||
|
'me-5': link.label === 'Previous',
|
||||||
|
})}
|
||||||
|
onClick={() => updatePage(link.page)}
|
||||||
|
style={{cursor: 'pointer'}}
|
||||||
|
>
|
||||||
|
{mappedLabel(link.label)}
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
<li
|
||||||
|
className={clsx('page-item', {
|
||||||
|
disabled: isLoading || pagination.page === (pagination.links?.length || 3) - 2,
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
<a
|
||||||
|
onClick={() => updatePage((pagination.links?.length || 3) - 2)}
|
||||||
|
style={{cursor: 'pointer'}}
|
||||||
|
className='page-link'
|
||||||
|
>
|
||||||
|
Last
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export {UsersListPagination}
|
||||||
@@ -0,0 +1,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}
|
||||||
+15
@@ -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}
|
||||||
+61
@@ -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}
|
||||||
+11
@@ -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}
|
||||||
+26
@@ -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}
|
||||||
+28
@@ -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}
|
||||||
+407
@@ -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}
|
||||||
+40
@@ -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}
|
||||||
+27
@@ -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,
|
stringifyRequestQuery,
|
||||||
WithChildren,
|
WithChildren,
|
||||||
} from '../../../../../../_digifi/helpers'
|
} from '../../../../../../_digifi/helpers'
|
||||||
import {getStartedUsers} from './_requests'
|
import {getAdminUserList} from './_requests'
|
||||||
import {User} from './_models'
|
import {User} from './_models'
|
||||||
import {useQueryRequest} from './QueryRequestProvider'
|
import {useQueryRequest} from './QueryRequestProvider'
|
||||||
|
|
||||||
@@ -32,9 +32,9 @@ const QueryResponseProvider: FC<WithChildren> = ({children}) => {
|
|||||||
refetch,
|
refetch,
|
||||||
data: response,
|
data: response,
|
||||||
} = useQuery(
|
} = useQuery(
|
||||||
`${QUERIES.USERS_LIST}-${query}`,
|
`${QUERIES.ADMIN_USERS_LIST}-${query}`,
|
||||||
() => {
|
() => {
|
||||||
return getStartedUsers(query)
|
return getAdminUserList(query)
|
||||||
},
|
},
|
||||||
{cacheTime: 0, keepPreviousData: true, refetchOnWindowFocus: false}
|
{cacheTime: 0, keepPreviousData: true, refetchOnWindowFocus: false}
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -29,6 +29,8 @@ export type User = {
|
|||||||
status?: string
|
status?: string
|
||||||
added?: string
|
added?: string
|
||||||
updated?: string
|
updated?: string
|
||||||
|
bvn?: string
|
||||||
|
username?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export type UsersQueryResponse = Response<Array<User>>
|
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}`)
|
// .get(`${GET_USERS_URL}?${query}`)
|
||||||
// .then((d: AxiosResponse<UsersQueryResponse>) => d.data);
|
// .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
|
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);
|
.then((d: AxiosResponse<UsersQueryResponse>) => d.data);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -50,10 +56,11 @@ const deleteSelectedUsers = (userIds: Array<ID>): Promise<void> => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export {
|
export {
|
||||||
getStartedUsers,
|
getCustomerList,
|
||||||
deleteUser,
|
deleteUser,
|
||||||
deleteSelectedUsers,
|
deleteSelectedUsers,
|
||||||
getUserById,
|
getUserById,
|
||||||
|
getAdminUserList,
|
||||||
createUser,
|
createUser,
|
||||||
updateUser,
|
updateUser,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,11 +0,0 @@
|
|||||||
import {FC} from 'react'
|
|
||||||
|
|
||||||
type Props = {
|
|
||||||
agent?: string
|
|
||||||
}
|
|
||||||
|
|
||||||
const AgentCell: FC<Props> = ({agent}) => (
|
|
||||||
<> {agent && <div className='badge badge-light-success fw-bolder'>{agent}</div>}</>
|
|
||||||
)
|
|
||||||
|
|
||||||
export {AgentCell}
|
|
||||||
@@ -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 {Column} from 'react-table'
|
||||||
import {UserInfoCell} from './UserInfoCell'
|
import {UserInfoCell} from './UserInfoCell'
|
||||||
import { PaymentMonthCell } from './UserLastLoginCell'
|
import { PaymentMonthCell } from './UserLastLoginCell'
|
||||||
import {AgentCell} from './AgentCell'
|
import {Status} from './Status'
|
||||||
import {UserActionsCell} from './UserActionsCell'
|
import {UserActionsCell} from './UserActionsCell'
|
||||||
import {UserSelectionCell} from './UserSelectionCell'
|
import {UserSelectionCell} from './UserSelectionCell'
|
||||||
import {UserCustomHeader} from './UserCustomHeader'
|
import {UserCustomHeader} from './UserCustomHeader'
|
||||||
@@ -20,23 +20,12 @@ const usersColumns: ReadonlyArray<Column<User>> = [
|
|||||||
id: 'firstname',
|
id: 'firstname',
|
||||||
Cell: ({...props}) => <UserInfoCell user={props.data[props.row.index]} />,
|
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) => (
|
Header: (props) => (
|
||||||
<UserCustomHeader tableProps={props} title='Payment Terms' className='min-w-125px' />
|
<UserCustomHeader tableProps={props} title='Status' className='min-w-125px' />
|
||||||
),
|
),
|
||||||
id: 'payment_month',
|
id: 'status',
|
||||||
Cell: ({...props}) => <PaymentMonthCell payment_month={props.data[props.row.index].payment_month} />,
|
Cell: ({...props}) => <Status status={props.data[props.row.index].status} />,
|
||||||
},
|
|
||||||
{
|
|
||||||
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) => (
|
Header: (props) => (
|
||||||
|
|||||||
@@ -16,70 +16,39 @@ const AuthLayout = () => {
|
|||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
return (
|
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 h-100vh"
|
||||||
|
style={{backgroundImage: 'url(../../../../public/media/auth/digifi_bko_home.jpg)', backgroundRepeat: 'none', backgroundSize: 'cover'}}
|
||||||
|
>
|
||||||
{/* begin::Body */}
|
{/* begin::Body */}
|
||||||
<div className="d-flex flex-column flex-lg-row-fluid w-lg-50 p-10 order-2 order-lg-1">
|
<div className="flex flex-column align-items-center w-lg-50 p-10">
|
||||||
{/* begin::Form */}
|
{/* 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 */}
|
{/* begin::Wrapper */}
|
||||||
<div className="w-lg-500px p-10">
|
<div className="w-lg-500px p-10 bg-white shadow-sm">
|
||||||
|
{/* 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 />
|
<Outlet />
|
||||||
</div>
|
</div>
|
||||||
{/* end::Wrapper */}
|
{/* end::Wrapper */}
|
||||||
</div>
|
</div>
|
||||||
{/* end::Form */}
|
{/* 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>
|
</div>
|
||||||
{/* end::Body */}
|
{/* 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>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -2,7 +2,8 @@ import React, { useEffect, useState } from 'react'
|
|||||||
import {useLocation, useNavigate} from 'react-router-dom'
|
import {useLocation, useNavigate} from 'react-router-dom'
|
||||||
import { Content } from '../../../../_digifi/layout/components/content'
|
import { Content } from '../../../../_digifi/layout/components/content'
|
||||||
import { ToolbarWrapper } from '../../../../_digifi/layout/components/toolbar'
|
import { ToolbarWrapper } from '../../../../_digifi/layout/components/toolbar'
|
||||||
import { UsersListLoading } from '../user-started/components/loading/UsersListLoading'
|
import { CustomLoading, UsersListLoading } from '../user-started/components/loading/UsersListLoading'
|
||||||
|
import { getVerifiedLoanDetailsByUID } from '../core/_requests'
|
||||||
|
|
||||||
export default function ApproveRejectPage() {
|
export default function ApproveRejectPage() {
|
||||||
const {state:{selectedUser}} = useLocation()
|
const {state:{selectedUser}} = useLocation()
|
||||||
@@ -10,6 +11,8 @@ export default function ApproveRejectPage() {
|
|||||||
|
|
||||||
const [requestStatus, setRequestStatus] = useState<any>({loading:false, status:false, data:null})
|
const [requestStatus, setRequestStatus] = useState<any>({loading:false, status:false, data:null})
|
||||||
|
|
||||||
|
const [loanDetails, setLoanDetails] = useState<any>({loading:true, data:null})
|
||||||
|
|
||||||
const handleSubmit = ():any => {
|
const handleSubmit = ():any => {
|
||||||
setRequestStatus({loading:true, status:false, data:null})
|
setRequestStatus({loading:true, status:false, data:null})
|
||||||
setTimeout(()=>{
|
setTimeout(()=>{
|
||||||
@@ -19,59 +22,109 @@ export default function ApproveRejectPage() {
|
|||||||
|
|
||||||
useEffect(()=>{
|
useEffect(()=>{
|
||||||
if(!selectedUser){
|
if(!selectedUser){
|
||||||
navigate('/', {replace:true})
|
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 (
|
return (
|
||||||
<>
|
<>
|
||||||
{/* <ToolbarWrapper /> */}
|
{/* <ToolbarWrapper /> */}
|
||||||
<Content>
|
<Content>
|
||||||
<div className='w-100'>
|
{loanDetails.loading ?
|
||||||
<h3 className='py-3 py-xl-5 card-title text-gray-800 fw-bold'>Processing: {selectedUser?.uid}</h3>
|
<div className='position-relative w-100 vh-100'>
|
||||||
|
<CustomLoading />
|
||||||
</div>
|
</div>
|
||||||
{/* begin::Row */}
|
:
|
||||||
<div className="row g-5 g-xl-10 mb-5 mb-xl-10">
|
<>
|
||||||
{/* begin::Col */}
|
<div className='w-100'>
|
||||||
<div className="col-xl-6 mb-md-5 mb-xl-10">
|
<h3 className='py-3 py-xl-5 card-title text-gray-800 fw-bold'>Processing: {selectedUser?.uid}</h3>
|
||||||
<div className="card card-flash flex flex-col justify-content-between p-4 h-md-50 mb-5 mb-xl-10 bg-secondary">
|
</div>
|
||||||
<h3 className='card-title text-gray-800 fw-bold'>Process Loan</h3>
|
{/* begin::Row */}
|
||||||
<div className='w-100 d-flex justify-content-between'>
|
<div className="row g-5 g-xl-10 mb-5 mb-xl-10">
|
||||||
<button
|
{/* begin::Col */}
|
||||||
className='btn btn-light btn-active-light-secondary text-success btn-lg'
|
<div className="col-xl-6 mb-md-5 mb-xl-10">
|
||||||
onClick={()=>navigate('/loan/pages/process/verified', {replace:true})}
|
<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>
|
||||||
Return
|
<div className='w-100 d-flex justify-content-between'>
|
||||||
</button>
|
<button
|
||||||
<button
|
className='btn btn-light btn-active-light-secondary text-success btn-lg'
|
||||||
className='btn btn-light btn-active-light-secondary text-danger btn-lg'
|
onClick={()=>navigate('/loan/pages/process/verified', {replace:true})}
|
||||||
onClick={handleSubmit}
|
>
|
||||||
>
|
Return
|
||||||
Reject
|
</button>
|
||||||
</button>
|
<button
|
||||||
<button
|
className='btn btn-light btn-active-light-secondary text-danger btn-lg'
|
||||||
className='btn btn-light btn-active-light-secondary text-primary btn-lg'
|
onClick={handleSubmit}
|
||||||
onClick={handleSubmit}
|
>
|
||||||
>
|
Reject
|
||||||
Approve
|
</button>
|
||||||
</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>
|
||||||
</div>
|
</div>
|
||||||
<div className="card card-flash flex flex-col justify-content-between p-4 h-md-50 mb-5 mb-xl-10 bg-secondary">
|
{/* end::Col */}
|
||||||
<h3 className='card-title text-gray-800 fw-bold'>Verification details</h3>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{/* end::Col */}
|
|
||||||
|
|
||||||
{/* begin::Col */}
|
{/* begin::Col */}
|
||||||
<div className="col-xl-6">
|
<div className="col-xl-6">
|
||||||
<div className="card card-flash flex flex-col justify-content-between p-4 h-md-100 bg-secondary">
|
<div className="card card-flash flex flex-col p-4 h-md-100 bg-secondary">
|
||||||
<h3 className='card-title text-gray-800 fw-bold'>Loan Details</h3>
|
<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>
|
</div>
|
||||||
</div>
|
{/* end::Col */}
|
||||||
{/* end::Col */}
|
</div>
|
||||||
</div>
|
{/* end::Row */}
|
||||||
{/* end::Row */}
|
</>
|
||||||
|
}
|
||||||
</Content>
|
</Content>
|
||||||
{requestStatus.loading && <UsersListLoading />}
|
{requestStatus.loading && <UsersListLoading />}
|
||||||
</>
|
</>
|
||||||
|
|||||||
@@ -33,8 +33,57 @@ export type User = {
|
|||||||
employer_name?: string
|
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 UsersQueryResponse = Response<Array<User>>
|
||||||
|
|
||||||
|
export type VerifiedLoanDetailsResponse = Response<VerifiedLoanDetails>
|
||||||
|
|
||||||
export const initialUser: User = {
|
export const initialUser: User = {
|
||||||
avatar: 'avatars/300-6.jpg',
|
avatar: 'avatars/300-6.jpg',
|
||||||
position: 'Art Director',
|
position: 'Art Director',
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import axios, { AxiosResponse } from "axios";
|
import axios, { AxiosResponse } from "axios";
|
||||||
import { ID, Response } from "../../../../_digifi/helpers"
|
import { ID, Response } from "../../../../_digifi/helpers"
|
||||||
import { User, UsersQueryResponse } from "./_models";
|
import { User, UsersQueryResponse, VerifiedLoanDetailsResponse } from "./_models";
|
||||||
import { postAuxEnd } from "../../auth/core/AxiosCallHelper";
|
import { postAuxEnd, getAuxEnd } from "../../auth/core/AxiosCallHelper";
|
||||||
|
|
||||||
const API_URL = import.meta.env.VITE_APP_THEME_API_URL;
|
const API_URL = import.meta.env.VITE_APP_THEME_API_URL;
|
||||||
const USER_URL = `${API_URL}/user`;
|
const USER_URL = `${API_URL}/user`;
|
||||||
@@ -54,6 +54,10 @@ const employersVerify = (uid: ID): Promise<UsersQueryResponse> => { // FUNCTION
|
|||||||
return postAuxEnd('/employers/verify', {application_uid:uid})
|
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> => {
|
const getUserById = (id: ID): Promise<User | undefined> => {
|
||||||
return axios
|
return axios
|
||||||
.get(`${USER_URL}/${id}`)
|
.get(`${USER_URL}/${id}`)
|
||||||
@@ -92,6 +96,8 @@ export {
|
|||||||
getVerifiedUsers,
|
getVerifiedUsers,
|
||||||
getApprovedUsers,
|
getApprovedUsers,
|
||||||
employersVerify,
|
employersVerify,
|
||||||
|
getVerifiedLoanDetailsByUID,
|
||||||
|
|
||||||
deleteUser,
|
deleteUser,
|
||||||
deleteSelectedUsers,
|
deleteSelectedUsers,
|
||||||
getUserById,
|
getUserById,
|
||||||
|
|||||||
@@ -15,4 +15,21 @@ const UsersListLoading = () => {
|
|||||||
return <div style={{...styles, position: 'absolute', textAlign: 'center'}}>Processing...</div>
|
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}
|
||||||
|
|||||||
@@ -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">
|
<div className="col-md-6 col-lg-6 col-xl-6 col-xxl-3 mb-md-5 mb-xl-10">
|
||||||
<CardsWidget20
|
<CardsWidget20
|
||||||
className="h-md-50 mb-5 mb-xl-10"
|
className="h-md-50 mb-5 mb-xl-10"
|
||||||
description="Active Projects"
|
description="Active Applications"
|
||||||
color="#F1416C"
|
color="#F1416C"
|
||||||
img={toAbsoluteUrl('media/patterns/vector-1.png')}
|
img={toAbsoluteUrl('media/patterns/vector-1.png')}
|
||||||
|
dashData={dashDetails}
|
||||||
/>
|
/>
|
||||||
<CardsWidget7
|
<CardsWidget7
|
||||||
className="h-md-50 mb-5 mb-xl-10"
|
className="h-md-50 mb-5 mb-xl-10"
|
||||||
description="Professionals"
|
description="Todays Applications"
|
||||||
icon={false}
|
icon={false}
|
||||||
stats={357}
|
stats={357}
|
||||||
labelColor="dark"
|
labelColor="dark"
|
||||||
textColor="gray-300"
|
textColor="gray-300"
|
||||||
|
dashData={dashDetails}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
{/* end::Col */}
|
{/* end::Col */}
|
||||||
|
|
||||||
{/* begin::Col */}
|
{/* begin::Col */}
|
||||||
<div className="col-md-6 col-lg-6 col-xl-6 col-xxl-3 mb-md-5 mb-xl-10">
|
<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" />
|
<ListsWidget26 className="h-lg-50" />
|
||||||
</div>
|
</div>
|
||||||
{/* end::Col */}
|
{/* end::Col */}
|
||||||
|
|||||||
@@ -32,6 +32,20 @@ export type RecentBVNProps = {
|
|||||||
nationality?: string | null
|
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 = {
|
export type DashDataProps = {
|
||||||
loading: boolean,
|
loading: boolean,
|
||||||
@@ -39,5 +53,6 @@ export type DashDataProps = {
|
|||||||
call_return?: string
|
call_return?: string
|
||||||
recent_applications? : RecentApplicationsProps
|
recent_applications? : RecentApplicationsProps
|
||||||
recent_bvn?: RecentBVNProps
|
recent_bvn?: RecentBVNProps
|
||||||
|
dash_data? : DataProps
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Reference in New Issue
Block a user