Compare commits
42 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| c14f5b9e22 | |||
| bc103a747a | |||
| fb6092c359 | |||
| c711e000b3 | |||
| 0a28d478d8 | |||
| 0d9318ddd9 | |||
| c53c37611a | |||
| f7d82c0958 | |||
| e1535de92c | |||
| 93f3c0c526 | |||
| 4cb347cfa0 | |||
| 75699342c7 | |||
| f2741f3325 | |||
| 3839962e33 | |||
| 7cf4f9dbbe | |||
| 2526a5b627 | |||
| 395fe1a648 | |||
| c6fe75c9f8 | |||
| 2be706c577 | |||
| 9936a912bc | |||
| 0645f32e52 | |||
| 269229f07c | |||
| c2d353d532 | |||
| de994bc681 | |||
| 81d99e1537 | |||
| 1f14da1778 | |||
| 2eb39b773a | |||
| 4e97119644 | |||
| 7bc39a449c | |||
| f7f728c066 | |||
| 3845ecbefe | |||
| 43b0e60db2 | |||
| 51e06716e5 | |||
| 9d7605ebfe | |||
| 54b550b624 | |||
| cead31a6d1 | |||
| 4508743be4 | |||
| 7d43300724 | |||
| e05d465c2f | |||
| 75a7496164 | |||
| 25b48416b7 | |||
| 996eee612d |
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 15 KiB |
|
After Width: | Height: | Size: 28 KiB |
|
Before Width: | Height: | Size: 18 KiB After Width: | Height: | Size: 6.9 KiB |
|
Before Width: | Height: | Size: 84 KiB After Width: | Height: | Size: 22 KiB |
|
Before Width: | Height: | Size: 16 KiB After Width: | Height: | Size: 6.1 KiB |
|
Before Width: | Height: | Size: 713 B After Width: | Height: | Size: 490 B |
|
Before Width: | Height: | Size: 1.6 KiB After Width: | Height: | Size: 950 B |
|
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 15 KiB |
@@ -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
|
||||
|
||||
@@ -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?: {
|
||||
|
||||
@@ -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,52 @@ 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=""
|
||||
style={{ cursor: 'auto' }}
|
||||
/>
|
||||
</div>
|
||||
<HeaderUserMenu />
|
||||
{/* <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'}}
|
||||
>
|
||||
< 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 >
|
||||
</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'}}
|
||||
>
|
||||
< 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 >
|
||||
</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>
|
||||
)
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
@@ -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;
|
||||
}
|
||||
@@ -21,7 +21,7 @@ const HeaderUserMenu: FC = () => {
|
||||
<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'>
|
||||
{currentUser?.email}
|
||||
|
||||
@@ -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 & 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 & 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}
|
||||
@@ -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
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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,37 @@
|
||||
import { KTCard } from "../../../../_digifi/helpers"
|
||||
import { Content } from "../../../../_digifi/layout/components/content"
|
||||
import { ToolbarWrapper } from "../../../../_digifi/layout/components/toolbar"
|
||||
import { UsersListHeader } from "../user-approved/users-list/components/header/UsersListHeader"
|
||||
import { ListViewProvider, useListView } from "../user-approved/users-list/core/ListViewProvider"
|
||||
import { QueryRequestProvider } from "../user-approved/users-list/core/QueryRequestProvider"
|
||||
import { QueryResponseProvider } from "../user-approved/users-list/core/QueryResponseProvider"
|
||||
import { UsersTable } from "../user-approved/users-list/table/UsersTable"
|
||||
import { UserEditModal } from "../user-approved/users-list/user-edit-modal/UserEditModal"
|
||||
|
||||
const UsersList = () => {
|
||||
const {itemIdForUpdate} = useListView()
|
||||
return (
|
||||
<>
|
||||
<KTCard>
|
||||
<UsersListHeader />
|
||||
<UsersTable />
|
||||
</KTCard>
|
||||
{itemIdForUpdate !== undefined && <UserEditModal />}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
const UserApprovedList = () => (
|
||||
<QueryRequestProvider>
|
||||
<QueryResponseProvider>
|
||||
<ListViewProvider>
|
||||
<ToolbarWrapper />
|
||||
<Content>
|
||||
<UsersList />
|
||||
</Content>
|
||||
</ListViewProvider>
|
||||
</QueryResponseProvider>
|
||||
</QueryRequestProvider>
|
||||
)
|
||||
|
||||
export {UserApprovedList}
|
||||
@@ -0,0 +1,37 @@
|
||||
import { KTCard } from "../../../../_digifi/helpers"
|
||||
import { Content } from "../../../../_digifi/layout/components/content"
|
||||
import { ToolbarWrapper } from "../../../../_digifi/layout/components/toolbar"
|
||||
import { UsersListHeader } from "../user-pending/users-list/components/header/UsersListHeader"
|
||||
import { ListViewProvider, useListView } from "../user-pending/users-list/core/ListViewProvider"
|
||||
import { QueryRequestProvider } from "../user-pending/users-list/core/QueryRequestProvider"
|
||||
import { QueryResponseProvider } from "../user-pending/users-list/core/QueryResponseProvider"
|
||||
import { UsersTable } from "../user-pending/users-list/table/UsersTable"
|
||||
import { UserEditModal } from "../user-pending/users-list/user-edit-modal/UserEditModal"
|
||||
|
||||
const UsersList = () => {
|
||||
const {itemIdForUpdate} = useListView()
|
||||
return (
|
||||
<>
|
||||
<KTCard>
|
||||
<UsersListHeader />
|
||||
<UsersTable />
|
||||
</KTCard>
|
||||
{itemIdForUpdate !== undefined && <UserEditModal />}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
const UserPendingList = () => (
|
||||
<QueryRequestProvider>
|
||||
<QueryResponseProvider>
|
||||
<ListViewProvider>
|
||||
<ToolbarWrapper />
|
||||
<Content>
|
||||
<UsersList />
|
||||
</Content>
|
||||
</ListViewProvider>
|
||||
</QueryResponseProvider>
|
||||
</QueryRequestProvider>
|
||||
)
|
||||
|
||||
export {UserPendingList}
|
||||
@@ -0,0 +1,37 @@
|
||||
import { KTCard } from "../../../../_digifi/helpers"
|
||||
import { Content } from "../../../../_digifi/layout/components/content"
|
||||
import { ToolbarWrapper } from "../../../../_digifi/layout/components/toolbar"
|
||||
import { UsersListHeader } from "../user-ready/users-list/components/header/UsersListHeader"
|
||||
import { ListViewProvider, useListView } from "../user-ready/users-list/core/ListViewProvider"
|
||||
import { QueryRequestProvider } from "../user-ready/users-list/core/QueryRequestProvider"
|
||||
import { QueryResponseProvider } from "../user-ready/users-list/core/QueryResponseProvider"
|
||||
import { UsersTable } from "../user-ready/users-list/table/UsersTable"
|
||||
import { UserEditModal } from "../user-ready/users-list/user-edit-modal/UserEditModal"
|
||||
|
||||
const UsersList = () => {
|
||||
const {itemIdForUpdate} = useListView()
|
||||
return (
|
||||
<>
|
||||
<KTCard>
|
||||
<UsersListHeader />
|
||||
<UsersTable />
|
||||
</KTCard>
|
||||
{itemIdForUpdate !== undefined && <UserEditModal />}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
const UserReadyList = () => (
|
||||
<QueryRequestProvider>
|
||||
<QueryResponseProvider>
|
||||
<ListViewProvider>
|
||||
<ToolbarWrapper />
|
||||
<Content>
|
||||
<UsersList />
|
||||
</Content>
|
||||
</ListViewProvider>
|
||||
</QueryResponseProvider>
|
||||
</QueryRequestProvider>
|
||||
)
|
||||
|
||||
export {UserReadyList}
|
||||
@@ -0,0 +1,37 @@
|
||||
import { KTCard } from "../../../../_digifi/helpers"
|
||||
import { Content } from "../../../../_digifi/layout/components/content"
|
||||
import { ToolbarWrapper } from "../../../../_digifi/layout/components/toolbar"
|
||||
import { UsersListHeader } from "../user-rejected/users-list/components/header/UsersListHeader"
|
||||
import { ListViewProvider, useListView } from "../user-rejected/users-list/core/ListViewProvider"
|
||||
import { QueryRequestProvider } from "../user-rejected/users-list/core/QueryRequestProvider"
|
||||
import { QueryResponseProvider } from "../user-rejected/users-list/core/QueryResponseProvider"
|
||||
import { UsersTable } from "../user-rejected/users-list/table/UsersTable"
|
||||
import { UserEditModal } from "../user-rejected/users-list/user-edit-modal/UserEditModal"
|
||||
|
||||
const UsersList = () => {
|
||||
const {itemIdForUpdate} = useListView()
|
||||
return (
|
||||
<>
|
||||
<KTCard>
|
||||
<UsersListHeader />
|
||||
<UsersTable />
|
||||
</KTCard>
|
||||
{itemIdForUpdate !== undefined && <UserEditModal />}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
const UserRejectedList = () => (
|
||||
<QueryRequestProvider>
|
||||
<QueryResponseProvider>
|
||||
<ListViewProvider>
|
||||
<ToolbarWrapper />
|
||||
<Content>
|
||||
<UsersList />
|
||||
</Content>
|
||||
</ListViewProvider>
|
||||
</QueryResponseProvider>
|
||||
</QueryRequestProvider>
|
||||
)
|
||||
|
||||
export {UserRejectedList}
|
||||
@@ -0,0 +1,37 @@
|
||||
import { KTCard } from "../../../../_digifi/helpers"
|
||||
import { Content } from "../../../../_digifi/layout/components/content"
|
||||
import { ToolbarWrapper } from "../../../../_digifi/layout/components/toolbar"
|
||||
import { UsersListHeader } from "../user-started/users-list/components/header/UsersListHeader"
|
||||
import { ListViewProvider, useListView } from "../user-started/users-list/core/ListViewProvider"
|
||||
import { QueryRequestProvider } from "../user-started/users-list/core/QueryRequestProvider"
|
||||
import { QueryResponseProvider } from "../user-started/users-list/core/QueryResponseProvider"
|
||||
import { UsersTable } from "../user-started/users-list/table/UsersTable"
|
||||
import { UserEditModal } from "../user-started/users-list/user-edit-modal/UserEditModal"
|
||||
|
||||
const UsersList = () => {
|
||||
const {itemIdForUpdate} = useListView()
|
||||
return (
|
||||
<>
|
||||
<KTCard>
|
||||
<UsersListHeader />
|
||||
<UsersTable />
|
||||
</KTCard>
|
||||
{itemIdForUpdate !== undefined && <UserEditModal />}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
const UserStartedList = () => (
|
||||
<QueryRequestProvider>
|
||||
<QueryResponseProvider>
|
||||
<ListViewProvider>
|
||||
<ToolbarWrapper />
|
||||
<Content>
|
||||
<UsersList />
|
||||
</Content>
|
||||
</ListViewProvider>
|
||||
</QueryResponseProvider>
|
||||
</QueryRequestProvider>
|
||||
)
|
||||
|
||||
export {UserStartedList}
|
||||
@@ -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: '',
|
||||
}
|
||||
@@ -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,39 @@
|
||||
import {Route, Routes, Outlet, Navigate} from 'react-router-dom'
|
||||
import {PageLink, PageTitle} from '../../../../_digifi/layout/core'
|
||||
import {UsersListWrapper} from './users-list/UsersList'
|
||||
|
||||
const usersBreadcrumbs: Array<PageLink> = [
|
||||
{
|
||||
title: 'User Management',
|
||||
path: '/apps/user-management/users',
|
||||
isSeparator: false,
|
||||
isActive: false,
|
||||
},
|
||||
{
|
||||
title: '',
|
||||
path: '',
|
||||
isSeparator: true,
|
||||
isActive: false,
|
||||
},
|
||||
]
|
||||
|
||||
const UsersPage = () => {
|
||||
return (
|
||||
<Routes>
|
||||
<Route element={<Outlet />}>
|
||||
<Route
|
||||
path='users'
|
||||
element={
|
||||
<>
|
||||
<PageTitle breadcrumbs={usersBreadcrumbs}>Users list</PageTitle>
|
||||
<UsersListWrapper />
|
||||
</>
|
||||
}
|
||||
/>
|
||||
</Route>
|
||||
<Route index element={<Navigate to='/apps/user-management/users' />} />
|
||||
</Routes>
|
||||
)
|
||||
}
|
||||
|
||||
export default UsersPage
|
||||
@@ -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,32 @@
|
||||
import {KTIcon} from '../../../../../../../_digifi/helpers'
|
||||
import {useListView} from '../../core/ListViewProvider'
|
||||
import {UsersListFilter} from './UsersListFilter'
|
||||
|
||||
const UsersListToolbar = () => {
|
||||
const {setItemIdForUpdate} = useListView()
|
||||
const openAddUserModal = () => {
|
||||
setItemIdForUpdate(null)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className='d-flex justify-content-end' data-kt-user-table-toolbar='base'>
|
||||
<UsersListFilter />
|
||||
|
||||
{/* begin::Export */}
|
||||
{/* <button type='button' className='btn btn-light-primary me-3'>
|
||||
<KTIcon iconName='exit-up' className='fs-2' />
|
||||
Export
|
||||
</button> */}
|
||||
{/* end::Export */}
|
||||
|
||||
{/* begin::Add user */}
|
||||
{/* <button type='button' className='btn btn-primary' onClick={openAddUserModal}>
|
||||
<KTIcon iconName='plus' className='fs-2' />
|
||||
Add User
|
||||
</button> */}
|
||||
{/* end::Add user */}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export {UsersListToolbar}
|
||||
@@ -0,0 +1,133 @@
|
||||
import {useEffect, useState} from 'react'
|
||||
import {MenuComponent} from '../../../../../../../_digifi/assets/ts/components'
|
||||
import {initialQueryState, KTIcon} from '../../../../../../../_digifi/helpers'
|
||||
import {useQueryRequest} from '../../core/QueryRequestProvider'
|
||||
import {useQueryResponse} from '../../core/QueryResponseProvider'
|
||||
|
||||
const UsersListFilter = () => {
|
||||
const {updateState} = useQueryRequest()
|
||||
const {isLoading} = useQueryResponse()
|
||||
const [role, setRole] = useState<string | undefined>()
|
||||
const [lastLogin, setLastLogin] = useState<string | undefined>()
|
||||
|
||||
useEffect(() => {
|
||||
MenuComponent.reinitialization()
|
||||
}, [])
|
||||
|
||||
const resetData = () => {
|
||||
updateState({filter: undefined, ...initialQueryState})
|
||||
}
|
||||
|
||||
const filterData = () => {
|
||||
updateState({
|
||||
filter: {role, last_login: lastLogin},
|
||||
...initialQueryState,
|
||||
})
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{/* begin::Filter Button */}
|
||||
<button
|
||||
disabled={isLoading}
|
||||
type='button'
|
||||
className='btn btn-light-primary me-3'
|
||||
data-kt-menu-trigger='click'
|
||||
data-kt-menu-placement='bottom-end'
|
||||
>
|
||||
<KTIcon iconName='filter' className='fs-2' />
|
||||
Filter
|
||||
</button>
|
||||
{/* end::Filter Button */}
|
||||
{/* begin::SubMenu */}
|
||||
<div className='menu menu-sub menu-sub-dropdown w-300px w-md-325px' data-kt-menu='true'>
|
||||
{/* begin::Header */}
|
||||
<div className='px-7 py-5'>
|
||||
<div className='fs-5 text-gray-900 fw-bolder'>Filter Options</div>
|
||||
</div>
|
||||
{/* end::Header */}
|
||||
|
||||
{/* begin::Separator */}
|
||||
<div className='separator border-gray-200'></div>
|
||||
{/* end::Separator */}
|
||||
|
||||
{/* begin::Content */}
|
||||
<div className='px-7 py-5' data-kt-user-table-filter='form'>
|
||||
{/* begin::Input group */}
|
||||
<div className='mb-10'>
|
||||
<label className='form-label fs-6 fw-bold'>Role:</label>
|
||||
<select
|
||||
className='form-select form-select-solid fw-bolder'
|
||||
data-kt-select2='true'
|
||||
data-placeholder='Select option'
|
||||
data-allow-clear='true'
|
||||
data-kt-user-table-filter='role'
|
||||
data-hide-search='true'
|
||||
onChange={(e) => setRole(e.target.value)}
|
||||
value={role}
|
||||
>
|
||||
<option value=''></option>
|
||||
<option value='Administrator'>Administrator</option>
|
||||
<option value='Analyst'>Analyst</option>
|
||||
<option value='Developer'>Developer</option>
|
||||
<option value='Support'>Support</option>
|
||||
<option value='Trial'>Trial</option>
|
||||
</select>
|
||||
</div>
|
||||
{/* end::Input group */}
|
||||
|
||||
{/* begin::Input group */}
|
||||
<div className='mb-10'>
|
||||
<label className='form-label fs-6 fw-bold'>Last login:</label>
|
||||
<select
|
||||
className='form-select form-select-solid fw-bolder'
|
||||
data-kt-select2='true'
|
||||
data-placeholder='Select option'
|
||||
data-allow-clear='true'
|
||||
data-kt-user-table-filter='two-step'
|
||||
data-hide-search='true'
|
||||
onChange={(e) => setLastLogin(e.target.value)}
|
||||
value={lastLogin}
|
||||
>
|
||||
<option value=''></option>
|
||||
<option value='Yesterday'>Yesterday</option>
|
||||
<option value='20 mins ago'>20 mins ago</option>
|
||||
<option value='5 hours ago'>5 hours ago</option>
|
||||
<option value='2 days ago'>2 days ago</option>
|
||||
</select>
|
||||
</div>
|
||||
{/* end::Input group */}
|
||||
|
||||
{/* begin::Actions */}
|
||||
<div className='d-flex justify-content-end'>
|
||||
<button
|
||||
type='button'
|
||||
disabled={isLoading}
|
||||
onClick={filterData}
|
||||
className='btn btn-light btn-active-light-primary fw-bold me-2 px-6'
|
||||
data-kt-menu-dismiss='true'
|
||||
data-kt-user-table-filter='reset'
|
||||
>
|
||||
Reset
|
||||
</button>
|
||||
<button
|
||||
disabled={isLoading}
|
||||
type='button'
|
||||
onClick={resetData}
|
||||
className='btn btn-primary fw-bold px-6'
|
||||
data-kt-menu-dismiss='true'
|
||||
data-kt-user-table-filter='filter'
|
||||
>
|
||||
Apply
|
||||
</button>
|
||||
</div>
|
||||
{/* end::Actions */}
|
||||
</div>
|
||||
{/* end::Content */}
|
||||
</div>
|
||||
{/* end::SubMenu */}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export {UsersListFilter}
|
||||
@@ -0,0 +1,38 @@
|
||||
import {useQueryClient, useMutation} from 'react-query'
|
||||
import {QUERIES} from '../../../../../../../_digifi/helpers'
|
||||
import {useListView} from '../../core/ListViewProvider'
|
||||
import {useQueryResponse} from '../../core/QueryResponseProvider'
|
||||
import {deleteSelectedUsers} from '../../core/_requests'
|
||||
|
||||
const UsersListGrouping = () => {
|
||||
const {selected, clearSelected} = useListView()
|
||||
const queryClient = useQueryClient()
|
||||
const {query} = useQueryResponse()
|
||||
|
||||
const deleteSelectedItems = useMutation(() => deleteSelectedUsers(selected), {
|
||||
// 💡 response of the mutation is passed to onSuccess
|
||||
onSuccess: () => {
|
||||
// ✅ update detail view directly
|
||||
queryClient.invalidateQueries([`${QUERIES.USERS_LIST}-${query}`])
|
||||
clearSelected()
|
||||
},
|
||||
})
|
||||
|
||||
return (
|
||||
<div className='d-flex justify-content-end align-items-center'>
|
||||
<div className='fw-bolder me-5'>
|
||||
<span className='me-2'>{selected.length}</span> Selected
|
||||
</div>
|
||||
|
||||
<button
|
||||
type='button'
|
||||
className='btn btn-danger'
|
||||
onClick={async () => await deleteSelectedItems.mutateAsync()}
|
||||
>
|
||||
Delete Selected
|
||||
</button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export {UsersListGrouping}
|
||||
@@ -0,0 +1,22 @@
|
||||
import {useListView} from '../../core/ListViewProvider'
|
||||
import {UsersListToolbar} from './UserListToolbar'
|
||||
import {UsersListGrouping} from './UsersListGrouping'
|
||||
import {UsersListSearchComponent} from './UsersListSearchComponent'
|
||||
|
||||
const UsersListHeader = () => {
|
||||
const {selected} = useListView()
|
||||
return (
|
||||
<div className='card-header border-0 pt-6'>
|
||||
<UsersListSearchComponent />
|
||||
{/* begin::Card toolbar */}
|
||||
<div className='card-toolbar'>
|
||||
{/* begin::Group actions */}
|
||||
{selected.length > 0 ? <UsersListGrouping /> : <UsersListToolbar />}
|
||||
{/* end::Group actions */}
|
||||
</div>
|
||||
{/* end::Card toolbar */}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export {UsersListHeader}
|
||||
@@ -0,0 +1,45 @@
|
||||
/* eslint-disable react-hooks/exhaustive-deps */
|
||||
|
||||
import {useEffect, useState} from 'react'
|
||||
import {initialQueryState, KTIcon, useDebounce} from '../../../../../../../_digifi/helpers'
|
||||
import {useQueryRequest} from '../../core/QueryRequestProvider'
|
||||
|
||||
const UsersListSearchComponent = () => {
|
||||
const {updateState} = useQueryRequest()
|
||||
const [searchTerm, setSearchTerm] = useState<string>('')
|
||||
// Debounce search term so that it only gives us latest value ...
|
||||
// ... if searchTerm has not been updated within last 500ms.
|
||||
// The goal is to only have the API call fire when user stops typing ...
|
||||
// ... so that we aren't hitting our API rapidly.
|
||||
const debouncedSearchTerm = useDebounce(searchTerm, 150)
|
||||
// Effect for API call
|
||||
useEffect(
|
||||
() => {
|
||||
if (debouncedSearchTerm !== undefined && searchTerm !== undefined) {
|
||||
updateState({search: debouncedSearchTerm, ...initialQueryState})
|
||||
}
|
||||
},
|
||||
[debouncedSearchTerm] // Only call effect if debounced search term changes
|
||||
// More details about useDebounce: https://usehooks.com/useDebounce/
|
||||
)
|
||||
|
||||
return (
|
||||
<div className='card-title'>
|
||||
{/* begin::Search */}
|
||||
<div className='d-flex align-items-center position-relative my-1'>
|
||||
<KTIcon iconName='magnifier' className='fs-1 position-absolute ms-6' />
|
||||
<input
|
||||
type='text'
|
||||
data-kt-user-table-filter='search'
|
||||
className='form-control form-control-solid w-250px ps-14'
|
||||
placeholder='Search user'
|
||||
value={searchTerm}
|
||||
onChange={(e) => setSearchTerm(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
{/* end::Search */}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export {UsersListSearchComponent}
|
||||
@@ -0,0 +1,18 @@
|
||||
const UsersListLoading = () => {
|
||||
const styles = {
|
||||
borderRadius: '0.475rem',
|
||||
boxShadow: '0 0 50px 0 rgb(82 63 105 / 15%)',
|
||||
backgroundColor: '#fff',
|
||||
color: '#7e8299',
|
||||
fontWeight: '500',
|
||||
margin: '0',
|
||||
width: 'auto',
|
||||
padding: '1rem 2rem',
|
||||
top: 'calc(50% - 2rem)',
|
||||
left: 'calc(50% - 4rem)',
|
||||
}
|
||||
|
||||
return <div style={{...styles, position: 'absolute', textAlign: 'center'}}>Processing...</div>
|
||||
}
|
||||
|
||||
export {UsersListLoading}
|
||||
@@ -0,0 +1,156 @@
|
||||
|
||||
import clsx from 'clsx'
|
||||
import {useQueryResponseLoading, useQueryResponsePagination} from '../../core/QueryResponseProvider'
|
||||
import {useQueryRequest} from '../../core/QueryRequestProvider'
|
||||
import {PaginationState} from '../../../../../../../_digifi/helpers'
|
||||
import {useMemo} from 'react'
|
||||
|
||||
const mappedLabel = (label: string): string => {
|
||||
if (label === '« Previous') {
|
||||
return 'Previous'
|
||||
}
|
||||
|
||||
if (label === 'Next »') {
|
||||
return 'Next'
|
||||
}
|
||||
|
||||
return label
|
||||
}
|
||||
|
||||
const UsersListPagination = () => {
|
||||
const pagination = useQueryResponsePagination()
|
||||
const isLoading = useQueryResponseLoading()
|
||||
const {updateState} = useQueryRequest()
|
||||
const updatePage = (page: number | undefined | null) => {
|
||||
if (!page || isLoading || pagination.page === page) {
|
||||
return
|
||||
}
|
||||
|
||||
updateState({page, items_per_page: pagination.items_per_page || 10})
|
||||
}
|
||||
|
||||
const PAGINATION_PAGES_COUNT = 5
|
||||
const sliceLinks = (pagination?: PaginationState) => {
|
||||
if (!pagination?.links?.length) {
|
||||
return []
|
||||
}
|
||||
|
||||
const scopedLinks = [...pagination.links]
|
||||
|
||||
let pageLinks: Array<{
|
||||
label: string
|
||||
active: boolean
|
||||
url: string | null
|
||||
page: number | null
|
||||
}> = []
|
||||
const previousLink: {label: string; active: boolean; url: string | null; page: number | null} =
|
||||
scopedLinks.shift()!
|
||||
const nextLink: {label: string; active: boolean; url: string | null; page: number | null} =
|
||||
scopedLinks.pop()!
|
||||
|
||||
const halfOfPagesCount = Math.floor(PAGINATION_PAGES_COUNT / 2)
|
||||
|
||||
pageLinks.push(previousLink)
|
||||
|
||||
if (
|
||||
pagination.page <= Math.round(PAGINATION_PAGES_COUNT / 2) ||
|
||||
scopedLinks.length <= PAGINATION_PAGES_COUNT
|
||||
) {
|
||||
pageLinks = [...pageLinks, ...scopedLinks.slice(0, PAGINATION_PAGES_COUNT)]
|
||||
}
|
||||
|
||||
if (
|
||||
pagination.page > scopedLinks.length - halfOfPagesCount &&
|
||||
scopedLinks.length > PAGINATION_PAGES_COUNT
|
||||
) {
|
||||
pageLinks = [
|
||||
...pageLinks,
|
||||
...scopedLinks.slice(scopedLinks.length - PAGINATION_PAGES_COUNT, scopedLinks.length),
|
||||
]
|
||||
}
|
||||
|
||||
if (
|
||||
!(
|
||||
pagination.page <= Math.round(PAGINATION_PAGES_COUNT / 2) ||
|
||||
scopedLinks.length <= PAGINATION_PAGES_COUNT
|
||||
) &&
|
||||
!(pagination.page > scopedLinks.length - halfOfPagesCount)
|
||||
) {
|
||||
pageLinks = [
|
||||
...pageLinks,
|
||||
...scopedLinks.slice(
|
||||
pagination.page - 1 - halfOfPagesCount,
|
||||
pagination.page + halfOfPagesCount
|
||||
),
|
||||
]
|
||||
}
|
||||
|
||||
pageLinks.push(nextLink)
|
||||
|
||||
return pageLinks
|
||||
}
|
||||
|
||||
const paginationLinks = useMemo(() => sliceLinks(pagination), [pagination])
|
||||
|
||||
return (
|
||||
<div className='row'>
|
||||
<div className='col-sm-12 col-md-5 d-flex align-items-center justify-content-center justify-content-md-start'></div>
|
||||
<div className='col-sm-12 col-md-7 d-flex align-items-center justify-content-center justify-content-md-end'>
|
||||
<div id='kt_table_users_paginate'>
|
||||
<ul className='pagination'>
|
||||
<li
|
||||
className={clsx('page-item', {
|
||||
disabled: isLoading || pagination.page === 1,
|
||||
})}
|
||||
>
|
||||
<a onClick={() => updatePage(1)} style={{cursor: 'pointer'}} className='page-link'>
|
||||
First
|
||||
</a>
|
||||
</li>
|
||||
{paginationLinks
|
||||
?.map((link) => {
|
||||
return {...link, label: mappedLabel(link.label)}
|
||||
})
|
||||
.map((link) => (
|
||||
<li
|
||||
key={link.label}
|
||||
className={clsx('page-item', {
|
||||
active: pagination.page === link.page,
|
||||
disabled: isLoading,
|
||||
previous: link.label === 'Previous',
|
||||
next: link.label === 'Next',
|
||||
})}
|
||||
>
|
||||
<a
|
||||
className={clsx('page-link', {
|
||||
'page-text': link.label === 'Previous' || link.label === 'Next',
|
||||
'me-5': link.label === 'Previous',
|
||||
})}
|
||||
onClick={() => updatePage(link.page)}
|
||||
style={{cursor: 'pointer'}}
|
||||
>
|
||||
{mappedLabel(link.label)}
|
||||
</a>
|
||||
</li>
|
||||
))}
|
||||
<li
|
||||
className={clsx('page-item', {
|
||||
disabled: isLoading || pagination.page === (pagination.links?.length || 3) - 2,
|
||||
})}
|
||||
>
|
||||
<a
|
||||
onClick={() => updatePage((pagination.links?.length || 3) - 2)}
|
||||
style={{cursor: 'pointer'}}
|
||||
className='page-link'
|
||||
>
|
||||
Last
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export {UsersListPagination}
|
||||
@@ -0,0 +1,51 @@
|
||||
/* eslint-disable react-refresh/only-export-components */
|
||||
import {FC, useState, createContext, useContext, useMemo} from 'react'
|
||||
import {
|
||||
ID,
|
||||
calculatedGroupingIsDisabled,
|
||||
calculateIsAllDataSelected,
|
||||
groupingOnSelect,
|
||||
initialListView,
|
||||
ListViewContextProps,
|
||||
groupingOnSelectAll,
|
||||
WithChildren,
|
||||
} from '../../../../../../_digifi/helpers'
|
||||
import {useQueryResponse, useQueryResponseData} from './QueryResponseProvider'
|
||||
|
||||
const ListViewContext = createContext<ListViewContextProps>(initialListView)
|
||||
|
||||
const ListViewProvider: FC<WithChildren> = ({children}) => {
|
||||
const [selected, setSelected] = useState<Array<ID>>(initialListView.selected)
|
||||
const [itemIdForUpdate, setItemIdForUpdate] = useState<ID>(initialListView.itemIdForUpdate)
|
||||
const {isLoading} = useQueryResponse()
|
||||
const data = useQueryResponseData()
|
||||
const disabled = useMemo(() => calculatedGroupingIsDisabled(isLoading, data), [isLoading, data])
|
||||
const isAllSelected = useMemo(() => calculateIsAllDataSelected(data, selected), [data, selected])
|
||||
|
||||
return (
|
||||
<ListViewContext.Provider
|
||||
value={{
|
||||
selected,
|
||||
itemIdForUpdate,
|
||||
setItemIdForUpdate,
|
||||
disabled,
|
||||
isAllSelected,
|
||||
onSelect: (id: ID) => {
|
||||
groupingOnSelect(id, selected, setSelected)
|
||||
},
|
||||
onSelectAll: () => {
|
||||
groupingOnSelectAll(isAllSelected, setSelected, data)
|
||||
},
|
||||
clearSelected: () => {
|
||||
setSelected([])
|
||||
},
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</ListViewContext.Provider>
|
||||
)
|
||||
}
|
||||
|
||||
const useListView = () => useContext(ListViewContext)
|
||||
|
||||
export {ListViewProvider, useListView}
|
||||
@@ -0,0 +1,28 @@
|
||||
/* eslint-disable react-refresh/only-export-components */
|
||||
import {FC, useState, createContext, useContext} from 'react'
|
||||
import {
|
||||
QueryState,
|
||||
QueryRequestContextProps,
|
||||
initialQueryRequest,
|
||||
WithChildren,
|
||||
} from '../../../../../../_digifi/helpers'
|
||||
|
||||
const QueryRequestContext = createContext<QueryRequestContextProps>(initialQueryRequest)
|
||||
|
||||
const QueryRequestProvider: FC<WithChildren> = ({children}) => {
|
||||
const [state, setState] = useState<QueryState>(initialQueryRequest.state)
|
||||
|
||||
const updateState = (updates: Partial<QueryState>) => {
|
||||
const updatedState = {...state, ...updates} as QueryState
|
||||
setState(updatedState)
|
||||
}
|
||||
|
||||
return (
|
||||
<QueryRequestContext.Provider value={{state, updateState}}>
|
||||
{children}
|
||||
</QueryRequestContext.Provider>
|
||||
)
|
||||
}
|
||||
|
||||
const useQueryRequest = () => useContext(QueryRequestContext)
|
||||
export {QueryRequestProvider, useQueryRequest}
|
||||
@@ -0,0 +1,85 @@
|
||||
/* eslint-disable react-refresh/only-export-components */
|
||||
/* eslint-disable react-hooks/exhaustive-deps */
|
||||
import {FC, useContext, useState, useEffect, useMemo} from 'react'
|
||||
import {useQuery} from 'react-query'
|
||||
import {
|
||||
createResponseContext,
|
||||
initialQueryResponse,
|
||||
initialQueryState,
|
||||
PaginationState,
|
||||
QUERIES,
|
||||
stringifyRequestQuery,
|
||||
WithChildren,
|
||||
} from '../../../../../../_digifi/helpers'
|
||||
import {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,42 @@
|
||||
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: '',
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
import axios, { AxiosResponse } from "axios";
|
||||
import { ID, Response } from "../../../../../../_digifi/helpers";
|
||||
import { User, UsersQueryResponse } from "./_models";
|
||||
|
||||
const API_URL = import.meta.env.VITE_APP_THEME_API_URL;
|
||||
const USER_URL = `${API_URL}/user`;
|
||||
// const GET_USERS_URL = `${API_URL}/users/query`;
|
||||
|
||||
const NEW_USER_ENDPOINT = import.meta.env.VITE_APP_USER_ENDPOINT
|
||||
|
||||
// const getStartedUsers = (query: string): Promise<UsersQueryResponse> => {
|
||||
// return axios
|
||||
// .get(`${GET_USERS_URL}?${query}`)
|
||||
// .then((d: AxiosResponse<UsersQueryResponse>) => d.data);
|
||||
// };
|
||||
const getStartedUsers = (query: string): Promise<UsersQueryResponse> => { // FUNCTION TO GET USERS THAT HAVE STARTED LOAN APPLICATION
|
||||
return axios
|
||||
.get(`${NEW_USER_ENDPOINT}/loan/started`)
|
||||
.then((d: AxiosResponse<UsersQueryResponse>) => d.data);
|
||||
};
|
||||
|
||||
const getUserById = (id: ID): Promise<User | undefined> => {
|
||||
return axios
|
||||
.get(`${USER_URL}/${id}`)
|
||||
.then((response: AxiosResponse<Response<User>>) => response.data)
|
||||
.then((response: Response<User>) => response.data);
|
||||
};
|
||||
|
||||
const createUser = (user: User): Promise<User | undefined> => {
|
||||
return axios
|
||||
.put(USER_URL, user)
|
||||
.then((response: AxiosResponse<Response<User>>) => response.data)
|
||||
.then((response: Response<User>) => response.data);
|
||||
};
|
||||
|
||||
const updateUser = (user: User): Promise<User | undefined> => {
|
||||
return axios
|
||||
.post(`${USER_URL}/${user.id}`, user)
|
||||
.then((response: AxiosResponse<Response<User>>) => response.data)
|
||||
.then((response: Response<User>) => response.data);
|
||||
};
|
||||
|
||||
const deleteUser = (userId: ID): Promise<void> => {
|
||||
return axios.delete(`${USER_URL}/${userId}`).then(() => {});
|
||||
};
|
||||
|
||||
const deleteSelectedUsers = (userIds: Array<ID>): Promise<void> => {
|
||||
const requests = userIds.map((id) => axios.delete(`${USER_URL}/${id}`));
|
||||
return axios.all(requests).then(() => {});
|
||||
};
|
||||
|
||||
export {
|
||||
getStartedUsers,
|
||||
deleteUser,
|
||||
deleteSelectedUsers,
|
||||
getUserById,
|
||||
createUser,
|
||||
updateUser,
|
||||
};
|
||||
@@ -0,0 +1,62 @@
|
||||
import {useMemo} from 'react'
|
||||
import {useTable, ColumnInstance, Row} from 'react-table'
|
||||
import {CustomHeaderColumn} from './columns/CustomHeaderColumn'
|
||||
import {CustomRow} from './columns/CustomRow'
|
||||
import {useQueryResponseData, useQueryResponseLoading} from '../core/QueryResponseProvider'
|
||||
import {usersColumns} from './columns/_columns'
|
||||
import {User} from '../../../core/_models'
|
||||
import {UsersListLoading} from '../components/loading/UsersListLoading'
|
||||
import {UsersListPagination} from '../components/pagination/UsersListPagination'
|
||||
import {KTCardBody} from '../../../../../../_digifi/helpers'
|
||||
|
||||
const UsersTable = () => {
|
||||
const users = useQueryResponseData()
|
||||
// console.log('users', users)
|
||||
const isLoading = useQueryResponseLoading()
|
||||
const data = useMemo(() => users, [users])
|
||||
const columns = useMemo(() => usersColumns, [])
|
||||
const {getTableProps, getTableBodyProps, headers, rows, prepareRow} = useTable({
|
||||
columns,
|
||||
data,
|
||||
})
|
||||
|
||||
return (
|
||||
<KTCardBody className='py-4'>
|
||||
<div className='table-responsive'>
|
||||
<table
|
||||
id='kt_table_users'
|
||||
className='table align-middle table-row-dashed fs-6 gy-5 dataTable no-footer'
|
||||
{...getTableProps()}
|
||||
>
|
||||
<thead>
|
||||
<tr className='text-start text-muted fw-bolder fs-7 text-uppercase gs-0'>
|
||||
{headers.map((column: ColumnInstance<User>) => (
|
||||
<CustomHeaderColumn key={column.id} column={column} />
|
||||
))}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className='text-gray-600 fw-bold' {...getTableBodyProps()}>
|
||||
{rows.length > 0 ? (
|
||||
rows.map((row: Row<User>, i) => {
|
||||
prepareRow(row)
|
||||
return <CustomRow row={row} key={`row-${i}-${row.id}`} />
|
||||
})
|
||||
) : (
|
||||
<tr>
|
||||
<td colSpan={7}>
|
||||
<div className='d-flex text-center w-100 align-content-center justify-content-center'>
|
||||
No matching records found
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
)}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<UsersListPagination />
|
||||
{isLoading && <UsersListLoading />}
|
||||
</KTCardBody>
|
||||
)
|
||||
}
|
||||
|
||||
export {UsersTable}
|
||||
@@ -0,0 +1,12 @@
|
||||
import {FC} from 'react'
|
||||
import { NewDateTimeFormatter } from '../../../../../../../_digifi/lib/NewDateTimeFormatter'
|
||||
|
||||
type Props = {
|
||||
added?: string
|
||||
}
|
||||
|
||||
const AddedCell: FC<Props> = ({added}) => (
|
||||
<div className='badge badge-light fw-bolder'>{NewDateTimeFormatter((added))}</div>
|
||||
)
|
||||
|
||||
export {AddedCell}
|
||||
@@ -0,0 +1,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-125px py-4'
|
||||
data-kt-menu='true'
|
||||
>
|
||||
{/* begin::Menu item */}
|
||||
<div className='menu-item px-3'>
|
||||
<a className='menu-link px-3' onClick={openEditModal}>
|
||||
Edit
|
||||
</a>
|
||||
</div>
|
||||
{/* end::Menu item */}
|
||||
|
||||
{/* begin::Menu item */}
|
||||
<div className='menu-item px-3'>
|
||||
<a
|
||||
className='menu-link px-3'
|
||||
data-kt-users-table-filter='delete_row'
|
||||
onClick={async () => await deleteItem.mutateAsync()}
|
||||
>
|
||||
Delete
|
||||
</a>
|
||||
</div>
|
||||
{/* end::Menu item */}
|
||||
</div>
|
||||
{/* end::Menu */}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export {UserActionsCell}
|
||||
@@ -0,0 +1,61 @@
|
||||
import clsx from 'clsx'
|
||||
import {FC, PropsWithChildren, useMemo} from 'react'
|
||||
import {HeaderProps} from 'react-table'
|
||||
import {initialQueryState} from '../../../../../../../_digifi/helpers'
|
||||
import {useQueryRequest} from '../../core/QueryRequestProvider'
|
||||
import {User} from '../../core/_models'
|
||||
|
||||
type Props = {
|
||||
className?: string
|
||||
title?: string
|
||||
tableProps: PropsWithChildren<HeaderProps<User>>
|
||||
}
|
||||
const UserCustomHeader: FC<Props> = ({className, title, tableProps}) => {
|
||||
const id = tableProps.column.id
|
||||
const {state, updateState} = useQueryRequest()
|
||||
|
||||
const isSelectedForSorting = useMemo(() => {
|
||||
return state.sort && state.sort === id
|
||||
}, [state, id])
|
||||
const order: 'asc' | 'desc' | undefined = useMemo(() => state.order, [state])
|
||||
|
||||
const sortColumn = () => {
|
||||
// avoid sorting for these columns
|
||||
if (id === 'actions' || id === 'selection') {
|
||||
return
|
||||
}
|
||||
|
||||
if (!isSelectedForSorting) {
|
||||
// enable sort asc
|
||||
updateState({sort: id, order: 'asc', ...initialQueryState})
|
||||
return
|
||||
}
|
||||
|
||||
if (isSelectedForSorting && order !== undefined) {
|
||||
if (order === 'asc') {
|
||||
// enable sort desc
|
||||
updateState({sort: id, order: 'desc', ...initialQueryState})
|
||||
return
|
||||
}
|
||||
|
||||
// disable sort
|
||||
updateState({sort: undefined, order: undefined, ...initialQueryState})
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<th
|
||||
{...tableProps.column.getHeaderProps()}
|
||||
className={clsx(
|
||||
className,
|
||||
isSelectedForSorting && order !== undefined && `table-sort-${order}`
|
||||
)}
|
||||
style={{cursor: 'pointer'}}
|
||||
onClick={sortColumn}
|
||||
>
|
||||
{title}
|
||||
</th>
|
||||
)
|
||||
}
|
||||
|
||||
export {UserCustomHeader}
|
||||
@@ -0,0 +1,42 @@
|
||||
|
||||
import clsx from 'clsx'
|
||||
import {FC} from 'react'
|
||||
import {toAbsoluteUrl} from '../../../../../../../_digifi/helpers'
|
||||
import {User} from '../../core/_models'
|
||||
|
||||
type Props = {
|
||||
user: User
|
||||
}
|
||||
|
||||
const UserInfoCell: FC<Props> = ({user}) => (
|
||||
<div className='d-flex align-items-center'>
|
||||
{/* begin:: Avatar */}
|
||||
<div className='symbol symbol-circle symbol-50px overflow-hidden me-3'>
|
||||
<a href='#'>
|
||||
{user.avatar ? (
|
||||
<div className='symbol-label'>
|
||||
<img src={toAbsoluteUrl(`media/${user.avatar}`)} alt={user.name} className='w-100' />
|
||||
</div>
|
||||
) : (
|
||||
<div
|
||||
className={clsx(
|
||||
'symbol-label fs-3',
|
||||
`bg-light-${user.initials?.state}`,
|
||||
`text-${user.initials?.state}`
|
||||
)}
|
||||
>
|
||||
{user.firstname?.substring(0,1).toUpperCase()} {user.lastname?.substring(0,1).toUpperCase()}
|
||||
</div>
|
||||
)}
|
||||
</a>
|
||||
</div>
|
||||
<div className='d-flex flex-column'>
|
||||
<a href='#' className='text-gray-800 text-hover-primary mb-1'>
|
||||
{user.firstname} {user.lastname}
|
||||
</a>
|
||||
<span>{user.email}</span>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
||||
export {UserInfoCell}
|
||||
@@ -0,0 +1,11 @@
|
||||
import {FC} from 'react'
|
||||
|
||||
type Props = {
|
||||
payment_month?: string
|
||||
}
|
||||
|
||||
const PaymentMonthCell: FC<Props> = ({payment_month}) => (
|
||||
<div className='badge badge-light fw-bolder'>{payment_month}</div>
|
||||
)
|
||||
|
||||
export {PaymentMonthCell}
|
||||
@@ -0,0 +1,26 @@
|
||||
import {FC, useMemo} from 'react'
|
||||
import {ID} from '../../../../../../../_digifi/helpers'
|
||||
import {useListView} from '../../core/ListViewProvider'
|
||||
|
||||
type Props = {
|
||||
id: ID
|
||||
}
|
||||
|
||||
const UserSelectionCell: FC<Props> = ({id}) => {
|
||||
const {selected, onSelect} = useListView()
|
||||
const isSelected = useMemo(() => selected.includes(id), [id, selected])
|
||||
return (
|
||||
<div className='form-check form-check-custom form-check-solid'>
|
||||
<input
|
||||
className='form-check-input'
|
||||
type='checkbox'
|
||||
data-kt-check={isSelected}
|
||||
data-kt-check-target='#kt_table_users .form-check-input'
|
||||
checked={isSelected}
|
||||
onChange={() => onSelect(id)}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export {UserSelectionCell}
|
||||
@@ -0,0 +1,28 @@
|
||||
import {FC, PropsWithChildren} from 'react'
|
||||
import {HeaderProps} from 'react-table'
|
||||
import {useListView} from '../../core/ListViewProvider'
|
||||
import {User} from '../../core/_models'
|
||||
|
||||
type Props = {
|
||||
tableProps: PropsWithChildren<HeaderProps<User>>
|
||||
}
|
||||
|
||||
const UserSelectionHeader: FC<Props> = ({tableProps}) => {
|
||||
const {isAllSelected, onSelectAll} = useListView()
|
||||
return (
|
||||
<th {...tableProps.column.getHeaderProps()} className='w-10px pe-2'>
|
||||
<div className='form-check form-check-sm form-check-custom form-check-solid me-3'>
|
||||
<input
|
||||
className='form-check-input'
|
||||
type='checkbox'
|
||||
data-kt-check={isAllSelected}
|
||||
data-kt-check-target='#kt_table_users .form-check-input'
|
||||
checked={isAllSelected}
|
||||
onChange={onSelectAll}
|
||||
/>
|
||||
</div>
|
||||
</th>
|
||||
)
|
||||
}
|
||||
|
||||
export {UserSelectionHeader}
|
||||
@@ -0,0 +1,57 @@
|
||||
import {Column} from 'react-table'
|
||||
import {UserInfoCell} from './UserInfoCell'
|
||||
import { PaymentMonthCell } from './UserLastLoginCell'
|
||||
import {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,407 @@
|
||||
import {FC, useState} from 'react'
|
||||
import * as Yup from 'yup'
|
||||
import {useFormik} from 'formik'
|
||||
import {isNotEmpty, toAbsoluteUrl} from '../../../../../../_digifi/helpers'
|
||||
import {initialUser, User} from '../core/_models'
|
||||
import clsx from 'clsx'
|
||||
import {useListView} from '../core/ListViewProvider'
|
||||
import {UsersListLoading} from '../components/loading/UsersListLoading'
|
||||
import {createUser, updateUser} from '../core/_requests'
|
||||
import {useQueryResponse} from '../core/QueryResponseProvider'
|
||||
|
||||
type Props = {
|
||||
isUserLoading: boolean
|
||||
user: User
|
||||
}
|
||||
|
||||
const editUserSchema = Yup.object().shape({
|
||||
email: Yup.string()
|
||||
.email('Wrong email format')
|
||||
.min(3, 'Minimum 3 symbols')
|
||||
.max(50, 'Maximum 50 symbols')
|
||||
.required('Email is required'),
|
||||
name: Yup.string()
|
||||
.min(3, 'Minimum 3 symbols')
|
||||
.max(50, 'Maximum 50 symbols')
|
||||
.required('Name is required'),
|
||||
})
|
||||
|
||||
const UserEditModalForm: FC<Props> = ({user, isUserLoading}) => {
|
||||
const {setItemIdForUpdate} = useListView()
|
||||
const {refetch} = useQueryResponse()
|
||||
|
||||
const [userForEdit] = useState<User>({
|
||||
...user,
|
||||
avatar: user.avatar || initialUser.avatar,
|
||||
role: user.role || initialUser.role,
|
||||
position: user.position || initialUser.position,
|
||||
name: user.name || initialUser.name,
|
||||
email: user.email || initialUser.email,
|
||||
})
|
||||
|
||||
const cancel = (withRefresh?: boolean) => {
|
||||
if (withRefresh) {
|
||||
refetch()
|
||||
}
|
||||
setItemIdForUpdate(undefined)
|
||||
}
|
||||
|
||||
const blankImg = toAbsoluteUrl('media/svg/avatars/blank.svg')
|
||||
const userAvatarImg = toAbsoluteUrl(`media/${userForEdit.avatar}`)
|
||||
|
||||
const formik = useFormik({
|
||||
initialValues: userForEdit,
|
||||
validationSchema: editUserSchema,
|
||||
onSubmit: async (values, {setSubmitting}) => {
|
||||
setSubmitting(true)
|
||||
try {
|
||||
if (isNotEmpty(values.id)) {
|
||||
await updateUser(values)
|
||||
} else {
|
||||
await createUser(values)
|
||||
}
|
||||
} catch (ex) {
|
||||
console.error(ex)
|
||||
} finally {
|
||||
setSubmitting(true)
|
||||
cancel(true)
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
return (
|
||||
<>
|
||||
<form id='kt_modal_add_user_form' className='form' onSubmit={formik.handleSubmit} noValidate>
|
||||
{/* begin::Scroll */}
|
||||
<div
|
||||
className='d-flex flex-column scroll-y me-n7 pe-7'
|
||||
id='kt_modal_add_user_scroll'
|
||||
data-kt-scroll='true'
|
||||
data-kt-scroll-activate='{default: false, lg: true}'
|
||||
data-kt-scroll-max-height='auto'
|
||||
data-kt-scroll-dependencies='#kt_modal_add_user_header'
|
||||
data-kt-scroll-wrappers='#kt_modal_add_user_scroll'
|
||||
data-kt-scroll-offset='300px'
|
||||
>
|
||||
{/* begin::Input group */}
|
||||
<div className='fv-row mb-7'>
|
||||
{/* begin::Label */}
|
||||
<label className='d-block fw-bold fs-6 mb-5'>Avatar</label>
|
||||
{/* end::Label */}
|
||||
|
||||
{/* begin::Image input */}
|
||||
<div
|
||||
className='image-input image-input-outline'
|
||||
data-kt-image-input='true'
|
||||
style={{backgroundImage: `url('${blankImg}')`}}
|
||||
>
|
||||
{/* begin::Preview existing avatar */}
|
||||
<div
|
||||
className='image-input-wrapper w-125px h-125px'
|
||||
style={{backgroundImage: `url('${userAvatarImg}')`}}
|
||||
></div>
|
||||
{/* end::Preview existing avatar */}
|
||||
|
||||
{/* begin::Label */}
|
||||
{/* <label
|
||||
className='btn btn-icon btn-circle btn-active-color-primary w-25px h-25px bg-body shadow'
|
||||
data-kt-image-input-action='change'
|
||||
data-bs-toggle='tooltip'
|
||||
title='Change avatar'
|
||||
>
|
||||
<i className='bi bi-pencil-fill fs-7'></i>
|
||||
|
||||
<input type='file' name='avatar' accept='.png, .jpg, .jpeg' />
|
||||
<input type='hidden' name='avatar_remove' />
|
||||
</label> */}
|
||||
{/* end::Label */}
|
||||
|
||||
{/* begin::Cancel */}
|
||||
{/* <span
|
||||
className='btn btn-icon btn-circle btn-active-color-primary w-25px h-25px bg-body shadow'
|
||||
data-kt-image-input-action='cancel'
|
||||
data-bs-toggle='tooltip'
|
||||
title='Cancel avatar'
|
||||
>
|
||||
<i className='bi bi-x fs-2'></i>
|
||||
</span> */}
|
||||
{/* end::Cancel */}
|
||||
|
||||
{/* begin::Remove */}
|
||||
{/* <span
|
||||
className='btn btn-icon btn-circle btn-active-color-primary w-25px h-25px bg-body shadow'
|
||||
data-kt-image-input-action='remove'
|
||||
data-bs-toggle='tooltip'
|
||||
title='Remove avatar'
|
||||
>
|
||||
<i className='bi bi-x fs-2'></i>
|
||||
</span> */}
|
||||
{/* end::Remove */}
|
||||
</div>
|
||||
{/* end::Image input */}
|
||||
|
||||
{/* begin::Hint */}
|
||||
{/* <div className='form-text'>Allowed file types: png, jpg, jpeg.</div> */}
|
||||
{/* end::Hint */}
|
||||
</div>
|
||||
{/* end::Input group */}
|
||||
|
||||
{/* begin::Input group */}
|
||||
<div className='fv-row mb-7'>
|
||||
{/* begin::Label */}
|
||||
<label className='required fw-bold fs-6 mb-2'>Full Name</label>
|
||||
{/* end::Label */}
|
||||
|
||||
{/* begin::Input */}
|
||||
<input
|
||||
placeholder='Full name'
|
||||
{...formik.getFieldProps('name')}
|
||||
type='text'
|
||||
name='name'
|
||||
className={clsx(
|
||||
'form-control form-control-solid mb-3 mb-lg-0',
|
||||
{'is-invalid': formik.touched.name && formik.errors.name},
|
||||
{
|
||||
'is-valid': formik.touched.name && !formik.errors.name,
|
||||
}
|
||||
)}
|
||||
autoComplete='off'
|
||||
disabled={formik.isSubmitting || isUserLoading}
|
||||
/>
|
||||
{formik.touched.name && formik.errors.name && (
|
||||
<div className='fv-plugins-message-container'>
|
||||
<div className='fv-help-block'>
|
||||
<span role='alert'>{formik.errors.name}</span>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{/* end::Input */}
|
||||
</div>
|
||||
{/* end::Input group */}
|
||||
|
||||
{/* begin::Input group */}
|
||||
<div className='fv-row mb-7'>
|
||||
{/* begin::Label */}
|
||||
<label className='required fw-bold fs-6 mb-2'>Email</label>
|
||||
{/* end::Label */}
|
||||
|
||||
{/* begin::Input */}
|
||||
<input
|
||||
placeholder='Email'
|
||||
{...formik.getFieldProps('email')}
|
||||
className={clsx(
|
||||
'form-control form-control-solid mb-3 mb-lg-0',
|
||||
{'is-invalid': formik.touched.email && formik.errors.email},
|
||||
{
|
||||
'is-valid': formik.touched.email && !formik.errors.email,
|
||||
}
|
||||
)}
|
||||
type='email'
|
||||
name='email'
|
||||
autoComplete='off'
|
||||
disabled={formik.isSubmitting || isUserLoading}
|
||||
/>
|
||||
{/* end::Input */}
|
||||
{formik.touched.email && formik.errors.email && (
|
||||
<div className='fv-plugins-message-container'>
|
||||
<span role='alert'>{formik.errors.email}</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{/* end::Input group */}
|
||||
|
||||
{/* begin::Input group */}
|
||||
<div className='mb-7'>
|
||||
{/* begin::Label */}
|
||||
<label className='required fw-bold fs-6 mb-5'>Role</label>
|
||||
{/* end::Label */}
|
||||
{/* begin::Roles */}
|
||||
{/* begin::Input row */}
|
||||
<div className='d-flex fv-row'>
|
||||
{/* begin::Radio */}
|
||||
<div className='form-check form-check-custom form-check-solid'>
|
||||
{/* begin::Input */}
|
||||
<input
|
||||
className='form-check-input me-3'
|
||||
{...formik.getFieldProps('role')}
|
||||
name='role'
|
||||
type='radio'
|
||||
value='Administrator'
|
||||
id='kt_modal_update_role_option_0'
|
||||
checked={formik.values.role === 'Administrator'}
|
||||
disabled={formik.isSubmitting || isUserLoading}
|
||||
/>
|
||||
|
||||
{/* end::Input */}
|
||||
{/* begin::Label */}
|
||||
<label className='form-check-label' htmlFor='kt_modal_update_role_option_0'>
|
||||
<div className='fw-bolder text-gray-800'>Administrator</div>
|
||||
<div className='text-gray-600'>
|
||||
Best for business owners and company administrators
|
||||
</div>
|
||||
</label>
|
||||
{/* end::Label */}
|
||||
</div>
|
||||
{/* end::Radio */}
|
||||
</div>
|
||||
{/* end::Input row */}
|
||||
<div className='separator separator-dashed my-5'></div>
|
||||
{/* begin::Input row */}
|
||||
<div className='d-flex fv-row'>
|
||||
{/* begin::Radio */}
|
||||
<div className='form-check form-check-custom form-check-solid'>
|
||||
{/* begin::Input */}
|
||||
<input
|
||||
className='form-check-input me-3'
|
||||
{...formik.getFieldProps('role')}
|
||||
name='role'
|
||||
type='radio'
|
||||
value='Developer'
|
||||
id='kt_modal_update_role_option_1'
|
||||
checked={formik.values.role === 'Developer'}
|
||||
disabled={formik.isSubmitting || isUserLoading}
|
||||
/>
|
||||
{/* end::Input */}
|
||||
{/* begin::Label */}
|
||||
<label className='form-check-label' htmlFor='kt_modal_update_role_option_1'>
|
||||
<div className='fw-bolder text-gray-800'>Developer</div>
|
||||
<div className='text-gray-600'>
|
||||
Best for developers or people primarily using the API
|
||||
</div>
|
||||
</label>
|
||||
{/* end::Label */}
|
||||
</div>
|
||||
{/* end::Radio */}
|
||||
</div>
|
||||
{/* end::Input row */}
|
||||
<div className='separator separator-dashed my-5'></div>
|
||||
{/* begin::Input row */}
|
||||
<div className='d-flex fv-row'>
|
||||
{/* begin::Radio */}
|
||||
<div className='form-check form-check-custom form-check-solid'>
|
||||
{/* begin::Input */}
|
||||
<input
|
||||
className='form-check-input me-3'
|
||||
{...formik.getFieldProps('role')}
|
||||
name='role'
|
||||
type='radio'
|
||||
value='Analyst'
|
||||
id='kt_modal_update_role_option_2'
|
||||
checked={formik.values.role === 'Analyst'}
|
||||
disabled={formik.isSubmitting || isUserLoading}
|
||||
/>
|
||||
|
||||
{/* end::Input */}
|
||||
{/* begin::Label */}
|
||||
<label className='form-check-label' htmlFor='kt_modal_update_role_option_2'>
|
||||
<div className='fw-bolder text-gray-800'>Analyst</div>
|
||||
<div className='text-gray-600'>
|
||||
Best for people who need full access to analytics data, but don't need to update
|
||||
business settings
|
||||
</div>
|
||||
</label>
|
||||
{/* end::Label */}
|
||||
</div>
|
||||
{/* end::Radio */}
|
||||
</div>
|
||||
{/* end::Input row */}
|
||||
<div className='separator separator-dashed my-5'></div>
|
||||
{/* begin::Input row */}
|
||||
<div className='d-flex fv-row'>
|
||||
{/* begin::Radio */}
|
||||
<div className='form-check form-check-custom form-check-solid'>
|
||||
{/* begin::Input */}
|
||||
<input
|
||||
className='form-check-input me-3'
|
||||
{...formik.getFieldProps('role')}
|
||||
name='role'
|
||||
type='radio'
|
||||
value='Support'
|
||||
id='kt_modal_update_role_option_3'
|
||||
checked={formik.values.role === 'Support'}
|
||||
disabled={formik.isSubmitting || isUserLoading}
|
||||
/>
|
||||
{/* end::Input */}
|
||||
{/* begin::Label */}
|
||||
<label className='form-check-label' htmlFor='kt_modal_update_role_option_3'>
|
||||
<div className='fw-bolder text-gray-800'>Support</div>
|
||||
<div className='text-gray-600'>
|
||||
Best for employees who regularly refund payments and respond to disputes
|
||||
</div>
|
||||
</label>
|
||||
{/* end::Label */}
|
||||
</div>
|
||||
{/* end::Radio */}
|
||||
</div>
|
||||
{/* end::Input row */}
|
||||
<div className='separator separator-dashed my-5'></div>
|
||||
{/* begin::Input row */}
|
||||
<div className='d-flex fv-row'>
|
||||
{/* begin::Radio */}
|
||||
<div className='form-check form-check-custom form-check-solid'>
|
||||
{/* begin::Input */}
|
||||
<input
|
||||
className='form-check-input me-3'
|
||||
{...formik.getFieldProps('role')}
|
||||
name='role'
|
||||
type='radio'
|
||||
id='kt_modal_update_role_option_4'
|
||||
value='Trial'
|
||||
checked={formik.values.role === 'Trial'}
|
||||
disabled={formik.isSubmitting || isUserLoading}
|
||||
/>
|
||||
{/* end::Input */}
|
||||
{/* begin::Label */}
|
||||
<label className='form-check-label' htmlFor='kt_modal_update_role_option_4'>
|
||||
<div className='fw-bolder text-gray-800'>Trial</div>
|
||||
<div className='text-gray-600'>
|
||||
Best for people who need to preview content data, but don't need to make any
|
||||
updates
|
||||
</div>
|
||||
</label>
|
||||
{/* end::Label */}
|
||||
</div>
|
||||
{/* end::Radio */}
|
||||
</div>
|
||||
{/* end::Input row */}
|
||||
{/* end::Roles */}
|
||||
</div>
|
||||
{/* end::Input group */}
|
||||
</div>
|
||||
{/* end::Scroll */}
|
||||
|
||||
{/* begin::Actions */}
|
||||
<div className='text-center pt-15'>
|
||||
<button
|
||||
type='reset'
|
||||
onClick={() => cancel()}
|
||||
className='btn btn-light me-3'
|
||||
data-kt-users-modal-action='cancel'
|
||||
disabled={formik.isSubmitting || isUserLoading}
|
||||
>
|
||||
Discard
|
||||
</button>
|
||||
|
||||
<button
|
||||
type='submit'
|
||||
className='btn btn-primary'
|
||||
data-kt-users-modal-action='submit'
|
||||
disabled={isUserLoading || formik.isSubmitting || !formik.isValid || !formik.touched}
|
||||
>
|
||||
<span className='indicator-label'>Submit</span>
|
||||
{(formik.isSubmitting || isUserLoading) && (
|
||||
<span className='indicator-progress'>
|
||||
Please wait...{' '}
|
||||
<span className='spinner-border spinner-border-sm align-middle ms-2'></span>
|
||||
</span>
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
{/* end::Actions */}
|
||||
</form>
|
||||
{(formik.isSubmitting || isUserLoading) && <UsersListLoading />}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export {UserEditModalForm}
|
||||
@@ -0,0 +1,40 @@
|
||||
import {useQuery} from 'react-query'
|
||||
import {UserEditModalForm} from './UserEditModalForm'
|
||||
import {isNotEmpty, QUERIES} from '../../../../../../_digifi/helpers'
|
||||
import {useListView} from '../core/ListViewProvider'
|
||||
import {getUserById} from '../core/_requests'
|
||||
|
||||
const UserEditModalFormWrapper = () => {
|
||||
const {itemIdForUpdate, setItemIdForUpdate} = useListView()
|
||||
const enabledQuery: boolean = isNotEmpty(itemIdForUpdate)
|
||||
const {
|
||||
isLoading,
|
||||
data: user,
|
||||
error,
|
||||
} = useQuery(
|
||||
`${QUERIES.USERS_LIST}-user-${itemIdForUpdate}`,
|
||||
() => {
|
||||
return getUserById(itemIdForUpdate)
|
||||
},
|
||||
{
|
||||
cacheTime: 0,
|
||||
enabled: enabledQuery,
|
||||
onError: (err) => {
|
||||
setItemIdForUpdate(undefined)
|
||||
console.error(err)
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
if (!itemIdForUpdate) {
|
||||
return <UserEditModalForm isUserLoading={isLoading} user={{id: undefined}} />
|
||||
}
|
||||
|
||||
if (!isLoading && !error && user) {
|
||||
return <UserEditModalForm isUserLoading={isLoading} user={user} />
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
export {UserEditModalFormWrapper}
|
||||
@@ -0,0 +1,27 @@
|
||||
import {KTIcon} from '../../../../../../_digifi/helpers'
|
||||
import {useListView} from '../core/ListViewProvider'
|
||||
|
||||
const UserEditModalHeader = () => {
|
||||
const {setItemIdForUpdate} = useListView()
|
||||
|
||||
return (
|
||||
<div className='modal-header'>
|
||||
{/* begin::Modal title */}
|
||||
<h2 className='fw-bolder'>Add User</h2>
|
||||
{/* end::Modal title */}
|
||||
|
||||
{/* begin::Close */}
|
||||
<div
|
||||
className='btn btn-icon btn-sm btn-active-icon-primary'
|
||||
data-kt-users-modal-action='close'
|
||||
onClick={() => setItemIdForUpdate(undefined)}
|
||||
style={{cursor: 'pointer'}}
|
||||
>
|
||||
<KTIcon iconName='cross' className='fs-1' />
|
||||
</div>
|
||||
{/* end::Close */}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export {UserEditModalHeader}
|
||||
@@ -0,0 +1,39 @@
|
||||
import {Route, Routes, Outlet, Navigate} from 'react-router-dom'
|
||||
import {PageLink, PageTitle} from '../../../../_digifi/layout/core'
|
||||
import {UsersListWrapper} from './users-list/UsersList'
|
||||
|
||||
const usersBreadcrumbs: Array<PageLink> = [
|
||||
{
|
||||
title: 'User Management',
|
||||
path: '/apps/user-management/users',
|
||||
isSeparator: false,
|
||||
isActive: false,
|
||||
},
|
||||
{
|
||||
title: '',
|
||||
path: '',
|
||||
isSeparator: true,
|
||||
isActive: false,
|
||||
},
|
||||
]
|
||||
|
||||
const UsersPage = () => {
|
||||
return (
|
||||
<Routes>
|
||||
<Route element={<Outlet />}>
|
||||
<Route
|
||||
path='users'
|
||||
element={
|
||||
<>
|
||||
<PageTitle breadcrumbs={usersBreadcrumbs}>Users list</PageTitle>
|
||||
<UsersListWrapper />
|
||||
</>
|
||||
}
|
||||
/>
|
||||
</Route>
|
||||
<Route index element={<Navigate to='/apps/user-management/users' />} />
|
||||
</Routes>
|
||||
)
|
||||
}
|
||||
|
||||
export default UsersPage
|
||||
@@ -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,32 @@
|
||||
import {KTIcon} from '../../../../../../../_digifi/helpers'
|
||||
import {useListView} from '../../core/ListViewProvider'
|
||||
import {UsersListFilter} from './UsersListFilter'
|
||||
|
||||
const UsersListToolbar = () => {
|
||||
const {setItemIdForUpdate} = useListView()
|
||||
const openAddUserModal = () => {
|
||||
setItemIdForUpdate(null)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className='d-flex justify-content-end' data-kt-user-table-toolbar='base'>
|
||||
<UsersListFilter />
|
||||
|
||||
{/* begin::Export */}
|
||||
{/* <button type='button' className='btn btn-light-primary me-3'>
|
||||
<KTIcon iconName='exit-up' className='fs-2' />
|
||||
Export
|
||||
</button> */}
|
||||
{/* end::Export */}
|
||||
|
||||
{/* begin::Add user */}
|
||||
{/* <button type='button' className='btn btn-primary' onClick={openAddUserModal}>
|
||||
<KTIcon iconName='plus' className='fs-2' />
|
||||
Add User
|
||||
</button> */}
|
||||
{/* end::Add user */}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export {UsersListToolbar}
|
||||
@@ -0,0 +1,133 @@
|
||||
import {useEffect, useState} from 'react'
|
||||
import {MenuComponent} from '../../../../../../../_digifi/assets/ts/components'
|
||||
import {initialQueryState, KTIcon} from '../../../../../../../_digifi/helpers'
|
||||
import {useQueryRequest} from '../../core/QueryRequestProvider'
|
||||
import {useQueryResponse} from '../../core/QueryResponseProvider'
|
||||
|
||||
const UsersListFilter = () => {
|
||||
const {updateState} = useQueryRequest()
|
||||
const {isLoading} = useQueryResponse()
|
||||
const [role, setRole] = useState<string | undefined>()
|
||||
const [lastLogin, setLastLogin] = useState<string | undefined>()
|
||||
|
||||
useEffect(() => {
|
||||
MenuComponent.reinitialization()
|
||||
}, [])
|
||||
|
||||
const resetData = () => {
|
||||
updateState({filter: undefined, ...initialQueryState})
|
||||
}
|
||||
|
||||
const filterData = () => {
|
||||
updateState({
|
||||
filter: {role, last_login: lastLogin},
|
||||
...initialQueryState,
|
||||
})
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{/* begin::Filter Button */}
|
||||
<button
|
||||
disabled={isLoading}
|
||||
type='button'
|
||||
className='btn btn-light-primary me-3'
|
||||
data-kt-menu-trigger='click'
|
||||
data-kt-menu-placement='bottom-end'
|
||||
>
|
||||
<KTIcon iconName='filter' className='fs-2' />
|
||||
Filter
|
||||
</button>
|
||||
{/* end::Filter Button */}
|
||||
{/* begin::SubMenu */}
|
||||
<div className='menu menu-sub menu-sub-dropdown w-300px w-md-325px' data-kt-menu='true'>
|
||||
{/* begin::Header */}
|
||||
<div className='px-7 py-5'>
|
||||
<div className='fs-5 text-gray-900 fw-bolder'>Filter Options</div>
|
||||
</div>
|
||||
{/* end::Header */}
|
||||
|
||||
{/* begin::Separator */}
|
||||
<div className='separator border-gray-200'></div>
|
||||
{/* end::Separator */}
|
||||
|
||||
{/* begin::Content */}
|
||||
<div className='px-7 py-5' data-kt-user-table-filter='form'>
|
||||
{/* begin::Input group */}
|
||||
<div className='mb-10'>
|
||||
<label className='form-label fs-6 fw-bold'>Role:</label>
|
||||
<select
|
||||
className='form-select form-select-solid fw-bolder'
|
||||
data-kt-select2='true'
|
||||
data-placeholder='Select option'
|
||||
data-allow-clear='true'
|
||||
data-kt-user-table-filter='role'
|
||||
data-hide-search='true'
|
||||
onChange={(e) => setRole(e.target.value)}
|
||||
value={role}
|
||||
>
|
||||
<option value=''></option>
|
||||
<option value='Administrator'>Administrator</option>
|
||||
<option value='Analyst'>Analyst</option>
|
||||
<option value='Developer'>Developer</option>
|
||||
<option value='Support'>Support</option>
|
||||
<option value='Trial'>Trial</option>
|
||||
</select>
|
||||
</div>
|
||||
{/* end::Input group */}
|
||||
|
||||
{/* begin::Input group */}
|
||||
<div className='mb-10'>
|
||||
<label className='form-label fs-6 fw-bold'>Last login:</label>
|
||||
<select
|
||||
className='form-select form-select-solid fw-bolder'
|
||||
data-kt-select2='true'
|
||||
data-placeholder='Select option'
|
||||
data-allow-clear='true'
|
||||
data-kt-user-table-filter='two-step'
|
||||
data-hide-search='true'
|
||||
onChange={(e) => setLastLogin(e.target.value)}
|
||||
value={lastLogin}
|
||||
>
|
||||
<option value=''></option>
|
||||
<option value='Yesterday'>Yesterday</option>
|
||||
<option value='20 mins ago'>20 mins ago</option>
|
||||
<option value='5 hours ago'>5 hours ago</option>
|
||||
<option value='2 days ago'>2 days ago</option>
|
||||
</select>
|
||||
</div>
|
||||
{/* end::Input group */}
|
||||
|
||||
{/* begin::Actions */}
|
||||
<div className='d-flex justify-content-end'>
|
||||
<button
|
||||
type='button'
|
||||
disabled={isLoading}
|
||||
onClick={filterData}
|
||||
className='btn btn-light btn-active-light-primary fw-bold me-2 px-6'
|
||||
data-kt-menu-dismiss='true'
|
||||
data-kt-user-table-filter='reset'
|
||||
>
|
||||
Reset
|
||||
</button>
|
||||
<button
|
||||
disabled={isLoading}
|
||||
type='button'
|
||||
onClick={resetData}
|
||||
className='btn btn-primary fw-bold px-6'
|
||||
data-kt-menu-dismiss='true'
|
||||
data-kt-user-table-filter='filter'
|
||||
>
|
||||
Apply
|
||||
</button>
|
||||
</div>
|
||||
{/* end::Actions */}
|
||||
</div>
|
||||
{/* end::Content */}
|
||||
</div>
|
||||
{/* end::SubMenu */}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export {UsersListFilter}
|
||||
@@ -0,0 +1,38 @@
|
||||
import {useQueryClient, useMutation} from 'react-query'
|
||||
import {QUERIES} from '../../../../../../../_digifi/helpers'
|
||||
import {useListView} from '../../core/ListViewProvider'
|
||||
import {useQueryResponse} from '../../core/QueryResponseProvider'
|
||||
import {deleteSelectedUsers} from '../../core/_requests'
|
||||
|
||||
const UsersListGrouping = () => {
|
||||
const {selected, clearSelected} = useListView()
|
||||
const queryClient = useQueryClient()
|
||||
const {query} = useQueryResponse()
|
||||
|
||||
const deleteSelectedItems = useMutation(() => deleteSelectedUsers(selected), {
|
||||
// 💡 response of the mutation is passed to onSuccess
|
||||
onSuccess: () => {
|
||||
// ✅ update detail view directly
|
||||
queryClient.invalidateQueries([`${QUERIES.USERS_LIST}-${query}`])
|
||||
clearSelected()
|
||||
},
|
||||
})
|
||||
|
||||
return (
|
||||
<div className='d-flex justify-content-end align-items-center'>
|
||||
<div className='fw-bolder me-5'>
|
||||
<span className='me-2'>{selected.length}</span> Selected
|
||||
</div>
|
||||
|
||||
<button
|
||||
type='button'
|
||||
className='btn btn-danger'
|
||||
onClick={async () => await deleteSelectedItems.mutateAsync()}
|
||||
>
|
||||
Delete Selected
|
||||
</button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export {UsersListGrouping}
|
||||
@@ -0,0 +1,22 @@
|
||||
import {useListView} from '../../core/ListViewProvider'
|
||||
import {UsersListToolbar} from './UserListToolbar'
|
||||
import {UsersListGrouping} from './UsersListGrouping'
|
||||
import {UsersListSearchComponent} from './UsersListSearchComponent'
|
||||
|
||||
const UsersListHeader = () => {
|
||||
const {selected} = useListView()
|
||||
return (
|
||||
<div className='card-header border-0 pt-6'>
|
||||
<UsersListSearchComponent />
|
||||
{/* begin::Card toolbar */}
|
||||
<div className='card-toolbar'>
|
||||
{/* begin::Group actions */}
|
||||
{selected.length > 0 ? <UsersListGrouping /> : <UsersListToolbar />}
|
||||
{/* end::Group actions */}
|
||||
</div>
|
||||
{/* end::Card toolbar */}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export {UsersListHeader}
|
||||
@@ -0,0 +1,45 @@
|
||||
/* eslint-disable react-hooks/exhaustive-deps */
|
||||
|
||||
import {useEffect, useState} from 'react'
|
||||
import {initialQueryState, KTIcon, useDebounce} from '../../../../../../../_digifi/helpers'
|
||||
import {useQueryRequest} from '../../core/QueryRequestProvider'
|
||||
|
||||
const UsersListSearchComponent = () => {
|
||||
const {updateState} = useQueryRequest()
|
||||
const [searchTerm, setSearchTerm] = useState<string>('')
|
||||
// Debounce search term so that it only gives us latest value ...
|
||||
// ... if searchTerm has not been updated within last 500ms.
|
||||
// The goal is to only have the API call fire when user stops typing ...
|
||||
// ... so that we aren't hitting our API rapidly.
|
||||
const debouncedSearchTerm = useDebounce(searchTerm, 150)
|
||||
// Effect for API call
|
||||
useEffect(
|
||||
() => {
|
||||
if (debouncedSearchTerm !== undefined && searchTerm !== undefined) {
|
||||
updateState({search: debouncedSearchTerm, ...initialQueryState})
|
||||
}
|
||||
},
|
||||
[debouncedSearchTerm] // Only call effect if debounced search term changes
|
||||
// More details about useDebounce: https://usehooks.com/useDebounce/
|
||||
)
|
||||
|
||||
return (
|
||||
<div className='card-title'>
|
||||
{/* begin::Search */}
|
||||
<div className='d-flex align-items-center position-relative my-1'>
|
||||
<KTIcon iconName='magnifier' className='fs-1 position-absolute ms-6' />
|
||||
<input
|
||||
type='text'
|
||||
data-kt-user-table-filter='search'
|
||||
className='form-control form-control-solid w-250px ps-14'
|
||||
placeholder='Search user'
|
||||
value={searchTerm}
|
||||
onChange={(e) => setSearchTerm(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
{/* end::Search */}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export {UsersListSearchComponent}
|
||||
@@ -0,0 +1,18 @@
|
||||
const UsersListLoading = () => {
|
||||
const styles = {
|
||||
borderRadius: '0.475rem',
|
||||
boxShadow: '0 0 50px 0 rgb(82 63 105 / 15%)',
|
||||
backgroundColor: '#fff',
|
||||
color: '#7e8299',
|
||||
fontWeight: '500',
|
||||
margin: '0',
|
||||
width: 'auto',
|
||||
padding: '1rem 2rem',
|
||||
top: 'calc(50% - 2rem)',
|
||||
left: 'calc(50% - 4rem)',
|
||||
}
|
||||
|
||||
return <div style={{...styles, position: 'absolute', textAlign: 'center'}}>Processing...</div>
|
||||
}
|
||||
|
||||
export {UsersListLoading}
|
||||
@@ -0,0 +1,156 @@
|
||||
|
||||
import clsx from 'clsx'
|
||||
import {useQueryResponseLoading, useQueryResponsePagination} from '../../core/QueryResponseProvider'
|
||||
import {useQueryRequest} from '../../core/QueryRequestProvider'
|
||||
import {PaginationState} from '../../../../../../../_digifi/helpers'
|
||||
import {useMemo} from 'react'
|
||||
|
||||
const mappedLabel = (label: string): string => {
|
||||
if (label === '« Previous') {
|
||||
return 'Previous'
|
||||
}
|
||||
|
||||
if (label === 'Next »') {
|
||||
return 'Next'
|
||||
}
|
||||
|
||||
return label
|
||||
}
|
||||
|
||||
const UsersListPagination = () => {
|
||||
const pagination = useQueryResponsePagination()
|
||||
const isLoading = useQueryResponseLoading()
|
||||
const {updateState} = useQueryRequest()
|
||||
const updatePage = (page: number | undefined | null) => {
|
||||
if (!page || isLoading || pagination.page === page) {
|
||||
return
|
||||
}
|
||||
|
||||
updateState({page, items_per_page: pagination.items_per_page || 10})
|
||||
}
|
||||
|
||||
const PAGINATION_PAGES_COUNT = 5
|
||||
const sliceLinks = (pagination?: PaginationState) => {
|
||||
if (!pagination?.links?.length) {
|
||||
return []
|
||||
}
|
||||
|
||||
const scopedLinks = [...pagination.links]
|
||||
|
||||
let pageLinks: Array<{
|
||||
label: string
|
||||
active: boolean
|
||||
url: string | null
|
||||
page: number | null
|
||||
}> = []
|
||||
const previousLink: {label: string; active: boolean; url: string | null; page: number | null} =
|
||||
scopedLinks.shift()!
|
||||
const nextLink: {label: string; active: boolean; url: string | null; page: number | null} =
|
||||
scopedLinks.pop()!
|
||||
|
||||
const halfOfPagesCount = Math.floor(PAGINATION_PAGES_COUNT / 2)
|
||||
|
||||
pageLinks.push(previousLink)
|
||||
|
||||
if (
|
||||
pagination.page <= Math.round(PAGINATION_PAGES_COUNT / 2) ||
|
||||
scopedLinks.length <= PAGINATION_PAGES_COUNT
|
||||
) {
|
||||
pageLinks = [...pageLinks, ...scopedLinks.slice(0, PAGINATION_PAGES_COUNT)]
|
||||
}
|
||||
|
||||
if (
|
||||
pagination.page > scopedLinks.length - halfOfPagesCount &&
|
||||
scopedLinks.length > PAGINATION_PAGES_COUNT
|
||||
) {
|
||||
pageLinks = [
|
||||
...pageLinks,
|
||||
...scopedLinks.slice(scopedLinks.length - PAGINATION_PAGES_COUNT, scopedLinks.length),
|
||||
]
|
||||
}
|
||||
|
||||
if (
|
||||
!(
|
||||
pagination.page <= Math.round(PAGINATION_PAGES_COUNT / 2) ||
|
||||
scopedLinks.length <= PAGINATION_PAGES_COUNT
|
||||
) &&
|
||||
!(pagination.page > scopedLinks.length - halfOfPagesCount)
|
||||
) {
|
||||
pageLinks = [
|
||||
...pageLinks,
|
||||
...scopedLinks.slice(
|
||||
pagination.page - 1 - halfOfPagesCount,
|
||||
pagination.page + halfOfPagesCount
|
||||
),
|
||||
]
|
||||
}
|
||||
|
||||
pageLinks.push(nextLink)
|
||||
|
||||
return pageLinks
|
||||
}
|
||||
|
||||
const paginationLinks = useMemo(() => sliceLinks(pagination), [pagination])
|
||||
|
||||
return (
|
||||
<div className='row'>
|
||||
<div className='col-sm-12 col-md-5 d-flex align-items-center justify-content-center justify-content-md-start'></div>
|
||||
<div className='col-sm-12 col-md-7 d-flex align-items-center justify-content-center justify-content-md-end'>
|
||||
<div id='kt_table_users_paginate'>
|
||||
<ul className='pagination'>
|
||||
<li
|
||||
className={clsx('page-item', {
|
||||
disabled: isLoading || pagination.page === 1,
|
||||
})}
|
||||
>
|
||||
<a onClick={() => updatePage(1)} style={{cursor: 'pointer'}} className='page-link'>
|
||||
First
|
||||
</a>
|
||||
</li>
|
||||
{paginationLinks
|
||||
?.map((link) => {
|
||||
return {...link, label: mappedLabel(link.label)}
|
||||
})
|
||||
.map((link) => (
|
||||
<li
|
||||
key={link.label}
|
||||
className={clsx('page-item', {
|
||||
active: pagination.page === link.page,
|
||||
disabled: isLoading,
|
||||
previous: link.label === 'Previous',
|
||||
next: link.label === 'Next',
|
||||
})}
|
||||
>
|
||||
<a
|
||||
className={clsx('page-link', {
|
||||
'page-text': link.label === 'Previous' || link.label === 'Next',
|
||||
'me-5': link.label === 'Previous',
|
||||
})}
|
||||
onClick={() => updatePage(link.page)}
|
||||
style={{cursor: 'pointer'}}
|
||||
>
|
||||
{mappedLabel(link.label)}
|
||||
</a>
|
||||
</li>
|
||||
))}
|
||||
<li
|
||||
className={clsx('page-item', {
|
||||
disabled: isLoading || pagination.page === (pagination.links?.length || 3) - 2,
|
||||
})}
|
||||
>
|
||||
<a
|
||||
onClick={() => updatePage((pagination.links?.length || 3) - 2)}
|
||||
style={{cursor: 'pointer'}}
|
||||
className='page-link'
|
||||
>
|
||||
Last
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export {UsersListPagination}
|
||||
@@ -0,0 +1,51 @@
|
||||
/* eslint-disable react-refresh/only-export-components */
|
||||
import {FC, useState, createContext, useContext, useMemo} from 'react'
|
||||
import {
|
||||
ID,
|
||||
calculatedGroupingIsDisabled,
|
||||
calculateIsAllDataSelected,
|
||||
groupingOnSelect,
|
||||
initialListView,
|
||||
ListViewContextProps,
|
||||
groupingOnSelectAll,
|
||||
WithChildren,
|
||||
} from '../../../../../../_digifi/helpers'
|
||||
import {useQueryResponse, useQueryResponseData} from './QueryResponseProvider'
|
||||
|
||||
const ListViewContext = createContext<ListViewContextProps>(initialListView)
|
||||
|
||||
const ListViewProvider: FC<WithChildren> = ({children}) => {
|
||||
const [selected, setSelected] = useState<Array<ID>>(initialListView.selected)
|
||||
const [itemIdForUpdate, setItemIdForUpdate] = useState<ID>(initialListView.itemIdForUpdate)
|
||||
const {isLoading} = useQueryResponse()
|
||||
const data = useQueryResponseData()
|
||||
const disabled = useMemo(() => calculatedGroupingIsDisabled(isLoading, data), [isLoading, data])
|
||||
const isAllSelected = useMemo(() => calculateIsAllDataSelected(data, selected), [data, selected])
|
||||
|
||||
return (
|
||||
<ListViewContext.Provider
|
||||
value={{
|
||||
selected,
|
||||
itemIdForUpdate,
|
||||
setItemIdForUpdate,
|
||||
disabled,
|
||||
isAllSelected,
|
||||
onSelect: (id: ID) => {
|
||||
groupingOnSelect(id, selected, setSelected)
|
||||
},
|
||||
onSelectAll: () => {
|
||||
groupingOnSelectAll(isAllSelected, setSelected, data)
|
||||
},
|
||||
clearSelected: () => {
|
||||
setSelected([])
|
||||
},
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</ListViewContext.Provider>
|
||||
)
|
||||
}
|
||||
|
||||
const useListView = () => useContext(ListViewContext)
|
||||
|
||||
export {ListViewProvider, useListView}
|
||||
@@ -0,0 +1,28 @@
|
||||
/* eslint-disable react-refresh/only-export-components */
|
||||
import {FC, useState, createContext, useContext} from 'react'
|
||||
import {
|
||||
QueryState,
|
||||
QueryRequestContextProps,
|
||||
initialQueryRequest,
|
||||
WithChildren,
|
||||
} from '../../../../../../_digifi/helpers'
|
||||
|
||||
const QueryRequestContext = createContext<QueryRequestContextProps>(initialQueryRequest)
|
||||
|
||||
const QueryRequestProvider: FC<WithChildren> = ({children}) => {
|
||||
const [state, setState] = useState<QueryState>(initialQueryRequest.state)
|
||||
|
||||
const updateState = (updates: Partial<QueryState>) => {
|
||||
const updatedState = {...state, ...updates} as QueryState
|
||||
setState(updatedState)
|
||||
}
|
||||
|
||||
return (
|
||||
<QueryRequestContext.Provider value={{state, updateState}}>
|
||||
{children}
|
||||
</QueryRequestContext.Provider>
|
||||
)
|
||||
}
|
||||
|
||||
const useQueryRequest = () => useContext(QueryRequestContext)
|
||||
export {QueryRequestProvider, useQueryRequest}
|
||||
@@ -0,0 +1,85 @@
|
||||
/* eslint-disable react-refresh/only-export-components */
|
||||
/* eslint-disable react-hooks/exhaustive-deps */
|
||||
import {FC, useContext, useState, useEffect, useMemo} from 'react'
|
||||
import {useQuery} from 'react-query'
|
||||
import {
|
||||
createResponseContext,
|
||||
initialQueryResponse,
|
||||
initialQueryState,
|
||||
PaginationState,
|
||||
QUERIES,
|
||||
stringifyRequestQuery,
|
||||
WithChildren,
|
||||
} from '../../../../../../_digifi/helpers'
|
||||
import {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,42 @@
|
||||
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: '',
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
import axios, { AxiosResponse } from "axios";
|
||||
import { ID, Response } from "../../../../../../_digifi/helpers";
|
||||
import { User, UsersQueryResponse } from "./_models";
|
||||
|
||||
const API_URL = import.meta.env.VITE_APP_THEME_API_URL;
|
||||
const USER_URL = `${API_URL}/user`;
|
||||
// const GET_USERS_URL = `${API_URL}/users/query`;
|
||||
|
||||
const NEW_USER_ENDPOINT = import.meta.env.VITE_APP_USER_ENDPOINT
|
||||
|
||||
// const getStartedUsers = (query: string): Promise<UsersQueryResponse> => {
|
||||
// return axios
|
||||
// .get(`${GET_USERS_URL}?${query}`)
|
||||
// .then((d: AxiosResponse<UsersQueryResponse>) => d.data);
|
||||
// };
|
||||
const getStartedUsers = (query: string): Promise<UsersQueryResponse> => { // FUNCTION TO GET USERS THAT HAVE STARTED LOAN APPLICATION
|
||||
return axios
|
||||
.get(`${NEW_USER_ENDPOINT}/loan/started`)
|
||||
.then((d: AxiosResponse<UsersQueryResponse>) => d.data);
|
||||
};
|
||||
|
||||
const getUserById = (id: ID): Promise<User | undefined> => {
|
||||
return axios
|
||||
.get(`${USER_URL}/${id}`)
|
||||
.then((response: AxiosResponse<Response<User>>) => response.data)
|
||||
.then((response: Response<User>) => response.data);
|
||||
};
|
||||
|
||||
const createUser = (user: User): Promise<User | undefined> => {
|
||||
return axios
|
||||
.put(USER_URL, user)
|
||||
.then((response: AxiosResponse<Response<User>>) => response.data)
|
||||
.then((response: Response<User>) => response.data);
|
||||
};
|
||||
|
||||
const updateUser = (user: User): Promise<User | undefined> => {
|
||||
return axios
|
||||
.post(`${USER_URL}/${user.id}`, user)
|
||||
.then((response: AxiosResponse<Response<User>>) => response.data)
|
||||
.then((response: Response<User>) => response.data);
|
||||
};
|
||||
|
||||
const deleteUser = (userId: ID): Promise<void> => {
|
||||
return axios.delete(`${USER_URL}/${userId}`).then(() => {});
|
||||
};
|
||||
|
||||
const deleteSelectedUsers = (userIds: Array<ID>): Promise<void> => {
|
||||
const requests = userIds.map((id) => axios.delete(`${USER_URL}/${id}`));
|
||||
return axios.all(requests).then(() => {});
|
||||
};
|
||||
|
||||
export {
|
||||
getStartedUsers,
|
||||
deleteUser,
|
||||
deleteSelectedUsers,
|
||||
getUserById,
|
||||
createUser,
|
||||
updateUser,
|
||||
};
|
||||
@@ -0,0 +1,62 @@
|
||||
import {useMemo} from 'react'
|
||||
import {useTable, ColumnInstance, Row} from 'react-table'
|
||||
import {CustomHeaderColumn} from './columns/CustomHeaderColumn'
|
||||
import {CustomRow} from './columns/CustomRow'
|
||||
import {useQueryResponseData, useQueryResponseLoading} from '../core/QueryResponseProvider'
|
||||
import {usersColumns} from './columns/_columns'
|
||||
import {User} from '../../../core/_models'
|
||||
import {UsersListLoading} from '../components/loading/UsersListLoading'
|
||||
import {UsersListPagination} from '../components/pagination/UsersListPagination'
|
||||
import {KTCardBody} from '../../../../../../_digifi/helpers'
|
||||
|
||||
const UsersTable = () => {
|
||||
const users = useQueryResponseData()
|
||||
// console.log('users', users)
|
||||
const isLoading = useQueryResponseLoading()
|
||||
const data = useMemo(() => users, [users])
|
||||
const columns = useMemo(() => usersColumns, [])
|
||||
const {getTableProps, getTableBodyProps, headers, rows, prepareRow} = useTable({
|
||||
columns,
|
||||
data,
|
||||
})
|
||||
|
||||
return (
|
||||
<KTCardBody className='py-4'>
|
||||
<div className='table-responsive'>
|
||||
<table
|
||||
id='kt_table_users'
|
||||
className='table align-middle table-row-dashed fs-6 gy-5 dataTable no-footer'
|
||||
{...getTableProps()}
|
||||
>
|
||||
<thead>
|
||||
<tr className='text-start text-muted fw-bolder fs-7 text-uppercase gs-0'>
|
||||
{headers.map((column: ColumnInstance<User>) => (
|
||||
<CustomHeaderColumn key={column.id} column={column} />
|
||||
))}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className='text-gray-600 fw-bold' {...getTableBodyProps()}>
|
||||
{rows.length > 0 ? (
|
||||
rows.map((row: Row<User>, i) => {
|
||||
prepareRow(row)
|
||||
return <CustomRow row={row} key={`row-${i}-${row.id}`} />
|
||||
})
|
||||
) : (
|
||||
<tr>
|
||||
<td colSpan={7}>
|
||||
<div className='d-flex text-center w-100 align-content-center justify-content-center'>
|
||||
No matching records found
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
)}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<UsersListPagination />
|
||||
{isLoading && <UsersListLoading />}
|
||||
</KTCardBody>
|
||||
)
|
||||
}
|
||||
|
||||
export {UsersTable}
|
||||
@@ -0,0 +1,12 @@
|
||||
import {FC} from 'react'
|
||||
import { NewDateTimeFormatter } from '../../../../../../../_digifi/lib/NewDateTimeFormatter'
|
||||
|
||||
type Props = {
|
||||
added?: string
|
||||
}
|
||||
|
||||
const AddedCell: FC<Props> = ({added}) => (
|
||||
<div className='badge badge-light fw-bolder'>{NewDateTimeFormatter((added))}</div>
|
||||
)
|
||||
|
||||
export {AddedCell}
|
||||
@@ -0,0 +1,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,42 @@
|
||||
|
||||
import clsx from 'clsx'
|
||||
import {FC} from 'react'
|
||||
import {toAbsoluteUrl} from '../../../../../../../_digifi/helpers'
|
||||
import {User} from '../../core/_models'
|
||||
|
||||
type Props = {
|
||||
user: User
|
||||
}
|
||||
|
||||
const UserInfoCell: FC<Props> = ({user}) => (
|
||||
<div className='d-flex align-items-center'>
|
||||
{/* begin:: Avatar */}
|
||||
<div className='symbol symbol-circle symbol-50px overflow-hidden me-3'>
|
||||
<a href='#'>
|
||||
{user.avatar ? (
|
||||
<div className='symbol-label'>
|
||||
<img src={toAbsoluteUrl(`media/${user.avatar}`)} alt={user.name} className='w-100' />
|
||||
</div>
|
||||
) : (
|
||||
<div
|
||||
className={clsx(
|
||||
'symbol-label fs-3',
|
||||
`bg-light-${user.initials?.state}`,
|
||||
`text-${user.initials?.state}`
|
||||
)}
|
||||
>
|
||||
{user.firstname?.substring(0,1).toUpperCase()} {user.lastname?.substring(0,1).toUpperCase()}
|
||||
</div>
|
||||
)}
|
||||
</a>
|
||||
</div>
|
||||
<div className='d-flex flex-column'>
|
||||
<a href='#' className='text-gray-800 text-hover-primary mb-1'>
|
||||
{user.firstname} {user.lastname}
|
||||
</a>
|
||||
<span>{user.email}</span>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
||||
export {UserInfoCell}
|
||||
@@ -0,0 +1,11 @@
|
||||
import {FC} from 'react'
|
||||
|
||||
type Props = {
|
||||
payment_month?: string
|
||||
}
|
||||
|
||||
const PaymentMonthCell: FC<Props> = ({payment_month}) => (
|
||||
<div className='badge badge-light fw-bolder'>{payment_month}</div>
|
||||
)
|
||||
|
||||
export {PaymentMonthCell}
|
||||
@@ -0,0 +1,26 @@
|
||||
import {FC, useMemo} from 'react'
|
||||
import {ID} from '../../../../../../../_digifi/helpers'
|
||||
import {useListView} from '../../core/ListViewProvider'
|
||||
|
||||
type Props = {
|
||||
id: ID
|
||||
}
|
||||
|
||||
const UserSelectionCell: FC<Props> = ({id}) => {
|
||||
const {selected, onSelect} = useListView()
|
||||
const isSelected = useMemo(() => selected.includes(id), [id, selected])
|
||||
return (
|
||||
<div className='form-check form-check-custom form-check-solid'>
|
||||
<input
|
||||
className='form-check-input'
|
||||
type='checkbox'
|
||||
data-kt-check={isSelected}
|
||||
data-kt-check-target='#kt_table_users .form-check-input'
|
||||
checked={isSelected}
|
||||
onChange={() => onSelect(id)}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export {UserSelectionCell}
|
||||