Compare commits

..

47 Commits

Author SHA1 Message Date
victorAnumudu fb6dc18073 nav bar signout hover side menu removed 2024-05-16 15:22:41 +01:00
ameye 7d955381c7 Merge branch 'redundant-pages' of DigiFi/digifi-bko into master 2024-05-16 12:26:08 +00:00
ameye 8df8fa45db Merge branch 'logout_fix' of DigiFi/digifi-bko into master 2024-05-16 12:26:01 +00:00
victorAnumudu 4e60059290 updated pending list action menu 2024-05-15 20:52:53 +01:00
victorAnumudu c6fe8c7d5a updated pending list action menu 2024-05-15 20:52:07 +01:00
victorAnumudu 5eb07b2057 removal of redundant pages 2024-05-15 20:47:29 +01:00
victorAnumudu 3a35a34266 initial commit 2024-05-15 20:22:33 +01:00
Elias de5cb74241 restored logout 2024-05-15 19:00:39 +01:00
ameye c711e000b3 Merge branch 'route-update' of DigiFi/digifi-bko into master 2024-05-15 17:01:19 +00:00
victorAnumudu 0a28d478d8 correct loan route added 2024-05-15 17:41:41 +01:00
ameye 0d9318ddd9 Merge branch 'remove_extra_backoffice_menu' of DigiFi/digifi-bko into master 2024-05-15 08:55:29 +00:00
Elias c53c37611a remove extra backoffice menu 2024-05-14 11:00:13 +01:00
tokslaw f7d82c0958 Merge branch 'dashboard-table-adjustment' of DigiFi/digifi-bko into master 2024-05-12 00:47:30 +00:00
victorAnumudu e1535de92c adjusted table list display 2024-05-10 18:41:03 +01:00
ameye 93f3c0c526 Merge branch 'list-pagination-modified' of DigiFi/digifi-bko into master 2024-05-10 13:10:40 +00:00
victorAnumudu 4cb347cfa0 modified table pagination 2024-05-10 12:46:15 +01:00
victorAnumudu 75699342c7 modified table pagination 2024-05-10 11:58:34 +01:00
ameye f2741f3325 Merge branch 'dash-details' of DigiFi/digifi-bko into master 2024-05-10 10:45:21 +00:00
victorAnumudu 3839962e33 aplication and bvn list added 2024-05-09 07:25:29 +01:00
victorAnumudu 7cf4f9dbbe BVN verification table updated 2024-05-08 19:30:34 +01:00
victorAnumudu 2526a5b627 initial commit 2024-05-08 18:54:58 +01:00
Elias 395fe1a648 Merge branch 'master' of https://gitlab.chiefsoft.net/DigiFi/digifi-bko 2024-05-07 09:47:27 +01:00
Elias c6fe75c9f8 Remove extra section in dashboard 2024-05-07 09:46:16 +01:00
ameye 2be706c577 Merge branch 'dashboard-text-update' of DigiFi/digifi-bko into master 2024-05-06 23:57:49 +00:00
victorAnumudu 9936a912bc removed unwanted icons and text 2024-05-06 19:50:32 +01:00
Elias 0645f32e52 re-generate package-lock.json 2024-05-06 18:16:52 +01:00
ameye 269229f07c Merge branch 'nav-item-removal' of DigiFi/digifi-bko into master 2024-05-05 10:15:44 +00:00
victorAnumudu c2d353d532 added component for each list display 2024-05-04 23:08:24 +01:00
victorAnumudu de994bc681 route bug fix 2024-05-03 19:07:04 +01:00
victorAnumudu 81d99e1537 removal of unwanted nav menu item 2024-05-03 19:02:56 +01:00
ameye 1f14da1778 Merge branch 'loan-started-list' of DigiFi/digifi-bko into master 2024-05-03 10:33:00 +00:00
victorAnumudu 2eb39b773a loan started list display 2024-05-03 10:48:00 +01:00
victorAnumudu 4e97119644 initial commit 2024-05-03 04:49:59 +01:00
ameye 7bc39a449c Merge branch 'login-new-api' of DigiFi/digifi-bko into master 2024-05-02 16:59:18 +00:00
victorAnumudu f7f728c066 added new login API 2024-05-02 14:00:57 +01:00
CHIEFSOFT\ameye 3845ecbefe env 2024-05-02 05:44:34 -04:00
ameye 43b0e60db2 Merge branch 'get-loan-started' of DigiFi/digifi-bko into master 2024-05-02 09:21:44 +00:00
victorAnumudu 51e06716e5 get loan started API added 2024-05-02 10:19:38 +01:00
ameye 9d7605ebfe Merge branch 'table-headers' of DigiFi/digifi-bko into master 2024-05-01 23:52:03 +00:00
victorAnumudu 54b550b624 removed used variable 2024-05-01 23:36:11 +01:00
victorAnumudu cead31a6d1 updated table header names 2024-05-01 23:33:32 +01:00
ameye 4508743be4 Merge branch 'axios-call-helper' of DigiFi/digifi-bko into master 2024-05-01 14:00:50 +00:00
victorAnumudu 7d43300724 added axios verb call function 2024-05-01 14:42:43 +01:00
CHIEFSOFT\ameye e05d465c2f icons update 2024-04-29 16:15:32 -04:00
ameye 75a7496164 Merge branch 'page-tables' of DigiFi/digifi-bko into master 2024-04-25 17:02:41 +00:00
victorAnumudu 25b48416b7 page dummy table 2024-04-25 16:48:09 +01:00
ameye 996eee612d Merge branch 'help-component' of DigiFi/digifi-bko into master 2024-04-25 13:16:55 +00:00
184 changed files with 15487 additions and 4313 deletions
+4 -1
View File
@@ -11,4 +11,7 @@ VITE_APP_PURCHASE_URL=https://www.digifi.com/
VITE_APP_PREVIEW_URL=https://www.digifi.com/demo1/
VITE_APP_PREVIEW_REACT_URL=https://www.digifi.com
VITE_APP_PREVIEW_DOCS_URL=https://www.digifi.com/
VITE_APP_THEME_API_URL=https://api.digifi/api/api
VITE_APP_THEME_API_URL=https://api.digifi/api/api
# CUSTOM ENV VARIABLES ADDED
VITE_APP_USER_ENDPOINT=https://digifi-apidev.chiefsoft.net/digibko/v1
+12 -9
View File
@@ -1,14 +1,17 @@
PORT=3011
VITE_APP_BASE_LAYOUT_CONFIG_KEY='metronic-react-demo1-8150'
VITE_APP_API_URL=https://preview.keenthemes.com/metronic8/laravel/api
VITE_APP_BASE_LAYOUT_CONFIG_KEY='digifi-agent'
VITE_APP_API_URL=https://dev-agents.digifi.com
VITE_APP_VERSION=v8.2.3
VITE_APP_THEME_NAME=WrenchBoard
VITE_APP_THEME_DEMO=demo1
VITE_APP_THEME_NAME=digifi
VITE_APP_THEME_DEMO=digifi
VITE_APP_BOOTSTRAP_DOCS_LINK=https://getbootstrap.com/docs/5.0
VITE_APP_SASS_PATH=src/_digifi/assets/sass/core/components
VITE_APP_SASS_VARIABLES_PATH=src/_digifi/assets/sass/core/components/_variables.scss
VITE_APP_PURCHASE_URL=https://themeforest.net/item/metronic-responsive-admin-dashboard-template/4021469
VITE_APP_PREVIEW_URL=https://preview.keenthemes.com/metronic8/react/demo1/
VITE_APP_PREVIEW_REACT_URL=https://preview.keenthemes.com/metronic8/react
VITE_APP_PREVIEW_DOCS_URL=https://preview.keenthemes.com/metronic8/react/docs
VITE_APP_THEME_API_URL=https://preview.keenthemes.com/theme-api/api
VITE_APP_PURCHASE_URL=https://www.digifi.com/
VITE_APP_PREVIEW_URL=https://www.digifi.com/demo1/
VITE_APP_PREVIEW_REACT_URL=https://www.digifi.com
VITE_APP_PREVIEW_DOCS_URL=https://www.digifi.com/
VITE_APP_THEME_API_URL=https://api.digifi/api/api
# CUSTOM ENV VARIABLES ADDED
VITE_APP_USER_ENDPOINT=https://digifi-apidev.chiefsoft.net/digibko/v1
+12 -9
View File
@@ -1,14 +1,17 @@
PORT=3011
VITE_APP_BASE_LAYOUT_CONFIG_KEY='metronic-react-demo1-8150'
VITE_APP_API_URL=https://preview.keenthemes.com/metronic8/laravel/api
VITE_APP_BASE_LAYOUT_CONFIG_KEY='digifi-agent'
VITE_APP_API_URL=https://dev-agents.digifi.com
VITE_APP_VERSION=v8.2.3
VITE_APP_THEME_NAME=DigiFi
VITE_APP_THEME_DEMO=demo1
VITE_APP_THEME_NAME=digifi
VITE_APP_THEME_DEMO=digifi
VITE_APP_BOOTSTRAP_DOCS_LINK=https://getbootstrap.com/docs/5.0
VITE_APP_SASS_PATH=src/_digifi/assets/sass/core/components
VITE_APP_SASS_VARIABLES_PATH=src/_digifi/assets/sass/core/components/_variables.scss
VITE_APP_PURCHASE_URL=https://themeforest.net/item/metronic-responsive-admin-dashboard-template/4021469
VITE_APP_PREVIEW_URL=https://preview.keenthemes.com/metronic8/react/demo1/
VITE_APP_PREVIEW_REACT_URL=https://preview.keenthemes.com/metronic8/react
VITE_APP_PREVIEW_DOCS_URL=https://preview.keenthemes.com/metronic8/react/docs
VITE_APP_THEME_API_URL=https://preview.keenthemes.com/theme-api/api
VITE_APP_PURCHASE_URL=https://www.digifi.com/
VITE_APP_PREVIEW_URL=https://www.digifi.com/demo1/
VITE_APP_PREVIEW_REACT_URL=https://www.digifi.com
VITE_APP_PREVIEW_DOCS_URL=https://www.digifi.com/
VITE_APP_THEME_API_URL=https://api.digifi/api/api
# CUSTOM ENV VARIABLES ADDED
VITE_APP_USER_ENDPOINT=https://digifi-apidev.chiefsoft.net/digibko/v1
+6037 -3512
View File
File diff suppressed because it is too large Load Diff
Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 15 KiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 18 KiB

After

Width:  |  Height:  |  Size: 6.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 84 KiB

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

After

Width:  |  Height:  |  Size: 6.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 713 B

After

Width:  |  Height:  |  Size: 490 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.6 KiB

After

Width:  |  Height:  |  Size: 950 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 15 KiB

+2 -2
View File
@@ -75,7 +75,7 @@ function groupingOnSelect(
function groupingOnSelectAll<T>(
isAllSelected: boolean,
setSelected: Dispatch<SetStateAction<Array<ID>>>,
data?: Array<T & {id?: ID}>
data?: Array<T & {uid?: ID}>
) {
if (isAllSelected) {
setSelected([])
@@ -86,7 +86,7 @@ function groupingOnSelectAll<T>(
return
}
setSelected(data.filter((item) => item.id).map((item) => item.id))
setSelected(data.filter((item) => item.uid).map((item) => item.uid))
}
// Hook
+2 -1
View File
@@ -1,6 +1,6 @@
import {Dispatch, SetStateAction} from 'react'
export type ID = undefined | null | number
export type ID = undefined | null | number | string
export type PaginationState = {
page: number
@@ -23,6 +23,7 @@ export type SearchState = {
export type Response<T> = {
data?: T
records?: Array<any>
payload?: {
message?: string
errors?: {
+39 -29
View File
@@ -1,29 +1,34 @@
import clsx from 'clsx'
import {KTIcon, toAbsoluteUrl} from '../../../helpers'
import {HeaderNotificationsMenu, HeaderUserMenu, Search, ThemeModeSwitcher} from '../../../partials'
import {useLayout} from '../../core'
import clsx from 'clsx';
import { KTIcon, toAbsoluteUrl } from '../../../helpers';
import {
HeaderNotificationsMenu,
HeaderUserMenu,
Search,
ThemeModeSwitcher,
} from '../../../partials';
import { useLayout } from '../../core';
const itemClass = 'ms-1 ms-md-4'
const itemClass = 'ms-1 ms-md-4';
const btnClass =
'btn btn-icon btn-custom btn-icon-muted btn-active-light btn-active-color-primary w-35px h-35px'
const userAvatarClass = 'symbol-35px'
const btnIconClass = 'fs-2'
'btn btn-icon btn-custom btn-icon-muted btn-active-light btn-active-color-primary w-35px h-35px';
const userAvatarClass = 'symbol-35px';
const btnIconClass = 'fs-2';
const Navbar = () => {
const {config} = useLayout()
const { config } = useLayout();
return (
<div className='app-navbar flex-shrink-0'>
<div className={clsx('app-navbar-item align-items-stretch', itemClass)}>
<div className="app-navbar flex-shrink-0">
{/* <div className={clsx('app-navbar-item align-items-stretch', itemClass)}>
<Search />
</div>
</div> */}
<div className={clsx('app-navbar-item', itemClass)}>
{/* <div className={clsx('app-navbar-item', itemClass)}>
<div id='kt_activities_toggle' className={btnClass}>
<KTIcon iconName='chart-simple' className={btnIconClass} />
</div>
</div>
</div> */}
<div className={clsx('app-navbar-item', itemClass)}>
{/* <div className={clsx('app-navbar-item', itemClass)}>
<div
data-kt-menu-trigger="{default: 'click'}"
data-kt-menu-attach='parent'
@@ -33,43 +38,48 @@ const Navbar = () => {
<KTIcon iconName='element-plus' className={btnIconClass} />
</div>
<HeaderNotificationsMenu />
</div>
</div> */}
<div className={clsx('app-navbar-item', itemClass)}>
{/* <div className={clsx('app-navbar-item', itemClass)}>
<div className={clsx('position-relative', btnClass)} id='kt_drawer_chat_toggle'>
<KTIcon iconName='message-text-2' className={btnIconClass} />
<span className='bullet bullet-dot bg-success h-6px w-6px position-absolute translate-middle top-0 start-50 animation-blink' />
</div>
</div>
</div> */}
<div className={clsx('app-navbar-item', itemClass)}>
<ThemeModeSwitcher toggleBtnClass={clsx('btn-active-light-primary btn-custom')} />
<ThemeModeSwitcher
toggleBtnClass={clsx('btn-active-light-primary btn-custom')}
/>
</div>
<div className={clsx('app-navbar-item', itemClass)}>
<div
className={clsx('cursor-pointer symbol', userAvatarClass)}
data-kt-menu-trigger="{default: 'click'}"
data-kt-menu-attach='parent'
data-kt-menu-placement='bottom-end'
data-kt-menu-attach="parent"
data-kt-menu-placement="bottom-end"
>
<img src={toAbsoluteUrl('media/avatars/300-3.jpg')} alt='' />
<img src={toAbsoluteUrl('media/avatars/300-3.jpg')} alt="" />
</div>
<HeaderUserMenu />
</div>
{config.app?.header?.default?.menu?.display && (
<div className='app-navbar-item d-lg-none ms-2 me-n3' title='Show header menu'>
<div
className="app-navbar-item d-lg-none ms-2 me-n3"
title="Show header menu"
>
<div
className='btn btn-icon btn-active-color-primary w-35px h-35px'
id='kt_app_header_menu_toggle'
className="btn btn-icon btn-active-color-primary w-35px h-35px"
id="kt_app_header_menu_toggle"
>
<KTIcon iconName='text-align-left' className={btnIconClass} />
<KTIcon iconName="text-align-left" className={btnIconClass} />
</div>
</div>
)}
</div>
)
}
);
};
export {Navbar}
export { Navbar };
@@ -0,0 +1,140 @@
import { ReactNode, useEffect, useState } from "react";
import { RecentBVNProps } from "../../../../app/pages/dashboard/model";
type PaginatedListProps = {
data: RecentBVNProps,
itemsPerPage?: number,
filterItem?: string[],
tableTitle?: string,
titleClass?:string,
children: (data:RecentBVNProps) => ReactNode;
}
export default function RecentBVNList({
data,
itemsPerPage = 5,
filterItem,
tableTitle,
titleClass,
children,
}:PaginatedListProps) {
const [searchTerm, setSearchTerm] = useState("");
const [filteredData, setFilteredData] = useState(data);
const [currentPage, setCurrentPage] = useState(0);
const [newData, setNewData] = useState<any>([]);
const numberOfSelection = itemsPerPage;
const handlePrev = () => {
if (currentPage != 0) {
setCurrentPage((prev) => prev - numberOfSelection);
}
};
const handleNext = () => {
if (currentPage < data.length) {
setCurrentPage((prev) => prev + numberOfSelection);
}
};
const handleSearch = ({ target: { value } }:{target: {value:string}}, name:string) => {
setSearchTerm(value);
let newFilteredData:any = data.filter((item:any) =>
item[name].toLowerCase().startsWith(value.toLowerCase())
);
setFilteredData(newFilteredData);
setCurrentPage(0);
};
useEffect(() => {
setNewData(
filteredData?.slice(currentPage, numberOfSelection + currentPage)
);
}, [currentPage, filteredData]);
useEffect(()=>{
setCurrentPage(0)
},[itemsPerPage])
return (
<div className="w-full d-flex flex-column h-100">
<h1 className={`text-2xl mb-5 font-semibold ${titleClass && titleClass}`}>{tableTitle}</h1>
{data.length > 0 && filterItem && (
<div className="mb-10 flex justify-end items-center gap-2">
{filterItem.map((item, index) => (
<label
key={index}
className="flex flex-col sm:flex-row items-center gap-2 text-slate-600 dark:text-slate-100 transition-all duration-500"
>
Search by {item[0].toUpperCase() + item.slice(1)}
<input
name={item}
type="text"
className="py-1 px-2 text-sm min-w-[100px] text-black dark:text-white bg-white dark:bg-slate-800 rounded-full border-0 outline-none ring-1 ring-slate-300 dark:ring-white transition-all duration-500"
value={searchTerm}
onChange={(e) => {
handleSearch(e, item);
}}
/>
</label>
))}
</div>
)}
{children(newData)}
{/* show prev and next button if data exist */}
{(data.length > 0 && data.length > itemsPerPage) && (
<div className="w-full h-100 d-flex gap-4 justify-content-center align-items-end">
<button
onClick={handlePrev}
className={`text-sm md:text-lg d-flex justify-content-center align-items-center border-1 transition-all duration-300 ${
currentPage == 0
? "text-slate-400 border-slate-400 dark:text-slate-400 dark:border-slate-400 pe-none"
: "text-slate-600 border-slate-600 dark:text-white dark:border-white"
}`}
disabled={currentPage == 0}
// style={{width:'30px', height:'30px'}}
>
&lt; Previous
</button>
{/* {data.length && data.map((item, index)=>{
item = item
if(index%itemsPerPage == 0 && index >= currentPage && index <= currentPage+itemsPerPage){
return (
<button
key={index}
onClick={handleNext}
className={`text-sm md:text-lg rounded-circle d-flex justify-content-center align-items-center border-1 transition-all duration-300 ${
currentPage != index
? "text-slate-400 border-slate-400 dark:text-slate-400 dark:border-slate-400"
: "text-slate-600 border-slate-600 dark:text-white dark:border-white pe-none"
}`}
disabled={currentPage != index}
style={{width:'30px', height:'30px'}}
>
{index/itemsPerPage +1}
</button>
)
}
})} */}
<button
onClick={handleNext}
className={`text-sm md:text-lg d-flex justify-content-center align-items-center border-1 transition-all duration-300 ${
currentPage + numberOfSelection >= data.length
? "text-slate-400 border-slate-400 dark:text-slate-400 dark:border-slate-400 pe-none"
: "text-slate-600 border-slate-600 dark:text-white dark:border-white"
}`}
disabled={currentPage + numberOfSelection >= data.length}
// style={{width:'30px', height:'30px'}}
>
Next &gt;
</button>
</div>
)}
</div>
);
}
@@ -0,0 +1,140 @@
import { ReactNode, useEffect, useState } from "react";
import { RecentApplicationsProps } from "../../../../app/pages/dashboard/model";
type PaginatedListProps = {
data: RecentApplicationsProps,
itemsPerPage?: number,
filterItem?: string[],
tableTitle?: string,
titleClass?:string,
children: (data:RecentApplicationsProps) => ReactNode;
}
export default function RecentLoanAppList({
data,
itemsPerPage = 5,
filterItem,
tableTitle,
titleClass,
children,
}:PaginatedListProps) {
const [searchTerm, setSearchTerm] = useState("");
const [filteredData, setFilteredData] = useState(data);
const [currentPage, setCurrentPage] = useState(0);
const [newData, setNewData] = useState<any>([]);
const numberOfSelection = itemsPerPage;
const handlePrev = () => {
if (currentPage != 0) {
setCurrentPage((prev) => prev - numberOfSelection);
}
};
const handleNext = () => {
if (currentPage < data.length) {
setCurrentPage((prev) => prev + numberOfSelection);
}
};
const handleSearch = ({ target: { value } }:{target: {value:string}}, name:string) => {
setSearchTerm(value);
let newFilteredData:any = data.filter((item:any) =>
item[name].toLowerCase().startsWith(value.toLowerCase())
);
setFilteredData(newFilteredData);
setCurrentPage(0);
};
useEffect(() => {
setNewData(
filteredData?.slice(currentPage, numberOfSelection + currentPage)
);
}, [currentPage, filteredData]);
useEffect(()=>{
setCurrentPage(0)
},[itemsPerPage])
return (
<div className="w-full d-flex flex-column h-100">
<h1 className={`text-2xl mb-5 font-semibold ${titleClass && titleClass}`}>{tableTitle}</h1>
{data.length > 0 && filterItem && (
<div className="mb-10 flex justify-end items-center gap-2">
{filterItem.map((item, index) => (
<label
key={index}
className="flex flex-col sm:flex-row items-center gap-2 text-slate-600 dark:text-slate-100 transition-all duration-500"
>
Search by {item[0].toUpperCase() + item.slice(1)}
<input
name={item}
type="text"
className="py-1 px-2 text-sm min-w-[100px] text-black dark:text-white bg-white dark:bg-slate-800 rounded-full border-0 outline-none ring-1 ring-slate-300 dark:ring-white transition-all duration-500"
value={searchTerm}
onChange={(e) => {
handleSearch(e, item);
}}
/>
</label>
))}
</div>
)}
{children(newData)}
{/* show prev and next button if data exist */}
{(data.length > 0 && data.length > itemsPerPage) && (
<div className="w-full h-100 d-flex gap-4 justify-content-center align-items-end">
<button
onClick={handlePrev}
className={`text-sm md:text-lg d-flex justify-content-center align-items-center border-1 transition-all duration-300 ${
currentPage == 0
? "text-slate-400 border-slate-400 dark:text-slate-400 dark:border-slate-400 pe-none"
: "text-slate-600 border-slate-600 dark:text-white dark:border-white"
}`}
disabled={currentPage == 0}
// style={{width:'30px', height:'30px'}}
>
&lt; Previous
</button>
{/* {data.length && data.map((item, index)=>{
item = item
if(index%itemsPerPage == 0 && index >= currentPage && index <= currentPage+itemsPerPage){
return (
<button
key={index}
onClick={handleNext}
className={`text-sm md:text-lg rounded-circle d-flex justify-content-center align-items-center border-1 transition-all duration-300 ${
currentPage != index
? "text-slate-400 border-slate-400 dark:text-slate-400 dark:border-slate-400"
: "text-slate-600 border-slate-600 dark:text-white dark:border-white pe-none"
}`}
disabled={currentPage != index}
style={{width:'30px', height:'30px'}}
>
{index/itemsPerPage +1}
</button>
)
}
})} */}
<button
onClick={handleNext}
className={`text-sm md:text-lg d-flex justify-content-center align-items-center border-1 transition-all duration-300 ${
currentPage + numberOfSelection >= data.length
? "text-slate-400 border-slate-400 dark:text-slate-400 dark:border-slate-400 pe-none"
: "text-slate-600 border-slate-600 dark:text-white dark:border-white"
}`}
disabled={currentPage + numberOfSelection >= data.length}
// style={{width:'30px', height:'30px'}}
>
Next &gt;
</button>
</div>
)}
</div>
);
}
@@ -16,7 +16,7 @@ const ToolbarClassic = () => {
<div className='d-flex align-items-center gap-2 gap-lg-3'>
{config.app?.toolbar?.filterButton && (
<div className='m-0'>
<a
{/* <a
href='#'
className={clsx('btn btn-sm btn-flex fw-bold', daterangepickerButtonClass)}
data-kt-menu-trigger='click'
@@ -25,7 +25,7 @@ const ToolbarClassic = () => {
<KTIcon iconName='filter' className='fs-6 text-muted me-1' />
Filter
</a>
<Dropdown1 />
<Dropdown1 /> */}
</div>
)}
@@ -49,7 +49,7 @@ const ToolbarClassic = () => {
</a>
)}
{config.app?.toolbar?.primaryButton && (
{/* {config.app?.toolbar?.primaryButton && (
<a
href='#'
onClick={() => setShowCreateAppModal(true)}
@@ -57,7 +57,7 @@ const ToolbarClassic = () => {
>
Create
</a>
)}
)} */}
<CreateAppModal show={showCreateAppModal} handleClose={() => setShowCreateAppModal(false)} />
</div>
)
+24
View File
@@ -0,0 +1,24 @@
// FUNCTION TO RETURN AMOUNT TO TWO DECIMAL PLACES
export const AmountTo2DP = (
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;
};
+18
View File
@@ -0,0 +1,18 @@
export function NewDateTimeFormatter(isoDateString:any, addHour = true) {
const date = new Date(isoDateString);
if (addHour) {
date.setTime(date.getTime() + 1 * 60 * 60 * 1000);
}
const formattedDate = date.toLocaleDateString("en-US", {
year: "numeric",
month: "numeric",
day: "numeric",
hour: "2-digit",
minute: "2-digit",
// second: "2-digit",
hour12: true,
timeZone: "UTC",
});
return formattedDate;
}
@@ -1,64 +1,63 @@
import {FC} from 'react'
import {Link} from 'react-router-dom'
import {useAuth} from '../../../../app/modules/auth'
import {Languages} from './Languages'
import {toAbsoluteUrl} from '../../../helpers'
import { FC } from 'react';
import { Link } from 'react-router-dom';
import { useAuth } from '../../../../app/modules/auth';
import { Languages } from './Languages';
import { toAbsoluteUrl } from '../../../helpers';
const HeaderUserMenu: FC = () => {
const {currentUser, logout} = useAuth()
const { currentUser, logout } = useAuth();
return (
<div
className='menu menu-sub menu-sub-dropdown menu-column menu-rounded menu-gray-600 menu-state-bg menu-state-primary fw-bold py-4 fs-6 w-275px'
data-kt-menu='true'
className="menu menu-sub menu-sub-dropdown menu-column menu-rounded menu-gray-600 menu-state-bg menu-state-primary fw-bold py-4 fs-6 w-275px"
data-kt-menu="true"
>
<div className='menu-item px-3'>
<div className='menu-content d-flex align-items-center px-3'>
<div className='symbol symbol-50px me-5'>
<img alt='Logo' src={toAbsoluteUrl('media/avatars/300-3.jpg')} />
<div className="menu-item px-3">
<div className="menu-content d-flex align-items-center px-3">
<div className="symbol symbol-50px me-5">
<img alt="Logo" src={toAbsoluteUrl('media/avatars/300-3.jpg')} />
</div>
<div className='d-flex flex-column'>
<div className='fw-bolder d-flex align-items-center fs-5'>
<div className="d-flex flex-column">
<div className="fw-bolder d-flex align-items-center fs-5">
{currentUser?.first_name} {currentUser?.first_name}
<span className='badge badge-light-success fw-bolder fs-8 px-2 py-1 ms-2'>Pro</span>
{/*<span className='badge badge-light-success fw-bolder fs-8 px-2 py-1 ms-2'>Pro</span>*/}
</div>
<a href='#' className='fw-bold text-muted text-hover-primary fs-7'>
<a href="#" className="fw-bold text-muted text-hover-primary fs-7">
{currentUser?.email}
</a>
</div>
</div>
</div>
<div className='separator my-2'></div>
{/* <div className="separator my-2"></div> */}
<div className='menu-item px-5'>
{/* <div className='menu-item px-5'>
<Link to={'/crafted/pages/profile'} className='menu-link px-5'>
My Profile
</Link>
</div>
</div> */}
<div className='menu-item px-5'>
{/* <div className='menu-item px-5'>
<a href='#' className='menu-link px-5'>
<span className='menu-text'>My Projects</span>
<span className='menu-badge'>
<span className='badge badge-light-danger badge-circle fw-bolder fs-7'>3</span>
</span>
</a>
</div>
</div> */}
<div
className='menu-item px-5'
data-kt-menu-trigger='hover'
data-kt-menu-placement='left-start'
data-kt-menu-flip='bottom'
{/* <div
className="menu-item px-5"
data-kt-menu-trigger="hover"
data-kt-menu-placement="left-start"
data-kt-menu-flip="bottom"
>
<a href='#' className='menu-link px-5'>
<span className='menu-title'>My Subscription</span>
<span className='menu-arrow'></span>
</a>
<div className='menu-sub menu-sub-dropdown w-175px py-4'>
<div className="menu-sub menu-sub-dropdown w-175px py-4">
<div className='menu-item px-3'>
<a href='#' className='menu-link px-5'>
Referrals
@@ -88,7 +87,7 @@ const HeaderUserMenu: FC = () => {
</a>
</div>
<div className='separator my-2'></div>
<div className="separator my-2"></div>
<div className='menu-item px-3'>
<div className='menu-content px-3'>
@@ -105,31 +104,31 @@ const HeaderUserMenu: FC = () => {
</div>
</div>
</div>
</div>
</div> */}
<div className='menu-item px-5'>
{/* <div className='menu-item px-5'>
<a href='#' className='menu-link px-5'>
My Statements
</a>
</div>
</div> */}
<div className='separator my-2'></div>
<div className="separator my-2"></div>
<Languages />
{/* <Languages /> */}
<div className='menu-item px-5 my-1'>
{/* <div className='menu-item px-5 my-1'>
<Link to='/crafted/account/settings' className='menu-link px-5'>
Account Settings
</Link>
</div>
</div> */}
<div className='menu-item px-5'>
<a onClick={logout} className='menu-link px-5'>
<div className="menu-item px-5">
<a onClick={logout} className="menu-link px-5">
Sign Out
</a>
</div>
</div>
)
}
);
};
export {HeaderUserMenu}
export { HeaderUserMenu };
@@ -1,20 +1,24 @@
import React from 'react'
import {KTIcon} from '../../../helpers'
import {Dropdown1} from '../../content/dropdown/Dropdown1'
import { NewDateTimeFormatter } from '../../../lib/NewDateTimeFormatter'
// import {KTIcon} from '../../../helpers'
// import {Dropdown1} from '../../content/dropdown/Dropdown1'
import { DashDataProps, RecentBVNProps } from '../../../../app/pages/dashboard/model'
import RecentBVNList from '../../../layout/components/paginatedListing/RecentBVNList'
type Props = {
className: string
dashData?: DashDataProps
}
const ListsWidget3: React.FC<Props> = ({className}) => {
const ListsWidget3: React.FC<Props> = ({dashData, className}) => {
return (
<div className={`card ${className}`}>
{/* begin::Header */}
<div className='card-header border-0'>
<h3 className='card-title fw-bold text-gray-900'>Todo</h3>
<div className='card-toolbar'>
{/* begin::Menu */}
<h3 className='card-title fw-bold text-gray-900'>BVN Verification</h3>
{/* begin::Menu */}
{/* <div className='card-toolbar'>
<button
type='button'
className='btn btn-sm btn-icon btn-color-primary btn-active-light-primary'
@@ -25,138 +29,49 @@ const ListsWidget3: React.FC<Props> = ({className}) => {
<KTIcon iconName='category' className='fs-2' />
</button>
<Dropdown1 />
{/* end::Menu */}
</div>
</div> */}
{/* end::Menu */}
</div>
{/* end::Header */}
{/* begin::Body */}
<div className='card-body pt-2'>
{/* begin::Item */}
<div className='d-flex align-items-center mb-8'>
{/* begin::Bullet */}
<span className='bullet bullet-vertical h-40px bg-success'></span>
{/* end::Bullet */}
{/* begin::Checkbox */}
<div className='form-check form-check-custom form-check-solid mx-5'>
<input className='form-check-input' type='checkbox' value='' />
</div>
{/* end::Checkbox */}
{/* begin::Description */}
<div className='flex-grow-1'>
<a href='#' className='text-gray-800 text-hover-primary fw-bold fs-6'>
Create FireStone Logo
</a>
<span className='text-muted fw-semibold d-block'>Due in 2 Days</span>
</div>
{/* end::Description */}
<span className='badge badge-light-success fs-8 fw-bold'>New</span>
</div>
{/* end:Item */}
{/* begin::Item */}
<div className='d-flex align-items-center mb-8'>
{/* begin::Bullet */}
<span className='bullet bullet-vertical h-40px bg-primary'></span>
{/* end::Bullet */}
{/* begin::Checkbox */}
<div className='form-check form-check-custom form-check-solid mx-5'>
<input className='form-check-input' type='checkbox' value='' />
</div>
{/* end::Checkbox */}
{/* begin::Description */}
<div className='flex-grow-1'>
<a href='#' className='text-gray-800 text-hover-primary fw-bold fs-6'>
Stakeholder Meeting
</a>
<span className='text-muted fw-semibold d-block'>Due in 3 Days</span>
</div>
{/* end::Description */}
<span className='badge badge-light-primary fs-8 fw-bold'>New</span>
</div>
{/* end:Item */}
{/* begin::Item */}
<div className='d-flex align-items-center mb-8'>
{/* begin::Bullet */}
<span className='bullet bullet-vertical h-40px bg-warning'></span>
{/* end::Bullet */}
{/* begin::Checkbox */}
<div className='form-check form-check-custom form-check-solid mx-5'>
<input className='form-check-input' type='checkbox' value='' />
</div>
{/* end::Checkbox */}
{/* begin::Description */}
<div className='flex-grow-1'>
<a href='#' className='text-gray-800 text-hover-primary fw-bold fs-6'>
Scoping &amp; Estimations
</a>
<span className='text-muted fw-semibold d-block'>Due in 5 Days</span>
</div>
{/* end::Description */}
<span className='badge badge-light-warning fs-8 fw-bold'>New</span>
</div>
{/* end:Item */}
{/* begin::Item */}
<div className='d-flex align-items-center mb-8'>
{/* begin::Bullet */}
<span className='bullet bullet-vertical h-40px bg-primary'></span>
{/* end::Bullet */}
{/* begin::Checkbox */}
<div className='form-check form-check-custom form-check-solid mx-5'>
<input className='form-check-input' type='checkbox' value='' />
</div>
{/* end::Checkbox */}
{/* begin::Description */}
<div className='flex-grow-1'>
<a href='#' className='text-gray-800 text-hover-primary fw-bold fs-6'>
KPI App Showcase
</a>
<span className='text-muted fw-semibold d-block'>Due in 2 Days</span>
</div>
{/* end::Description */}
<span className='badge badge-light-primary fs-8 fw-bold'>New</span>
</div>
{/* end:Item */}
{/* begin::Item */}
<div className='d-flex align-items-center mb-8'>
{/* begin::Bullet */}
<span className='bullet bullet-vertical h-40px bg-danger'></span>
{/* end::Bullet */}
{/* begin::Checkbox */}
<div className='form-check form-check-custom form-check-solid mx-5'>
<input className='form-check-input' type='checkbox' value='' />
</div>
{/* end::Checkbox */}
{/* begin::Description */}
<div className='flex-grow-1'>
<a href='#' className='text-gray-800 text-hover-primary fw-bold fs-6'>
Project Meeting
</a>
<span className='text-muted fw-semibold d-block'>Due in 12 Days</span>
</div>
{/* end::Description */}
<span className='badge badge-light-danger fs-8 fw-bold'>New</span>
</div>
{/* end:Item */}
{/* begin::Item */}
<div className='d-flex align-items-center'>
{/* begin::Bullet */}
<span className='bullet bullet-vertical h-40px bg-success'></span>
{/* end::Bullet */}
{/* begin::Checkbox */}
<div className='form-check form-check-custom form-check-solid mx-5'>
<input className='form-check-input' type='checkbox' value='' />
</div>
{/* end::Checkbox */}
{/* begin::Description */}
<div className='flex-grow-1'>
<a href='#' className='text-gray-800 text-hover-primary fw-bold fs-6'>
Customers Update
</a>
<span className='text-muted fw-semibold d-block'>Due in 1 week</span>
</div>
{/* end::Description */}
<span className='badge badge-light-success fs-8 fw-bold'>New</span>
</div>
{/* end:Item */}
<div className='card-body pt-0'>
{dashData?.loading ?
null
:
dashData?.data?.recent_bvn && dashData?.data?.recent_bvn.length ?
<RecentBVNList
data = {dashData?.data?.recent_bvn}
itemsPerPage={7}
>
{(data:RecentBVNProps) => (
<>
{data?.map(item => (
<div key={item?.uid} className='d-flex align-items-center mb-8'>
{/* begin::Bullet */}
<span className='bullet bullet-vertical h-40px bg-primary me-5'></span>
{/* end::Bullet */}
{/* begin::Checkbox */}
{/* <div className='form-check form-check-custom form-check-solid mx-5'>
<input className='form-check-input' type='checkbox' value='' />
</div> */}
{/* end::Checkbox */}
{/* begin::Description */}
<div className='flex-grow-1'>
<span className='text-gray-800 fw-bold fs-6'>
{item?.bvn}
</span>
<span className='text-muted fw-semibold d-block'>{NewDateTimeFormatter(item?.added)}</span>
</div>
{/* end::Description */}
<span className='badge badge-light-primary fs-8 fw-bold'>status: {item?.status}</span>
</div>
))}
</>
)}
</RecentBVNList>
:
<p>No list Found!</p>
}
</div>
{/* end::Body */}
</div>
@@ -1,21 +1,27 @@
import { FC } from 'react'
import {KTIcon, toAbsoluteUrl} from '../../../helpers'
import { DashDataProps } from '../../../../app/pages/dashboard/model'
import { NewDateTimeFormatter } from '../../../lib/NewDateTimeFormatter'
import { Link } from 'react-router-dom'
import { AmountTo2DP } from '../../../lib/AmountFormatter'
type Props = {
className: string
dashData?: DashDataProps
}
const TablesWidget10: FC<Props> = ({className}) => {
const TablesWidget10: FC<Props> = ({className, dashData}) => {
return (
<div className={`card ${className}`}>
{/* begin::Header */}
<div className='card-header border-0 pt-5'>
<h3 className='card-title align-items-start flex-column'>
<span className='card-label fw-bold fs-3 mb-1'>Members Statistics</span>
<div className='card-header border-0'>
<h3 className='card-title fw-bold text-gray-900'>Recent Loan Application</h3>
{/* <h3 className='card-title align-items-start flex-column'>
<span className='card-label fw-bold fs-3'>Recent Loan Application</span>
<span className='text-muted mt-1 fw-semibold fs-7'>Over 500 members</span>
</h3>
<div
</h3> */}
{/* <div
className='card-toolbar'
data-bs-toggle='tooltip'
data-bs-placement='top'
@@ -25,380 +31,141 @@ const TablesWidget10: FC<Props> = ({className}) => {
<a
href='#'
className='btn btn-sm btn-light-primary'
// data-bs-toggle='modal'
// data-bs-target='#kt_modal_invite_friends'
>
<KTIcon iconName='plus' className='fs-3' />
New Member
</a>
</div>
</div> */}
</div>
{/* end::Header */}
{/* begin::Body */}
<div className='card-body py-3'>
{/* begin::Table container */}
<div className='table-responsive'>
{/* begin::Table */}
<table className='table table-row-dashed table-row-gray-300 align-middle gs-0 gy-4'>
{/* begin::Table head */}
<thead>
<tr className='fw-bold text-muted'>
<th className='w-25px'>
<div className='form-check form-check-sm form-check-custom form-check-solid'>
<input
className='form-check-input'
type='checkbox'
value='1'
data-kt-check='true'
data-kt-check-target='.widget-9-check'
/>
</div>
</th>
<th className='min-w-150px'>Authors</th>
<th className='min-w-140px'>Company</th>
<th className='min-w-120px'>Progress</th>
<th className='min-w-100px text-end'>Actions</th>
</tr>
</thead>
{/* end::Table head */}
{/* begin::Table body */}
<tbody>
<tr>
<td>
<div className='form-check form-check-sm form-check-custom form-check-solid'>
<input className='form-check-input widget-9-check' type='checkbox' value='1' />
</div>
</td>
<td>
<div className='d-flex align-items-center'>
<div className='symbol symbol-45px me-5'>
<img src={toAbsoluteUrl('media/avatars/300-14.jpg')} alt='' />
{dashData?.loading ?
null
: dashData?.data?.recent_applications ?
<>
<div className='card-body py-0'>
{/* begin::Table container */}
<div className='table-responsive'>
{/* begin::Table */}
<table className='table table-row-dashed table-row-gray-300 align-middle gs-0 gy-4'>
{/* begin::Table head */}
<thead>
<tr className='fw-bold text-muted'>
<th className='w-25px'>
<div className='form-check form-check-sm form-check-custom form-check-solid'>
<input
className='form-check-input'
type='checkbox'
value='1'
data-kt-check='true'
data-kt-check-target='.widget-9-check'
/>
</div>
<div className='d-flex justify-content-start flex-column'>
<a href='#' className='text-gray-900 fw-bold text-hover-primary fs-6'>
Ana Simmons
</a>
<span className='text-muted fw-semibold text-muted d-block fs-7'>
HTML, JS, ReactJS
</span>
</div>
</div>
</td>
<td>
<a href='#' className='text-gray-900 fw-bold text-hover-primary d-block fs-6'>
Intertico
</a>
<span className='text-muted fw-semibold text-muted d-block fs-7'>
Web, UI/UX Design
</span>
</td>
<td className='text-end'>
<div className='d-flex flex-column w-100 me-2'>
<div className='d-flex flex-stack mb-2'>
<span className='text-muted me-2 fs-7 fw-semibold'>50%</span>
</div>
<div className='progress h-6px w-100'>
<div
className='progress-bar bg-primary'
role='progressbar'
style={{width: '50%'}}
></div>
</div>
</div>
</td>
<td>
<div className='d-flex justify-content-end flex-shrink-0'>
<a
href='#'
className='btn btn-icon btn-bg-light btn-active-color-primary btn-sm me-1'
>
<KTIcon iconName='switch' className='fs-3' />
</a>
<a
href='#'
className='btn btn-icon btn-bg-light btn-active-color-primary btn-sm me-1'
>
<KTIcon iconName='pencil' className='fs-3' />
</a>
<a
href='#'
className='btn btn-icon btn-bg-light btn-active-color-primary btn-sm'
>
<KTIcon iconName='trash' className='fs-3' />
</a>
</div>
</td>
</tr>
<tr>
<td>
<div className='form-check form-check-sm form-check-custom form-check-solid'>
<input className='form-check-input widget-9-check' type='checkbox' value='1' />
</div>
</td>
<td>
<div className='d-flex align-items-center'>
<div className='symbol symbol-45px me-5'>
<img src={toAbsoluteUrl('media/avatars/300-2.jpg')} alt='' />
</div>
<div className='d-flex justify-content-start flex-column'>
<a href='#' className='text-gray-900 fw-bold text-hover-primary fs-6'>
Jessie Clarcson
</a>
<span className='text-muted fw-semibold text-muted d-block fs-7'>
C#, ASP.NET, MS SQL
</span>
</div>
</div>
</td>
<td>
<a href='#' className='text-gray-900 fw-bold text-hover-primary d-block fs-6'>
Agoda
</a>
<span className='text-muted fw-semibold text-muted d-block fs-7'>
Houses &amp; Hotels
</span>
</td>
<td className='text-end'>
<div className='d-flex flex-column w-100 me-2'>
<div className='d-flex flex-stack mb-2'>
<span className='text-muted me-2 fs-7 fw-semibold'>70%</span>
</div>
<div className='progress h-6px w-100'>
<div
className='progress-bar bg-danger'
role='progressbar'
style={{width: '70%'}}
></div>
</div>
</div>
</td>
<td>
<div className='d-flex justify-content-end flex-shrink-0'>
<a
href='#'
className='btn btn-icon btn-bg-light btn-active-color-primary btn-sm me-1'
>
<KTIcon iconName='switch' className='fs-3' />
</a>
<a
href='#'
className='btn btn-icon btn-bg-light btn-active-color-primary btn-sm me-1'
>
<KTIcon iconName='pencil' className='fs-3' />
</a>
<a
href='#'
className='btn btn-icon btn-bg-light btn-active-color-primary btn-sm'
>
<KTIcon iconName='trash' className='fs-3' />
</a>
</div>
</td>
</tr>
<tr>
<td>
<div className='form-check form-check-sm form-check-custom form-check-solid'>
<input className='form-check-input widget-9-check' type='checkbox' value='1' />
</div>
</td>
<td>
<div className='d-flex align-items-center'>
<div className='symbol symbol-45px me-5'>
<img src={toAbsoluteUrl('media/avatars/300-5.jpg')} alt='' />
</div>
<div className='d-flex justify-content-start flex-column'>
<a href='#' className='text-gray-900 fw-bold text-hover-primary fs-6'>
Lebron Wayde
</a>
<span className='text-muted fw-semibold text-muted d-block fs-7'>
PHP, Laravel, VueJS
</span>
</div>
</div>
</td>
<td>
<a href='#' className='text-gray-900 fw-bold text-hover-primary d-block fs-6'>
RoadGee
</a>
<span className='text-muted fw-semibold text-muted d-block fs-7'>
Transportation
</span>
</td>
<td className='text-end'>
<div className='d-flex flex-column w-100 me-2'>
<div className='d-flex flex-stack mb-2'>
<span className='text-muted me-2 fs-7 fw-semibold'>60%</span>
</div>
<div className='progress h-6px w-100'>
<div
className='progress-bar bg-success'
role='progressbar'
style={{width: '60%'}}
></div>
</div>
</div>
</td>
<td>
<div className='d-flex justify-content-end flex-shrink-0'>
<a
href='#'
className='btn btn-icon btn-bg-light btn-active-color-primary btn-sm me-1'
>
<KTIcon iconName='switch' className='fs-3' />
</a>
<a
href='#'
className='btn btn-icon btn-bg-light btn-active-color-primary btn-sm me-1'
>
<KTIcon iconName='pencil' className='fs-3' />
</a>
<a
href='#'
className='btn btn-icon btn-bg-light btn-active-color-primary btn-sm'
>
<KTIcon iconName='trash' className='fs-3' />
</a>
</div>
</td>
</tr>
<tr>
<td>
<div className='form-check form-check-sm form-check-custom form-check-solid'>
<input className='form-check-input widget-9-check' type='checkbox' value='1' />
</div>
</td>
<td>
<div className='d-flex align-items-center'>
<div className='symbol symbol-45px me-5'>
<img src={toAbsoluteUrl('media/avatars/300-20.jpg')} alt='' />
</div>
<div className='d-flex justify-content-start flex-column'>
<a href='#' className='text-gray-900 fw-bold text-hover-primary fs-6'>
Natali Goodwin
</a>
<span className='text-muted fw-semibold text-muted d-block fs-7'>
Python, PostgreSQL, ReactJS
</span>
</div>
</div>
</td>
<td>
<a href='#' className='text-gray-900 fw-bold text-hover-primary d-block fs-6'>
The Hill
</a>
<span className='text-muted fw-semibold text-muted d-block fs-7'>Insurance</span>
</td>
<td className='text-end'>
<div className='d-flex flex-column w-100 me-2'>
<div className='d-flex flex-stack mb-2'>
<span className='text-muted me-2 fs-7 fw-semibold'>50%</span>
</div>
<div className='progress h-6px w-100'>
<div
className='progress-bar bg-warning'
role='progressbar'
style={{width: '50%'}}
></div>
</div>
</div>
</td>
<td>
<div className='d-flex justify-content-end flex-shrink-0'>
<a
href='#'
className='btn btn-icon btn-bg-light btn-active-color-primary btn-sm me-1'
>
<KTIcon iconName='switch' className='fs-3' />
</a>
<a
href='#'
className='btn btn-icon btn-bg-light btn-active-color-primary btn-sm me-1'
>
<KTIcon iconName='pencil' className='fs-3' />
</a>
<a
href='#'
className='btn btn-icon btn-bg-light btn-active-color-primary btn-sm'
>
<KTIcon iconName='trash' className='fs-3' />
</a>
</div>
</td>
</tr>
<tr>
<td>
<div className='form-check form-check-sm form-check-custom form-check-solid'>
<input className='form-check-input widget-9-check' type='checkbox' value='1' />
</div>
</td>
<td>
<div className='d-flex align-items-center'>
<div className='symbol symbol-45px me-5'>
<img src={toAbsoluteUrl('media/avatars/300-23.jpg')} alt='' />
</div>
<div className='d-flex justify-content-start flex-column'>
<a href='#' className='text-gray-900 fw-bold text-hover-primary fs-6'>
Kevin Leonard
</a>
<span className='text-muted fw-semibold text-muted d-block fs-7'>
HTML, JS, ReactJS
</span>
</div>
</div>
</td>
<td>
<a href='#' className='text-gray-900 fw-bold text-hover-primary d-block fs-6'>
RoadGee
</a>
<span className='text-muted fw-semibold text-muted d-block fs-7'>
Art Director
</span>
</td>
<td className='text-end'>
<div className='d-flex flex-column w-100 me-2'>
<div className='d-flex flex-stack mb-2'>
<span className='text-muted me-2 fs-7 fw-semibold'>90%</span>
</div>
<div className='progress h-6px w-100'>
<div
className='progress-bar bg-info'
role='progressbar'
style={{width: '90%'}}
></div>
</div>
</div>
</td>
<td>
<div className='d-flex justify-content-end flex-shrink-0'>
<a
href='#'
className='btn btn-icon btn-bg-light btn-active-color-primary btn-sm me-1'
>
<KTIcon iconName='switch' className='fs-3' />
</a>
<a
href='#'
className='btn btn-icon btn-bg-light btn-active-color-primary btn-sm me-1'
>
<KTIcon iconName='pencil' className='fs-3' />
</a>
<a
href='#'
className='btn btn-icon btn-bg-light btn-active-color-primary btn-sm'
>
<KTIcon iconName='trash' className='fs-3' />
</a>
</div>
</td>
</tr>
</tbody>
{/* end::Table body */}
</table>
{/* end::Table */}
</th>
<th className='min-w-150px'>Authors</th>
<th className='min-w-140px'>Amount (NGN)</th>
<th className='min-w-120px'>Progress</th>
<th className='min-w-100px text-end'>Actions</th>
</tr>
</thead>
{/* end::Table head */}
{/* begin::Table body */}
<tbody>
{dashData?.data?.recent_applications && dashData?.data?.recent_applications.length ?
dashData?.data?.recent_applications.map((item, index:any) => {
if(index < 6){
return (
<tr key={item?.uid}>
<td>
<div className='form-check form-check-sm form-check-custom form-check-solid'>
<input className='form-check-input widget-9-check' type='checkbox' value='1' />
</div>
</td>
<td>
<div className='d-flex align-items-center'>
<div className='symbol symbol-45px me-5'>
<img src={toAbsoluteUrl('media/avatars/avatar1.jpg')} alt='' />
</div>
<div className='d-flex justify-content-start flex-column'>
<span className='text-gray-900 fw-bold fs-6'>
{item?.firstname} {item?.lastname}
</span>
<span className='text-muted fw-semibold text-muted d-block fs-7'>
{NewDateTimeFormatter(item?.added)}
</span>
</div>
</div>
</td>
<td>
<span className='text-gray-900 fw-bold d-block fs-6'>
{AmountTo2DP(item.loan_amount)}
</span>
<span className='text-muted fw-semibold text-muted d-block fs-7'>
{item?.sales_agent? `Agent: ${item?.sales_agent}` : ``}
</span>
</td>
<td className='text-end'>
<div className='d-flex flex-column w-100 me-2'>
<div className='d-flex flex-stack mb-2'>
<span className='text-muted me-2 fs-7 fw-semibold'>50%</span>
</div>
<div className='progress h-6px w-100'>
<div
className='progress-bar bg-primary'
role='progressbar'
style={{width: '50%'}}
></div>
</div>
</div>
</td>
<td>
<div className='d-flex justify-content-end flex-shrink-0'>
<a
href='#'
className='btn btn-icon btn-bg-light btn-active-color-primary btn-sm me-1'
>
<KTIcon iconName='switch' className='fs-3' />
</a>
<a
href='#'
className='btn btn-icon btn-bg-light btn-active-color-primary btn-sm me-1'
>
<KTIcon iconName='pencil' className='fs-3' />
</a>
<a
href='#'
className='btn btn-icon btn-bg-light btn-active-color-primary btn-sm'
>
<KTIcon iconName='trash' className='fs-3' />
</a>
</div>
</td>
</tr>
)
}
})
:
<tr>
<td colSpan={5}>No data found!</td>
</tr>
}
</tbody>
{/* end::Table body */}
</table>
{/* end::Table */}
<p className='py-1 w-100 text-center text-hover-primary'>
<Link to='/loan/pages/process/started'>more applications</Link>
</p>
</div>
{/* end::Table container */}
</div>
{/* end::Table container */}
</div>
</>
:
null
}
{/* begin::Body */}
</div>
)
}
@@ -13,17 +13,17 @@ const UsersListToolbar = () => {
<UsersListFilter />
{/* begin::Export */}
<button type='button' className='btn btn-light-primary me-3'>
{/* <button type='button' className='btn btn-light-primary me-3'>
<KTIcon iconName='exit-up' className='fs-2' />
Export
</button>
</button> */}
{/* end::Export */}
{/* begin::Add user */}
<button type='button' className='btn btn-primary' onClick={openAddUserModal}>
{/* <button type='button' className='btn btn-primary' onClick={openAddUserModal}>
<KTIcon iconName='plus' className='fs-2' />
Add User
</button>
</button> */}
{/* end::Add user */}
</div>
)
@@ -11,7 +11,7 @@ import {
stringifyRequestQuery,
WithChildren,
} from '../../../../../../_digifi/helpers'
import {getUsers} from './_requests'
import {getStartedUsers} from './_requests'
import {User} from './_models'
import {useQueryRequest} from './QueryRequestProvider'
@@ -34,7 +34,7 @@ const QueryResponseProvider: FC<WithChildren> = ({children}) => {
} = useQuery(
`${QUERIES.USERS_LIST}-${query}`,
() => {
return getUsers(query)
return getStartedUsers(query)
},
{cacheTime: 0, keepPreviousData: true, refetchOnWindowFocus: false}
)
@@ -54,7 +54,7 @@ const useQueryResponseData = () => {
return []
}
return response?.data || []
return response?.records || []
}
const useQueryResponsePagination = () => {
@@ -3,7 +3,7 @@ export type User = {
id?: ID
name?: string
avatar?: string
email?: string
// email?: string
position?: string
role?: string
last_login?: string
@@ -14,6 +14,21 @@ export type User = {
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
}
export type UsersQueryResponse = Response<Array<User>>
@@ -4,11 +4,18 @@ 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 GET_USERS_URL = `${API_URL}/users/query`;
const getUsers = (query: string): Promise<UsersQueryResponse> => {
const NEW_USER_ENDPOINT = import.meta.env.VITE_APP_USER_ENDPOINT
// const getStartedUsers = (query: string): Promise<UsersQueryResponse> => {
// return axios
// .get(`${GET_USERS_URL}?${query}`)
// .then((d: AxiosResponse<UsersQueryResponse>) => d.data);
// };
const getStartedUsers = (query: string): Promise<UsersQueryResponse> => { // FUNCTION TO GET USERS THAT HAVE STARTED LOAN APPLICATION
return axios
.get(`${GET_USERS_URL}?${query}`)
.get(`${NEW_USER_ENDPOINT}/loan/started`)
.then((d: AxiosResponse<UsersQueryResponse>) => d.data);
};
@@ -43,7 +50,7 @@ const deleteSelectedUsers = (userIds: Array<ID>): Promise<void> => {
};
export {
getUsers,
getStartedUsers,
deleteUser,
deleteSelectedUsers,
getUserById,
@@ -11,6 +11,7 @@ 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, [])
@@ -0,0 +1,12 @@
import {FC} from 'react'
import { NewDateTimeFormatter } from '../../../../../../../_digifi/lib/NewDateTimeFormatter'
type Props = {
added?: string
}
const AddedCell: FC<Props> = ({added}) => (
<div className='badge badge-light fw-bolder'>{NewDateTimeFormatter((added))}</div>
)
export {AddedCell}
@@ -0,0 +1,11 @@
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}
@@ -25,14 +25,14 @@ const UserInfoCell: FC<Props> = ({user}) => (
`text-${user.initials?.state}`
)}
>
{user.initials?.label}
{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.name}
{user.firstname} {user.lastname}
</a>
<span>{user.email}</span>
</div>
@@ -1,11 +1,11 @@
import {FC} from 'react'
type Props = {
last_login?: string
payment_month?: string
}
const UserLastLoginCell: FC<Props> = ({last_login}) => (
<div className='badge badge-light fw-bolder'>{last_login}</div>
const PaymentMonthCell: FC<Props> = ({payment_month}) => (
<div className='badge badge-light fw-bolder'>{payment_month}</div>
)
export {UserLastLoginCell}
export {PaymentMonthCell}
@@ -1,11 +0,0 @@
import {FC} from 'react'
type Props = {
two_steps?: boolean
}
const UserTwoStepsCell: FC<Props> = ({two_steps}) => (
<> {two_steps && <div className='badge badge-light-success fw-bolder'>Enabled</div>}</>
)
export {UserTwoStepsCell}
@@ -1,55 +1,57 @@
import {Column} from 'react-table'
import {UserInfoCell} from './UserInfoCell'
import {UserLastLoginCell} from './UserLastLoginCell'
import {UserTwoStepsCell} from './UserTwoStepsCell'
import { PaymentMonthCell } from './UserLastLoginCell'
import {AgentCell} from './AgentCell'
import {UserActionsCell} from './UserActionsCell'
import {UserSelectionCell} from './UserSelectionCell'
import {UserCustomHeader} from './UserCustomHeader'
import {UserSelectionHeader} from './UserSelectionHeader'
import {User} from '../../core/_models'
import { AddedCell } from './AddedCell'
const usersColumns: ReadonlyArray<Column<User>> = [
{
Header: (props) => <UserSelectionHeader tableProps={props} />,
id: 'selection',
Cell: ({...props}) => <UserSelectionCell id={props.data[props.row.index].id} />,
Cell: ({...props}) => <UserSelectionCell id={props.data[props.row.index].uid} />,
},
{
Header: (props) => <UserCustomHeader tableProps={props} title='Name' className='min-w-125px' />,
id: 'name',
id: 'firstname',
Cell: ({...props}) => <UserInfoCell user={props.data[props.row.index]} />,
},
{
Header: (props) => <UserCustomHeader tableProps={props} title='Role' className='min-w-125px' />,
accessor: 'role',
Header: (props) => <UserCustomHeader tableProps={props} title='Amount' className='min-w-125px' />,
accessor: 'loan_amount',
},
{
Header: (props) => (
<UserCustomHeader tableProps={props} title='Last login' className='min-w-125px' />
<UserCustomHeader tableProps={props} title='Payment Terms' className='min-w-125px' />
),
id: 'last_login',
Cell: ({...props}) => <UserLastLoginCell last_login={props.data[props.row.index].last_login} />,
id: 'payment_month',
Cell: ({...props}) => <PaymentMonthCell payment_month={props.data[props.row.index].payment_month} />,
},
{
Header: (props) => (
<UserCustomHeader tableProps={props} title='Two steps' className='min-w-125px' />
<UserCustomHeader tableProps={props} title='Agent' className='min-w-125px' />
),
id: 'two_steps',
Cell: ({...props}) => <UserTwoStepsCell two_steps={props.data[props.row.index].two_steps} />,
id: 'sales_agent',
Cell: ({...props}) => <AgentCell agent={props.data[props.row.index].sales_agent} />,
},
{
Header: (props) => (
<UserCustomHeader tableProps={props} title='Joined day' className='min-w-125px' />
<UserCustomHeader tableProps={props} title='Added' className='min-w-125px' />
),
accessor: 'joined_day',
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].id} />,
Cell: ({...props}) => <UserActionsCell id={props.data[props.row.index].uid} />,
},
]
export {usersColumns}
export {usersColumns}
+3 -3
View File
@@ -53,9 +53,9 @@ export function setupAxios(axios: any) {
axios.interceptors.request.use(
(config: {headers: {Authorization: string}}) => {
const auth = getAuth()
if (auth && auth.api_token) {
config.headers.Authorization = `Bearer ${auth.api_token}`
}
// if (auth && auth.api_token) {
// config.headers.Authorization = `Bearer ${auth.api_token}`
// }
return config
},
@@ -0,0 +1,73 @@
import axios from "axios";
export function postAuxEnd(uri:string, reqData:any):Promise<any> {
const endPoint = import.meta.env.VITE_APP_USER_ENDPOINT + uri;
const formData = new FormData();
for (let value in reqData) {
formData.append(value, reqData[value]);
}
return axios.post(endPoint, formData)
.then((response) => {
console.log(response);
// if (response.data.internal_return == "-9999") {
// localStorage.clear();
// window.location.href = `/login?sessionExpired=true`;
// }
return response;
})
.catch((error) => {
if (error.response) {
//response status is an error code
console.log(
"ERROR-------------------------------------------------------"
);
console.log(error.response.status);
console.log(
"ERROR-------------------------------------------------------"
);
} else if (error.request) {
//response not received though the request was sent
console.log(
"ERROR2-------------------------------------------------------"
);
console.log(error?.request);
console.log(
"ERROR2-------------------------------------------------------"
);
} else {
//an error occurred when setting up the request
console.log(
"ERROR3-------------------------------------------------------"
);
console.log(error);
console.log(
"ERROR3-------------------------------------------------------"
);
}
});
}
export function getAuxEnd(uri: string, reqData?: any): Promise<any> {
const endPoint = import.meta.env.VITE_APP_USER_ENDPOINT + uri;
const formData = new FormData();
for (let value in reqData) {
formData.append(value, reqData[value]);
}
return axios
.get(endPoint, reqData)
.then((response: {}) => {
// if (response.data.internal_return == "-9999") {
// localStorage.clear();
// window.location.href = `/login?sessionExpired=true`;
// }
return response;
})
.catch((error: any) => {
console.log(
"ERROR3-------------------------------------------------------", error
);
});
}
+21
View File
@@ -1,6 +1,18 @@
export interface AuthModel {
api_token: string
refreshToken?: string
message?: string
call_return?: string
username: string
token?: string
id?: string
first_name?: string
last_name?: string
email?: string
email_verified_at?: string
created_at?: string
updated_at?: string
}
export interface UserAddressModel {
@@ -64,4 +76,13 @@ export interface UserModel {
communication?: UserCommunicationModel
address?: UserAddressModel
socialNetworks?: UserSocialNetworksModel
api_token: string
refreshToken?: string
message?: string
call_return?: string
token?: string
email_verified_at?: string
created_at?: string
updated_at?: string
}
+25 -10
View File
@@ -1,19 +1,26 @@
import axios from "axios";
import { AuthModel, UserModel } from "./_models";
import { postAuxEnd, getAuxEnd } from "./AxiosCallHelper";
const API_URL = import.meta.env.VITE_APP_API_URL;
export const GET_USER_BY_ACCESSTOKEN_URL = `${API_URL}/verify_token`;
export const LOGIN_URL = `${API_URL}/login`;
// export const GET_USER_BY_ACCESSTOKEN_URL = `${API_URL}/verify_token`;
// export const LOGIN_URL = `${API_URL}/login`;
export const GET_USER_BY_ACCESSTOKEN_URL = '/identity/verify_token'
export const LOGIN_URL = '/identity/token'
export const USER_DASH_DETAILS = `/dash`;
export const REGISTER_URL = `${API_URL}/register`;
export const REQUEST_PASSWORD_URL = `${API_URL}/forgot_password`;
// Server should return AuthModel
//Function to login user in // Server should return AuthModel
export function login(email: string, password: string) {
return axios.post<AuthModel>(LOGIN_URL, {
email,
password,
});
// return axios.post<AuthModel>(LOGIN_URL, {
// email,
// password,
// });
return postAuxEnd(LOGIN_URL, {username:email, pass:password})
}
// Server should return AuthModel
@@ -40,8 +47,16 @@ export function requestPassword(email: string) {
});
}
// export function getUserByToken(token: string) {
// return axios.post<UserModel>(GET_USER_BY_ACCESSTOKEN_URL, {
// api_token: token,
// });
// }
export function getUserByToken(token: string) {
return axios.post<UserModel>(GET_USER_BY_ACCESSTOKEN_URL, {
api_token: token,
});
return postAuxEnd(GET_USER_BY_ACCESSTOKEN_URL, {token})
}
export function getUserDashDetails() {
return getAuxEnd(USER_DASH_DETAILS)
}
+18 -13
View File
@@ -1,11 +1,16 @@
import {Navigate, Routes, Route, Outlet} from 'react-router-dom'
import {PageLink, PageTitle} from '../../../_digifi/layout/core'
import {Overview} from './components/Overview'
import {Projects} from './components/Projects'
import {Campaigns} from './components/Campaigns'
import {Documents} from './components/Documents'
import {Connections} from './components/Connections'
import {ProcessHeader} from './ProcessHeader'
// import {Overview} from './components/Overview'
// import {Projects} from './components/Projects'
// import {Campaigns} from './components/Campaigns'
// import {Documents} from './components/Documents'
// import {Connections} from './components/Connections'
// import {ProcessHeader} from './ProcessHeader'
import { UserStartedList } from './components/UserStartedList'
import { UserPendingList } from './components/UserPendingList'
import { UserReadyList } from './components/UserReadyList'
import { UserApprovedList } from './components/UserApprovedList'
import { UserRejectedList } from './components/UserRejectedList'
const processBreadCrumbs: Array<PageLink> = [
{
@@ -27,7 +32,7 @@ const ProcessPage = () => (
<Route
element={
<>
<ProcessHeader />
{/* <ProcessHeader /> */}
<Outlet />
</>
}
@@ -37,7 +42,7 @@ const ProcessPage = () => (
element={
<>
<PageTitle breadcrumbs={processBreadCrumbs}>Started</PageTitle>
<Overview />
<UserStartedList />
</>
}
/>
@@ -46,7 +51,7 @@ const ProcessPage = () => (
element={
<>
<PageTitle breadcrumbs={processBreadCrumbs}>Pending</PageTitle>
<Projects />
<UserPendingList />
</>
}
/>
@@ -55,7 +60,7 @@ const ProcessPage = () => (
element={
<>
<PageTitle breadcrumbs={processBreadCrumbs}>Ready</PageTitle>
<Campaigns />
<UserReadyList />
</>
}
/>
@@ -64,7 +69,7 @@ const ProcessPage = () => (
element={
<>
<PageTitle breadcrumbs={processBreadCrumbs}>Approved</PageTitle>
<Documents />
<UserApprovedList />
</>
}
/>
@@ -73,11 +78,11 @@ const ProcessPage = () => (
element={
<>
<PageTitle breadcrumbs={processBreadCrumbs}>Rejected</PageTitle>
<Connections />
<UserRejectedList />
</>
}
/>
<Route index element={<Navigate to='/loan/pages/profile/started' />} />
<Route index element={<Navigate to='/loan/pages/process/started' />} />
</Route>
</Routes>
)
@@ -0,0 +1,5 @@
import { UsersListWrapper } from "../user-approved/UsersList";
const UserApprovedList = () => <UsersListWrapper />;
export { UserApprovedList };
@@ -0,0 +1,5 @@
import { UsersListWrapper } from "../user-pending/UsersList";
const UserPendingList = () => <UsersListWrapper />;
export { UserPendingList };
@@ -0,0 +1,5 @@
import { UsersListWrapper } from "../user-ready/UsersList";
const UserReadyList = () => <UsersListWrapper />;
export { UserReadyList };
@@ -0,0 +1,5 @@
import { UsersListWrapper } from "../user-rejected/UsersList";
const UserRejectedList = () => <UsersListWrapper />;
export { UserRejectedList };
@@ -0,0 +1,5 @@
import { UsersListWrapper } from "../user-started/UsersList";
const UserStartedList = () => <UsersListWrapper />;
export { UserStartedList };
+43
View File
@@ -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
}
export type UsersQueryResponse = Response<Array<User>>
export const initialUser: User = {
avatar: 'avatars/300-6.jpg',
position: 'Art Director',
role: 'Administrator',
name: '',
email: '',
}
+87
View File
@@ -0,0 +1,87 @@
import axios, { AxiosResponse } from "axios";
import { ID, Response } from "../../../../_digifi/helpers"
import { User, UsersQueryResponse } from "./_models";
const API_URL = import.meta.env.VITE_APP_THEME_API_URL;
const USER_URL = `${API_URL}/user`;
// const GET_USERS_URL = `${API_URL}/users/query`;
const NEW_USER_ENDPOINT = import.meta.env.VITE_APP_USER_ENDPOINT
// const getStartedUsers = (query: string): Promise<UsersQueryResponse> => {
// return axios
// .get(`${GET_USERS_URL}?${query}`)
// .then((d: AxiosResponse<UsersQueryResponse>) => d.data);
// };
const getStartedUsers = (query: string): Promise<UsersQueryResponse> => { // FUNCTION TO GET USERS THAT HAVE STARTED LOAN APPLICATION
return axios
.get(`${NEW_USER_ENDPOINT}/loan/started`)
.then((d: AxiosResponse<UsersQueryResponse>) => d.data);
};
const getRejectedUsers = (query: string): Promise<UsersQueryResponse> => { // FUNCTION TO GET USERS THAT HAVE REJECTED LOAN APPLICATION
return axios
.get(`${NEW_USER_ENDPOINT}/loan/rejected`)
.then((d: AxiosResponse<UsersQueryResponse>) => d.data);
};
const getPendingUsers = (query: string): Promise<UsersQueryResponse> => { // FUNCTION TO GET USERS THAT HAVE PENDING LOAN APPLICATION
return axios
.get(`${NEW_USER_ENDPOINT}/loan/pending`)
.then((d: AxiosResponse<UsersQueryResponse>) => d.data);
};
const getReadyUsers = (query: string): Promise<UsersQueryResponse> => { // FUNCTION TO GET USERS THAT HAVE READY LOAN APPLICATION
return axios
.get(`${NEW_USER_ENDPOINT}/loan/ready`)
.then((d: AxiosResponse<UsersQueryResponse>) => d.data);
};
const getApprovedUsers = (query: string): Promise<UsersQueryResponse> => { // FUNCTION TO GET USERS THAT HAVE APPROVED LOAN APPLICATION
return axios
.get(`${NEW_USER_ENDPOINT}/loan/approved`)
.then((d: AxiosResponse<UsersQueryResponse>) => d.data);
};
const getUserById = (id: ID): Promise<User | undefined> => {
return axios
.get(`${USER_URL}/${id}`)
.then((response: AxiosResponse<Response<User>>) => response.data)
.then((response: Response<User>) => response.data);
};
const createUser = (user: User): Promise<User | undefined> => {
return axios
.put(USER_URL, user)
.then((response: AxiosResponse<Response<User>>) => response.data)
.then((response: Response<User>) => response.data);
};
const updateUser = (user: User): Promise<User | undefined> => {
return axios
.post(`${USER_URL}/${user.id}`, user)
.then((response: AxiosResponse<Response<User>>) => response.data)
.then((response: Response<User>) => response.data);
};
const deleteUser = (userId: ID): Promise<void> => {
return axios.delete(`${USER_URL}/${userId}`).then(() => {});
};
const deleteSelectedUsers = (userIds: Array<ID>): Promise<void> => {
const requests = userIds.map((id) => axios.delete(`${USER_URL}/${id}`));
return axios.all(requests).then(() => {});
};
export {
getStartedUsers,
getRejectedUsers,
getPendingUsers,
getReadyUsers,
getApprovedUsers,
deleteUser,
deleteSelectedUsers,
getUserById,
createUser,
updateUser,
};
@@ -0,0 +1,37 @@
import { ListViewProvider, useListView } from "./core/ListViewProvider";
import { QueryRequestProvider } from "./core/QueryRequestProvider";
import { QueryResponseProvider } from "./core/QueryResponseProvider";
import { UsersListHeader } from "./components/header/UsersListHeader";
import { UsersTable } from "./table/UsersTable";
import { UserEditModal } from "./user-edit-modal/UserEditModal";
import { KTCard } from "../../../../_digifi/helpers";
import { ToolbarWrapper } from "../../../../_digifi/layout/components/toolbar";
import { Content } from "../../../../_digifi/layout/components/content";
const UsersList = () => {
const { itemIdForUpdate } = useListView();
return (
<>
<KTCard>
<UsersListHeader />
<UsersTable />
</KTCard>
{itemIdForUpdate !== undefined && <UserEditModal />}
</>
);
};
const UsersListWrapper = () => (
<QueryRequestProvider>
<QueryResponseProvider>
<ListViewProvider>
<ToolbarWrapper />
<Content>
<UsersList />
</Content>
</ListViewProvider>
</QueryResponseProvider>
</QueryRequestProvider>
);
export { UsersListWrapper };
@@ -0,0 +1,35 @@
import { KTIcon } from "../../../../../../_digifi/helpers";
import { useListView } from "../../core/ListViewProvider";
import { UsersListFilter } from "./UsersListFilter";
const UsersListToolbar = () => {
const { setItemIdForUpdate } = useListView();
const openAddUserModal = () => {
setItemIdForUpdate(null);
};
return (
<div
className="d-flex justify-content-end"
data-kt-user-table-toolbar="base"
>
<UsersListFilter />
{/* begin::Export */}
{/* <button type='button' className='btn btn-light-primary me-3'>
<KTIcon iconName='exit-up' className='fs-2' />
Export
</button> */}
{/* end::Export */}
{/* begin::Add user */}
{/* <button type='button' className='btn btn-primary' onClick={openAddUserModal}>
<KTIcon iconName='plus' className='fs-2' />
Add User
</button> */}
{/* end::Add user */}
</div>
);
};
export { UsersListToolbar };
@@ -0,0 +1,136 @@
import { useEffect, useState } from "react";
import { MenuComponent } from "../../../../../../_digifi/assets/ts/components";
import { initialQueryState, KTIcon } from "../../../../../../_digifi/helpers";
import { useQueryRequest } from "../../core/QueryRequestProvider";
import { useQueryResponse } from "../../core/QueryResponseProvider";
const UsersListFilter = () => {
const { updateState } = useQueryRequest();
const { isLoading } = useQueryResponse();
const [role, setRole] = useState<string | undefined>();
const [lastLogin, setLastLogin] = useState<string | undefined>();
useEffect(() => {
MenuComponent.reinitialization();
}, []);
const resetData = () => {
updateState({ filter: undefined, ...initialQueryState });
};
const filterData = () => {
updateState({
filter: { role, last_login: lastLogin },
...initialQueryState,
});
};
return (
<>
{/* begin::Filter Button */}
<button
disabled={isLoading}
type="button"
className="btn btn-light-primary me-3"
data-kt-menu-trigger="click"
data-kt-menu-placement="bottom-end"
>
<KTIcon iconName="filter" className="fs-2" />
Filter
</button>
{/* end::Filter Button */}
{/* begin::SubMenu */}
<div
className="menu menu-sub menu-sub-dropdown w-300px w-md-325px"
data-kt-menu="true"
>
{/* begin::Header */}
<div className="px-7 py-5">
<div className="fs-5 text-gray-900 fw-bolder">Filter Options</div>
</div>
{/* end::Header */}
{/* begin::Separator */}
<div className="separator border-gray-200"></div>
{/* end::Separator */}
{/* begin::Content */}
<div className="px-7 py-5" data-kt-user-table-filter="form">
{/* begin::Input group */}
<div className="mb-10">
<label className="form-label fs-6 fw-bold">Role:</label>
<select
className="form-select form-select-solid fw-bolder"
data-kt-select2="true"
data-placeholder="Select option"
data-allow-clear="true"
data-kt-user-table-filter="role"
data-hide-search="true"
onChange={(e) => setRole(e.target.value)}
value={role}
>
<option value=""></option>
<option value="Administrator">Administrator</option>
<option value="Analyst">Analyst</option>
<option value="Developer">Developer</option>
<option value="Support">Support</option>
<option value="Trial">Trial</option>
</select>
</div>
{/* end::Input group */}
{/* begin::Input group */}
<div className="mb-10">
<label className="form-label fs-6 fw-bold">Last login:</label>
<select
className="form-select form-select-solid fw-bolder"
data-kt-select2="true"
data-placeholder="Select option"
data-allow-clear="true"
data-kt-user-table-filter="two-step"
data-hide-search="true"
onChange={(e) => setLastLogin(e.target.value)}
value={lastLogin}
>
<option value=""></option>
<option value="Yesterday">Yesterday</option>
<option value="20 mins ago">20 mins ago</option>
<option value="5 hours ago">5 hours ago</option>
<option value="2 days ago">2 days ago</option>
</select>
</div>
{/* end::Input group */}
{/* begin::Actions */}
<div className="d-flex justify-content-end">
<button
type="button"
disabled={isLoading}
onClick={filterData}
className="btn btn-light btn-active-light-primary fw-bold me-2 px-6"
data-kt-menu-dismiss="true"
data-kt-user-table-filter="reset"
>
Reset
</button>
<button
disabled={isLoading}
type="button"
onClick={resetData}
className="btn btn-primary fw-bold px-6"
data-kt-menu-dismiss="true"
data-kt-user-table-filter="filter"
>
Apply
</button>
</div>
{/* end::Actions */}
</div>
{/* end::Content */}
</div>
{/* end::SubMenu */}
</>
);
};
export { UsersListFilter };
@@ -0,0 +1,38 @@
import { useQueryClient, useMutation } from "react-query";
import { QUERIES } from "../../../../../../_digifi/helpers";
import { useListView } from "../../core/ListViewProvider";
import { useQueryResponse } from "../../core/QueryResponseProvider";
import { deleteSelectedUsers } from "../../../core/_requests";
const UsersListGrouping = () => {
const { selected, clearSelected } = useListView();
const queryClient = useQueryClient();
const { query } = useQueryResponse();
const deleteSelectedItems = useMutation(() => deleteSelectedUsers(selected), {
// 💡 response of the mutation is passed to onSuccess
onSuccess: () => {
// ✅ update detail view directly
queryClient.invalidateQueries([`${QUERIES.USERS_LIST}-${query}`]);
clearSelected();
},
});
return (
<div className="d-flex justify-content-end align-items-center">
<div className="fw-bolder me-5">
<span className="me-2">{selected.length}</span> Selected
</div>
<button
type="button"
className="btn btn-danger"
onClick={async () => await deleteSelectedItems.mutateAsync()}
>
Delete Selected
</button>
</div>
);
};
export { UsersListGrouping };
@@ -0,0 +1,22 @@
import {useListView} from '../../core/ListViewProvider'
import {UsersListToolbar} from './UserListToolbar'
import {UsersListGrouping} from './UsersListGrouping'
import {UsersListSearchComponent} from './UsersListSearchComponent'
const UsersListHeader = () => {
const {selected} = useListView()
return (
<div className='card-header border-0 pt-6'>
<UsersListSearchComponent />
{/* begin::Card toolbar */}
<div className='card-toolbar'>
{/* begin::Group actions */}
{selected.length > 0 ? <UsersListGrouping /> : <UsersListToolbar />}
{/* end::Group actions */}
</div>
{/* end::Card toolbar */}
</div>
)
}
export {UsersListHeader}
@@ -0,0 +1,49 @@
/* eslint-disable react-hooks/exhaustive-deps */
import { useEffect, useState } from "react";
import {
initialQueryState,
KTIcon,
useDebounce,
} from "../../../../../../_digifi/helpers";
import { useQueryRequest } from "../../core/QueryRequestProvider";
const UsersListSearchComponent = () => {
const { updateState } = useQueryRequest();
const [searchTerm, setSearchTerm] = useState<string>("");
// Debounce search term so that it only gives us latest value ...
// ... if searchTerm has not been updated within last 500ms.
// The goal is to only have the API call fire when user stops typing ...
// ... so that we aren't hitting our API rapidly.
const debouncedSearchTerm = useDebounce(searchTerm, 150);
// Effect for API call
useEffect(
() => {
if (debouncedSearchTerm !== undefined && searchTerm !== undefined) {
updateState({ search: debouncedSearchTerm, ...initialQueryState });
}
},
[debouncedSearchTerm] // Only call effect if debounced search term changes
// More details about useDebounce: https://usehooks.com/useDebounce/
);
return (
<div className="card-title">
{/* begin::Search */}
<div className="d-flex align-items-center position-relative my-1">
<KTIcon iconName="magnifier" className="fs-1 position-absolute ms-6" />
<input
type="text"
data-kt-user-table-filter="search"
className="form-control form-control-solid w-250px ps-14"
placeholder="Search user"
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
/>
</div>
{/* end::Search */}
</div>
);
};
export { UsersListSearchComponent };
@@ -0,0 +1,18 @@
const UsersListLoading = () => {
const styles = {
borderRadius: '0.475rem',
boxShadow: '0 0 50px 0 rgb(82 63 105 / 15%)',
backgroundColor: '#fff',
color: '#7e8299',
fontWeight: '500',
margin: '0',
width: 'auto',
padding: '1rem 2rem',
top: 'calc(50% - 2rem)',
left: 'calc(50% - 4rem)',
}
return <div style={{...styles, position: 'absolute', textAlign: 'center'}}>Processing...</div>
}
export {UsersListLoading}
@@ -0,0 +1,179 @@
import clsx from "clsx";
import {
useQueryResponseLoading,
useQueryResponsePagination,
} from "../../core/QueryResponseProvider";
import { useQueryRequest } from "../../core/QueryRequestProvider";
import { PaginationState } from "../../../../../../_digifi/helpers";
import { useMemo } from "react";
const mappedLabel = (label: string): string => {
if (label === "&laquo; Previous") {
return "Previous";
}
if (label === "Next &raquo;") {
return "Next";
}
return label;
};
const UsersListPagination = () => {
const pagination = useQueryResponsePagination();
const isLoading = useQueryResponseLoading();
const { updateState } = useQueryRequest();
const updatePage = (page: number | undefined | null) => {
if (!page || isLoading || pagination.page === page) {
return;
}
updateState({ page, items_per_page: pagination.items_per_page || 10 });
};
const PAGINATION_PAGES_COUNT = 5;
const sliceLinks = (pagination?: PaginationState) => {
if (!pagination?.links?.length) {
return [];
}
const scopedLinks = [...pagination.links];
let pageLinks: Array<{
label: string;
active: boolean;
url: string | null;
page: number | null;
}> = [];
const previousLink: {
label: string;
active: boolean;
url: string | null;
page: number | null;
} = scopedLinks.shift()!;
const nextLink: {
label: string;
active: boolean;
url: string | null;
page: number | null;
} = scopedLinks.pop()!;
const halfOfPagesCount = Math.floor(PAGINATION_PAGES_COUNT / 2);
pageLinks.push(previousLink);
if (
pagination.page <= Math.round(PAGINATION_PAGES_COUNT / 2) ||
scopedLinks.length <= PAGINATION_PAGES_COUNT
) {
pageLinks = [
...pageLinks,
...scopedLinks.slice(0, PAGINATION_PAGES_COUNT),
];
}
if (
pagination.page > scopedLinks.length - halfOfPagesCount &&
scopedLinks.length > PAGINATION_PAGES_COUNT
) {
pageLinks = [
...pageLinks,
...scopedLinks.slice(
scopedLinks.length - PAGINATION_PAGES_COUNT,
scopedLinks.length
),
];
}
if (
!(
pagination.page <= Math.round(PAGINATION_PAGES_COUNT / 2) ||
scopedLinks.length <= PAGINATION_PAGES_COUNT
) &&
!(pagination.page > scopedLinks.length - halfOfPagesCount)
) {
pageLinks = [
...pageLinks,
...scopedLinks.slice(
pagination.page - 1 - halfOfPagesCount,
pagination.page + halfOfPagesCount
),
];
}
pageLinks.push(nextLink);
return pageLinks;
};
const paginationLinks = useMemo(() => sliceLinks(pagination), [pagination]);
return (
<div className="row">
<div className="col-sm-12 col-md-5 d-flex align-items-center justify-content-center justify-content-md-start"></div>
<div className="col-sm-12 col-md-7 d-flex align-items-center justify-content-center justify-content-md-end">
<div id="kt_table_users_paginate">
<ul className="pagination">
<li
className={clsx("page-item", {
disabled: isLoading || pagination.page === 1,
})}
>
<a
onClick={() => updatePage(1)}
style={{ cursor: "pointer" }}
className="page-link"
>
First
</a>
</li>
{paginationLinks
?.map((link) => {
return { ...link, label: mappedLabel(link.label) };
})
.map((link) => (
<li
key={link.label}
className={clsx("page-item", {
active: pagination.page === link.page,
disabled: isLoading,
previous: link.label === "Previous",
next: link.label === "Next",
})}
>
<a
className={clsx("page-link", {
"page-text":
link.label === "Previous" || link.label === "Next",
"me-5": link.label === "Previous",
})}
onClick={() => updatePage(link.page)}
style={{ cursor: "pointer" }}
>
{mappedLabel(link.label)}
</a>
</li>
))}
<li
className={clsx("page-item", {
disabled:
isLoading ||
pagination.page === (pagination.links?.length || 3) - 2,
})}
>
<a
onClick={() => updatePage((pagination.links?.length || 3) - 2)}
style={{ cursor: "pointer" }}
className="page-link"
>
Last
</a>
</li>
</ul>
</div>
</div>
</div>
);
};
export { UsersListPagination };
@@ -0,0 +1,62 @@
/* eslint-disable react-refresh/only-export-components */
import { FC, useState, createContext, useContext, useMemo } from "react";
import {
ID,
calculatedGroupingIsDisabled,
calculateIsAllDataSelected,
groupingOnSelect,
initialListView,
ListViewContextProps,
groupingOnSelectAll,
WithChildren,
} from "../../../../../_digifi/helpers";
import {
useQueryResponse,
useQueryResponseData,
} from "./QueryResponseProvider";
const ListViewContext = createContext<ListViewContextProps>(initialListView);
const ListViewProvider: FC<WithChildren> = ({ children }) => {
const [selected, setSelected] = useState<Array<ID>>(initialListView.selected);
const [itemIdForUpdate, setItemIdForUpdate] = useState<ID>(
initialListView.itemIdForUpdate
);
const { isLoading } = useQueryResponse();
const data = useQueryResponseData();
const disabled = useMemo(
() => calculatedGroupingIsDisabled(isLoading, data),
[isLoading, data]
);
const isAllSelected = useMemo(
() => calculateIsAllDataSelected(data, selected),
[data, selected]
);
return (
<ListViewContext.Provider
value={{
selected,
itemIdForUpdate,
setItemIdForUpdate,
disabled,
isAllSelected,
onSelect: (id: ID) => {
groupingOnSelect(id, selected, setSelected);
},
onSelectAll: () => {
groupingOnSelectAll(isAllSelected, setSelected, data);
},
clearSelected: () => {
setSelected([]);
},
}}
>
{children}
</ListViewContext.Provider>
);
};
const useListView = () => useContext(ListViewContext);
export { ListViewProvider, useListView };
@@ -0,0 +1,29 @@
/* eslint-disable react-refresh/only-export-components */
import { FC, useState, createContext, useContext } from "react";
import {
QueryState,
QueryRequestContextProps,
initialQueryRequest,
WithChildren,
} from "../../../../../_digifi/helpers";
const QueryRequestContext =
createContext<QueryRequestContextProps>(initialQueryRequest);
const QueryRequestProvider: FC<WithChildren> = ({ children }) => {
const [state, setState] = useState<QueryState>(initialQueryRequest.state);
const updateState = (updates: Partial<QueryState>) => {
const updatedState = { ...state, ...updates } as QueryState;
setState(updatedState);
};
return (
<QueryRequestContext.Provider value={{ state, updateState }}>
{children}
</QueryRequestContext.Provider>
);
};
const useQueryRequest = () => useContext(QueryRequestContext);
export { QueryRequestProvider, useQueryRequest };
@@ -0,0 +1,87 @@
/* eslint-disable react-refresh/only-export-components */
/* eslint-disable react-hooks/exhaustive-deps */
import { FC, useContext, useState, useEffect, useMemo } from "react";
import { useQuery } from "react-query";
import {
createResponseContext,
initialQueryResponse,
initialQueryState,
PaginationState,
QUERIES,
stringifyRequestQuery,
WithChildren,
} from "../../../../../_digifi/helpers";
import { getApprovedUsers } from "../../core/_requests";
import { User } from "../../core/_models";
import { useQueryRequest } from "./QueryRequestProvider";
const QueryResponseContext = createResponseContext<User>(initialQueryResponse);
const QueryResponseProvider: FC<WithChildren> = ({ children }) => {
const { state } = useQueryRequest();
const [query, setQuery] = useState<string>(stringifyRequestQuery(state));
const updatedQuery = useMemo(() => stringifyRequestQuery(state), [state]);
useEffect(() => {
if (query !== updatedQuery) {
setQuery(updatedQuery);
}
}, [updatedQuery]);
const {
isFetching,
refetch,
data: response,
} = useQuery(
`${QUERIES.USERS_LIST}-${query}`,
() => {
return getApprovedUsers(query);
},
{ cacheTime: 0, keepPreviousData: true, refetchOnWindowFocus: false }
);
return (
<QueryResponseContext.Provider
value={{ isLoading: isFetching, refetch, response, query }}
>
{children}
</QueryResponseContext.Provider>
);
};
const useQueryResponse = () => useContext(QueryResponseContext);
const useQueryResponseData = () => {
const { response } = useQueryResponse();
if (!response) {
return [];
}
return response?.records || [];
};
const useQueryResponsePagination = () => {
const defaultPaginationState: PaginationState = {
links: [],
...initialQueryState,
};
const { response } = useQueryResponse();
if (!response || !response.payload || !response.payload.pagination) {
return defaultPaginationState;
}
return response.payload.pagination;
};
const useQueryResponseLoading = (): boolean => {
const { isLoading } = useQueryResponse();
return isLoading;
};
export {
QueryResponseProvider,
useQueryResponse,
useQueryResponseData,
useQueryResponsePagination,
useQueryResponseLoading,
};
@@ -0,0 +1,66 @@
import { useMemo } from "react";
import { useTable, ColumnInstance, Row } from "react-table";
import { CustomHeaderColumn } from "./columns/CustomHeaderColumn";
import { CustomRow } from "./columns/CustomRow";
import {
useQueryResponseData,
useQueryResponseLoading,
} from "../core/QueryResponseProvider";
import { usersColumns } from "./columns/_columns";
import { User } from "../../core/_models";
import { UsersListLoading } from "../components/loading/UsersListLoading";
import { UsersListPagination } from "../components/pagination/UsersListPagination";
import { KTCardBody } from "../../../../../_digifi/helpers";
const UsersTable = () => {
const users = useQueryResponseData();
// console.log('users', users)
const isLoading = useQueryResponseLoading();
const data = useMemo(() => users, [users]);
const columns = useMemo(() => usersColumns, []);
const { getTableProps, getTableBodyProps, headers, rows, prepareRow } =
useTable({
columns,
data,
});
return (
<KTCardBody className="py-4">
<div className="table-responsive">
<table
id="kt_table_users"
className="table align-middle table-row-dashed fs-6 gy-5 dataTable no-footer"
{...getTableProps()}
>
<thead>
<tr className="text-start text-muted fw-bolder fs-7 text-uppercase gs-0">
{headers.map((column: ColumnInstance<User>) => (
<CustomHeaderColumn key={column.id} column={column} />
))}
</tr>
</thead>
<tbody className="text-gray-600 fw-bold" {...getTableBodyProps()}>
{rows.length > 0 ? (
rows.map((row: Row<User>, i) => {
prepareRow(row);
return <CustomRow row={row} key={`row-${i}-${row.id}`} />;
})
) : (
<tr>
<td colSpan={7}>
<div className="d-flex text-center w-100 align-content-center justify-content-center">
No matching records found
</div>
</td>
</tr>
)}
</tbody>
</table>
</div>
<UsersListPagination />
{isLoading && <UsersListLoading />}
</KTCardBody>
);
};
export { UsersTable };
@@ -0,0 +1,14 @@
import { FC } from "react";
import { NewDateTimeFormatter } from "../../../../../../_digifi/lib/NewDateTimeFormatter";
type Props = {
added?: string;
};
const AddedCell: FC<Props> = ({ added }) => (
<div className="badge badge-light fw-bolder">
{NewDateTimeFormatter(added)}
</div>
);
export { AddedCell };
@@ -0,0 +1,11 @@
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,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,75 @@
import { FC, useEffect } from "react";
import { useMutation, useQueryClient } from "react-query";
import { MenuComponent } from "../../../../../../_digifi/assets/ts/components";
import { ID, KTIcon, QUERIES } from "../../../../../../_digifi/helpers";
import { useListView } from "../../core/ListViewProvider";
import { useQueryResponse } from "../../core/QueryResponseProvider";
import { deleteUser } from "../../../core/_requests";
type Props = {
id: ID;
};
const UserActionsCell: FC<Props> = ({ id }) => {
const { setItemIdForUpdate } = useListView();
const { query } = useQueryResponse();
const queryClient = useQueryClient();
useEffect(() => {
MenuComponent.reinitialization();
}, []);
const openEditModal = () => {
setItemIdForUpdate(id);
};
const deleteItem = useMutation(() => deleteUser(id), {
// 💡 response of the mutation is passed to onSuccess
onSuccess: () => {
// ✅ update detail view directly
queryClient.invalidateQueries([`${QUERIES.USERS_LIST}-${query}`]);
},
});
return (
<>
<a
href="#"
className="btn btn-light btn-active-light-primary btn-sm"
data-kt-menu-trigger="click"
data-kt-menu-placement="bottom-end"
>
Actions
<KTIcon iconName="down" className="fs-5 m-0" />
</a>
{/* begin::Menu */}
<div
className="menu menu-sub menu-sub-dropdown menu-column menu-rounded menu-gray-600 menu-state-bg-light-primary fw-bold fs-7 w-125px py-4"
data-kt-menu="true"
>
{/* begin::Menu item */}
<div className="menu-item px-3">
<a className="menu-link px-3" onClick={openEditModal}>
Edit
</a>
</div>
{/* end::Menu item */}
{/* begin::Menu item */}
<div className="menu-item px-3">
<a
className="menu-link px-3"
data-kt-users-table-filter="delete_row"
onClick={async () => await deleteItem.mutateAsync()}
>
Delete
</a>
</div>
{/* end::Menu item */}
</div>
{/* end::Menu */}
</>
);
};
export { UserActionsCell };
@@ -0,0 +1,61 @@
import clsx from "clsx";
import { FC, PropsWithChildren, useMemo } from "react";
import { HeaderProps } from "react-table";
import { initialQueryState } from "../../../../../../_digifi/helpers";
import { useQueryRequest } from "../../core/QueryRequestProvider";
import { User } from "../../../core/_models";
type Props = {
className?: string;
title?: string;
tableProps: PropsWithChildren<HeaderProps<User>>;
};
const UserCustomHeader: FC<Props> = ({ className, title, tableProps }) => {
const id = tableProps.column.id;
const { state, updateState } = useQueryRequest();
const isSelectedForSorting = useMemo(() => {
return state.sort && state.sort === id;
}, [state, id]);
const order: "asc" | "desc" | undefined = useMemo(() => state.order, [state]);
const sortColumn = () => {
// avoid sorting for these columns
if (id === "actions" || id === "selection") {
return;
}
if (!isSelectedForSorting) {
// enable sort asc
updateState({ sort: id, order: "asc", ...initialQueryState });
return;
}
if (isSelectedForSorting && order !== undefined) {
if (order === "asc") {
// enable sort desc
updateState({ sort: id, order: "desc", ...initialQueryState });
return;
}
// disable sort
updateState({ sort: undefined, order: undefined, ...initialQueryState });
}
};
return (
<th
{...tableProps.column.getHeaderProps()}
className={clsx(
className,
isSelectedForSorting && order !== undefined && `table-sort-${order}`
)}
style={{ cursor: "pointer" }}
onClick={sortColumn}
>
{title}
</th>
);
};
export { UserCustomHeader };
@@ -0,0 +1,46 @@
import clsx from "clsx";
import { FC } from "react";
import { toAbsoluteUrl } from "../../../../../../_digifi/helpers";
import { User } from "../../../core/_models";
type Props = {
user: User;
};
const UserInfoCell: FC<Props> = ({ user }) => (
<div className="d-flex align-items-center">
{/* begin:: Avatar */}
<div className="symbol symbol-circle symbol-50px overflow-hidden me-3">
<a href="#">
{user.avatar ? (
<div className="symbol-label">
<img
src={toAbsoluteUrl(`media/${user.avatar}`)}
alt={user.name}
className="w-100"
/>
</div>
) : (
<div
className={clsx(
"symbol-label fs-3",
`bg-light-${user.initials?.state}`,
`text-${user.initials?.state}`
)}
>
{user.firstname?.substring(0, 1).toUpperCase()}{" "}
{user.lastname?.substring(0, 1).toUpperCase()}
</div>
)}
</a>
</div>
<div className="d-flex flex-column">
<a href="#" className="text-gray-800 text-hover-primary mb-1">
{user.firstname} {user.lastname}
</a>
<span>{user.email}</span>
</div>
</div>
);
export { UserInfoCell };
@@ -0,0 +1,11 @@
import {FC} from 'react'
type Props = {
payment_month?: string
}
const PaymentMonthCell: FC<Props> = ({payment_month}) => (
<div className='badge badge-light fw-bolder'>{payment_month}</div>
)
export {PaymentMonthCell}
@@ -0,0 +1,26 @@
import { FC, useMemo } from "react";
import { ID } from "../../../../../../_digifi/helpers";
import { useListView } from "../../core/ListViewProvider";
type Props = {
id: ID;
};
const UserSelectionCell: FC<Props> = ({ id }) => {
const { selected, onSelect } = useListView();
const isSelected = useMemo(() => selected.includes(id), [id, selected]);
return (
<div className="form-check form-check-custom form-check-solid">
<input
className="form-check-input"
type="checkbox"
data-kt-check={isSelected}
data-kt-check-target="#kt_table_users .form-check-input"
checked={isSelected}
onChange={() => onSelect(id)}
/>
</div>
);
};
export { UserSelectionCell };
@@ -0,0 +1,28 @@
import {FC, PropsWithChildren} from 'react'
import {HeaderProps} from 'react-table'
import {useListView} from '../../core/ListViewProvider'
import {User} from '../../../core/_models'
type Props = {
tableProps: PropsWithChildren<HeaderProps<User>>
}
const UserSelectionHeader: FC<Props> = ({tableProps}) => {
const {isAllSelected, onSelectAll} = useListView()
return (
<th {...tableProps.column.getHeaderProps()} className='w-10px pe-2'>
<div className='form-check form-check-sm form-check-custom form-check-solid me-3'>
<input
className='form-check-input'
type='checkbox'
data-kt-check={isAllSelected}
data-kt-check-target='#kt_table_users .form-check-input'
checked={isAllSelected}
onChange={onSelectAll}
/>
</div>
</th>
)
}
export {UserSelectionHeader}
@@ -0,0 +1,57 @@
import {Column} from 'react-table'
import {UserInfoCell} from './UserInfoCell'
import { PaymentMonthCell } from './UserLastLoginCell'
import {AgentCell} from './AgentCell'
import {UserActionsCell} from './UserActionsCell'
import {UserSelectionCell} from './UserSelectionCell'
import {UserCustomHeader} from './UserCustomHeader'
import {UserSelectionHeader} from './UserSelectionHeader'
import {User} from '../../../core/_models'
import { AddedCell } from './AddedCell'
const usersColumns: ReadonlyArray<Column<User>> = [
{
Header: (props) => <UserSelectionHeader tableProps={props} />,
id: 'selection',
Cell: ({...props}) => <UserSelectionCell id={props.data[props.row.index].uid} />,
},
{
Header: (props) => <UserCustomHeader tableProps={props} title='Name' className='min-w-125px' />,
id: 'firstname',
Cell: ({...props}) => <UserInfoCell user={props.data[props.row.index]} />,
},
{
Header: (props) => <UserCustomHeader tableProps={props} title='Amount' className='min-w-125px' />,
accessor: 'loan_amount',
},
{
Header: (props) => (
<UserCustomHeader tableProps={props} title='Payment Terms' className='min-w-125px' />
),
id: 'payment_month',
Cell: ({...props}) => <PaymentMonthCell payment_month={props.data[props.row.index].payment_month} />,
},
{
Header: (props) => (
<UserCustomHeader tableProps={props} title='Agent' className='min-w-125px' />
),
id: 'sales_agent',
Cell: ({...props}) => <AgentCell agent={props.data[props.row.index].sales_agent} />,
},
{
Header: (props) => (
<UserCustomHeader tableProps={props} title='Added' className='min-w-125px' />
),
id: 'added',
Cell: ({...props}) => <AddedCell added={props.data[props.row.index].added} />,
},
{
Header: (props) => (
<UserCustomHeader tableProps={props} title='Actions' className='text-end min-w-100px' />
),
id: 'actions',
Cell: ({...props}) => <UserActionsCell id={props.data[props.row.index].uid} />,
},
]
export {usersColumns}
@@ -0,0 +1,44 @@
import {useEffect} from 'react'
import {UserEditModalHeader} from './UserEditModalHeader'
import {UserEditModalFormWrapper} from './UserEditModalFormWrapper'
const UserEditModal = () => {
useEffect(() => {
document.body.classList.add('modal-open')
return () => {
document.body.classList.remove('modal-open')
}
}, [])
return (
<>
<div
className='modal fade show d-block'
id='kt_modal_add_user'
role='dialog'
tabIndex={-1}
aria-modal='true'
>
{/* begin::Modal dialog */}
<div className='modal-dialog modal-dialog-centered mw-650px'>
{/* begin::Modal content */}
<div className='modal-content'>
<UserEditModalHeader />
{/* begin::Modal body */}
<div className='modal-body scroll-y mx-5 mx-xl-15 my-7'>
<UserEditModalFormWrapper />
</div>
{/* end::Modal body */}
</div>
{/* end::Modal content */}
</div>
{/* end::Modal dialog */}
</div>
{/* begin::Modal Backdrop */}
<div className='modal-backdrop fade show'></div>
{/* end::Modal Backdrop */}
</>
)
}
export {UserEditModal}
@@ -0,0 +1,433 @@
import { FC, useState } from "react";
import * as Yup from "yup";
import { useFormik } from "formik";
import { isNotEmpty, toAbsoluteUrl } from "../../../../../_digifi/helpers";
import { initialUser, User } from "../../core/_models";
import clsx from "clsx";
import { useListView } from "../core/ListViewProvider";
import { UsersListLoading } from "../components/loading/UsersListLoading";
import { createUser, updateUser } from "../../core/_requests";
import { useQueryResponse } from "../core/QueryResponseProvider";
type Props = {
isUserLoading: boolean;
user: User;
};
const editUserSchema = Yup.object().shape({
email: Yup.string()
.email("Wrong email format")
.min(3, "Minimum 3 symbols")
.max(50, "Maximum 50 symbols")
.required("Email is required"),
name: Yup.string()
.min(3, "Minimum 3 symbols")
.max(50, "Maximum 50 symbols")
.required("Name is required"),
});
const UserEditModalForm: FC<Props> = ({ user, isUserLoading }) => {
const { setItemIdForUpdate } = useListView();
const { refetch } = useQueryResponse();
const [userForEdit] = useState<User>({
...user,
avatar: user.avatar || initialUser.avatar,
role: user.role || initialUser.role,
position: user.position || initialUser.position,
name: user.name || initialUser.name,
email: user.email || initialUser.email,
});
const cancel = (withRefresh?: boolean) => {
if (withRefresh) {
refetch();
}
setItemIdForUpdate(undefined);
};
const blankImg = toAbsoluteUrl("media/svg/avatars/blank.svg");
const userAvatarImg = toAbsoluteUrl(`media/${userForEdit.avatar}`);
const formik = useFormik({
initialValues: userForEdit,
validationSchema: editUserSchema,
onSubmit: async (values, { setSubmitting }) => {
setSubmitting(true);
try {
if (isNotEmpty(values.id)) {
await updateUser(values);
} else {
await createUser(values);
}
} catch (ex) {
console.error(ex);
} finally {
setSubmitting(true);
cancel(true);
}
},
});
return (
<>
<form
id="kt_modal_add_user_form"
className="form"
onSubmit={formik.handleSubmit}
noValidate
>
{/* begin::Scroll */}
<div
className="d-flex flex-column scroll-y me-n7 pe-7"
id="kt_modal_add_user_scroll"
data-kt-scroll="true"
data-kt-scroll-activate="{default: false, lg: true}"
data-kt-scroll-max-height="auto"
data-kt-scroll-dependencies="#kt_modal_add_user_header"
data-kt-scroll-wrappers="#kt_modal_add_user_scroll"
data-kt-scroll-offset="300px"
>
{/* begin::Input group */}
<div className="fv-row mb-7">
{/* begin::Label */}
<label className="d-block fw-bold fs-6 mb-5">Avatar</label>
{/* end::Label */}
{/* begin::Image input */}
<div
className="image-input image-input-outline"
data-kt-image-input="true"
style={{ backgroundImage: `url('${blankImg}')` }}
>
{/* begin::Preview existing avatar */}
<div
className="image-input-wrapper w-125px h-125px"
style={{ backgroundImage: `url('${userAvatarImg}')` }}
></div>
{/* end::Preview existing avatar */}
{/* begin::Label */}
{/* <label
className='btn btn-icon btn-circle btn-active-color-primary w-25px h-25px bg-body shadow'
data-kt-image-input-action='change'
data-bs-toggle='tooltip'
title='Change avatar'
>
<i className='bi bi-pencil-fill fs-7'></i>
<input type='file' name='avatar' accept='.png, .jpg, .jpeg' />
<input type='hidden' name='avatar_remove' />
</label> */}
{/* end::Label */}
{/* begin::Cancel */}
{/* <span
className='btn btn-icon btn-circle btn-active-color-primary w-25px h-25px bg-body shadow'
data-kt-image-input-action='cancel'
data-bs-toggle='tooltip'
title='Cancel avatar'
>
<i className='bi bi-x fs-2'></i>
</span> */}
{/* end::Cancel */}
{/* begin::Remove */}
{/* <span
className='btn btn-icon btn-circle btn-active-color-primary w-25px h-25px bg-body shadow'
data-kt-image-input-action='remove'
data-bs-toggle='tooltip'
title='Remove avatar'
>
<i className='bi bi-x fs-2'></i>
</span> */}
{/* end::Remove */}
</div>
{/* end::Image input */}
{/* begin::Hint */}
{/* <div className='form-text'>Allowed file types: png, jpg, jpeg.</div> */}
{/* end::Hint */}
</div>
{/* end::Input group */}
{/* begin::Input group */}
<div className="fv-row mb-7">
{/* begin::Label */}
<label className="required fw-bold fs-6 mb-2">Full Name</label>
{/* end::Label */}
{/* begin::Input */}
<input
placeholder="Full name"
{...formik.getFieldProps("name")}
type="text"
name="name"
className={clsx(
"form-control form-control-solid mb-3 mb-lg-0",
{ "is-invalid": formik.touched.name && formik.errors.name },
{
"is-valid": formik.touched.name && !formik.errors.name,
}
)}
autoComplete="off"
disabled={formik.isSubmitting || isUserLoading}
/>
{formik.touched.name && formik.errors.name && (
<div className="fv-plugins-message-container">
<div className="fv-help-block">
<span role="alert">{formik.errors.name}</span>
</div>
</div>
)}
{/* end::Input */}
</div>
{/* end::Input group */}
{/* begin::Input group */}
<div className="fv-row mb-7">
{/* begin::Label */}
<label className="required fw-bold fs-6 mb-2">Email</label>
{/* end::Label */}
{/* begin::Input */}
<input
placeholder="Email"
{...formik.getFieldProps("email")}
className={clsx(
"form-control form-control-solid mb-3 mb-lg-0",
{ "is-invalid": formik.touched.email && formik.errors.email },
{
"is-valid": formik.touched.email && !formik.errors.email,
}
)}
type="email"
name="email"
autoComplete="off"
disabled={formik.isSubmitting || isUserLoading}
/>
{/* end::Input */}
{formik.touched.email && formik.errors.email && (
<div className="fv-plugins-message-container">
<span role="alert">{formik.errors.email}</span>
</div>
)}
</div>
{/* end::Input group */}
{/* begin::Input group */}
<div className="mb-7">
{/* begin::Label */}
<label className="required fw-bold fs-6 mb-5">Role</label>
{/* end::Label */}
{/* begin::Roles */}
{/* begin::Input row */}
<div className="d-flex fv-row">
{/* begin::Radio */}
<div className="form-check form-check-custom form-check-solid">
{/* begin::Input */}
<input
className="form-check-input me-3"
{...formik.getFieldProps("role")}
name="role"
type="radio"
value="Administrator"
id="kt_modal_update_role_option_0"
checked={formik.values.role === "Administrator"}
disabled={formik.isSubmitting || isUserLoading}
/>
{/* end::Input */}
{/* begin::Label */}
<label
className="form-check-label"
htmlFor="kt_modal_update_role_option_0"
>
<div className="fw-bolder text-gray-800">Administrator</div>
<div className="text-gray-600">
Best for business owners and company administrators
</div>
</label>
{/* end::Label */}
</div>
{/* end::Radio */}
</div>
{/* end::Input row */}
<div className="separator separator-dashed my-5"></div>
{/* begin::Input row */}
<div className="d-flex fv-row">
{/* begin::Radio */}
<div className="form-check form-check-custom form-check-solid">
{/* begin::Input */}
<input
className="form-check-input me-3"
{...formik.getFieldProps("role")}
name="role"
type="radio"
value="Developer"
id="kt_modal_update_role_option_1"
checked={formik.values.role === "Developer"}
disabled={formik.isSubmitting || isUserLoading}
/>
{/* end::Input */}
{/* begin::Label */}
<label
className="form-check-label"
htmlFor="kt_modal_update_role_option_1"
>
<div className="fw-bolder text-gray-800">Developer</div>
<div className="text-gray-600">
Best for developers or people primarily using the API
</div>
</label>
{/* end::Label */}
</div>
{/* end::Radio */}
</div>
{/* end::Input row */}
<div className="separator separator-dashed my-5"></div>
{/* begin::Input row */}
<div className="d-flex fv-row">
{/* begin::Radio */}
<div className="form-check form-check-custom form-check-solid">
{/* begin::Input */}
<input
className="form-check-input me-3"
{...formik.getFieldProps("role")}
name="role"
type="radio"
value="Analyst"
id="kt_modal_update_role_option_2"
checked={formik.values.role === "Analyst"}
disabled={formik.isSubmitting || isUserLoading}
/>
{/* end::Input */}
{/* begin::Label */}
<label
className="form-check-label"
htmlFor="kt_modal_update_role_option_2"
>
<div className="fw-bolder text-gray-800">Analyst</div>
<div className="text-gray-600">
Best for people who need full access to analytics data, but
don't need to update business settings
</div>
</label>
{/* end::Label */}
</div>
{/* end::Radio */}
</div>
{/* end::Input row */}
<div className="separator separator-dashed my-5"></div>
{/* begin::Input row */}
<div className="d-flex fv-row">
{/* begin::Radio */}
<div className="form-check form-check-custom form-check-solid">
{/* begin::Input */}
<input
className="form-check-input me-3"
{...formik.getFieldProps("role")}
name="role"
type="radio"
value="Support"
id="kt_modal_update_role_option_3"
checked={formik.values.role === "Support"}
disabled={formik.isSubmitting || isUserLoading}
/>
{/* end::Input */}
{/* begin::Label */}
<label
className="form-check-label"
htmlFor="kt_modal_update_role_option_3"
>
<div className="fw-bolder text-gray-800">Support</div>
<div className="text-gray-600">
Best for employees who regularly refund payments and respond
to disputes
</div>
</label>
{/* end::Label */}
</div>
{/* end::Radio */}
</div>
{/* end::Input row */}
<div className="separator separator-dashed my-5"></div>
{/* begin::Input row */}
<div className="d-flex fv-row">
{/* begin::Radio */}
<div className="form-check form-check-custom form-check-solid">
{/* begin::Input */}
<input
className="form-check-input me-3"
{...formik.getFieldProps("role")}
name="role"
type="radio"
id="kt_modal_update_role_option_4"
value="Trial"
checked={formik.values.role === "Trial"}
disabled={formik.isSubmitting || isUserLoading}
/>
{/* end::Input */}
{/* begin::Label */}
<label
className="form-check-label"
htmlFor="kt_modal_update_role_option_4"
>
<div className="fw-bolder text-gray-800">Trial</div>
<div className="text-gray-600">
Best for people who need to preview content data, but don't
need to make any updates
</div>
</label>
{/* end::Label */}
</div>
{/* end::Radio */}
</div>
{/* end::Input row */}
{/* end::Roles */}
</div>
{/* end::Input group */}
</div>
{/* end::Scroll */}
{/* begin::Actions */}
<div className="text-center pt-15">
<button
type="reset"
onClick={() => cancel()}
className="btn btn-light me-3"
data-kt-users-modal-action="cancel"
disabled={formik.isSubmitting || isUserLoading}
>
Discard
</button>
<button
type="submit"
className="btn btn-primary"
data-kt-users-modal-action="submit"
disabled={
isUserLoading ||
formik.isSubmitting ||
!formik.isValid ||
!formik.touched
}
>
<span className="indicator-label">Submit</span>
{(formik.isSubmitting || isUserLoading) && (
<span className="indicator-progress">
Please wait...{" "}
<span className="spinner-border spinner-border-sm align-middle ms-2"></span>
</span>
)}
</button>
</div>
{/* end::Actions */}
</form>
{(formik.isSubmitting || isUserLoading) && <UsersListLoading />}
</>
);
};
export { UserEditModalForm };
@@ -0,0 +1,42 @@
import { useQuery } from "react-query";
import { UserEditModalForm } from "./UserEditModalForm";
import { isNotEmpty, QUERIES } from "../../../../../_digifi/helpers";
import { useListView } from "../core/ListViewProvider";
import { getUserById } from "../../core/_requests";
const UserEditModalFormWrapper = () => {
const { itemIdForUpdate, setItemIdForUpdate } = useListView();
const enabledQuery: boolean = isNotEmpty(itemIdForUpdate);
const {
isLoading,
data: user,
error,
} = useQuery(
`${QUERIES.USERS_LIST}-user-${itemIdForUpdate}`,
() => {
return getUserById(itemIdForUpdate);
},
{
cacheTime: 0,
enabled: enabledQuery,
onError: (err) => {
setItemIdForUpdate(undefined);
console.error(err);
},
}
);
if (!itemIdForUpdate) {
return (
<UserEditModalForm isUserLoading={isLoading} user={{ id: undefined }} />
);
}
if (!isLoading && !error && user) {
return <UserEditModalForm isUserLoading={isLoading} user={user} />;
}
return null;
};
export { UserEditModalFormWrapper };
@@ -0,0 +1,27 @@
import { KTIcon } from "../../../../../_digifi/helpers";
import { useListView } from "../core/ListViewProvider";
const UserEditModalHeader = () => {
const { setItemIdForUpdate } = useListView();
return (
<div className="modal-header">
{/* begin::Modal title */}
<h2 className="fw-bolder">Add User</h2>
{/* end::Modal title */}
{/* begin::Close */}
<div
className="btn btn-icon btn-sm btn-active-icon-primary"
data-kt-users-modal-action="close"
onClick={() => setItemIdForUpdate(undefined)}
style={{ cursor: "pointer" }}
>
<KTIcon iconName="cross" className="fs-1" />
</div>
{/* end::Close */}
</div>
);
};
export { UserEditModalHeader };
@@ -0,0 +1,40 @@
import {
ListViewProvider,
useListView,
} from "./core/ListViewProvider";
import { QueryRequestProvider } from "./core/QueryRequestProvider";
import { QueryResponseProvider } from "./core/QueryResponseProvider";
import { UsersListHeader } from "./components/header/UsersListHeader";
import { UsersTable } from "./table/UsersTable";
import { UserEditModal } from "./user-edit-modal/UserEditModal";
import { KTCard } from "../../../../_digifi/helpers";
import { ToolbarWrapper } from "../../../../_digifi/layout/components/toolbar";
import { Content } from "../../../../_digifi/layout/components/content";
const UsersList = () => {
const { itemIdForUpdate } = useListView();
return (
<>
<KTCard>
<UsersListHeader />
<UsersTable />
</KTCard>
{itemIdForUpdate !== undefined && <UserEditModal />}
</>
);
};
const UsersListWrapper = () => (
<QueryRequestProvider>
<QueryResponseProvider>
<ListViewProvider>
<ToolbarWrapper />
<Content>
<UsersList />
</Content>
</ListViewProvider>
</QueryResponseProvider>
</QueryRequestProvider>
);
export { UsersListWrapper };
@@ -0,0 +1,35 @@
import { KTIcon } from "../../../../../../_digifi/helpers";
import { useListView } from "../../core/ListViewProvider";
import { UsersListFilter } from "./UsersListFilter";
const UsersListToolbar = () => {
const { setItemIdForUpdate } = useListView();
const openAddUserModal = () => {
setItemIdForUpdate(null);
};
return (
<div
className="d-flex justify-content-end"
data-kt-user-table-toolbar="base"
>
<UsersListFilter />
{/* begin::Export */}
{/* <button type='button' className='btn btn-light-primary me-3'>
<KTIcon iconName='exit-up' className='fs-2' />
Export
</button> */}
{/* end::Export */}
{/* begin::Add user */}
{/* <button type='button' className='btn btn-primary' onClick={openAddUserModal}>
<KTIcon iconName='plus' className='fs-2' />
Add User
</button> */}
{/* end::Add user */}
</div>
);
};
export { UsersListToolbar };
@@ -0,0 +1,136 @@
import { useEffect, useState } from "react";
import { MenuComponent } from "../../../../../../_digifi/assets/ts/components";
import { initialQueryState, KTIcon } from "../../../../../../_digifi/helpers";
import { useQueryRequest } from "../../core/QueryRequestProvider";
import { useQueryResponse } from "../../core/QueryResponseProvider";
const UsersListFilter = () => {
const { updateState } = useQueryRequest();
const { isLoading } = useQueryResponse();
const [role, setRole] = useState<string | undefined>();
const [lastLogin, setLastLogin] = useState<string | undefined>();
useEffect(() => {
MenuComponent.reinitialization();
}, []);
const resetData = () => {
updateState({ filter: undefined, ...initialQueryState });
};
const filterData = () => {
updateState({
filter: { role, last_login: lastLogin },
...initialQueryState,
});
};
return (
<>
{/* begin::Filter Button */}
<button
disabled={isLoading}
type="button"
className="btn btn-light-primary me-3"
data-kt-menu-trigger="click"
data-kt-menu-placement="bottom-end"
>
<KTIcon iconName="filter" className="fs-2" />
Filter
</button>
{/* end::Filter Button */}
{/* begin::SubMenu */}
<div
className="menu menu-sub menu-sub-dropdown w-300px w-md-325px"
data-kt-menu="true"
>
{/* begin::Header */}
<div className="px-7 py-5">
<div className="fs-5 text-gray-900 fw-bolder">Filter Options</div>
</div>
{/* end::Header */}
{/* begin::Separator */}
<div className="separator border-gray-200"></div>
{/* end::Separator */}
{/* begin::Content */}
<div className="px-7 py-5" data-kt-user-table-filter="form">
{/* begin::Input group */}
<div className="mb-10">
<label className="form-label fs-6 fw-bold">Role:</label>
<select
className="form-select form-select-solid fw-bolder"
data-kt-select2="true"
data-placeholder="Select option"
data-allow-clear="true"
data-kt-user-table-filter="role"
data-hide-search="true"
onChange={(e) => setRole(e.target.value)}
value={role}
>
<option value=""></option>
<option value="Administrator">Administrator</option>
<option value="Analyst">Analyst</option>
<option value="Developer">Developer</option>
<option value="Support">Support</option>
<option value="Trial">Trial</option>
</select>
</div>
{/* end::Input group */}
{/* begin::Input group */}
<div className="mb-10">
<label className="form-label fs-6 fw-bold">Last login:</label>
<select
className="form-select form-select-solid fw-bolder"
data-kt-select2="true"
data-placeholder="Select option"
data-allow-clear="true"
data-kt-user-table-filter="two-step"
data-hide-search="true"
onChange={(e) => setLastLogin(e.target.value)}
value={lastLogin}
>
<option value=""></option>
<option value="Yesterday">Yesterday</option>
<option value="20 mins ago">20 mins ago</option>
<option value="5 hours ago">5 hours ago</option>
<option value="2 days ago">2 days ago</option>
</select>
</div>
{/* end::Input group */}
{/* begin::Actions */}
<div className="d-flex justify-content-end">
<button
type="button"
disabled={isLoading}
onClick={filterData}
className="btn btn-light btn-active-light-primary fw-bold me-2 px-6"
data-kt-menu-dismiss="true"
data-kt-user-table-filter="reset"
>
Reset
</button>
<button
disabled={isLoading}
type="button"
onClick={resetData}
className="btn btn-primary fw-bold px-6"
data-kt-menu-dismiss="true"
data-kt-user-table-filter="filter"
>
Apply
</button>
</div>
{/* end::Actions */}
</div>
{/* end::Content */}
</div>
{/* end::SubMenu */}
</>
);
};
export { UsersListFilter };
@@ -0,0 +1,38 @@
import { useQueryClient, useMutation } from "react-query";
import { QUERIES } from "../../../../../../_digifi/helpers";
import { useListView } from "../../core/ListViewProvider";
import { useQueryResponse } from "../../core/QueryResponseProvider";
import { deleteSelectedUsers } from "../../../core/_requests";
const UsersListGrouping = () => {
const { selected, clearSelected } = useListView();
const queryClient = useQueryClient();
const { query } = useQueryResponse();
const deleteSelectedItems = useMutation(() => deleteSelectedUsers(selected), {
// 💡 response of the mutation is passed to onSuccess
onSuccess: () => {
// ✅ update detail view directly
queryClient.invalidateQueries([`${QUERIES.USERS_LIST}-${query}`]);
clearSelected();
},
});
return (
<div className="d-flex justify-content-end align-items-center">
<div className="fw-bolder me-5">
<span className="me-2">{selected.length}</span> Selected
</div>
<button
type="button"
className="btn btn-danger"
onClick={async () => await deleteSelectedItems.mutateAsync()}
>
Delete Selected
</button>
</div>
);
};
export { UsersListGrouping };
@@ -0,0 +1,22 @@
import {useListView} from '../../core/ListViewProvider'
import {UsersListToolbar} from './UserListToolbar'
import {UsersListGrouping} from './UsersListGrouping'
import {UsersListSearchComponent} from './UsersListSearchComponent'
const UsersListHeader = () => {
const {selected} = useListView()
return (
<div className='card-header border-0 pt-6'>
<UsersListSearchComponent />
{/* begin::Card toolbar */}
<div className='card-toolbar'>
{/* begin::Group actions */}
{selected.length > 0 ? <UsersListGrouping /> : <UsersListToolbar />}
{/* end::Group actions */}
</div>
{/* end::Card toolbar */}
</div>
)
}
export {UsersListHeader}
@@ -0,0 +1,49 @@
/* eslint-disable react-hooks/exhaustive-deps */
import { useEffect, useState } from "react";
import {
initialQueryState,
KTIcon,
useDebounce,
} from "../../../../../../_digifi/helpers";
import { useQueryRequest } from "../../core/QueryRequestProvider";
const UsersListSearchComponent = () => {
const { updateState } = useQueryRequest();
const [searchTerm, setSearchTerm] = useState<string>("");
// Debounce search term so that it only gives us latest value ...
// ... if searchTerm has not been updated within last 500ms.
// The goal is to only have the API call fire when user stops typing ...
// ... so that we aren't hitting our API rapidly.
const debouncedSearchTerm = useDebounce(searchTerm, 150);
// Effect for API call
useEffect(
() => {
if (debouncedSearchTerm !== undefined && searchTerm !== undefined) {
updateState({ search: debouncedSearchTerm, ...initialQueryState });
}
},
[debouncedSearchTerm] // Only call effect if debounced search term changes
// More details about useDebounce: https://usehooks.com/useDebounce/
);
return (
<div className="card-title">
{/* begin::Search */}
<div className="d-flex align-items-center position-relative my-1">
<KTIcon iconName="magnifier" className="fs-1 position-absolute ms-6" />
<input
type="text"
data-kt-user-table-filter="search"
className="form-control form-control-solid w-250px ps-14"
placeholder="Search user"
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
/>
</div>
{/* end::Search */}
</div>
);
};
export { UsersListSearchComponent };
@@ -0,0 +1,18 @@
const UsersListLoading = () => {
const styles = {
borderRadius: '0.475rem',
boxShadow: '0 0 50px 0 rgb(82 63 105 / 15%)',
backgroundColor: '#fff',
color: '#7e8299',
fontWeight: '500',
margin: '0',
width: 'auto',
padding: '1rem 2rem',
top: 'calc(50% - 2rem)',
left: 'calc(50% - 4rem)',
}
return <div style={{...styles, position: 'absolute', textAlign: 'center'}}>Processing...</div>
}
export {UsersListLoading}
@@ -0,0 +1,179 @@
import clsx from "clsx";
import {
useQueryResponseLoading,
useQueryResponsePagination,
} from "../../core/QueryResponseProvider";
import { useQueryRequest } from "../../core/QueryRequestProvider";
import { PaginationState } from "../../../../../../_digifi/helpers";
import { useMemo } from "react";
const mappedLabel = (label: string): string => {
if (label === "&laquo; Previous") {
return "Previous";
}
if (label === "Next &raquo;") {
return "Next";
}
return label;
};
const UsersListPagination = () => {
const pagination = useQueryResponsePagination();
const isLoading = useQueryResponseLoading();
const { updateState } = useQueryRequest();
const updatePage = (page: number | undefined | null) => {
if (!page || isLoading || pagination.page === page) {
return;
}
updateState({ page, items_per_page: pagination.items_per_page || 10 });
};
const PAGINATION_PAGES_COUNT = 5;
const sliceLinks = (pagination?: PaginationState) => {
if (!pagination?.links?.length) {
return [];
}
const scopedLinks = [...pagination.links];
let pageLinks: Array<{
label: string;
active: boolean;
url: string | null;
page: number | null;
}> = [];
const previousLink: {
label: string;
active: boolean;
url: string | null;
page: number | null;
} = scopedLinks.shift()!;
const nextLink: {
label: string;
active: boolean;
url: string | null;
page: number | null;
} = scopedLinks.pop()!;
const halfOfPagesCount = Math.floor(PAGINATION_PAGES_COUNT / 2);
pageLinks.push(previousLink);
if (
pagination.page <= Math.round(PAGINATION_PAGES_COUNT / 2) ||
scopedLinks.length <= PAGINATION_PAGES_COUNT
) {
pageLinks = [
...pageLinks,
...scopedLinks.slice(0, PAGINATION_PAGES_COUNT),
];
}
if (
pagination.page > scopedLinks.length - halfOfPagesCount &&
scopedLinks.length > PAGINATION_PAGES_COUNT
) {
pageLinks = [
...pageLinks,
...scopedLinks.slice(
scopedLinks.length - PAGINATION_PAGES_COUNT,
scopedLinks.length
),
];
}
if (
!(
pagination.page <= Math.round(PAGINATION_PAGES_COUNT / 2) ||
scopedLinks.length <= PAGINATION_PAGES_COUNT
) &&
!(pagination.page > scopedLinks.length - halfOfPagesCount)
) {
pageLinks = [
...pageLinks,
...scopedLinks.slice(
pagination.page - 1 - halfOfPagesCount,
pagination.page + halfOfPagesCount
),
];
}
pageLinks.push(nextLink);
return pageLinks;
};
const paginationLinks = useMemo(() => sliceLinks(pagination), [pagination]);
return (
<div className="row">
<div className="col-sm-12 col-md-5 d-flex align-items-center justify-content-center justify-content-md-start"></div>
<div className="col-sm-12 col-md-7 d-flex align-items-center justify-content-center justify-content-md-end">
<div id="kt_table_users_paginate">
<ul className="pagination">
<li
className={clsx("page-item", {
disabled: isLoading || pagination.page === 1,
})}
>
<a
onClick={() => updatePage(1)}
style={{ cursor: "pointer" }}
className="page-link"
>
First
</a>
</li>
{paginationLinks
?.map((link) => {
return { ...link, label: mappedLabel(link.label) };
})
.map((link) => (
<li
key={link.label}
className={clsx("page-item", {
active: pagination.page === link.page,
disabled: isLoading,
previous: link.label === "Previous",
next: link.label === "Next",
})}
>
<a
className={clsx("page-link", {
"page-text":
link.label === "Previous" || link.label === "Next",
"me-5": link.label === "Previous",
})}
onClick={() => updatePage(link.page)}
style={{ cursor: "pointer" }}
>
{mappedLabel(link.label)}
</a>
</li>
))}
<li
className={clsx("page-item", {
disabled:
isLoading ||
pagination.page === (pagination.links?.length || 3) - 2,
})}
>
<a
onClick={() => updatePage((pagination.links?.length || 3) - 2)}
style={{ cursor: "pointer" }}
className="page-link"
>
Last
</a>
</li>
</ul>
</div>
</div>
</div>
);
};
export { UsersListPagination };
@@ -0,0 +1,62 @@
/* eslint-disable react-refresh/only-export-components */
import { FC, useState, createContext, useContext, useMemo } from "react";
import {
ID,
calculatedGroupingIsDisabled,
calculateIsAllDataSelected,
groupingOnSelect,
initialListView,
ListViewContextProps,
groupingOnSelectAll,
WithChildren,
} from "../../../../../_digifi/helpers";
import {
useQueryResponse,
useQueryResponseData,
} from "./QueryResponseProvider";
const ListViewContext = createContext<ListViewContextProps>(initialListView);
const ListViewProvider: FC<WithChildren> = ({ children }) => {
const [selected, setSelected] = useState<Array<ID>>(initialListView.selected);
const [itemIdForUpdate, setItemIdForUpdate] = useState<ID>(
initialListView.itemIdForUpdate
);
const { isLoading } = useQueryResponse();
const data = useQueryResponseData();
const disabled = useMemo(
() => calculatedGroupingIsDisabled(isLoading, data),
[isLoading, data]
);
const isAllSelected = useMemo(
() => calculateIsAllDataSelected(data, selected),
[data, selected]
);
return (
<ListViewContext.Provider
value={{
selected,
itemIdForUpdate,
setItemIdForUpdate,
disabled,
isAllSelected,
onSelect: (id: ID) => {
groupingOnSelect(id, selected, setSelected);
},
onSelectAll: () => {
groupingOnSelectAll(isAllSelected, setSelected, data);
},
clearSelected: () => {
setSelected([]);
},
}}
>
{children}
</ListViewContext.Provider>
);
};
const useListView = () => useContext(ListViewContext);
export { ListViewProvider, useListView };
@@ -0,0 +1,29 @@
/* eslint-disable react-refresh/only-export-components */
import { FC, useState, createContext, useContext } from "react";
import {
QueryState,
QueryRequestContextProps,
initialQueryRequest,
WithChildren,
} from "../../../../../_digifi/helpers";
const QueryRequestContext =
createContext<QueryRequestContextProps>(initialQueryRequest);
const QueryRequestProvider: FC<WithChildren> = ({ children }) => {
const [state, setState] = useState<QueryState>(initialQueryRequest.state);
const updateState = (updates: Partial<QueryState>) => {
const updatedState = { ...state, ...updates } as QueryState;
setState(updatedState);
};
return (
<QueryRequestContext.Provider value={{ state, updateState }}>
{children}
</QueryRequestContext.Provider>
);
};
const useQueryRequest = () => useContext(QueryRequestContext);
export { QueryRequestProvider, useQueryRequest };
@@ -0,0 +1,87 @@
/* eslint-disable react-refresh/only-export-components */
/* eslint-disable react-hooks/exhaustive-deps */
import { FC, useContext, useState, useEffect, useMemo } from "react";
import { useQuery } from "react-query";
import {
createResponseContext,
initialQueryResponse,
initialQueryState,
PaginationState,
QUERIES,
stringifyRequestQuery,
WithChildren,
} from "../../../../../_digifi/helpers";
import { getPendingUsers } from "../../core/_requests";
import { User } from "../../core/_models";
import { useQueryRequest } from "./QueryRequestProvider";
const QueryResponseContext = createResponseContext<User>(initialQueryResponse);
const QueryResponseProvider: FC<WithChildren> = ({ children }) => {
const { state } = useQueryRequest();
const [query, setQuery] = useState<string>(stringifyRequestQuery(state));
const updatedQuery = useMemo(() => stringifyRequestQuery(state), [state]);
useEffect(() => {
if (query !== updatedQuery) {
setQuery(updatedQuery);
}
}, [updatedQuery]);
const {
isFetching,
refetch,
data: response,
} = useQuery(
`${QUERIES.USERS_LIST}-${query}`,
() => {
return getPendingUsers(query);
},
{ cacheTime: 0, keepPreviousData: true, refetchOnWindowFocus: false }
);
return (
<QueryResponseContext.Provider
value={{ isLoading: isFetching, refetch, response, query }}
>
{children}
</QueryResponseContext.Provider>
);
};
const useQueryResponse = () => useContext(QueryResponseContext);
const useQueryResponseData = () => {
const { response } = useQueryResponse();
if (!response) {
return [];
}
return response?.records || [];
};
const useQueryResponsePagination = () => {
const defaultPaginationState: PaginationState = {
links: [],
...initialQueryState,
};
const { response } = useQueryResponse();
if (!response || !response.payload || !response.payload.pagination) {
return defaultPaginationState;
}
return response.payload.pagination;
};
const useQueryResponseLoading = (): boolean => {
const { isLoading } = useQueryResponse();
return isLoading;
};
export {
QueryResponseProvider,
useQueryResponse,
useQueryResponseData,
useQueryResponsePagination,
useQueryResponseLoading,
};
@@ -0,0 +1,66 @@
import { useMemo } from "react";
import { useTable, ColumnInstance, Row } from "react-table";
import { CustomHeaderColumn } from "./columns/CustomHeaderColumn";
import { CustomRow } from "./columns/CustomRow";
import {
useQueryResponseData,
useQueryResponseLoading,
} from "../core/QueryResponseProvider";
import { usersColumns } from "./columns/_columns";
import { User } from "../../core/_models";
import { UsersListLoading } from "../components/loading/UsersListLoading";
import { UsersListPagination } from "../components/pagination/UsersListPagination";
import { KTCardBody } from "../../../../../_digifi/helpers";
const UsersTable = () => {
const users = useQueryResponseData();
// console.log('users', users)
const isLoading = useQueryResponseLoading();
const data = useMemo(() => users, [users]);
const columns = useMemo(() => usersColumns, []);
const { getTableProps, getTableBodyProps, headers, rows, prepareRow } =
useTable({
columns,
data,
});
return (
<KTCardBody className="py-4">
<div className="table-responsive">
<table
id="kt_table_users"
className="table align-middle table-row-dashed fs-6 gy-5 dataTable no-footer"
{...getTableProps()}
>
<thead>
<tr className="text-start text-muted fw-bolder fs-7 text-uppercase gs-0">
{headers.map((column: ColumnInstance<User>) => (
<CustomHeaderColumn key={column.id} column={column} />
))}
</tr>
</thead>
<tbody className="text-gray-600 fw-bold" {...getTableBodyProps()}>
{rows.length > 0 ? (
rows.map((row: Row<User>, i) => {
prepareRow(row);
return <CustomRow row={row} key={`row-${i}-${row.id}`} />;
})
) : (
<tr>
<td colSpan={7}>
<div className="d-flex text-center w-100 align-content-center justify-content-center">
No matching records found
</div>
</td>
</tr>
)}
</tbody>
</table>
</div>
<UsersListPagination />
{isLoading && <UsersListLoading />}
</KTCardBody>
);
};
export { UsersTable };
@@ -0,0 +1,14 @@
import { FC } from "react";
import { NewDateTimeFormatter } from "../../../../../../_digifi/lib/NewDateTimeFormatter";
type Props = {
added?: string;
};
const AddedCell: FC<Props> = ({ added }) => (
<div className="badge badge-light fw-bolder">
{NewDateTimeFormatter(added)}
</div>
);
export { AddedCell };
@@ -0,0 +1,11 @@
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,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,76 @@
import { FC, useEffect } from "react";
import { useMutation, useQueryClient } from "react-query";
import { MenuComponent } from "../../../../../../_digifi/assets/ts/components";
import { ID, KTIcon, QUERIES } from "../../../../../../_digifi/helpers";
import { useListView } from "../../core/ListViewProvider";
import { useQueryResponse } from "../../core/QueryResponseProvider";
import { deleteUser } from "../../../core/_requests";
type Props = {
id: ID;
};
const UserActionsCell: FC<Props> = ({ id }) => {
const { setItemIdForUpdate } = useListView();
const { query } = useQueryResponse();
const queryClient = useQueryClient();
useEffect(() => {
MenuComponent.reinitialization();
}, []);
const openEditModal = () => {
setItemIdForUpdate(id);
};
const deleteItem = useMutation(() => deleteUser(id), {
// 💡 response of the mutation is passed to onSuccess
onSuccess: () => {
// ✅ update detail view directly
queryClient.invalidateQueries([`${QUERIES.USERS_LIST}-${query}`]);
},
});
return (
<>
<a
href="#"
className="btn btn-light btn-active-light-primary btn-sm"
data-kt-menu-trigger="click"
data-kt-menu-placement="bottom-end"
>
Actions
<KTIcon iconName="down" className="fs-5 m-0" />
</a>
{/* begin::Menu */}
<div
className="menu menu-sub menu-sub-dropdown menu-column menu-rounded menu-gray-600 menu-state-bg-light-primary fw-bold fs-7 w-175px py-4"
data-kt-menu="true"
>
{/* begin::Menu item */}
<div className="menu-item px-3">
<a className="menu-link px-3" onClick={openEditModal}>
Cancel Application
</a>
</div>
{/* end::Menu item */}
{/* begin::Menu item */}
<div className="menu-item px-3">
<a
className="menu-link px-3"
data-kt-users-table-filter="delete_row"
onClick={async () => await deleteItem.mutateAsync()}
>
Start Processing
</a>
</div>
{/* end::Menu item */}
</div>
{/* end::Menu */}
</>
);
};
export { UserActionsCell };
@@ -0,0 +1,61 @@
import clsx from "clsx";
import { FC, PropsWithChildren, useMemo } from "react";
import { HeaderProps } from "react-table";
import { initialQueryState } from "../../../../../../_digifi/helpers";
import { useQueryRequest } from "../../core/QueryRequestProvider";
import { User } from "../../../core/_models";
type Props = {
className?: string;
title?: string;
tableProps: PropsWithChildren<HeaderProps<User>>;
};
const UserCustomHeader: FC<Props> = ({ className, title, tableProps }) => {
const id = tableProps.column.id;
const { state, updateState } = useQueryRequest();
const isSelectedForSorting = useMemo(() => {
return state.sort && state.sort === id;
}, [state, id]);
const order: "asc" | "desc" | undefined = useMemo(() => state.order, [state]);
const sortColumn = () => {
// avoid sorting for these columns
if (id === "actions" || id === "selection") {
return;
}
if (!isSelectedForSorting) {
// enable sort asc
updateState({ sort: id, order: "asc", ...initialQueryState });
return;
}
if (isSelectedForSorting && order !== undefined) {
if (order === "asc") {
// enable sort desc
updateState({ sort: id, order: "desc", ...initialQueryState });
return;
}
// disable sort
updateState({ sort: undefined, order: undefined, ...initialQueryState });
}
};
return (
<th
{...tableProps.column.getHeaderProps()}
className={clsx(
className,
isSelectedForSorting && order !== undefined && `table-sort-${order}`
)}
style={{ cursor: "pointer" }}
onClick={sortColumn}
>
{title}
</th>
);
};
export { UserCustomHeader };
@@ -0,0 +1,46 @@
import clsx from "clsx";
import { FC } from "react";
import { toAbsoluteUrl } from "../../../../../../_digifi/helpers";
import { User } from "../../../core/_models";
type Props = {
user: User;
};
const UserInfoCell: FC<Props> = ({ user }) => (
<div className="d-flex align-items-center">
{/* begin:: Avatar */}
<div className="symbol symbol-circle symbol-50px overflow-hidden me-3">
<a href="#">
{user.avatar ? (
<div className="symbol-label">
<img
src={toAbsoluteUrl(`media/${user.avatar}`)}
alt={user.name}
className="w-100"
/>
</div>
) : (
<div
className={clsx(
"symbol-label fs-3",
`bg-light-${user.initials?.state}`,
`text-${user.initials?.state}`
)}
>
{user.firstname?.substring(0, 1).toUpperCase()}{" "}
{user.lastname?.substring(0, 1).toUpperCase()}
</div>
)}
</a>
</div>
<div className="d-flex flex-column">
<a href="#" className="text-gray-800 text-hover-primary mb-1">
{user.firstname} {user.lastname}
</a>
<span>{user.email}</span>
</div>
</div>
);
export { UserInfoCell };
@@ -0,0 +1,11 @@
import {FC} from 'react'
type Props = {
payment_month?: string
}
const PaymentMonthCell: FC<Props> = ({payment_month}) => (
<div className='badge badge-light fw-bolder'>{payment_month}</div>
)
export {PaymentMonthCell}
@@ -0,0 +1,26 @@
import { FC, useMemo } from "react";
import { ID } from "../../../../../../_digifi/helpers";
import { useListView } from "../../core/ListViewProvider";
type Props = {
id: ID;
};
const UserSelectionCell: FC<Props> = ({ id }) => {
const { selected, onSelect } = useListView();
const isSelected = useMemo(() => selected.includes(id), [id, selected]);
return (
<div className="form-check form-check-custom form-check-solid">
<input
className="form-check-input"
type="checkbox"
data-kt-check={isSelected}
data-kt-check-target="#kt_table_users .form-check-input"
checked={isSelected}
onChange={() => onSelect(id)}
/>
</div>
);
};
export { UserSelectionCell };
@@ -0,0 +1,28 @@
import {FC, PropsWithChildren} from 'react'
import {HeaderProps} from 'react-table'
import {useListView} from '../../core/ListViewProvider'
import {User} from '../../../core/_models'
type Props = {
tableProps: PropsWithChildren<HeaderProps<User>>
}
const UserSelectionHeader: FC<Props> = ({tableProps}) => {
const {isAllSelected, onSelectAll} = useListView()
return (
<th {...tableProps.column.getHeaderProps()} className='w-10px pe-2'>
<div className='form-check form-check-sm form-check-custom form-check-solid me-3'>
<input
className='form-check-input'
type='checkbox'
data-kt-check={isAllSelected}
data-kt-check-target='#kt_table_users .form-check-input'
checked={isAllSelected}
onChange={onSelectAll}
/>
</div>
</th>
)
}
export {UserSelectionHeader}
@@ -0,0 +1,57 @@
import {Column} from 'react-table'
import {UserInfoCell} from './UserInfoCell'
import { PaymentMonthCell } from './UserLastLoginCell'
import {AgentCell} from './AgentCell'
import {UserActionsCell} from './UserActionsCell'
import {UserSelectionCell} from './UserSelectionCell'
import {UserCustomHeader} from './UserCustomHeader'
import {UserSelectionHeader} from './UserSelectionHeader'
import {User} from '../../../core/_models'
import { AddedCell } from './AddedCell'
const usersColumns: ReadonlyArray<Column<User>> = [
{
Header: (props) => <UserSelectionHeader tableProps={props} />,
id: 'selection',
Cell: ({...props}) => <UserSelectionCell id={props.data[props.row.index].uid} />,
},
{
Header: (props) => <UserCustomHeader tableProps={props} title='Name' className='min-w-125px' />,
id: 'firstname',
Cell: ({...props}) => <UserInfoCell user={props.data[props.row.index]} />,
},
{
Header: (props) => <UserCustomHeader tableProps={props} title='Amount' className='min-w-125px' />,
accessor: 'loan_amount',
},
{
Header: (props) => (
<UserCustomHeader tableProps={props} title='Payment Terms' className='min-w-125px' />
),
id: 'payment_month',
Cell: ({...props}) => <PaymentMonthCell payment_month={props.data[props.row.index].payment_month} />,
},
{
Header: (props) => (
<UserCustomHeader tableProps={props} title='Agent' className='min-w-125px' />
),
id: 'sales_agent',
Cell: ({...props}) => <AgentCell agent={props.data[props.row.index].sales_agent} />,
},
{
Header: (props) => (
<UserCustomHeader tableProps={props} title='Added' className='min-w-125px' />
),
id: 'added',
Cell: ({...props}) => <AddedCell added={props.data[props.row.index].added} />,
},
{
Header: (props) => (
<UserCustomHeader tableProps={props} title='Actions' className='text-end min-w-100px' />
),
id: 'actions',
Cell: ({...props}) => <UserActionsCell id={props.data[props.row.index].uid} />,
},
]
export {usersColumns}
@@ -0,0 +1,44 @@
import {useEffect} from 'react'
import {UserEditModalHeader} from './UserEditModalHeader'
import {UserEditModalFormWrapper} from './UserEditModalFormWrapper'
const UserEditModal = () => {
useEffect(() => {
document.body.classList.add('modal-open')
return () => {
document.body.classList.remove('modal-open')
}
}, [])
return (
<>
<div
className='modal fade show d-block'
id='kt_modal_add_user'
role='dialog'
tabIndex={-1}
aria-modal='true'
>
{/* begin::Modal dialog */}
<div className='modal-dialog modal-dialog-centered mw-650px'>
{/* begin::Modal content */}
<div className='modal-content'>
<UserEditModalHeader />
{/* begin::Modal body */}
<div className='modal-body scroll-y mx-5 mx-xl-15 my-7'>
<UserEditModalFormWrapper />
</div>
{/* end::Modal body */}
</div>
{/* end::Modal content */}
</div>
{/* end::Modal dialog */}
</div>
{/* begin::Modal Backdrop */}
<div className='modal-backdrop fade show'></div>
{/* end::Modal Backdrop */}
</>
)
}
export {UserEditModal}
@@ -0,0 +1,433 @@
import { FC, useState } from "react";
import * as Yup from "yup";
import { useFormik } from "formik";
import { isNotEmpty, toAbsoluteUrl } from "../../../../../_digifi/helpers";
import { initialUser, User } from "../../core/_models";
import clsx from "clsx";
import { useListView } from "../core/ListViewProvider";
import { UsersListLoading } from "../components/loading/UsersListLoading";
import { createUser, updateUser } from "../../core/_requests";
import { useQueryResponse } from "../core/QueryResponseProvider";
type Props = {
isUserLoading: boolean;
user: User;
};
const editUserSchema = Yup.object().shape({
email: Yup.string()
.email("Wrong email format")
.min(3, "Minimum 3 symbols")
.max(50, "Maximum 50 symbols")
.required("Email is required"),
name: Yup.string()
.min(3, "Minimum 3 symbols")
.max(50, "Maximum 50 symbols")
.required("Name is required"),
});
const UserEditModalForm: FC<Props> = ({ user, isUserLoading }) => {
const { setItemIdForUpdate } = useListView();
const { refetch } = useQueryResponse();
const [userForEdit] = useState<User>({
...user,
avatar: user.avatar || initialUser.avatar,
role: user.role || initialUser.role,
position: user.position || initialUser.position,
name: user.name || initialUser.name,
email: user.email || initialUser.email,
});
const cancel = (withRefresh?: boolean) => {
if (withRefresh) {
refetch();
}
setItemIdForUpdate(undefined);
};
const blankImg = toAbsoluteUrl("media/svg/avatars/blank.svg");
const userAvatarImg = toAbsoluteUrl(`media/${userForEdit.avatar}`);
const formik = useFormik({
initialValues: userForEdit,
validationSchema: editUserSchema,
onSubmit: async (values, { setSubmitting }) => {
setSubmitting(true);
try {
if (isNotEmpty(values.id)) {
await updateUser(values);
} else {
await createUser(values);
}
} catch (ex) {
console.error(ex);
} finally {
setSubmitting(true);
cancel(true);
}
},
});
return (
<>
<form
id="kt_modal_add_user_form"
className="form"
onSubmit={formik.handleSubmit}
noValidate
>
{/* begin::Scroll */}
<div
className="d-flex flex-column scroll-y me-n7 pe-7"
id="kt_modal_add_user_scroll"
data-kt-scroll="true"
data-kt-scroll-activate="{default: false, lg: true}"
data-kt-scroll-max-height="auto"
data-kt-scroll-dependencies="#kt_modal_add_user_header"
data-kt-scroll-wrappers="#kt_modal_add_user_scroll"
data-kt-scroll-offset="300px"
>
{/* begin::Input group */}
<div className="fv-row mb-7">
{/* begin::Label */}
<label className="d-block fw-bold fs-6 mb-5">Avatar</label>
{/* end::Label */}
{/* begin::Image input */}
<div
className="image-input image-input-outline"
data-kt-image-input="true"
style={{ backgroundImage: `url('${blankImg}')` }}
>
{/* begin::Preview existing avatar */}
<div
className="image-input-wrapper w-125px h-125px"
style={{ backgroundImage: `url('${userAvatarImg}')` }}
></div>
{/* end::Preview existing avatar */}
{/* begin::Label */}
{/* <label
className='btn btn-icon btn-circle btn-active-color-primary w-25px h-25px bg-body shadow'
data-kt-image-input-action='change'
data-bs-toggle='tooltip'
title='Change avatar'
>
<i className='bi bi-pencil-fill fs-7'></i>
<input type='file' name='avatar' accept='.png, .jpg, .jpeg' />
<input type='hidden' name='avatar_remove' />
</label> */}
{/* end::Label */}
{/* begin::Cancel */}
{/* <span
className='btn btn-icon btn-circle btn-active-color-primary w-25px h-25px bg-body shadow'
data-kt-image-input-action='cancel'
data-bs-toggle='tooltip'
title='Cancel avatar'
>
<i className='bi bi-x fs-2'></i>
</span> */}
{/* end::Cancel */}
{/* begin::Remove */}
{/* <span
className='btn btn-icon btn-circle btn-active-color-primary w-25px h-25px bg-body shadow'
data-kt-image-input-action='remove'
data-bs-toggle='tooltip'
title='Remove avatar'
>
<i className='bi bi-x fs-2'></i>
</span> */}
{/* end::Remove */}
</div>
{/* end::Image input */}
{/* begin::Hint */}
{/* <div className='form-text'>Allowed file types: png, jpg, jpeg.</div> */}
{/* end::Hint */}
</div>
{/* end::Input group */}
{/* begin::Input group */}
<div className="fv-row mb-7">
{/* begin::Label */}
<label className="required fw-bold fs-6 mb-2">Full Name</label>
{/* end::Label */}
{/* begin::Input */}
<input
placeholder="Full name"
{...formik.getFieldProps("name")}
type="text"
name="name"
className={clsx(
"form-control form-control-solid mb-3 mb-lg-0",
{ "is-invalid": formik.touched.name && formik.errors.name },
{
"is-valid": formik.touched.name && !formik.errors.name,
}
)}
autoComplete="off"
disabled={formik.isSubmitting || isUserLoading}
/>
{formik.touched.name && formik.errors.name && (
<div className="fv-plugins-message-container">
<div className="fv-help-block">
<span role="alert">{formik.errors.name}</span>
</div>
</div>
)}
{/* end::Input */}
</div>
{/* end::Input group */}
{/* begin::Input group */}
<div className="fv-row mb-7">
{/* begin::Label */}
<label className="required fw-bold fs-6 mb-2">Email</label>
{/* end::Label */}
{/* begin::Input */}
<input
placeholder="Email"
{...formik.getFieldProps("email")}
className={clsx(
"form-control form-control-solid mb-3 mb-lg-0",
{ "is-invalid": formik.touched.email && formik.errors.email },
{
"is-valid": formik.touched.email && !formik.errors.email,
}
)}
type="email"
name="email"
autoComplete="off"
disabled={formik.isSubmitting || isUserLoading}
/>
{/* end::Input */}
{formik.touched.email && formik.errors.email && (
<div className="fv-plugins-message-container">
<span role="alert">{formik.errors.email}</span>
</div>
)}
</div>
{/* end::Input group */}
{/* begin::Input group */}
<div className="mb-7">
{/* begin::Label */}
<label className="required fw-bold fs-6 mb-5">Role</label>
{/* end::Label */}
{/* begin::Roles */}
{/* begin::Input row */}
<div className="d-flex fv-row">
{/* begin::Radio */}
<div className="form-check form-check-custom form-check-solid">
{/* begin::Input */}
<input
className="form-check-input me-3"
{...formik.getFieldProps("role")}
name="role"
type="radio"
value="Administrator"
id="kt_modal_update_role_option_0"
checked={formik.values.role === "Administrator"}
disabled={formik.isSubmitting || isUserLoading}
/>
{/* end::Input */}
{/* begin::Label */}
<label
className="form-check-label"
htmlFor="kt_modal_update_role_option_0"
>
<div className="fw-bolder text-gray-800">Administrator</div>
<div className="text-gray-600">
Best for business owners and company administrators
</div>
</label>
{/* end::Label */}
</div>
{/* end::Radio */}
</div>
{/* end::Input row */}
<div className="separator separator-dashed my-5"></div>
{/* begin::Input row */}
<div className="d-flex fv-row">
{/* begin::Radio */}
<div className="form-check form-check-custom form-check-solid">
{/* begin::Input */}
<input
className="form-check-input me-3"
{...formik.getFieldProps("role")}
name="role"
type="radio"
value="Developer"
id="kt_modal_update_role_option_1"
checked={formik.values.role === "Developer"}
disabled={formik.isSubmitting || isUserLoading}
/>
{/* end::Input */}
{/* begin::Label */}
<label
className="form-check-label"
htmlFor="kt_modal_update_role_option_1"
>
<div className="fw-bolder text-gray-800">Developer</div>
<div className="text-gray-600">
Best for developers or people primarily using the API
</div>
</label>
{/* end::Label */}
</div>
{/* end::Radio */}
</div>
{/* end::Input row */}
<div className="separator separator-dashed my-5"></div>
{/* begin::Input row */}
<div className="d-flex fv-row">
{/* begin::Radio */}
<div className="form-check form-check-custom form-check-solid">
{/* begin::Input */}
<input
className="form-check-input me-3"
{...formik.getFieldProps("role")}
name="role"
type="radio"
value="Analyst"
id="kt_modal_update_role_option_2"
checked={formik.values.role === "Analyst"}
disabled={formik.isSubmitting || isUserLoading}
/>
{/* end::Input */}
{/* begin::Label */}
<label
className="form-check-label"
htmlFor="kt_modal_update_role_option_2"
>
<div className="fw-bolder text-gray-800">Analyst</div>
<div className="text-gray-600">
Best for people who need full access to analytics data, but
don't need to update business settings
</div>
</label>
{/* end::Label */}
</div>
{/* end::Radio */}
</div>
{/* end::Input row */}
<div className="separator separator-dashed my-5"></div>
{/* begin::Input row */}
<div className="d-flex fv-row">
{/* begin::Radio */}
<div className="form-check form-check-custom form-check-solid">
{/* begin::Input */}
<input
className="form-check-input me-3"
{...formik.getFieldProps("role")}
name="role"
type="radio"
value="Support"
id="kt_modal_update_role_option_3"
checked={formik.values.role === "Support"}
disabled={formik.isSubmitting || isUserLoading}
/>
{/* end::Input */}
{/* begin::Label */}
<label
className="form-check-label"
htmlFor="kt_modal_update_role_option_3"
>
<div className="fw-bolder text-gray-800">Support</div>
<div className="text-gray-600">
Best for employees who regularly refund payments and respond
to disputes
</div>
</label>
{/* end::Label */}
</div>
{/* end::Radio */}
</div>
{/* end::Input row */}
<div className="separator separator-dashed my-5"></div>
{/* begin::Input row */}
<div className="d-flex fv-row">
{/* begin::Radio */}
<div className="form-check form-check-custom form-check-solid">
{/* begin::Input */}
<input
className="form-check-input me-3"
{...formik.getFieldProps("role")}
name="role"
type="radio"
id="kt_modal_update_role_option_4"
value="Trial"
checked={formik.values.role === "Trial"}
disabled={formik.isSubmitting || isUserLoading}
/>
{/* end::Input */}
{/* begin::Label */}
<label
className="form-check-label"
htmlFor="kt_modal_update_role_option_4"
>
<div className="fw-bolder text-gray-800">Trial</div>
<div className="text-gray-600">
Best for people who need to preview content data, but don't
need to make any updates
</div>
</label>
{/* end::Label */}
</div>
{/* end::Radio */}
</div>
{/* end::Input row */}
{/* end::Roles */}
</div>
{/* end::Input group */}
</div>
{/* end::Scroll */}
{/* begin::Actions */}
<div className="text-center pt-15">
<button
type="reset"
onClick={() => cancel()}
className="btn btn-light me-3"
data-kt-users-modal-action="cancel"
disabled={formik.isSubmitting || isUserLoading}
>
Discard
</button>
<button
type="submit"
className="btn btn-primary"
data-kt-users-modal-action="submit"
disabled={
isUserLoading ||
formik.isSubmitting ||
!formik.isValid ||
!formik.touched
}
>
<span className="indicator-label">Submit</span>
{(formik.isSubmitting || isUserLoading) && (
<span className="indicator-progress">
Please wait...{" "}
<span className="spinner-border spinner-border-sm align-middle ms-2"></span>
</span>
)}
</button>
</div>
{/* end::Actions */}
</form>
{(formik.isSubmitting || isUserLoading) && <UsersListLoading />}
</>
);
};
export { UserEditModalForm };
@@ -0,0 +1,42 @@
import { useQuery } from "react-query";
import { UserEditModalForm } from "./UserEditModalForm";
import { isNotEmpty, QUERIES } from "../../../../../_digifi/helpers";
import { useListView } from "../core/ListViewProvider";
import { getUserById } from "../../core/_requests";
const UserEditModalFormWrapper = () => {
const { itemIdForUpdate, setItemIdForUpdate } = useListView();
const enabledQuery: boolean = isNotEmpty(itemIdForUpdate);
const {
isLoading,
data: user,
error,
} = useQuery(
`${QUERIES.USERS_LIST}-user-${itemIdForUpdate}`,
() => {
return getUserById(itemIdForUpdate);
},
{
cacheTime: 0,
enabled: enabledQuery,
onError: (err) => {
setItemIdForUpdate(undefined);
console.error(err);
},
}
);
if (!itemIdForUpdate) {
return (
<UserEditModalForm isUserLoading={isLoading} user={{ id: undefined }} />
);
}
if (!isLoading && !error && user) {
return <UserEditModalForm isUserLoading={isLoading} user={user} />;
}
return null;
};
export { UserEditModalFormWrapper };
@@ -0,0 +1,27 @@
import { KTIcon } from "../../../../../_digifi/helpers";
import { useListView } from "../core/ListViewProvider";
const UserEditModalHeader = () => {
const { setItemIdForUpdate } = useListView();
return (
<div className="modal-header">
{/* begin::Modal title */}
<h2 className="fw-bolder">Add User</h2>
{/* end::Modal title */}
{/* begin::Close */}
<div
className="btn btn-icon btn-sm btn-active-icon-primary"
data-kt-users-modal-action="close"
onClick={() => setItemIdForUpdate(undefined)}
style={{ cursor: "pointer" }}
>
<KTIcon iconName="cross" className="fs-1" />
</div>
{/* end::Close */}
</div>
);
};
export { UserEditModalHeader };

Some files were not shown because too many files have changed in this diff Show More