Compare commits

..

79 Commits

Author SHA1 Message Date
CHIEFSOFT\ameye 4c16690702 digi fi back office sne doffer 2025-01-13 20:45:43 -05:00
CHIEFSOFT\ameye 6987fbf9b4 Chnage text 2025-01-12 18:42:53 -05:00
ameye eb3d337a2e Merge branch 'action-btn-update' of DigiFi/digifi-bko into master 2025-01-12 23:40:34 +00:00
victorAnumudu ede0aa0358 updated btn 2024-10-22 05:33:10 +01:00
ameye e2ea8e18ed Merge branch 'btn-name-change' of DigiFi/digifi-bko into master 2024-09-11 15:56:56 +00:00
victorAnumudu 5e13f6225f updated button name 2024-09-10 16:48:08 +01:00
ameye 7fcf45fcf5 Merge branch 'user-list-table-update' of DigiFi/digifi-bko into master 2024-09-09 18:43:41 +00:00
victorAnumudu 9f30419367 added table list update 2024-09-09 19:10:28 +01:00
ameye 3a607071db Merge branch 'admin-list-bug-fix' of DigiFi/digifi-bko into master 2024-09-09 16:44:58 +00:00
victorAnumudu 5b1e24a299 bug fixed 2024-09-07 12:16:54 +01:00
ameye 021e80c0e9 Merge branch 'help-page' of DigiFi/digifi-bko into master 2024-09-05 18:41:21 +00:00
victorAnumudu 9b2e0a6857 help page fixed 2024-09-05 19:31:03 +01:00
ameye 724e846826 Merge branch 'rounded-radius' of DigiFi/digifi-bko into master 2024-09-05 17:26:39 +00:00
victorAnumudu 0a31a25162 added border radius 2024-09-05 17:25:51 +01:00
victor.ebuka 50e2385b50 Merge branch 'height-adjust' of DigiFi/digifi-bko into master 2024-09-04 20:17:20 +00:00
victorAnumudu 7117539fa5 adjusted height 2024-09-04 21:15:13 +01:00
ameye 8384b5fd09 Merge branch 'admin-list-api' of DigiFi/digifi-bko into master 2024-09-04 17:14:37 +00:00
victorAnumudu 2a7c25d160 admin list api added 2024-09-04 18:02:07 +01:00
ameye 237be24476 Merge branch 'dashboard-update' of DigiFi/digifi-bko into master 2024-09-03 21:02:12 +00:00
victorAnumudu cffaa9f379 dashboard update added 2024-09-03 21:57:40 +01:00
ameye 00aa5e57fa Merge branch 'customer-list' of DigiFi/digifi-bko into master 2024-07-18 00:34:30 +00:00
victorAnumudu 1d14205656 customer API added 2024-07-17 20:01:27 +01:00
ameye d98624574f Merge branch 'loan-details-display' of DigiFi/digifi-bko into master 2024-07-15 17:16:37 +00:00
victorAnumudu fa46cae1cc made all data to show on details column 2024-07-15 18:13:28 +01:00
ameye 704681c32d Merge branch 'loan-details-update' of DigiFi/digifi-bko into master 2024-07-15 17:00:34 +00:00
victorAnumudu 251fe95a6b updated loan details page 2024-07-15 17:57:01 +01:00
victorAnumudu ca7db4b0aa updated loan details page 2024-07-15 17:38:11 +01:00
ameye 5fe90c3ead Merge branch 'loan-process-page' of DigiFi/digifi-bko into master 2024-07-15 16:14:17 +00:00
victorAnumudu 827d0a1c30 added loan details API 2024-07-15 17:11:14 +01:00
victorAnumudu 0fc549f1b5 added loan details API 2024-07-15 17:09:06 +01:00
ameye ee1fe4a5b6 Merge branch 'process-page' of DigiFi/digifi-bko into master 2024-07-12 09:51:06 +00:00
victorAnumudu 4ac97537cd process page added 2024-07-12 10:22:05 +01:00
ameye fc214b4bad Merge branch 'side-menu-adjust' of DigiFi/digifi-bko into master 2024-07-11 16:52:59 +00:00
victorAnumudu 450ae649c3 side bar update 2024-07-11 17:31:34 +01:00
ameye 2eb12129bb Merge branch 'edit-signatory-update' of DigiFi/digifi-bko into master 2024-06-20 11:11:44 +00:00
victorAnumudu a26b9f5d11 edit signatory submit button renamed 2024-06-20 12:09:27 +01:00
ameye 35dcc853b8 Merge branch 'edit-signatory-modal' of DigiFi/digifi-bko into master 2024-06-19 23:16:31 +00:00
victorAnumudu 368ca22e62 edit signatory modal added 2024-06-19 20:34:49 +01:00
ameye c1c24db83d Merge branch 'create-signatory-update' of DigiFi/digifi-bko into master 2024-06-16 03:30:49 +00:00
victorAnumudu f52e06af65 employer uid added to request payload 2024-06-15 17:27:59 +01:00
ameye 697a467780 Merge branch 'employer-verify-update' of DigiFi/digifi-bko into master 2024-06-13 20:10:19 +00:00
victorAnumudu 9499422c9a employer verify application uid updated 2024-06-13 21:08:33 +01:00
ameye 148659b453 Merge branch 'edit-loan-modal' of DigiFi/digifi-bko into master 2024-06-13 18:58:52 +00:00
victorAnumudu 98454e2c80 edit loan modal added, employer verify api added 2024-06-13 19:56:17 +01:00
ameye 8a96b10e4b Merge branch 'ready-list-employer-column' of DigiFi/digifi-bko into master 2024-06-13 16:53:26 +00:00
ameye d17ca4f988 Merge branch 'edit-signatory-popout' of DigiFi/digifi-bko into master 2024-06-13 16:53:11 +00:00
victorAnumudu ea90bd6fc5 added employer column in ready table 2024-06-13 17:33:47 +01:00
victorAnumudu 4f99dacf76 edit signatory popout header changed 2024-06-11 09:19:38 +01:00
ameye c970467f16 Merge branch 'sessionTimeout' of DigiFi/digifi-bko into master 2024-06-10 16:48:26 +00:00
Elias 63090f0b74 minor changes 2024-06-07 19:04:20 +01:00
Elias afa81eacd8 session timeout after 7 minutes of inactivity 2024-06-07 17:59:03 +01:00
ameye eea90187e1 Merge branch 'add-signatory-modal' of DigiFi/digifi-bko into master 2024-06-07 13:17:04 +00:00
victorAnumudu 835cb6b074 add signatory API added 2024-06-07 14:12:37 +01:00
victorAnumudu a44c898a66 added modal 2024-06-07 10:10:52 +01:00
ameye 6f616c34cc Merge branch 'add-employer-form' of DigiFi/digifi-bko into master 2024-06-06 21:27:31 +00:00
victorAnumudu dc5aa92085 add employer API added 2024-06-06 22:19:45 +01:00
victorAnumudu 58875df9be Merge master into add-employer-form 2024-06-06 21:38:36 +01:00
victorAnumudu d8c9b59cde validated inputs to be submitted 2024-06-06 21:37:36 +01:00
victorAnumudu d53ad76e8b initial commit 2024-06-06 21:04:36 +01:00
ameye 2688e224fd Merge branch 'sidebar-menu-update' of DigiFi/digifi-bko into master 2024-06-06 17:19:24 +00:00
victorAnumudu f9e3674ece sidebar menu updated 2024-06-06 17:54:56 +01:00
ameye 3b6b0b580c Merge branch 'employer-list-api' of DigiFi/digifi-bko into master 2024-06-06 16:23:44 +00:00
victorAnumudu 50b12f2da4 added employer list and signatory API 2024-06-06 17:15:12 +01:00
ameye 19198b09ba Merge branch 'employer-list' of DigiFi/digifi-bko into master 2024-06-05 18:20:49 +00:00
victorAnumudu 22894a62d8 employers list added 2024-06-05 18:53:33 +01:00
ameye 5ad9413dc1 Merge branch 'adjust_page_items' of DigiFi/digifi-bko into master 2024-05-17 15:40:44 +00:00
ameye 7449dea29c Merge branch 'nav-bar-bug' of DigiFi/digifi-bko into master 2024-05-17 15:40:39 +00:00
Elias 886c7dd8b3 Back office loan list items:Amount/Filter 2024-05-17 01:09:38 +01:00
victorAnumudu fb6dc18073 nav bar signout hover side menu removed 2024-05-16 15:22:41 +01:00
ameye 7d955381c7 Merge branch 'redundant-pages' of DigiFi/digifi-bko into master 2024-05-16 12:26:08 +00:00
ameye 8df8fa45db Merge branch 'logout_fix' of DigiFi/digifi-bko into master 2024-05-16 12:26:01 +00:00
victorAnumudu 4e60059290 updated pending list action menu 2024-05-15 20:52:53 +01:00
victorAnumudu c6fe8c7d5a updated pending list action menu 2024-05-15 20:52:07 +01:00
victorAnumudu 5eb07b2057 removal of redundant pages 2024-05-15 20:47:29 +01:00
victorAnumudu 3a35a34266 initial commit 2024-05-15 20:22:33 +01:00
Elias de5cb74241 restored logout 2024-05-15 19:00:39 +01:00
ameye c711e000b3 Merge branch 'route-update' of DigiFi/digifi-bko into master 2024-05-15 17:01:19 +00:00
victorAnumudu 0a28d478d8 correct loan route added 2024-05-15 17:41:41 +01:00
ameye 0d9318ddd9 Merge branch 'remove_extra_backoffice_menu' of DigiFi/digifi-bko into master 2024-05-15 08:55:29 +00:00
345 changed files with 12452 additions and 2399 deletions
Binary file not shown.

After

Width:  |  Height:  |  Size: 602 KiB

+10
View File
@@ -1,5 +1,15 @@
const QUERIES = {
USERS_LIST: 'users-list',
STARTED_LIST: 'started-list',
READY_LIST: 'ready-list',
VERIFIED_LIST: 'verified-list',
PENDING_LIST: 'pending-list',
APPROVED_LIST: 'approved-list',
REJECTED_LIST: 'rejected-list',
CUSTOMERS_LIST: 'customers-list',
ADMIN_USERS_LIST: 'admin-users-list',
EMPLOYERS_LIST: 'employers-list',
SIGNATORY_LIST: 'signatory-list',
}
export {QUERIES}
+4 -1
View File
@@ -22,8 +22,11 @@ export type SearchState = {
}
export type Response<T> = {
call_return? : string | any
data?: T
records?: Array<any>
records?: T
salary_sources?: Array<any>
employer_sector?: Array<any>
payload?: {
message?: string
errors?: {
+32
View File
@@ -0,0 +1,32 @@
export const formatNumbers = (number: string | undefined): string | null => {
if(!number){
return null
}
return number.replace(/\B(?=(\d{3})+(?!\d))/g, ',');
};
// FUNCTION TO RETURN AMOUNT TO TWO DECIMAL PLACES
export const FormatAmount = (
amount = "00",
) => {
// Convert the number to a string
let numStr = String(amount);
// Split the string into integer and decimal parts
let parts = numStr.split(".");
let integerPart = parts[0] || "";
let decimalPart = parts[1] || "";
// Add thousands separators to the integer part
let formattedInteger = integerPart.replace(/\B(?=(\d{3})+(?!\d))/g, ',');
// Truncate or pad the decimal part to two decimal points
let formattedDecimal = decimalPart.slice(0, 2).padEnd(2, "0");
// Combine the formatted integer and decimal parts
let formattedNumber = '₦ ' + formattedInteger + '.' + formattedDecimal;
// return formattedNumber;
return formattedNumber;
};
@@ -60,13 +60,9 @@ const Navbar = () => {
data-kt-menu-attach="parent"
data-kt-menu-placement="bottom-end"
>
<img
src={toAbsoluteUrl('media/avatars/300-3.jpg')}
alt=""
style={{ cursor: 'auto' }}
/>
<img src={toAbsoluteUrl('media/avatars/300-3.jpg')} alt="" />
</div>
{/* <HeaderUserMenu /> */}
<HeaderUserMenu />
</div>
{config.app?.header?.default?.menu?.display && (
@@ -2,12 +2,17 @@ import {useIntl} from 'react-intl'
import {MenuItem} from './MenuItem'
import {MenuInnerWithSub} from './MenuInnerWithSub'
import {MegaMenu} from './MegaMenu'
import { useLocation } from 'react-router-dom'
export function MenuInner() {
const intl = useIntl()
const {pathname} = useLocation()
const isHelpPath = pathname == '/help'
return (
<>
<MenuItem title={intl.formatMessage({id: 'MENU.DASHBOARD'})} to='/dashboard' />
{/* <MenuItem title={intl.formatMessage({id: 'MENU.DASHBOARD'})} to='/dashboard' /> */}
<MenuItem title={`${isHelpPath ? 'Help' : 'Dashboard'}`} to={`${isHelpPath ? '/help' : '/dashboard'}`} />
{/* <MenuItem title='Layout Builder' to='/builder' /> */}
<MenuInnerWithSub
title='Crafted'
@@ -89,11 +89,7 @@ export default function RecentBVNList({
<div className="w-full h-100 d-flex gap-4 justify-content-center align-items-end">
<button
onClick={handlePrev}
className={`text-sm md:text-lg d-flex justify-content-center align-items-center border-1 transition-all duration-300 ${
currentPage == 0
? "text-slate-400 border-slate-400 dark:text-slate-400 dark:border-slate-400 pe-none"
: "text-slate-600 border-slate-600 dark:text-white dark:border-white"
}`}
className={`btn btn-primary `}
disabled={currentPage == 0}
// style={{width:'30px', height:'30px'}}
>
@@ -123,11 +119,7 @@ export default function RecentBVNList({
<button
onClick={handleNext}
className={`text-sm md:text-lg d-flex justify-content-center align-items-center border-1 transition-all duration-300 ${
currentPage + numberOfSelection >= data.length
? "text-slate-400 border-slate-400 dark:text-slate-400 dark:border-slate-400 pe-none"
: "text-slate-600 border-slate-600 dark:text-white dark:border-white"
}`}
className={`btn btn-primary`}
disabled={currentPage + numberOfSelection >= data.length}
// style={{width:'30px', height:'30px'}}
>
@@ -0,0 +1,142 @@
import { ReactNode, useEffect, useState } from "react";
import { UsersListHeader } from "../../../../app/modules/process/user-ready/components/header/UsersListHeader";
type PaginatedListProps = {
data: any,
itemsPerPage?: number,
filterItem?: string[],
tableTitle?: string,
titleClass?:string,
children: (data:any) => ReactNode;
}
export default function TestList({
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>
<UsersListHeader />
)}
{children(newData)}
{/* show prev and next button if data exist */}
{(data.length > 0 && data.length > itemsPerPage) && (
<div className="w-full h-100 d-flex gap-4 justify-content-center align-items-end">
<button
onClick={handlePrev}
className={`text-sm md:text-lg d-flex justify-content-center align-items-center border-1 transition-all duration-300 ${
currentPage == 0
? "text-slate-400 border-slate-400 dark:text-slate-400 dark:border-slate-400 pe-none"
: "text-slate-600 border-slate-600 dark:text-white dark:border-white"
}`}
disabled={currentPage == 0}
// style={{width:'30px', height:'30px'}}
>
&lt; Previous
</button>
{/* {data.length && data.map((item, index)=>{
item = item
if(index%itemsPerPage == 0 && index >= currentPage && index <= currentPage+itemsPerPage){
return (
<button
key={index}
onClick={handleNext}
className={`text-sm md:text-lg rounded-circle d-flex justify-content-center align-items-center border-1 transition-all duration-300 ${
currentPage != index
? "text-slate-400 border-slate-400 dark:text-slate-400 dark:border-slate-400"
: "text-slate-600 border-slate-600 dark:text-white dark:border-white pe-none"
}`}
disabled={currentPage != index}
style={{width:'30px', height:'30px'}}
>
{index/itemsPerPage +1}
</button>
)
}
})} */}
<button
onClick={handleNext}
className={`text-sm md:text-lg d-flex justify-content-center align-items-center border-1 transition-all duration-300 ${
currentPage + numberOfSelection >= data.length
? "text-slate-400 border-slate-400 dark:text-slate-400 dark:border-slate-400 pe-none"
: "text-slate-600 border-slate-600 dark:text-white dark:border-white"
}`}
disabled={currentPage + numberOfSelection >= data.length}
// style={{width:'30px', height:'30px'}}
>
Next &gt;
</button>
</div>
)}
</div>
);
}
@@ -10,6 +10,7 @@ type Props = {
icon?: string
fontIcon?: string
hasBullet?: boolean
menuIsOpen?: boolean
}
const SidebarMenuItemWithSub: React.FC<Props & WithChildren> = ({
@@ -19,9 +20,10 @@ const SidebarMenuItemWithSub: React.FC<Props & WithChildren> = ({
icon,
fontIcon,
hasBullet,
menuIsOpen=false
}) => {
const {pathname} = useLocation()
const isActive = checkIsActive(pathname, to)
const isActive = checkIsActive(pathname, to) || menuIsOpen
const {config} = useLayout()
const {app} = config
@@ -15,18 +15,19 @@ const SidebarMenuMain = () => {
fontIcon='bi-app-indicator'
/>
{/*<SidebarMenuItem to='/builder' icon='switch' title='Layout Builder' fontIcon='bi-layers' />*/}
<div className='menu-item'>
{/* <div className='menu-item'>
<div className='menu-content pt-8 pb-2'>
<span className='menu-section text-muted text-uppercase fs-8 ls-1'>Crafted</span>
<span className='menu-section text-muted text-uppercase fs-8 ls-1'>Loan</span>
</div>
</div>
</div> */}
<SidebarMenuItemWithSub
to='/loan/pages'
title='Process'
fontIcon='bi-archive'
icon='element-plus'
menuIsOpen={true}
>
<SidebarMenuItemWithSub to='/loan/pages/process' title='Loan' hasBullet={true}>
<SidebarMenuItemWithSub to='/loan/pages/process' title='Loan' hasBullet={true} menuIsOpen={true}>
<SidebarMenuItem to='/loan/pages/process/started' title='Started' hasBullet={true} />
<SidebarMenuItem to='/loan/pages/process/pending' title='Pending' hasBullet={true} />
<SidebarMenuItem
@@ -34,6 +35,7 @@ const SidebarMenuMain = () => {
title='Ready'
hasBullet={true}
/>
<SidebarMenuItem to='/loan/pages/process/verified' title='Verified' hasBullet={true} />
<SidebarMenuItem
to='/loan/pages/process/approved'
title='Approved'
@@ -87,7 +89,7 @@ const SidebarMenuMain = () => {
<div className='menu-item'>
<div className='menu-content pt-8 pb-2'>
<span className='menu-section text-muted text-uppercase fs-8 ls-1'>Apps</span>
<span className='menu-section text-muted text-uppercase fs-8 ls-1'>Tools</span>
</div>
</div>
@@ -103,9 +105,28 @@ const SidebarMenuMain = () => {
</SidebarMenuItemWithSub> */}
<SidebarMenuItem
to='/apps/user-management/users'
to='/tools/user-management/customers'
icon='abstract-28'
title='User management'
title='Customers'
fontIcon='bi-layers'
/>
<div className='menu-item'>
<div className='menu-content pt-8 pb-2'>
<span className='menu-section text-muted text-uppercase fs-8 ls-1'>Employers</span>
</div>
</div>
<SidebarMenuItem
to='employers/employerslist'
icon='abstract-28'
title='List'
fontIcon='bi-layers'
/>
<SidebarMenuItem
to='employers/signatory'
icon='abstract-28'
title='Signatory'
fontIcon='bi-layers'
/>
{/*<div className='menu-item'>*/}
@@ -120,6 +141,18 @@ const SidebarMenuMain = () => {
{/* <span className='menu-title'>Changelog {import.meta.env.VITE_APP_VERSION}</span>*/}
{/* </a>*/}
{/*</div>*/}
<div className='menu-item'>
<div className='menu-content pt-8 pb-2'>
<span className='menu-section text-muted text-uppercase fs-8 ls-1'>Admin</span>
</div>
</div>
<SidebarMenuItem
to='/tools/user-management/users'
icon='abstract-28'
title='Users'
fontIcon='bi-layers'
/>
</>
)
}
@@ -1,64 +1,99 @@
import {FC} from 'react'
import {Link} from 'react-router-dom'
import {useAuth} from '../../../../app/modules/auth'
import {Languages} from './Languages'
import {toAbsoluteUrl} from '../../../helpers'
import { FC, useEffect } from 'react';
import { Link } from 'react-router-dom';
import { useAuth } from '../../../../app/modules/auth';
import { Languages } from './Languages';
import { toAbsoluteUrl } from '../../../helpers';
const HeaderUserMenu: FC = () => {
const {currentUser, logout} = useAuth()
const { currentUser, logout } = useAuth();
// Listen for user activity and trigger logout
useEffect(() => {
let timeout: number;
const inactiveTime: number = 7 * 60 * 1000; //default inactive period (milliseconds)
// Logout user after inactiveTime minutes of inactivity
const resetTimeout = () => {
clearTimeout(timeout);
// Set logout timeout
timeout = window.setTimeout(() => {
logout();
}, inactiveTime);
};
const handleUserActivity: any = () => {
resetTimeout(); // reset session on user activity
};
document.addEventListener('mousemove', handleUserActivity);
document.addEventListener('keydown', handleUserActivity);
document.addEventListener('click', handleUserActivity);
document.addEventListener('focus', handleUserActivity);
// Initialize timeout
resetTimeout();
// Remove event listeners on unmount
return () => {
clearTimeout(timeout);
document.removeEventListener('mousemove', handleUserActivity);
document.removeEventListener('keydown', handleUserActivity);
document.removeEventListener('click', handleUserActivity);
document.removeEventListener('focus', handleUserActivity);
};
}, [logout]);
return (
<div
className='menu menu-sub menu-sub-dropdown menu-column menu-rounded menu-gray-600 menu-state-bg menu-state-primary fw-bold py-4 fs-6 w-275px'
data-kt-menu='true'
className="menu menu-sub menu-sub-dropdown menu-column menu-rounded menu-gray-600 menu-state-bg menu-state-primary fw-bold py-4 fs-6 w-275px"
data-kt-menu="true"
>
<div className='menu-item px-3'>
<div className='menu-content d-flex align-items-center px-3'>
<div className='symbol symbol-50px me-5'>
<img alt='Logo' src={toAbsoluteUrl('media/avatars/300-3.jpg')} />
<div className="menu-item px-3">
<div className="menu-content d-flex align-items-center px-3">
<div className="symbol symbol-50px me-5">
<img alt="Logo" src={toAbsoluteUrl('media/avatars/300-3.jpg')} />
</div>
<div className='d-flex flex-column'>
<div className='fw-bolder d-flex align-items-center fs-5'>
<div className="d-flex flex-column">
<div className="fw-bolder d-flex align-items-center fs-5">
{currentUser?.first_name} {currentUser?.first_name}
{/*<span className='badge badge-light-success fw-bolder fs-8 px-2 py-1 ms-2'>Pro</span>*/}
</div>
<a href='#' className='fw-bold text-muted text-hover-primary fs-7'>
<a href="#" className="fw-bold text-muted text-hover-primary fs-7">
{currentUser?.email}
</a>
</div>
</div>
</div>
<div className='separator my-2'></div>
{/* <div className="separator my-2"></div> */}
<div className='menu-item px-5'>
{/* <div className='menu-item px-5'>
<Link to={'/crafted/pages/profile'} className='menu-link px-5'>
My Profile
</Link>
</div>
</div> */}
<div className='menu-item px-5'>
{/* <div className='menu-item px-5'>
<a href='#' className='menu-link px-5'>
<span className='menu-text'>My Projects</span>
<span className='menu-badge'>
<span className='badge badge-light-danger badge-circle fw-bolder fs-7'>3</span>
</span>
</a>
</div>
</div> */}
<div
className='menu-item px-5'
data-kt-menu-trigger='hover'
data-kt-menu-placement='left-start'
data-kt-menu-flip='bottom'
{/* <div
className="menu-item px-5"
data-kt-menu-trigger="hover"
data-kt-menu-placement="left-start"
data-kt-menu-flip="bottom"
>
<a href='#' className='menu-link px-5'>
<span className='menu-title'>My Subscription</span>
<span className='menu-arrow'></span>
</a>
<div className='menu-sub menu-sub-dropdown w-175px py-4'>
<div className="menu-sub menu-sub-dropdown w-175px py-4">
<div className='menu-item px-3'>
<a href='#' className='menu-link px-5'>
Referrals
@@ -88,7 +123,7 @@ const HeaderUserMenu: FC = () => {
</a>
</div>
<div className='separator my-2'></div>
<div className="separator my-2"></div>
<div className='menu-item px-3'>
<div className='menu-content px-3'>
@@ -105,31 +140,31 @@ const HeaderUserMenu: FC = () => {
</div>
</div>
</div>
</div>
</div> */}
<div className='menu-item px-5'>
{/* <div className='menu-item px-5'>
<a href='#' className='menu-link px-5'>
My Statements
</a>
</div>
</div> */}
<div className='separator my-2'></div>
<div className="separator my-2"></div>
<Languages />
{/* <Languages /> */}
<div className='menu-item px-5 my-1'>
{/* <div className='menu-item px-5 my-1'>
<Link to='/crafted/account/settings' className='menu-link px-5'>
Account Settings
</Link>
</div>
</div> */}
<div className='menu-item px-5'>
<a onClick={logout} className='menu-link px-5'>
<div className="menu-item px-5">
<a onClick={logout} className="menu-link px-5">
Sign Out
</a>
</div>
</div>
)
}
);
};
export {HeaderUserMenu}
export { HeaderUserMenu };
@@ -3,12 +3,15 @@ import {FC, useEffect, useRef} from 'react'
import {KTIcon} from '../../../../helpers'
import {getCSSVariableValue} from '../../../../assets/ts/_utils'
import {useThemeMode} from '../../../layout/theme-mode/ThemeModeProvider'
import { DashDataProps } from '../../../../../app/pages/dashboard/model'
import { FormatAmount } from '../../../../helpers/formatNumbers'
type Props = {
className: string
chartSize?: number
chartLine?: number
chartRotate?: number
dashData?: DashDataProps
}
const CardsWidget17: FC<Props> = ({
@@ -16,6 +19,7 @@ const CardsWidget17: FC<Props> = ({
chartSize = 70,
chartLine = 11,
chartRotate = 145,
dashData
}) => {
const chartRef = useRef<HTMLDivElement | null>(null)
const {mode} = useThemeMode()
@@ -39,15 +43,15 @@ const CardsWidget17: FC<Props> = ({
<div className='card-header pt-5'>
<div className='card-title d-flex flex-column'>
<div className='d-flex align-items-center'>
<span className='fs-4 fw-semibold text-gray-500 me-1 align-self-start'>$</span>
{/* <span className='fs-4 fw-semibold text-gray-500 me-1 align-self-start'>$</span> */}
<span className='fs-2hx fw-bold text-gray-900 me-2 lh-1 ls-n2'>69,700</span>
<span className='fs-2hx fw-bold text-gray-900 me-2 lh-1 ls-n2'>{FormatAmount('69,700')}</span>
<span className='badge badge-light-success fs-base'>
<KTIcon iconName='arrow-up' className='fs-5 text-success ms-n1' /> 2.2%
</span>
</div>
<span className='text-gray-500 pt-1 fw-semibold fs-6'>Projects Earnings in April</span>
<span className='text-gray-500 pt-1 fw-semibold fs-6'>Application in {dashData?.loading ? 'Loading...': dashData?.data?.dash_data?.curr_month}</span>
</div>
</div>
@@ -65,21 +69,21 @@ const CardsWidget17: FC<Props> = ({
<div className='d-flex flex-column content-justify-center flex-row-fluid'>
<div className='d-flex fw-semibold align-items-center'>
<div className='bullet w-8px h-3px rounded-2 bg-success me-3'></div>
<div className='text-gray-500 flex-grow-1 me-4'>Leaf CRM</div>
<div className='fw-bolder text-gray-700 text-xxl-end'>$7,660</div>
<div className='text-gray-500 flex-grow-1 me-4'>Ready</div>
<div className='fw-bolder text-gray-700 text-xxl-end'>{dashData?.loading ? 'Loading...': FormatAmount(dashData?.data?.dash_data?.ready_loans)}</div>
</div>
<div className='d-flex fw-semibold align-items-center my-3'>
<div className='bullet w-8px h-3px rounded-2 bg-primary me-3'></div>
<div className='text-gray-500 flex-grow-1 me-4'>Mivy App</div>
<div className='fw-bolder text-gray-700 text-xxl-end'>$2,820</div>
<div className='text-gray-500 flex-grow-1 me-4'>Verified</div>
<div className='fw-bolder text-gray-700 text-xxl-end'>{dashData?.loading ? 'Loading...': FormatAmount(dashData?.data?.dash_data?.verified_loans)}</div>
</div>
<div className='d-flex fw-semibold align-items-center'>
<div
className='bullet w-8px h-3px rounded-2 me-3'
style={{backgroundColor: '#E4E6EF'}}
></div>
<div className='text-gray-500 flex-grow-1 me-4'>Others</div>
<div className=' fw-bolder text-gray-700 text-xxl-end'>$45,257</div>
<div className='text-gray-500 flex-grow-1 me-4'>Approved</div>
<div className=' fw-bolder text-gray-700 text-xxl-end'>{dashData?.loading ? 'Loading...': FormatAmount(dashData?.data?.dash_data?.approved_loans)}</div>
</div>
</div>
</div>
@@ -1,11 +1,14 @@
import { DashDataProps } from "../../../../../app/pages/dashboard/model"
type Props = {
className: string
description: string
color: string
img: string
dashData?: DashDataProps
}
const CardsWidget20 = ({className, description, color, img}: Props) => (
const CardsWidget20 = ({className, description, color, img, dashData}: Props) => (
<div
className={`card card-flush bgi-no-repeat bgi-size-contain bgi-position-x-end ${className}`}
style={{
@@ -15,7 +18,7 @@ const CardsWidget20 = ({className, description, color, img}: Props) => (
>
<div className='card-header pt-5'>
<div className='card-title d-flex flex-column'>
<span className='fs-2hx fw-bold text-white me-2 lh-1 ls-n2'>69</span>
<span className='fs-2hx fw-bold text-white me-2 lh-1 ls-n2'> {dashData?.loading ? 'Loading...': dashData?.data?.dash_data?.active_loans}</span>
<span className='text-white opacity-75 pt-1 fw-semibold fs-6'>{description}</span>
</div>
@@ -1,6 +1,7 @@
import clsx from 'clsx'
import {toAbsoluteUrl} from '../../../../helpers'
import { DashDataProps } from '../../../../../app/pages/dashboard/model'
type Props = {
className: string
@@ -9,6 +10,7 @@ type Props = {
stats: number
labelColor: string
textColor: string
dashData?: DashDataProps
}
const items: Array<{
@@ -25,18 +27,18 @@ const items: Array<{
{name: 'Barry Walter', src: toAbsoluteUrl('media/avatars/300-12.jpg')},
]
const CardsWidget7 = ({className, description, stats, labelColor, textColor}: Props) => (
const CardsWidget7 = ({className, description, stats, labelColor, textColor, dashData}: Props) => (
<div className={`card card-flush ${className}`}>
<div className='card-header pt-5'>
<div className='card-title d-flex flex-column'>
<div className='card-title d-flex flex-column'>
<span className='fs-2hx fw-bold text-gray-900 me-2 lh-1 ls-n2'>{stats}</span>
<span className='fs-2hx fw-bold text-gray-900 me-2 lh-1 ls-n2'>{dashData?.loading ? 'Loading...': dashData?.data?.dash_data?.applications}</span>
<span className='text-gray-500 pt-1 fw-semibold fs-6'>{description}</span>
</div>
</div>
</div>
<div className='card-body d-flex flex-column justify-content-end pe-0'>
<span className='fs-6 fw-bolder text-gray-800 d-block mb-2'>Todays Heroes</span>
<span className='fs-6 fw-bolder text-gray-800 d-block mb-2'>Recent Applications</span>
<div className='symbol-group symbol-hover flex-nowrap'>
{items.map((item, index) => (
<div
@@ -18,26 +18,27 @@ const EngageWidget10 = ({className}: Props) => (
<div className='mb-10'>
<div className='fs-2hx fw-bold text-gray-800 text-center mb-13'>
<span className='me-2'>
Try our all new Enviroment with
Need more help to manage the platform
<br />
<span className='position-relative d-inline-block text-danger'>
<Link
to='/crafted/pages/profile/overview'
to='/help'
// target='_blank'
className='text-danger
opacity-75-hover'
>
Pro Plan
Use our help
</Link>
<span className='position-absolute opacity-15 bottom-0 start-0 border-4 border-danger border-bottom w-100'></span>
</span>
</span>
for Free
{/* for Free */}
</div>
<div className='text-center'>
{/* <div className='text-center'>
<a href='#'>Upgrade Now</a>
</div>
</div> */}
</div>
<img
className='mx-auto h-150px h-lg-200px theme-light-show'
@@ -1,30 +1,31 @@
import {Fragment} from 'react'
import {KTIcon} from '../../../../helpers'
import { Link } from 'react-router-dom'
type Props = {
className: string
}
const rows: Array<{description: string}> = [
{description: 'Avg. Client Rating'},
{description: 'Instagram Followers'},
{description: 'Google Ads CPC'},
const rows: Array<{description: string, link: string}> = [
{description: 'Verified Loans', link: '/loan/pages/process/verified'},
{description: 'Approved Loans', link: '/loan/pages/process/approved'},
{description: 'Rejected Loans', link: '/loan/pages/process/rejected'},
]
const ListsWidget26 = ({className}: Props) => (
<div className={`card card-flush ${className}`}>
<div className='card-header pt-5'>
<h3 className='card-title text-gray-800 fw-bold'>External Links</h3>
<h3 className='card-title text-gray-800 fw-bold'>Other Links</h3>
<div className='card-toolbar'></div>
</div>
<div className='card-body pt-5'>
{rows.map((row, index) => (
<Fragment key={`lw26-rows-${index}`}>
<div className='d-flex flex-stack'>
<a href='#' className='text-primary fw-semibold fs-6 me-2'>
<Link to={row.link} className='text-primary fw-semibold fs-6 me-2'>
{row.description}
</a>
</Link>
<button
type='button'
className='btn btn-icon btn-sm h-auto btn-color-gray-500 btn-active-color-primary justify-content-end'
@@ -62,6 +62,12 @@ const ListsWidget3: React.FC<Props> = ({dashData, className}) => {
</span>
<span className='text-muted fw-semibold d-block'>{NewDateTimeFormatter(item?.added)}</span>
</div>
<div className='flex-grow-1'>
<span className='text-gray-800 fw-bold fs-6'>
Pin
</span>
<span className='text-muted fw-semibold d-block'>{item?.pin || 'dummy'}</span>
</div>
{/* end::Description */}
<span className='badge badge-light-primary fs-8 fw-bold'>status: {item?.status}</span>
</div>
@@ -108,37 +108,38 @@ const TablesWidget10: FC<Props> = ({className, dashData}) => {
<td className='text-end'>
<div className='d-flex flex-column w-100 me-2'>
<div className='d-flex flex-stack mb-2'>
<span className='text-muted me-2 fs-7 fw-semibold'>50%</span>
{/* <span className='text-muted me-2 fs-7 fw-semibold'>50%</span> */}
<span className='text-muted me-2 fs-7 fw-semibold'>{(Number(item?.status)/5)*100}%</span>
</div>
<div className='progress h-6px w-100'>
<div
className='progress-bar bg-primary'
role='progressbar'
style={{width: '50%'}}
style={{width: `${(Number(item?.status)/5)*100}%`}}
></div>
</div>
</div>
</td>
<td>
<div className='d-flex justify-content-end flex-shrink-0'>
<a
{/* <a
href='#'
className='btn btn-icon btn-bg-light btn-active-color-primary btn-sm me-1'
>
<KTIcon iconName='switch' className='fs-3' />
</a>
</a> */}
<a
href='#'
className='btn btn-icon btn-bg-light btn-active-color-primary btn-sm me-1'
>
<KTIcon iconName='pencil' className='fs-3' />
</a>
<a
{/* <a
href='#'
className='btn btn-icon btn-bg-light btn-active-color-primary btn-sm'
>
<KTIcon iconName='trash' className='fs-3' />
</a>
</a> */}
</div>
</td>
</tr>
+13 -6
View File
@@ -1,21 +1,28 @@
import {Suspense} from 'react'
import {Outlet} from 'react-router-dom'
import {Suspense, useEffect} from 'react'
import {Outlet, useLocation} from 'react-router-dom'
import {I18nProvider} from '../_digifi/i18n/i18nProvider'
import {LayoutProvider, LayoutSplashScreen} from '../_digifi/layout/core'
import {MasterInit} from '../_digifi/layout/MasterInit'
import {AuthInit} from './modules/auth'
import {ThemeModeProvider} from '../_digifi/partials'
import { CustomModalProvider } from '../context/CustomModal'
const App = () => {
const {pathname}= useLocation()
useEffect(()=>{
window.scrollTo(0,0)
},[pathname])
return (
<Suspense fallback={<LayoutSplashScreen />}>
<I18nProvider>
<LayoutProvider>
<ThemeModeProvider>
<AuthInit>
<Outlet />
<MasterInit />
</AuthInit>
<CustomModalProvider>
<AuthInit>
<Outlet />
<MasterInit />
</AuthInit>
</CustomModalProvider>
</ThemeModeProvider>
</LayoutProvider>
</I18nProvider>
@@ -1,11 +1,12 @@
import {Route, Routes, Outlet, Navigate} from 'react-router-dom'
import {Route, Routes, Outlet, Navigate } from 'react-router-dom'
import {PageLink, PageTitle} from '../../../../_digifi/layout/core'
import {UsersListWrapper} from './users-list/UsersList'
import { CustomersListWrapper } from './customers-list/UsersList'
const usersBreadcrumbs: Array<PageLink> = [
{
title: 'User Management',
path: '/apps/user-management/users',
path: '/tools/user-management/users',
isSeparator: false,
isActive: false,
},
@@ -18,6 +19,7 @@ const usersBreadcrumbs: Array<PageLink> = [
]
const UsersPage = () => {
return (
<Routes>
<Route element={<Outlet />}>
@@ -30,8 +32,17 @@ const UsersPage = () => {
</>
}
/>
<Route
path='customers'
element={
<>
<PageTitle breadcrumbs={usersBreadcrumbs}>Customers list</PageTitle>
<CustomersListWrapper />
</>
}
/>
</Route>
<Route index element={<Navigate to='/apps/user-management/users' />} />
<Route index element={<Navigate to='/tools/user-management/users' />} />
</Routes>
)
}
@@ -21,7 +21,7 @@ const UsersList = () => {
)
}
const UsersListWrapper = () => (
const CustomersListWrapper = () => (
<QueryRequestProvider>
<QueryResponseProvider>
<ListViewProvider>
@@ -34,4 +34,4 @@ const UsersListWrapper = () => (
</QueryRequestProvider>
)
export {UsersListWrapper}
export {CustomersListWrapper}
@@ -11,7 +11,7 @@ import {
stringifyRequestQuery,
WithChildren,
} from '../../../../../../_digifi/helpers'
import {getStartedUsers} from './_requests'
import {getCustomerList} from './_requests'
import {User} from './_models'
import {useQueryRequest} from './QueryRequestProvider'
@@ -32,9 +32,9 @@ const QueryResponseProvider: FC<WithChildren> = ({children}) => {
refetch,
data: response,
} = useQuery(
`${QUERIES.USERS_LIST}-${query}`,
`${QUERIES.CUSTOMERS_LIST}-${query}`,
() => {
return getStartedUsers(query)
return getCustomerList(query)
},
{cacheTime: 0, keepPreviousData: true, refetchOnWindowFocus: false}
)
@@ -29,6 +29,7 @@ export type User = {
status?: string
added?: string
updated?: string
bvn?: string
}
export type UsersQueryResponse = Response<Array<User>>
@@ -13,9 +13,15 @@ const NEW_USER_ENDPOINT = import.meta.env.VITE_APP_USER_ENDPOINT
// .get(`${GET_USERS_URL}?${query}`)
// .then((d: AxiosResponse<UsersQueryResponse>) => d.data);
// };
const getStartedUsers = (query: string): Promise<UsersQueryResponse> => { // FUNCTION TO GET USERS THAT HAVE STARTED LOAN APPLICATION
const getCustomerList = (query: string): Promise<UsersQueryResponse> => { // FUNCTION TO GET USERS THAT HAVE STARTED LOAN APPLICATION
return axios
.get(`${NEW_USER_ENDPOINT}/loan/started`)
.get(`${NEW_USER_ENDPOINT}/customers`)
.then((d: AxiosResponse<UsersQueryResponse>) => d.data);
};
const getAdminUserList = (query: string): Promise<UsersQueryResponse> => { // FUNCTION TO GET USERS THAT HAVE STARTED LOAN APPLICATION
return axios
.get(`${NEW_USER_ENDPOINT}/users`)
.then((d: AxiosResponse<UsersQueryResponse>) => d.data);
};
@@ -50,7 +56,8 @@ const deleteSelectedUsers = (userIds: Array<ID>): Promise<void> => {
};
export {
getStartedUsers,
getCustomerList,
getAdminUserList,
deleteUser,
deleteSelectedUsers,
getUserById,
@@ -0,0 +1,11 @@
import {FC} from 'react'
type Props = {
status?: string
}
const Status: FC<Props> = ({status}) => (
<> {status && <div className='badge badge-light-success fw-bolder'>{status}</div>}</>
)
export {Status}
@@ -40,13 +40,13 @@ const UserActionsCell: FC<Props> = ({id}) => {
data-kt-menu-trigger='click'
data-kt-menu-placement='bottom-end'
>
Actions
<KTIcon iconName='down' className='fs-5 m-0' />
View
{/* <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'
data-kt-menu='false'
>
{/* begin::Menu item */}
<div className='menu-item px-3'>
@@ -0,0 +1,57 @@
import {Column} from 'react-table'
import {UserInfoCell} from './UserInfoCell'
import { PaymentMonthCell } from './UserLastLoginCell'
import {Status} from './Status'
import {UserActionsCell} from './UserActionsCell'
import {UserSelectionCell} from './UserSelectionCell'
import {UserCustomHeader} from './UserCustomHeader'
import {UserSelectionHeader} from './UserSelectionHeader'
import {User} from '../../core/_models'
import { AddedCell } from './AddedCell'
const usersColumns: ReadonlyArray<Column<User>> = [
{
Header: (props) => <UserSelectionHeader tableProps={props} />,
id: 'selection',
Cell: ({...props}) => <UserSelectionCell id={props.data[props.row.index].uid} />,
},
{
Header: (props) => <UserCustomHeader tableProps={props} title='Name' className='min-w-125px' />,
id: 'firstname',
Cell: ({...props}) => <UserInfoCell user={props.data[props.row.index]} />,
},
{
Header: (props) => <UserCustomHeader tableProps={props} title='BVN' className='min-w-125px' />,
accessor: 'bvn',
},
// {
// Header: (props) => (
// <UserCustomHeader tableProps={props} title='Payment Terms' className='min-w-125px' />
// ),
// id: 'payment_month',
// Cell: ({...props}) => <PaymentMonthCell payment_month={props.data[props.row.index].payment_month} />,
// },
{
Header: (props) => (
<UserCustomHeader tableProps={props} title='Status' className='min-w-125px' />
),
id: 'status',
Cell: ({...props}) => <Status status={props.data[props.row.index].status} />,
},
{
Header: (props) => (
<UserCustomHeader tableProps={props} title='Added' className='min-w-125px' />
),
id: 'added',
Cell: ({...props}) => <AddedCell added={props.data[props.row.index].added} />,
},
{
Header: (props) => (
<UserCustomHeader tableProps={props} title='Actions' className='text-end min-w-100px' />
),
id: 'actions',
Cell: ({...props}) => <UserActionsCell id={props.data[props.row.index].uid} />,
},
]
export {usersColumns}
@@ -11,7 +11,7 @@ import {
stringifyRequestQuery,
WithChildren,
} from '../../../../../../_digifi/helpers'
import {getStartedUsers} from './_requests'
import {getAdminUserList} from './_requests'
import {User} from './_models'
import {useQueryRequest} from './QueryRequestProvider'
@@ -32,9 +32,9 @@ const QueryResponseProvider: FC<WithChildren> = ({children}) => {
refetch,
data: response,
} = useQuery(
`${QUERIES.USERS_LIST}-${query}`,
`${QUERIES.ADMIN_USERS_LIST}-${query}`,
() => {
return getStartedUsers(query)
return getAdminUserList(query)
},
{cacheTime: 0, keepPreviousData: true, refetchOnWindowFocus: false}
)
@@ -29,6 +29,8 @@ export type User = {
status?: string
added?: string
updated?: string
bvn?: string
username?: string
}
export type UsersQueryResponse = Response<Array<User>>
@@ -13,9 +13,15 @@ const NEW_USER_ENDPOINT = import.meta.env.VITE_APP_USER_ENDPOINT
// .get(`${GET_USERS_URL}?${query}`)
// .then((d: AxiosResponse<UsersQueryResponse>) => d.data);
// };
const getStartedUsers = (query: string): Promise<UsersQueryResponse> => { // FUNCTION TO GET USERS THAT HAVE STARTED LOAN APPLICATION
const getCustomerList = (query: string): Promise<UsersQueryResponse> => { // FUNCTION TO GET USERS THAT HAVE STARTED LOAN APPLICATION
return axios
.get(`${NEW_USER_ENDPOINT}/loan/started`)
.get(`${NEW_USER_ENDPOINT}/customers`)
.then((d: AxiosResponse<UsersQueryResponse>) => d.data);
};
const getAdminUserList = (query: string): Promise<UsersQueryResponse> => { // FUNCTION TO GET USERS THAT HAVE STARTED LOAN APPLICATION
return axios
.get(`${NEW_USER_ENDPOINT}/users`)
.then((d: AxiosResponse<UsersQueryResponse>) => d.data);
};
@@ -50,7 +56,8 @@ const deleteSelectedUsers = (userIds: Array<ID>): Promise<void> => {
};
export {
getStartedUsers,
getCustomerList,
getAdminUserList,
deleteUser,
deleteSelectedUsers,
getUserById,
@@ -0,0 +1,11 @@
import {FC} from 'react'
type Props = {
status?: string
}
const Status: FC<Props> = ({status}) => (
<> {status && <div className='badge badge-light-success fw-bolder'>{status}</div>}</>
)
export {Status}
@@ -34,7 +34,7 @@ const UserInfoCell: FC<Props> = ({user}) => (
<a href='#' className='text-gray-800 text-hover-primary mb-1'>
{user.firstname} {user.lastname}
</a>
<span>{user.email}</span>
<span>{user.username}</span>
</div>
</div>
)
@@ -1,7 +1,7 @@
import {Column} from 'react-table'
import {UserInfoCell} from './UserInfoCell'
import { PaymentMonthCell } from './UserLastLoginCell'
import {AgentCell} from './AgentCell'
import {Status} from './Status'
import {UserActionsCell} from './UserActionsCell'
import {UserSelectionCell} from './UserSelectionCell'
import {UserCustomHeader} from './UserCustomHeader'
@@ -20,23 +20,12 @@ const usersColumns: ReadonlyArray<Column<User>> = [
id: 'firstname',
Cell: ({...props}) => <UserInfoCell user={props.data[props.row.index]} />,
},
{
Header: (props) => <UserCustomHeader tableProps={props} title='Amount' className='min-w-125px' />,
accessor: 'loan_amount',
},
{
Header: (props) => (
<UserCustomHeader tableProps={props} title='Payment Terms' className='min-w-125px' />
<UserCustomHeader tableProps={props} title='Status' className='min-w-125px' />
),
id: 'payment_month',
Cell: ({...props}) => <PaymentMonthCell payment_month={props.data[props.row.index].payment_month} />,
},
{
Header: (props) => (
<UserCustomHeader tableProps={props} title='Agent' className='min-w-125px' />
),
id: 'sales_agent',
Cell: ({...props}) => <AgentCell agent={props.data[props.row.index].sales_agent} />,
id: 'status',
Cell: ({...props}) => <Status status={props.data[props.row.index].status} />,
},
{
Header: (props) => (
+23 -54
View File
@@ -16,70 +16,39 @@ const AuthLayout = () => {
}, []);
return (
<div className="d-flex flex-column flex-lg-row flex-column-fluid h-100">
<div className="d-flex flex-column flex-lg-row flex-column-fluid"
style={{backgroundImage: 'url(../../../../public/media/auth/digifi_bko_home.jpg)', backgroundRepeat: 'none', backgroundSize: 'cover'}}
>
{/* begin::Body */}
<div className="d-flex flex-column flex-lg-row-fluid w-lg-50 p-10 order-2 order-lg-1">
<div className="h-100 flex flex-column align-items-center w-lg-50 p-10">
{/* begin::Form */}
<div className="d-flex flex-center flex-column flex-lg-row-fluid">
<div
// className="d-flex flex-center flex-column flex-lg-row-fluid"
className="d-flex h-100 align-items-center"
>
{/* begin::Wrapper */}
<div className="w-lg-500px p-10">
<div className="w-lg-500px p-10 bg-white shadow-sm rounded">
{/* begin::Title */}
<h1 className="text-black fs-2qx fw-bolder text-center mb-7">
{/* begin::Logo */}
<Link to="/" className="mb-12">
<img
alt="Logo"
src={toAbsoluteUrl("media/logos/custom-1.png")}
className="h-75px"
/>
</Link>
{/* end::Logo */}
BackOffice
</h1>
{/* end::Title */}
<Outlet />
</div>
{/* end::Wrapper */}
</div>
{/* end::Form */}
{/* begin::Footer */}
<div className="d-flex flex-center flex-wrap px-5">
{/* begin::Links */}
<div className="d-flex fw-semibold text-primary fs-base">
<a href="#" className="px-5" target="_blank">
Terms
</a>
<a href="#" className="px-5" target="_blank">
Contact Us
</a>
</div>
{/* end::Links */}
</div>
{/* end::Footer */}
</div>
{/* end::Body */}
{/* begin::Aside */}
<div
className="d-flex flex-lg-row-fluid w-lg-50 bgi-size-cover bgi-position-center order-1 order-lg-2"
>
{/* begin::Content */}
<div className="d-flex flex-column flex-center py-15 px-5 px-md-15 w-100">
{/* begin::Logo */}
<Link to="/" className="mb-12">
<img
alt="Logo"
src={toAbsoluteUrl("media/logos/custom-1.png")}
className="h-75px"
/>
</Link>
{/* end::Logo */}
{/* begin::Image */}
<img
className="mx-auto w-275px w-md-50 w-xl-500px mb-10 mb-lg-20"
src={toAbsoluteUrl("media/misc/agents-auth-screens.png")}
alt=""
/>
{/* end::Image */}
{/* begin::Title */}
<h1 className="text-black fs-2qx fw-bolder text-center mb-7">
digiFi BackOffice
</h1>
{/* end::Title */}
</div>
{/* end::Content */}
</div>
{/* end::Aside */}
</div>
);
};
@@ -47,6 +47,53 @@ export function postAuxEnd(uri:string, reqData:any):Promise<any> {
});
}
export function patchAuxEnd(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.patch(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;
@@ -0,0 +1,49 @@
import {Route, Routes, Outlet, Navigate} from 'react-router-dom'
import {PageLink, PageTitle} from '../../../../_digifi/layout/core'
import {UsersListWrapper} from './users-list/UsersList'
import {UsersListWrapper as SignatoryList} from './signatory-list/UsersList'
const usersBreadcrumbs: Array<PageLink> = [
{
title: 'Employer Management',
path: '/employers/employerslist',
isSeparator: false,
isActive: false,
},
{
title: '',
path: '',
isSeparator: true,
isActive: false,
},
]
const EmployersPage = () => {
return (
<Routes>
<Route element={<Outlet />}>
<Route
path='employerslist'
element={
<>
<PageTitle breadcrumbs={usersBreadcrumbs}>Employers list</PageTitle>
<UsersListWrapper />
</>
}
/>
<Route
path='signatory'
element={
<>
<PageTitle breadcrumbs={usersBreadcrumbs}>Signatory list</PageTitle>
<SignatoryList />
</>
}
/>
</Route>
<Route index element={<Navigate to='/employers/employerslist' />} />
</Routes>
)
}
export default EmployersPage
@@ -1,15 +1,20 @@
import {ListViewProvider, useListView} from './core/ListViewProvider'
import {QueryRequestProvider} from './core/QueryRequestProvider'
import {QueryResponseProvider} from './core/QueryResponseProvider'
import {QueryResponseProvider, useAllResponse} 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'
import { useCustomModal } from '../../../../../context/CustomModal'
import { Modal } from './edit-signatory-modal/Modal'
const UsersList = () => {
const response = useAllResponse()
// console.log('RESPONSE', response)
const {itemIdForUpdate} = useListView()
const {MODALNAMES, showCustomModal} = useCustomModal()
return (
<>
<KTCard>
@@ -17,6 +22,7 @@ const UsersList = () => {
<UsersTable />
</KTCard>
{itemIdForUpdate !== undefined && <UserEditModal />}
{(showCustomModal && showCustomModal.name == MODALNAMES.editSignatory) && <Modal />}
</>
)
}
@@ -0,0 +1,44 @@
import {useEffect} from 'react'
import {AddSignatoryModalHeader} from './AddSignatoryModalHeader'
import {AddSignatoryModalFormWrapper} from './AddSignatoryModalFormWrapper'
const AddSignatoryModal = () => {
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'>
<AddSignatoryModalHeader />
{/* begin::Modal body */}
<div className='modal-body scroll-y mx-5 mx-xl-15 my-7'>
<AddSignatoryModalFormWrapper />
</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 {AddSignatoryModal}
@@ -26,7 +26,7 @@ const editUserSchema = Yup.object().shape({
.required('Name is required'),
})
const UserEditModalForm: FC<Props> = ({user, isUserLoading}) => {
const AddSignatoryModalForm: FC<Props> = ({user, isUserLoading}) => {
const {setItemIdForUpdate} = useListView()
const {refetch} = useQueryResponse()
@@ -404,4 +404,4 @@ const UserEditModalForm: FC<Props> = ({user, isUserLoading}) => {
)
}
export {UserEditModalForm}
export {AddSignatoryModalForm}
@@ -0,0 +1,40 @@
import {useQuery} from 'react-query'
import {AddSignatoryModalForm} from './AddSignatoryModalForm'
import {isNotEmpty, QUERIES} from '../../../../../../_digifi/helpers'
import {useListView} from '../core/ListViewProvider'
import {getUserById} from '../core/_requests'
const AddSignatoryModalFormWrapper = () => {
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 <AddSignatoryModalForm isUserLoading={isLoading} user={{id: undefined}} />
}
if (!isLoading && !error && user) {
return <AddSignatoryModalForm isUserLoading={isLoading} user={user} />
}
return null
}
export {AddSignatoryModalFormWrapper}
@@ -1,7 +1,7 @@
import {KTIcon} from '../../../../../../_digifi/helpers'
import {useListView} from '../core/ListViewProvider'
const UserEditModalHeader = () => {
const AddSignatoryModalHeader = () => {
const {setItemIdForUpdate} = useListView()
return (
@@ -24,4 +24,4 @@ const UserEditModalHeader = () => {
)
}
export {UserEditModalHeader}
export {AddSignatoryModalHeader}
@@ -10,9 +10,7 @@ const UsersListToolbar = () => {
return (
<div className='d-flex justify-content-end' data-kt-user-table-toolbar='base'>
<UsersListFilter />
{/* begin::Export */}
{/* begin::Export */}
{/* <button type='button' className='btn btn-light-primary me-3'>
<KTIcon iconName='exit-up' className='fs-2' />
Export
@@ -20,11 +18,13 @@ const UsersListToolbar = () => {
{/* end::Export */}
{/* begin::Add user */}
{/* <button type='button' className='btn btn-primary' onClick={openAddUserModal}>
{/* <button type='button' className='btn btn-primary me-3' onClick={openAddUserModal}>
<KTIcon iconName='plus' className='fs-2' />
Add User
Add New
</button> */}
{/* end::Add user */}
<UsersListFilter />
</div>
)
}
@@ -11,7 +11,7 @@ import {
stringifyRequestQuery,
WithChildren,
} from '../../../../../../_digifi/helpers'
import {getStartedUsers} from './_requests'
import {getSignatoryList} from './_requests'
import {User} from './_models'
import {useQueryRequest} from './QueryRequestProvider'
@@ -32,9 +32,9 @@ const QueryResponseProvider: FC<WithChildren> = ({children}) => {
refetch,
data: response,
} = useQuery(
`${QUERIES.USERS_LIST}-${query}`,
`${QUERIES.SIGNATORY_LIST}-${query}`,
() => {
return getStartedUsers(query)
return getSignatoryList(query)
},
{cacheTime: 0, keepPreviousData: true, refetchOnWindowFocus: false}
)
@@ -53,10 +53,17 @@ const useQueryResponseData = () => {
if (!response) {
return []
}
return response?.records || []
}
const useAllResponse = () => {
const {response} = useQueryResponse()
if (!response) {
return []
}
return response || []
}
const useQueryResponsePagination = () => {
const defaultPaginationState: PaginationState = {
links: [],
@@ -82,4 +89,5 @@ export {
useQueryResponseData,
useQueryResponsePagination,
useQueryResponseLoading,
useAllResponse
}
@@ -0,0 +1,36 @@
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
// }
uid?: string
added?: string
updated?: string
email?: string
employer_name?: string
title?: string
phone?: string
employer_id?: string
signatory_uid?: string
}
export type UsersQueryResponse = Response<Array<User>>
export const initialUser: User = {
avatar: 'avatars/300-6.jpg',
// position: 'Art Director',
// role: 'Administrator',
name: '',
email: '',
}
@@ -1,6 +1,7 @@
import axios, { AxiosResponse } from "axios";
import { ID, Response } from "../../../../../../_digifi/helpers";
import { User, UsersQueryResponse } from "./_models";
import { patchAuxEnd } from "../../../../auth/core/AxiosCallHelper";
const API_URL = import.meta.env.VITE_APP_THEME_API_URL;
const USER_URL = `${API_URL}/user`;
@@ -13,12 +14,16 @@ const NEW_USER_ENDPOINT = import.meta.env.VITE_APP_USER_ENDPOINT
// .get(`${GET_USERS_URL}?${query}`)
// .then((d: AxiosResponse<UsersQueryResponse>) => d.data);
// };
const getStartedUsers = (query: string): Promise<UsersQueryResponse> => { // FUNCTION TO GET USERS THAT HAVE STARTED LOAN APPLICATION
const getSignatoryList = (query: string): Promise<UsersQueryResponse> => { // FUNCTION TO GET EMPLOYERS SIGNATORY LIST
return axios
.get(`${NEW_USER_ENDPOINT}/loan/started`)
.get(`${NEW_USER_ENDPOINT}/employers/signatory`)
.then((d: AxiosResponse<UsersQueryResponse>) => d.data);
};
const updateUser = (user: User): Promise<User | undefined> => { // FUNCTION TO UPDATE EMPLOYERS SIGNATORY
return patchAuxEnd('/employers/signatory', user)
};
const getUserById = (id: ID): Promise<User | undefined> => {
return axios
.get(`${USER_URL}/${id}`)
@@ -33,12 +38,6 @@ const createUser = (user: User): Promise<User | undefined> => {
.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(() => {});
@@ -50,7 +49,7 @@ const deleteSelectedUsers = (userIds: Array<ID>): Promise<void> => {
};
export {
getStartedUsers,
getSignatoryList,
deleteUser,
deleteSelectedUsers,
getUserById,
@@ -0,0 +1,46 @@
import {useEffect} from 'react'
import { ModalHeader } from './ModalHeader'
import { ModalFormWrapper } from './ModalFormWrapper'
const Modal = () => {
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'>
<ModalHeader />
{/* begin::Modal body */}
<div className='modal-body scroll-y mx-5 mx-xl-15 my-7'>
<ModalFormWrapper />
</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 {Modal}
@@ -0,0 +1,336 @@
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 { initialUser, User } from "../../signatory-list/core/_models";
import clsx from "clsx";
import { useListView } from "../core/ListViewProvider";
import { UsersListLoading } from "../components/loading/UsersListLoading";
import { updateUser } from "../core/_requests";
import { useQueryResponse } from "../core/QueryResponseProvider";
import { useCustomModal } from "../../../../../../context/CustomModal";
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('required'),
name: Yup.string()
.min(3, "Minimum 3 symbols")
.max(50, "Maximum 50 symbols")
.required("required"),
phone: Yup.string()
.min(11, "Minimum 11 symbols")
.max(11, "Maximum 11 symbols")
.required("required"),
title: Yup.string()
.min(2, "Minimum 2 symbols")
.max(20, "Maximum 20 symbols")
.required("required"),
});
const ModalForm: FC<Props> = ({ user, isUserLoading }) => {
const {closeCustomModal, showCustomModal} = useCustomModal()
const { setItemIdForUpdate } = useListView();
const { refetch, isLoading } = 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);
closeCustomModal()
};
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 }) => {
let reqData = {
employer_id: values.employer_id,
signatory_uid: values.uid,
name: values.name,
email: values.email,
title: values.title,
phone: values.phone
}
setSubmitting(true);
try {
await updateUser({...reqData});
} 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'>
<label className='d-block fw-bold fs-6 mb-5'>Avatar</label>
<div
className='image-input image-input-outline'
data-kt-image-input='true'
style={{backgroundImage: `url('${blankImg}')`}}
>
<div
className='image-input-wrapper w-125px h-125px'
style={{backgroundImage: `url('${userAvatarImg}')`}}
></div>
<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>
<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>
<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>
</div>
<div className='form-text'>Allowed file types: png, jpg, jpeg.</div>
</div> */}
{/* end::Input group */}
{/* begin::Input group */}
<div className="fv-row mb-7">
{/* begin::Label */}
<label className="required fw-bold fs-6 mb-2">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">Title</label>
{/* end::Label */}
{/* begin::Input */}
<input
placeholder="title"
{...formik.getFieldProps("title")}
className={clsx(
"form-control form-control-solid mb-3 mb-lg-0",
{ "is-invalid": formik.touched.title && formik.errors.title },
{
"is-valid": formik.touched.title && !formik.errors.title,
}
)}
type="text"
name="title"
autoComplete="off"
disabled={formik.isSubmitting || isUserLoading}
/>
{/* end::Input */}
{formik.touched.title && formik.errors.title && (
// <div className="fv-plugins-message-container">
// <span role="alert">{formik.errors.title}</span>
// </div>
<div className="fv-plugins-message-container">
<div className="fv-help-block">
<span role="alert">{formik.errors.title}</span>
</div>
</div>
)}
</div>
{/* end::Input group */}
{/* begin::Input group */}
<div className="fv-row mb-7">
{/* begin::Label */}
<label className="required fw-bold fs-6 mb-2">Phone</label>
{/* end::Label */}
{/* begin::Input */}
<input
placeholder="Phone"
{...formik.getFieldProps("phone")}
className={clsx(
"form-control form-control-solid mb-3 mb-lg-0",
{ "is-invalid": formik.touched.title && formik.errors.title },
{
"is-valid": formik.touched.title && !formik.errors.title,
}
)}
type="text"
name="phone"
autoComplete="off"
disabled={formik.isSubmitting || isUserLoading}
/>
{/* end::Input */}
{formik.touched.phone && formik.errors.phone && (
<div className="fv-plugins-message-container">
<div className="fv-help-block">
<span role="alert">{formik.errors.phone}</span>
</div>
</div>
)}
</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">
<div className="fv-help-block">
<span role="alert">{formik.errors.email}</span>
</div>
</div>
)}
</div>
{/* end::Input group */}
</div>
{/* end::Scroll */}
{/* begin::Actions */}
<div className="text-center pt-15">
<button
type="reset"
onClick={() => cancel()}
className="btn btn-danger me-3"
data-kt-users-modal-action="cancel"
disabled={formik.isSubmitting || isUserLoading}
>
Cancel
</button>
<button
type="submit"
className="btn btn-primary"
data-kt-users-modal-action="submit"
disabled={
isUserLoading ||
formik.isSubmitting ||
!formik.isValid ||
!formik.touched
}
>
<span className="indicator-label">Update</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 { ModalForm };
@@ -0,0 +1,36 @@
import {useQuery} from 'react-query'
import {ModalForm} from './ModalForm'
import {isNotEmpty, QUERIES} from '../../../../../../_digifi/helpers'
import {useListView} from '../core/ListViewProvider'
import {getUserById} from '../core/_requests'
import { useCustomModal } from '../../../../../../context/CustomModal'
const ModalFormWrapper = () => {
const {itemIdForUpdate, setItemIdForUpdate} = useListView()
const enabledQuery: boolean = isNotEmpty(itemIdForUpdate)
const {showCustomModal} = useCustomModal()
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)
},
}
)
return <ModalForm isUserLoading={isLoading} user={{...showCustomModal.data}} />
}
export {ModalFormWrapper}
@@ -0,0 +1,34 @@
import {KTIcon} from '../../../../../../_digifi/helpers'
import {useListView} from '../core/ListViewProvider'
import { useCustomModal } from '../../../../../../context/CustomModal'
const ModalHeader = () => {
const {setItemIdForUpdate, itemIdForUpdate} = useListView()
const {closeCustomModal, showCustomModal} = useCustomModal()
const onClose = () => {
setItemIdForUpdate(undefined)
closeCustomModal()
}
return (
<div className='modal-header'>
{/* begin::Modal title */}
<h2 className='fw-bolder'>Edit Signatory - {showCustomModal?.data?.employer_name}</h2>
{/* end::Modal title */}
{/* begin::Close */}
<div
className='btn btn-icon btn-sm btn-active-icon-primary'
data-kt-users-modal-action='close'
onClick={onClose}
style={{cursor: 'pointer'}}
>
<KTIcon iconName='cross' className='fs-1' />
</div>
{/* end::Close */}
</div>
)
}
export {ModalHeader}
@@ -0,0 +1,11 @@
import {FC} from 'react'
type Props = {
phone?: string
}
const Phone: FC<Props> = ({phone}) => (
<div className='badge badge-light fw-bolder'>{phone}</div>
)
export {Phone}
@@ -7,14 +7,22 @@ import {useListView} from '../../core/ListViewProvider'
import {useQueryResponse} from '../../core/QueryResponseProvider'
import {deleteUser} from '../../core/_requests'
import { useCustomModal } from '../../../../../../../context/CustomModal'
import { User } from '../../core/_models'
type Props = {
id: ID
data: Array<User> | any
}
const UserActionsCell: FC<Props> = ({id}) => {
const UserActionsCell: FC<Props> = ({id, data}) => {
const {setItemIdForUpdate} = useListView()
const {query} = useQueryResponse()
const queryClient = useQueryClient()
const {MODALNAMES, openCustomModal} = useCustomModal()
const selectedUser:User = data.filter((item:User) => item?.uid == id)[0]
useEffect(() => {
MenuComponent.reinitialization()
@@ -50,12 +58,20 @@ const UserActionsCell: FC<Props> = ({id}) => {
>
{/* begin::Menu item */}
<div className='menu-item px-3'>
<a className='menu-link px-3' onClick={openEditModal}>
<a className='menu-link px-3' onClick={()=>{openCustomModal(MODALNAMES.editSignatory, {...selectedUser})}}>
Edit
</a>
</div>
{/* end::Menu item */}
{/* begin::Menu item */}
{/* <div className='menu-item px-3'>
<a className='menu-link px-3' onClick={openEditModal}>
Add Signatory
</a>
</div> */}
{/* end::Menu item */}
{/* begin::Menu item */}
<div className='menu-item px-3'>
<a
@@ -25,14 +25,14 @@ const UserInfoCell: FC<Props> = ({user}) => (
`text-${user.initials?.state}`
)}
>
{user.firstname?.substring(0,1).toUpperCase()} {user.lastname?.substring(0,1).toUpperCase()}
{user.name?.substring(0,1).toUpperCase()} {user.name?.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}
{user.name}
</a>
<span>{user.email}</span>
</div>
@@ -0,0 +1,58 @@
import {Column} from 'react-table'
import {UserInfoCell} from './UserInfoCell'
import { PaymentMonthCell } from './UserLastLoginCell'
import {AgentCell} from './AgentCell'
import {UserActionsCell} from './UserActionsCell'
import {UserSelectionCell} from './UserSelectionCell'
import {UserCustomHeader} from './UserCustomHeader'
import {UserSelectionHeader} from './UserSelectionHeader'
import {User} from '../../core/_models'
import { AddedCell } from './AddedCell'
import { Phone } from './Phone'
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: 'name',
Cell: ({...props}) => <UserInfoCell user={props.data[props.row.index]} />,
},
// {
// Header: (props) => <UserCustomHeader tableProps={props} title='Max Loan' className='min-w-125px' />,
// accessor: 'max_loan',
// },
{
Header: (props) => (
<UserCustomHeader tableProps={props} title='Phone' className='min-w-125px' />
),
id: 'phone',
Cell: ({...props}) => <Phone phone={props.data[props.row.index].phone} />,
},
{
Header: (props) => (
<UserCustomHeader tableProps={props} title='Employer Name' className='min-w-125px' />
),
id: 'employer_name',
Cell: ({...props}) => <PaymentMonthCell payment_month={props.data[props.row.index].employer_name} />,
},
{
Header: (props) => (
<UserCustomHeader tableProps={props} title='Added' className='min-w-125px' />
),
id: 'added',
Cell: ({...props}) => <AddedCell added={props.data[props.row.index].added} />,
},
{
Header: (props) => (
<UserCustomHeader tableProps={props} title='Actions' className='text-end min-w-100px' />
),
id: 'actions',
Cell: ({...props}) => <UserActionsCell id={props.data[props.row.index].uid} data={props.data} />,
},
]
export {usersColumns}
@@ -7,7 +7,7 @@ const UserEditModalHeader = () => {
return (
<div className='modal-header'>
{/* begin::Modal title */}
<h2 className='fw-bolder'>Add User</h2>
<h2 className='fw-bolder'>Edit Signatory</h2>
{/* end::Modal title */}
{/* begin::Close */}
@@ -3,20 +3,27 @@ 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 {AddEmployerModal} from './add-employer-modal/AddEmployerModal'
import {KTCard} from '../../../../../_digifi/helpers'
import { ToolbarWrapper } from '../../../../../_digifi/layout/components/toolbar'
import { Content } from '../../../../../_digifi/layout/components/content'
import { AddSignatoryModal } from './add-signatory-modal/Modal'
import { useCustomModal } from '../../../../../context/CustomModal'
import { EmployerEditModal } from './employer-edit-modal/EmployerEditModal'
const UsersList = () => {
const {itemIdForUpdate} = useListView()
const {MODALNAMES, showCustomModal} = useCustomModal()
return (
<>
<KTCard>
<UsersListHeader />
<UsersTable />
</KTCard>
{itemIdForUpdate !== undefined && <UserEditModal />}
{itemIdForUpdate !== undefined && <AddEmployerModal />}
{(showCustomModal && showCustomModal.name == MODALNAMES.addSignatory) && <AddSignatoryModal />}
{(showCustomModal && showCustomModal.name == MODALNAMES.editEmployer) && <EmployerEditModal />}
</>
)
}
@@ -0,0 +1,46 @@
import {useEffect} from 'react'
import {ModalHeader} from './ModalHeader'
import {ModalFormWrapper} from './ModalFormWrapper'
const AddEmployerModal = () => {
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'>
<ModalHeader />
{/* begin::Modal body */}
<div className='modal-body scroll-y mx-5 mx-xl-15 my-7'>
<ModalFormWrapper />
</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 {AddEmployerModal}
@@ -0,0 +1,465 @@
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 { useAllResponse, useQueryResponse } from "../core/QueryResponseProvider";
type Props = {
isUserLoading: boolean;
user: User;
};
type SelectProps = {
uid?: string
name?: string
};
const editUserSchema = Yup.object().shape({
// email: Yup.string()
// .email('Wrong email format')
// .min(3, 'Minimum 3 symbols')
// .max(50, 'Maximum 50 symbols')
// .required('required'),
name: Yup.string()
.min(3, "Minimum 3 symbols")
.max(50, "Maximum 50 symbols")
.required("required"),
percent_interest: Yup.number()
.typeError("Invalid number")
.min(1, "must be greater than 0")
// .test("no-e", "Invalid number", (value) => {
// if (value && /\d+e/.test(value)) {
// return false;
// }
// return true;
// })
.required("required"),
max_loan: Yup.number()
.typeError("Invalid number")
.min(1, "must be greater than 0")
.required("required"),
tenor: Yup.number()
.typeError("Invalid number")
.min(1, "must be greater than 0")
.required("required"),
retirement_age: Yup.number()
.typeError("Invalid number")
.min(1, "must be greater than 0")
.required("is required"),
sector: Yup.string()
.required("required"),
salary_source: Yup.string()
.required("required"),
});
const ModalForm: FC<Props> = ({ user, isUserLoading }) => {
const response:any = useAllResponse()
const { setItemIdForUpdate } = useListView();
const { refetch, isLoading } = 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'>
<label className='d-block fw-bold fs-6 mb-5'>Avatar</label>
<div
className='image-input image-input-outline'
data-kt-image-input='true'
style={{backgroundImage: `url('${blankImg}')`}}
>
<div
className='image-input-wrapper w-125px h-125px'
style={{backgroundImage: `url('${userAvatarImg}')`}}
></div>
<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>
<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>
<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>
</div>
<div className='form-text'>Allowed file types: png, jpg, jpeg.</div>
</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">Interest</label>
{/* end::Label */}
{/* begin::Input */}
<input
placeholder="Interest"
{...formik.getFieldProps("percent_interest")}
className={clsx(
"form-control form-control-solid mb-3 mb-lg-0",
{ "is-invalid": formik.touched.percent_interest && formik.errors.percent_interest },
{
"is-valid": formik.touched.percent_interest && !formik.errors.percent_interest,
}
)}
type="text"
name="percent_interest"
autoComplete="off"
disabled={formik.isSubmitting || isUserLoading}
/>
{/* end::Input */}
{formik.touched.percent_interest && formik.errors.percent_interest && (
// <div className="fv-plugins-message-container">
// <span role="alert">{formik.errors.percent_interest}</span>
// </div>
<div className="fv-plugins-message-container">
<div className="fv-help-block">
<span role="alert">{formik.errors.percent_interest}</span>
</div>
</div>
)}
</div>
{/* end::Input group */}
{/* begin::Input group */}
<div className="fv-row mb-7">
{/* begin::Label */}
<label className="required fw-bold fs-6 mb-2">Max Loan</label>
{/* end::Label */}
{/* begin::Input */}
<input
placeholder="Max Loan"
{...formik.getFieldProps("max_loan")}
className={clsx(
"form-control form-control-solid mb-3 mb-lg-0",
{ "is-invalid": formik.touched.max_loan && formik.errors.max_loan },
{
"is-valid": formik.touched.max_loan && !formik.errors.max_loan,
}
)}
type="text"
name="max_loan"
autoComplete="off"
disabled={formik.isSubmitting || isUserLoading}
/>
{/* end::Input */}
{formik.touched.max_loan && formik.errors.max_loan && (
<div className="fv-plugins-message-container">
<div className="fv-help-block">
<span role="alert">{formik.errors.max_loan}</span>
</div>
</div>
)}
</div>
{/* end::Input group */}
{/* begin::Input group */}
<div className="fv-row mb-7">
{/* begin::Label */}
<label className="required fw-bold fs-6 mb-2">Tenor</label>
{/* end::Label */}
{/* begin::Input */}
<input
placeholder="Tenor"
{...formik.getFieldProps("tenor")}
className={clsx(
"form-control form-control-solid mb-3 mb-lg-0",
{ "is-invalid": formik.touched.tenor && formik.errors.tenor },
{
"is-valid": formik.touched.tenor && !formik.errors.tenor,
}
)}
type="text"
name="tenor"
autoComplete="off"
disabled={formik.isSubmitting || isUserLoading}
/>
{/* end::Input */}
{formik.touched.tenor && formik.errors.tenor && (
<div className="fv-plugins-message-container">
<div className="fv-help-block">
<span role="alert">{formik.errors.tenor}</span>
</div>
</div>
)}
</div>
{/* end::Input group */}
{/* begin::Input group */}
<div className="fv-row mb-7">
{/* begin::Label */}
<label className="required fw-bold fs-6 mb-2">Ret Age</label>
{/* end::Label */}
{/* begin::Input */}
<input
placeholder="Retirement Age"
{...formik.getFieldProps("retirement_age")}
className={clsx(
"form-control form-control-solid mb-3 mb-lg-0",
{ "is-invalid": formik.touched.retirement_age && formik.errors.retirement_age },
{
"is-valid": formik.touched.retirement_age && !formik.errors.retirement_age,
}
)}
type="text"
name="retirement_age"
autoComplete="off"
disabled={formik.isSubmitting || isUserLoading}
/>
{/* end::Input */}
{formik.touched.retirement_age && formik.errors.retirement_age && (
<div className="fv-plugins-message-container">
<div className="fv-help-block">
<span role="alert">{formik.errors.retirement_age}</span>
</div>
</div>
)}
</div>
{/* end::Input group */}
{/* begin::Input group */}
<div className="fv-row mb-7">
<label className="required fw-bold fs-6 mb-2">Sector</label>
<select
{...formik.getFieldProps("sector")}
className={clsx(
"form-control form-control-solid mb-3 mb-lg-0",
{ "is-invalid": formik.touched.sector && formik.errors.sector },
{
"is-valid": formik.touched.sector && !formik.errors.sector,
}
)}
name="sector"
autoComplete="off"
disabled={formik.isSubmitting || isUserLoading}
>
{isLoading ?
<option value=''>Loading...</option>
:
<>
<option value=''>Select Sector</option>
{response?.employer_sector?.map((item:SelectProps) => (
<option key={item.uid} value={item.uid}>{item.name}</option>
))
}
</>
}
</select>
{/* end::Input */}
{formik.touched.sector && formik.errors.sector && (
<div className="fv-plugins-message-container">
<div className="fv-help-block">
<span role="alert">{formik.errors.sector}</span>
</div>
</div>
)}
</div>
{/* end::Input group */}
{/* begin::Input group */}
<div className="fv-row mb-7">
{/* begin::Label */}
<label className="required fw-bold fs-6 mb-2">Salary</label>
{/* end::Label */}
{/* begin::Input */}
<select
{...formik.getFieldProps("salary_source")}
className={clsx(
"form-control form-control-solid mb-3 mb-lg-0",
{ "is-invalid": formik.touched.salary_source && formik.errors.salary_source },
{
"is-valid": formik.touched.salary_source && !formik.errors.salary_source,
}
)}
name="salary_source"
autoComplete="off"
disabled={formik.isSubmitting || isUserLoading}
>
{isLoading ?
<option value=''>Loading...</option>
:
<>
<option value=''>Select Salary Source</option>
{response?.salary_sources?.map((item:SelectProps) => (
<option key={item.uid} value={item.uid}>{item.name}</option>
))
}
</>
}
</select>
{/* end::Input */}
{formik.touched.salary_source && formik.errors.salary_source && (
<div className="fv-plugins-message-container">
<div className="fv-help-block">
<span role="alert">{formik.errors.salary_source}</span>
</div>
</div>
)}
</div>
{/* end::Input group */}
</div>
{/* end::Scroll */}
{/* begin::Actions */}
<div className="text-center pt-15">
<button
type="reset"
onClick={() => cancel()}
className="btn btn-danger me-3"
data-kt-users-modal-action="cancel"
disabled={formik.isSubmitting || isUserLoading}
>
Cancel
</button>
<button
type="submit"
className="btn btn-primary"
data-kt-users-modal-action="submit"
disabled={
isUserLoading ||
formik.isSubmitting ||
!formik.isValid ||
!formik.touched
}
>
<span className="indicator-label">Add</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 { ModalForm };
@@ -1,10 +1,10 @@
import {useQuery} from 'react-query'
import {UserEditModalForm} from './UserEditModalForm'
import {ModalForm} from './ModalForm'
import {isNotEmpty, QUERIES} from '../../../../../../_digifi/helpers'
import {useListView} from '../core/ListViewProvider'
import {getUserById} from '../core/_requests'
const UserEditModalFormWrapper = () => {
const ModalFormWrapper = () => {
const {itemIdForUpdate, setItemIdForUpdate} = useListView()
const enabledQuery: boolean = isNotEmpty(itemIdForUpdate)
const {
@@ -27,14 +27,14 @@ const UserEditModalFormWrapper = () => {
)
if (!itemIdForUpdate) {
return <UserEditModalForm isUserLoading={isLoading} user={{id: undefined}} />
return <ModalForm isUserLoading={isLoading} user={{id: undefined}} />
}
if (!isLoading && !error && user) {
return <UserEditModalForm isUserLoading={isLoading} user={user} />
return <ModalForm isUserLoading={isLoading} user={user} />
}
return null
}
export {UserEditModalFormWrapper}
export {ModalFormWrapper}

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