diff --git a/src/_digifi/helpers/formatNumbers.ts b/src/_digifi/helpers/formatNumbers.ts index 05be6d5..e5409c9 100644 --- a/src/_digifi/helpers/formatNumbers.ts +++ b/src/_digifi/helpers/formatNumbers.ts @@ -4,3 +4,29 @@ export const formatNumbers = (number: string | undefined): string | null => { } return number.replace(/\B(?=(\d{3})+(?!\d))/g, ','); }; + + +// FUNCTION TO RETURN AMOUNT TO TWO DECIMAL PLACES +export const FormatAmount = ( + amount = "00", + ) => { + // Convert the number to a string + let numStr = String(amount); + + // Split the string into integer and decimal parts + let parts = numStr.split("."); + let integerPart = parts[0] || ""; + let decimalPart = parts[1] || ""; + + // Add thousands separators to the integer part + let formattedInteger = integerPart.replace(/\B(?=(\d{3})+(?!\d))/g, ','); + + // Truncate or pad the decimal part to two decimal points + let formattedDecimal = decimalPart.slice(0, 2).padEnd(2, "0"); + + // Combine the formatted integer and decimal parts + let formattedNumber = '₦ ' + formattedInteger + '.' + formattedDecimal; + + // return formattedNumber; + return formattedNumber; + }; diff --git a/src/_digifi/layout/components/paginatedListing/RecentBVNList.tsx b/src/_digifi/layout/components/paginatedListing/RecentBVNList.tsx index 075ce30..f693a61 100644 --- a/src/_digifi/layout/components/paginatedListing/RecentBVNList.tsx +++ b/src/_digifi/layout/components/paginatedListing/RecentBVNList.tsx @@ -89,11 +89,7 @@ export default function RecentBVNList({
*/} + +
+
+ Admin +
+
+ ) } diff --git a/src/_digifi/partials/widgets/_new/cards/CardsWidget17.tsx b/src/_digifi/partials/widgets/_new/cards/CardsWidget17.tsx index c02247a..396114f 100644 --- a/src/_digifi/partials/widgets/_new/cards/CardsWidget17.tsx +++ b/src/_digifi/partials/widgets/_new/cards/CardsWidget17.tsx @@ -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 = ({ @@ -16,6 +19,7 @@ const CardsWidget17: FC = ({ chartSize = 70, chartLine = 11, chartRotate = 145, + dashData }) => { const chartRef = useRef(null) const {mode} = useThemeMode() @@ -39,15 +43,15 @@ const CardsWidget17: FC = ({
- $ + {/* $ */} - 69,700 + {FormatAmount('69,700')} 2.2%
- Projects Earnings in April + Application in {dashData?.loading ? 'Loading...': dashData?.data?.dash_data?.curr_month}
@@ -65,21 +69,21 @@ const CardsWidget17: FC = ({
-
Leaf CRM
-
$7,660
+
Ready
+
{dashData?.loading ? 'Loading...': FormatAmount(dashData?.data?.dash_data?.ready_loans)}
-
Mivy App
-
$2,820
+
Verified
+
{dashData?.loading ? 'Loading...': FormatAmount(dashData?.data?.dash_data?.verified_loans)}
-
Others
-
$45,257
+
Approved
+
{dashData?.loading ? 'Loading...': FormatAmount(dashData?.data?.dash_data?.approved_loans)}
diff --git a/src/_digifi/partials/widgets/_new/cards/CardsWidget20.tsx b/src/_digifi/partials/widgets/_new/cards/CardsWidget20.tsx index 0859f86..83cc7da 100644 --- a/src/_digifi/partials/widgets/_new/cards/CardsWidget20.tsx +++ b/src/_digifi/partials/widgets/_new/cards/CardsWidget20.tsx @@ -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) => (
( >
- 69 + {dashData?.loading ? 'Loading...': dashData?.data?.dash_data?.active_loans} {description}
diff --git a/src/_digifi/partials/widgets/_new/cards/CardsWidget7.tsx b/src/_digifi/partials/widgets/_new/cards/CardsWidget7.tsx index ef16e98..0a57305 100644 --- a/src/_digifi/partials/widgets/_new/cards/CardsWidget7.tsx +++ b/src/_digifi/partials/widgets/_new/cards/CardsWidget7.tsx @@ -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) => (
- {stats} + {dashData?.loading ? 'Loading...': dashData?.data?.dash_data?.applications} {description}
- Today’s Heroes + Recent Applications
{items.map((item, index) => (
(
- Try our all new Enviroment with + Need more help to manage to the platform
( className='text-danger opacity-75-hover' > - Pro Plan + Use our help
- for Free + {/* for Free */}
-
+ {/* +
*/}
= [ - {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) => (
-

External Links

+

Other Links

{rows.map((row, index) => (
- + {row.description} - + */} + {/* end::Export */} + + {/* begin::Add user */} + {/* */} + {/* end::Add user */} +
+ ) +} + +export {UsersListToolbar} diff --git a/src/app/modules/apps/user-management/customers-list/components/header/UsersListFilter.tsx b/src/app/modules/apps/user-management/customers-list/components/header/UsersListFilter.tsx new file mode 100644 index 0000000..4ac79a9 --- /dev/null +++ b/src/app/modules/apps/user-management/customers-list/components/header/UsersListFilter.tsx @@ -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() + const [lastLogin, setLastLogin] = useState() + + useEffect(() => { + MenuComponent.reinitialization() + }, []) + + const resetData = () => { + updateState({filter: undefined, ...initialQueryState}) + } + + const filterData = () => { + updateState({ + filter: {role, last_login: lastLogin}, + ...initialQueryState, + }) + } + + return ( + <> + {/* begin::Filter Button */} + + {/* end::Filter Button */} + {/* begin::SubMenu */} +
+ {/* begin::Header */} +
+
Filter Options
+
+ {/* end::Header */} + + {/* begin::Separator */} +
+ {/* end::Separator */} + + {/* begin::Content */} +
+ {/* begin::Input group */} +
+ + +
+ {/* end::Input group */} + + {/* begin::Input group */} +
+ + +
+ {/* end::Input group */} + + {/* begin::Actions */} +
+ + +
+ {/* end::Actions */} +
+ {/* end::Content */} +
+ {/* end::SubMenu */} + + ) +} + +export {UsersListFilter} diff --git a/src/app/modules/apps/user-management/customers-list/components/header/UsersListGrouping.tsx b/src/app/modules/apps/user-management/customers-list/components/header/UsersListGrouping.tsx new file mode 100644 index 0000000..6400fd7 --- /dev/null +++ b/src/app/modules/apps/user-management/customers-list/components/header/UsersListGrouping.tsx @@ -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 ( +
+
+ {selected.length} Selected +
+ + +
+ ) +} + +export {UsersListGrouping} diff --git a/src/app/modules/apps/user-management/customers-list/components/header/UsersListHeader.tsx b/src/app/modules/apps/user-management/customers-list/components/header/UsersListHeader.tsx new file mode 100644 index 0000000..9d2a3a0 --- /dev/null +++ b/src/app/modules/apps/user-management/customers-list/components/header/UsersListHeader.tsx @@ -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 ( +
+ + {/* begin::Card toolbar */} +
+ {/* begin::Group actions */} + {selected.length > 0 ? : } + {/* end::Group actions */} +
+ {/* end::Card toolbar */} +
+ ) +} + +export {UsersListHeader} diff --git a/src/app/modules/apps/user-management/customers-list/components/header/UsersListSearchComponent.tsx b/src/app/modules/apps/user-management/customers-list/components/header/UsersListSearchComponent.tsx new file mode 100644 index 0000000..75cba1c --- /dev/null +++ b/src/app/modules/apps/user-management/customers-list/components/header/UsersListSearchComponent.tsx @@ -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('') + // 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 ( +
+ {/* begin::Search */} +
+ + setSearchTerm(e.target.value)} + /> +
+ {/* end::Search */} +
+ ) +} + +export {UsersListSearchComponent} diff --git a/src/app/modules/apps/user-management/customers-list/components/loading/UsersListLoading.tsx b/src/app/modules/apps/user-management/customers-list/components/loading/UsersListLoading.tsx new file mode 100644 index 0000000..2278f87 --- /dev/null +++ b/src/app/modules/apps/user-management/customers-list/components/loading/UsersListLoading.tsx @@ -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
Processing...
+} + +export {UsersListLoading} diff --git a/src/app/modules/apps/user-management/customers-list/components/pagination/UsersListPagination.tsx b/src/app/modules/apps/user-management/customers-list/components/pagination/UsersListPagination.tsx new file mode 100644 index 0000000..2879f48 --- /dev/null +++ b/src/app/modules/apps/user-management/customers-list/components/pagination/UsersListPagination.tsx @@ -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 ( + + ) +} + +export {UsersListPagination} diff --git a/src/app/modules/apps/user-management/customers-list/core/ListViewProvider.tsx b/src/app/modules/apps/user-management/customers-list/core/ListViewProvider.tsx new file mode 100644 index 0000000..91cc2cf --- /dev/null +++ b/src/app/modules/apps/user-management/customers-list/core/ListViewProvider.tsx @@ -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(initialListView) + +const ListViewProvider: FC = ({children}) => { + const [selected, setSelected] = useState>(initialListView.selected) + const [itemIdForUpdate, setItemIdForUpdate] = useState(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 ( + { + groupingOnSelect(id, selected, setSelected) + }, + onSelectAll: () => { + groupingOnSelectAll(isAllSelected, setSelected, data) + }, + clearSelected: () => { + setSelected([]) + }, + }} + > + {children} + + ) +} + +const useListView = () => useContext(ListViewContext) + +export {ListViewProvider, useListView} diff --git a/src/app/modules/apps/user-management/customers-list/core/QueryRequestProvider.tsx b/src/app/modules/apps/user-management/customers-list/core/QueryRequestProvider.tsx new file mode 100644 index 0000000..95f31cf --- /dev/null +++ b/src/app/modules/apps/user-management/customers-list/core/QueryRequestProvider.tsx @@ -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(initialQueryRequest) + +const QueryRequestProvider: FC = ({children}) => { + const [state, setState] = useState(initialQueryRequest.state) + + const updateState = (updates: Partial) => { + const updatedState = {...state, ...updates} as QueryState + setState(updatedState) + } + + return ( + + {children} + + ) +} + +const useQueryRequest = () => useContext(QueryRequestContext) +export {QueryRequestProvider, useQueryRequest} diff --git a/src/app/modules/apps/user-management/customers-list/core/QueryResponseProvider.tsx b/src/app/modules/apps/user-management/customers-list/core/QueryResponseProvider.tsx new file mode 100644 index 0000000..c05ad9d --- /dev/null +++ b/src/app/modules/apps/user-management/customers-list/core/QueryResponseProvider.tsx @@ -0,0 +1,85 @@ +/* eslint-disable react-refresh/only-export-components */ +/* eslint-disable react-hooks/exhaustive-deps */ +import {FC, useContext, useState, useEffect, useMemo} from 'react' +import {useQuery} from 'react-query' +import { + createResponseContext, + initialQueryResponse, + initialQueryState, + PaginationState, + QUERIES, + stringifyRequestQuery, + WithChildren, +} from '../../../../../../_digifi/helpers' +import {getCustomerList} from './_requests' +import {User} from './_models' +import {useQueryRequest} from './QueryRequestProvider' + +const QueryResponseContext = createResponseContext(initialQueryResponse) +const QueryResponseProvider: FC = ({children}) => { + const {state} = useQueryRequest() + const [query, setQuery] = useState(stringifyRequestQuery(state)) + const updatedQuery = useMemo(() => stringifyRequestQuery(state), [state]) + + useEffect(() => { + if (query !== updatedQuery) { + setQuery(updatedQuery) + } + }, [updatedQuery]) + + const { + isFetching, + refetch, + data: response, + } = useQuery( + `${QUERIES.CUSTOMERS_LIST}-${query}`, + () => { + return getCustomerList(query) + }, + {cacheTime: 0, keepPreviousData: true, refetchOnWindowFocus: false} + ) + + return ( + + {children} + + ) +} + +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, +} diff --git a/src/app/modules/apps/user-management/customers-list/core/_models.ts b/src/app/modules/apps/user-management/customers-list/core/_models.ts new file mode 100644 index 0000000..0b3df3a --- /dev/null +++ b/src/app/modules/apps/user-management/customers-list/core/_models.ts @@ -0,0 +1,43 @@ +import {ID, Response} from '../../../../../../_digifi/helpers' +export type User = { + id?: ID + name?: string + avatar?: string + // email?: string + position?: string + role?: string + last_login?: string + two_steps?: boolean + joined_day?: string + online?: boolean + initials?: { + label: string + state: string + } + firstname?: string, + lastname?: string + uid?: string + loan_amount?: string + payment_month?: string + sales_agent?: string + gender?: string | null + marital_status?: string + email?: string + address?: string + state?: string + country?: string + status?: string + added?: string + updated?: string + bvn?: string +} + +export type UsersQueryResponse = Response> + +export const initialUser: User = { + avatar: 'avatars/300-6.jpg', + position: 'Art Director', + role: 'Administrator', + name: '', + email: '', +} diff --git a/src/app/modules/apps/user-management/customers-list/core/_requests.ts b/src/app/modules/apps/user-management/customers-list/core/_requests.ts new file mode 100644 index 0000000..bb37b33 --- /dev/null +++ b/src/app/modules/apps/user-management/customers-list/core/_requests.ts @@ -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 => { +// return axios +// .get(`${GET_USERS_URL}?${query}`) +// .then((d: AxiosResponse) => d.data); +// }; +const getCustomerList = (query: string): Promise => { // FUNCTION TO GET USERS THAT HAVE STARTED LOAN APPLICATION + return axios + .get(`${NEW_USER_ENDPOINT}/customers`) + .then((d: AxiosResponse) => d.data); +}; + +const getUserById = (id: ID): Promise => { + return axios + .get(`${USER_URL}/${id}`) + .then((response: AxiosResponse>) => response.data) + .then((response: Response) => response.data); +}; + +const createUser = (user: User): Promise => { + return axios + .put(USER_URL, user) + .then((response: AxiosResponse>) => response.data) + .then((response: Response) => response.data); +}; + +const updateUser = (user: User): Promise => { + return axios + .post(`${USER_URL}/${user.id}`, user) + .then((response: AxiosResponse>) => response.data) + .then((response: Response) => response.data); +}; + +const deleteUser = (userId: ID): Promise => { + return axios.delete(`${USER_URL}/${userId}`).then(() => {}); +}; + +const deleteSelectedUsers = (userIds: Array): Promise => { + const requests = userIds.map((id) => axios.delete(`${USER_URL}/${id}`)); + return axios.all(requests).then(() => {}); +}; + +export { + getCustomerList, + deleteUser, + deleteSelectedUsers, + getUserById, + createUser, + updateUser, +}; diff --git a/src/app/modules/apps/user-management/customers-list/table/UsersTable.tsx b/src/app/modules/apps/user-management/customers-list/table/UsersTable.tsx new file mode 100644 index 0000000..f48b4b4 --- /dev/null +++ b/src/app/modules/apps/user-management/customers-list/table/UsersTable.tsx @@ -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 ( + +
+ + + + {headers.map((column: ColumnInstance) => ( + + ))} + + + + {rows.length > 0 ? ( + rows.map((row: Row, i) => { + prepareRow(row) + return + }) + ) : ( + + + + )} + +
+
+ No matching records found +
+
+
+ + {isLoading && } +
+ ) +} + +export {UsersTable} diff --git a/src/app/modules/apps/user-management/customers-list/table/columns/AddedCell.tsx b/src/app/modules/apps/user-management/customers-list/table/columns/AddedCell.tsx new file mode 100644 index 0000000..6f5bc2b --- /dev/null +++ b/src/app/modules/apps/user-management/customers-list/table/columns/AddedCell.tsx @@ -0,0 +1,12 @@ +import {FC} from 'react' +import { NewDateTimeFormatter } from '../../../../../../../_digifi/lib/NewDateTimeFormatter' + +type Props = { + added?: string +} + +const AddedCell: FC = ({added}) => ( +
{NewDateTimeFormatter((added))}
+) + +export {AddedCell} \ No newline at end of file diff --git a/src/app/modules/apps/user-management/customers-list/table/columns/CustomHeaderColumn.tsx b/src/app/modules/apps/user-management/customers-list/table/columns/CustomHeaderColumn.tsx new file mode 100644 index 0000000..838a12e --- /dev/null +++ b/src/app/modules/apps/user-management/customers-list/table/columns/CustomHeaderColumn.tsx @@ -0,0 +1,15 @@ +import {FC} from 'react' +import {ColumnInstance} from 'react-table' +import {User} from '../../core/_models' + +type Props = { + column: ColumnInstance +} + +const CustomHeaderColumn: FC = ({column}) => ( + <> + {column.Header && typeof column.Header === 'string' ? {column.render('Header')} : column.render('Header')} + +) + +export {CustomHeaderColumn} diff --git a/src/app/modules/apps/user-management/customers-list/table/columns/CustomRow.tsx b/src/app/modules/apps/user-management/customers-list/table/columns/CustomRow.tsx new file mode 100644 index 0000000..a869cfe --- /dev/null +++ b/src/app/modules/apps/user-management/customers-list/table/columns/CustomRow.tsx @@ -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 +} + +const CustomRow: FC = ({row}) => ( + + {row.cells.map((cell) => { + return ( + + {cell.render('Cell')} + + ) + })} + +) + +export {CustomRow} diff --git a/src/app/modules/apps/user-management/customers-list/table/columns/Status.tsx b/src/app/modules/apps/user-management/customers-list/table/columns/Status.tsx new file mode 100644 index 0000000..518667c --- /dev/null +++ b/src/app/modules/apps/user-management/customers-list/table/columns/Status.tsx @@ -0,0 +1,11 @@ +import {FC} from 'react' + +type Props = { + status?: string +} + +const Status: FC = ({status}) => ( + <> {status &&
{status}
} +) + +export {Status} diff --git a/src/app/modules/apps/user-management/customers-list/table/columns/UserActionsCell.tsx b/src/app/modules/apps/user-management/customers-list/table/columns/UserActionsCell.tsx new file mode 100644 index 0000000..9a91389 --- /dev/null +++ b/src/app/modules/apps/user-management/customers-list/table/columns/UserActionsCell.tsx @@ -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 = ({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 ( + <> + + Actions + + + {/* begin::Menu */} +
+ {/* begin::Menu item */} + + {/* end::Menu item */} + + {/* begin::Menu item */} + + {/* end::Menu item */} +
+ {/* end::Menu */} + + ) +} + +export {UserActionsCell} diff --git a/src/app/modules/apps/user-management/customers-list/table/columns/UserCustomHeader.tsx b/src/app/modules/apps/user-management/customers-list/table/columns/UserCustomHeader.tsx new file mode 100644 index 0000000..3d0b58a --- /dev/null +++ b/src/app/modules/apps/user-management/customers-list/table/columns/UserCustomHeader.tsx @@ -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> +} +const UserCustomHeader: FC = ({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 ( + + {title} + + ) +} + +export {UserCustomHeader} diff --git a/src/app/modules/apps/user-management/customers-list/table/columns/UserInfoCell.tsx b/src/app/modules/apps/user-management/customers-list/table/columns/UserInfoCell.tsx new file mode 100644 index 0000000..1840fef --- /dev/null +++ b/src/app/modules/apps/user-management/customers-list/table/columns/UserInfoCell.tsx @@ -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 = ({user}) => ( + +) + +export {UserInfoCell} diff --git a/src/app/modules/apps/user-management/customers-list/table/columns/UserLastLoginCell.tsx b/src/app/modules/apps/user-management/customers-list/table/columns/UserLastLoginCell.tsx new file mode 100644 index 0000000..a8a0ebe --- /dev/null +++ b/src/app/modules/apps/user-management/customers-list/table/columns/UserLastLoginCell.tsx @@ -0,0 +1,11 @@ +import {FC} from 'react' + +type Props = { + payment_month?: string +} + +const PaymentMonthCell: FC = ({payment_month}) => ( +
{payment_month}
+) + +export {PaymentMonthCell} diff --git a/src/app/modules/apps/user-management/customers-list/table/columns/UserSelectionCell.tsx b/src/app/modules/apps/user-management/customers-list/table/columns/UserSelectionCell.tsx new file mode 100644 index 0000000..dcd2bfb --- /dev/null +++ b/src/app/modules/apps/user-management/customers-list/table/columns/UserSelectionCell.tsx @@ -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 = ({id}) => { + const {selected, onSelect} = useListView() + const isSelected = useMemo(() => selected.includes(id), [id, selected]) + return ( +
+ onSelect(id)} + /> +
+ ) +} + +export {UserSelectionCell} diff --git a/src/app/modules/apps/user-management/customers-list/table/columns/UserSelectionHeader.tsx b/src/app/modules/apps/user-management/customers-list/table/columns/UserSelectionHeader.tsx new file mode 100644 index 0000000..bbb1eb0 --- /dev/null +++ b/src/app/modules/apps/user-management/customers-list/table/columns/UserSelectionHeader.tsx @@ -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> +} + +const UserSelectionHeader: FC = ({tableProps}) => { + const {isAllSelected, onSelectAll} = useListView() + return ( + +
+ +
+ + ) +} + +export {UserSelectionHeader} diff --git a/src/app/modules/apps/user-management/customers-list/table/columns/_columns.tsx b/src/app/modules/apps/user-management/customers-list/table/columns/_columns.tsx new file mode 100644 index 0000000..594d918 --- /dev/null +++ b/src/app/modules/apps/user-management/customers-list/table/columns/_columns.tsx @@ -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> = [ + { + Header: (props) => , + id: 'selection', + Cell: ({...props}) => , + }, + { + Header: (props) => , + id: 'firstname', + Cell: ({...props}) => , + }, + { + Header: (props) => , + accessor: 'bvn', + }, + // { + // Header: (props) => ( + // + // ), + // id: 'payment_month', + // Cell: ({...props}) => , + // }, + { + Header: (props) => ( + + ), + id: 'status', + Cell: ({...props}) => , + }, + { + Header: (props) => ( + + ), + id: 'added', + Cell: ({...props}) => , + }, + { + Header: (props) => ( + + ), + id: 'actions', + Cell: ({...props}) => , + }, +] + +export {usersColumns} \ No newline at end of file diff --git a/src/app/modules/apps/user-management/customers-list/user-edit-modal/UserEditModal.tsx b/src/app/modules/apps/user-management/customers-list/user-edit-modal/UserEditModal.tsx new file mode 100644 index 0000000..9bf605f --- /dev/null +++ b/src/app/modules/apps/user-management/customers-list/user-edit-modal/UserEditModal.tsx @@ -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 ( + <> + + {/* begin::Modal Backdrop */} +
+ {/* end::Modal Backdrop */} + + ) +} + +export {UserEditModal} diff --git a/src/app/modules/apps/user-management/customers-list/user-edit-modal/UserEditModalForm.tsx b/src/app/modules/apps/user-management/customers-list/user-edit-modal/UserEditModalForm.tsx new file mode 100644 index 0000000..6e8b0ec --- /dev/null +++ b/src/app/modules/apps/user-management/customers-list/user-edit-modal/UserEditModalForm.tsx @@ -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 = ({user, isUserLoading}) => { + const {setItemIdForUpdate} = useListView() + const {refetch} = useQueryResponse() + + const [userForEdit] = useState({ + ...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 ( + <> +
+ {/* begin::Scroll */} +
+ {/* begin::Input group */} +
+ {/* begin::Label */} + + {/* end::Label */} + + {/* begin::Image input */} +
+ {/* begin::Preview existing avatar */} +
+ {/* end::Preview existing avatar */} + + {/* begin::Label */} + {/* */} + {/* end::Label */} + + {/* begin::Cancel */} + {/* + + */} + {/* end::Cancel */} + + {/* begin::Remove */} + {/* + + */} + {/* end::Remove */} +
+ {/* end::Image input */} + + {/* begin::Hint */} + {/*
Allowed file types: png, jpg, jpeg.
*/} + {/* end::Hint */} +
+ {/* end::Input group */} + + {/* begin::Input group */} +
+ {/* begin::Label */} + + {/* end::Label */} + + {/* begin::Input */} + + {formik.touched.name && formik.errors.name && ( +
+
+ {formik.errors.name} +
+
+ )} + {/* end::Input */} +
+ {/* end::Input group */} + + {/* begin::Input group */} +
+ {/* begin::Label */} + + {/* end::Label */} + + {/* begin::Input */} + + {/* end::Input */} + {formik.touched.email && formik.errors.email && ( +
+ {formik.errors.email} +
+ )} +
+ {/* end::Input group */} + + {/* begin::Input group */} +
+ {/* begin::Label */} + + {/* end::Label */} + {/* begin::Roles */} + {/* begin::Input row */} +
+ {/* begin::Radio */} +
+ {/* begin::Input */} + + + {/* end::Input */} + {/* begin::Label */} + + {/* end::Label */} +
+ {/* end::Radio */} +
+ {/* end::Input row */} +
+ {/* begin::Input row */} +
+ {/* begin::Radio */} +
+ {/* begin::Input */} + + {/* end::Input */} + {/* begin::Label */} + + {/* end::Label */} +
+ {/* end::Radio */} +
+ {/* end::Input row */} +
+ {/* begin::Input row */} +
+ {/* begin::Radio */} +
+ {/* begin::Input */} + + + {/* end::Input */} + {/* begin::Label */} + + {/* end::Label */} +
+ {/* end::Radio */} +
+ {/* end::Input row */} +
+ {/* begin::Input row */} +
+ {/* begin::Radio */} +
+ {/* begin::Input */} + + {/* end::Input */} + {/* begin::Label */} + + {/* end::Label */} +
+ {/* end::Radio */} +
+ {/* end::Input row */} +
+ {/* begin::Input row */} +
+ {/* begin::Radio */} +
+ {/* begin::Input */} + + {/* end::Input */} + {/* begin::Label */} + + {/* end::Label */} +
+ {/* end::Radio */} +
+ {/* end::Input row */} + {/* end::Roles */} +
+ {/* end::Input group */} +
+ {/* end::Scroll */} + + {/* begin::Actions */} +
+ + + +
+ {/* end::Actions */} +
+ {(formik.isSubmitting || isUserLoading) && } + + ) +} + +export {UserEditModalForm} diff --git a/src/app/modules/apps/user-management/customers-list/user-edit-modal/UserEditModalFormWrapper.tsx b/src/app/modules/apps/user-management/customers-list/user-edit-modal/UserEditModalFormWrapper.tsx new file mode 100644 index 0000000..d60d7e3 --- /dev/null +++ b/src/app/modules/apps/user-management/customers-list/user-edit-modal/UserEditModalFormWrapper.tsx @@ -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 + } + + if (!isLoading && !error && user) { + return + } + + return null +} + +export {UserEditModalFormWrapper} diff --git a/src/app/modules/apps/user-management/customers-list/user-edit-modal/UserEditModalHeader.tsx b/src/app/modules/apps/user-management/customers-list/user-edit-modal/UserEditModalHeader.tsx new file mode 100644 index 0000000..cb0f5b6 --- /dev/null +++ b/src/app/modules/apps/user-management/customers-list/user-edit-modal/UserEditModalHeader.tsx @@ -0,0 +1,27 @@ +import {KTIcon} from '../../../../../../_digifi/helpers' +import {useListView} from '../core/ListViewProvider' + +const UserEditModalHeader = () => { + const {setItemIdForUpdate} = useListView() + + return ( +
+ {/* begin::Modal title */} +

Add User

+ {/* end::Modal title */} + + {/* begin::Close */} +
setItemIdForUpdate(undefined)} + style={{cursor: 'pointer'}} + > + +
+ {/* end::Close */} +
+ ) +} + +export {UserEditModalHeader} diff --git a/src/app/pages/dashboard/DashboardWrapper.tsx b/src/app/pages/dashboard/DashboardWrapper.tsx index e888ab1..5493f99 100644 --- a/src/app/pages/dashboard/DashboardWrapper.tsx +++ b/src/app/pages/dashboard/DashboardWrapper.tsx @@ -43,24 +43,26 @@ const DashboardPage: FC = () => {
{/* end::Col */} {/* begin::Col */}
- +
{/* end::Col */} diff --git a/src/app/pages/dashboard/model.ts b/src/app/pages/dashboard/model.ts index fa2dcf8..ac3e009 100644 --- a/src/app/pages/dashboard/model.ts +++ b/src/app/pages/dashboard/model.ts @@ -32,6 +32,20 @@ export type RecentBVNProps = { nationality?: string | null }[] +export type DataProps = { + active_loans?: string + applications?: string + today_application?: string | number + curr_month?: string + curr_application_amount?: string | number + curr_application_percentage?: string + curr_application_direction?: string + recent_applications?: Array<{[index: string]: string}>, + ready_loans?: string + verified_loans?: string + approved_loans?: string +} + export type DashDataProps = { loading: boolean, @@ -39,5 +53,6 @@ export type DashDataProps = { call_return?: string recent_applications? : RecentApplicationsProps recent_bvn?: RecentBVNProps + dash_data? : DataProps } } \ No newline at end of file