diff --git a/src/app/modules/process/ProcessPage.tsx b/src/app/modules/process/ProcessPage.tsx index a110781..951a50c 100644 --- a/src/app/modules/process/ProcessPage.tsx +++ b/src/app/modules/process/ProcessPage.tsx @@ -6,7 +6,11 @@ import {PageLink, PageTitle} from '../../../_digifi/layout/core' // import {Documents} from './components/Documents' // import {Connections} from './components/Connections' // import {ProcessHeader} from './ProcessHeader' -import { StartedUserList } from './components/StartedUserList' +import { UserStartedList } from './components/UserStartedList' +import { UserPendingList } from './components/UserPendingList' +import { UserReadyList } from './components/UserReadyList' +import { UserApprovedList } from './components/UserApprovedList' +import { UserRejectedList } from './components/UserRejectedList' const processBreadCrumbs: Array = [ { @@ -38,7 +42,7 @@ const ProcessPage = () => ( element={ <> Started - + } /> @@ -47,7 +51,7 @@ const ProcessPage = () => ( element={ <> Pending - + } /> @@ -56,7 +60,7 @@ const ProcessPage = () => ( element={ <> Ready - + } /> @@ -65,7 +69,7 @@ const ProcessPage = () => ( element={ <> Approved - + } /> @@ -74,7 +78,7 @@ const ProcessPage = () => ( element={ <> Rejected - + } /> diff --git a/src/app/modules/process/components/StartedUserList.tsx b/src/app/modules/process/components/UserApprovedList.tsx similarity index 51% rename from src/app/modules/process/components/StartedUserList.tsx rename to src/app/modules/process/components/UserApprovedList.tsx index 20638a8..8fc4cbf 100644 --- a/src/app/modules/process/components/StartedUserList.tsx +++ b/src/app/modules/process/components/UserApprovedList.tsx @@ -1,12 +1,12 @@ import { KTCard } from "../../../../_digifi/helpers" import { Content } from "../../../../_digifi/layout/components/content" import { ToolbarWrapper } from "../../../../_digifi/layout/components/toolbar" -import { UsersListHeader } from "../../apps/user-management/users-list/components/header/UsersListHeader" -import { ListViewProvider, useListView } from "../../apps/user-management/users-list/core/ListViewProvider" -import { QueryRequestProvider } from "../../apps/user-management/users-list/core/QueryRequestProvider" -import { QueryResponseProvider } from "../../apps/user-management/users-list/core/QueryResponseProvider" -import { UsersTable } from "../../apps/user-management/users-list/table/UsersTable" -import { UserEditModal } from "../../apps/user-management/users-list/user-edit-modal/UserEditModal" +import { UsersListHeader } from "../user-approved/users-list/components/header/UsersListHeader" +import { ListViewProvider, useListView } from "../user-approved/users-list/core/ListViewProvider" +import { QueryRequestProvider } from "../user-approved/users-list/core/QueryRequestProvider" +import { QueryResponseProvider } from "../user-approved/users-list/core/QueryResponseProvider" +import { UsersTable } from "../user-approved/users-list/table/UsersTable" +import { UserEditModal } from "../user-approved/users-list/user-edit-modal/UserEditModal" const UsersList = () => { const {itemIdForUpdate} = useListView() @@ -21,7 +21,7 @@ const UsersList = () => { ) } -const StartedUserList = () => ( +const UserApprovedList = () => ( @@ -34,4 +34,4 @@ const StartedUserList = () => ( ) -export {StartedUserList} +export {UserApprovedList} diff --git a/src/app/modules/process/components/UserPendingList.tsx b/src/app/modules/process/components/UserPendingList.tsx new file mode 100644 index 0000000..8a2ff75 --- /dev/null +++ b/src/app/modules/process/components/UserPendingList.tsx @@ -0,0 +1,37 @@ +import { KTCard } from "../../../../_digifi/helpers" +import { Content } from "../../../../_digifi/layout/components/content" +import { ToolbarWrapper } from "../../../../_digifi/layout/components/toolbar" +import { UsersListHeader } from "../user-pending/users-list/components/header/UsersListHeader" +import { ListViewProvider, useListView } from "../user-pending/users-list/core/ListViewProvider" +import { QueryRequestProvider } from "../user-pending/users-list/core/QueryRequestProvider" +import { QueryResponseProvider } from "../user-pending/users-list/core/QueryResponseProvider" +import { UsersTable } from "../user-pending/users-list/table/UsersTable" +import { UserEditModal } from "../user-pending/users-list/user-edit-modal/UserEditModal" + +const UsersList = () => { + const {itemIdForUpdate} = useListView() + return ( + <> + + + + + {itemIdForUpdate !== undefined && } + + ) +} + +const UserPendingList = () => ( + + + + + + + + + + +) + +export {UserPendingList} diff --git a/src/app/modules/process/components/UserReadyList.tsx b/src/app/modules/process/components/UserReadyList.tsx new file mode 100644 index 0000000..fae4d05 --- /dev/null +++ b/src/app/modules/process/components/UserReadyList.tsx @@ -0,0 +1,37 @@ +import { KTCard } from "../../../../_digifi/helpers" +import { Content } from "../../../../_digifi/layout/components/content" +import { ToolbarWrapper } from "../../../../_digifi/layout/components/toolbar" +import { UsersListHeader } from "../user-ready/users-list/components/header/UsersListHeader" +import { ListViewProvider, useListView } from "../user-ready/users-list/core/ListViewProvider" +import { QueryRequestProvider } from "../user-ready/users-list/core/QueryRequestProvider" +import { QueryResponseProvider } from "../user-ready/users-list/core/QueryResponseProvider" +import { UsersTable } from "../user-ready/users-list/table/UsersTable" +import { UserEditModal } from "../user-ready/users-list/user-edit-modal/UserEditModal" + +const UsersList = () => { + const {itemIdForUpdate} = useListView() + return ( + <> + + + + + {itemIdForUpdate !== undefined && } + + ) +} + +const UserReadyList = () => ( + + + + + + + + + + +) + +export {UserReadyList} diff --git a/src/app/modules/process/components/UserRejectedList.tsx b/src/app/modules/process/components/UserRejectedList.tsx new file mode 100644 index 0000000..9f41d7b --- /dev/null +++ b/src/app/modules/process/components/UserRejectedList.tsx @@ -0,0 +1,37 @@ +import { KTCard } from "../../../../_digifi/helpers" +import { Content } from "../../../../_digifi/layout/components/content" +import { ToolbarWrapper } from "../../../../_digifi/layout/components/toolbar" +import { UsersListHeader } from "../user-rejected/users-list/components/header/UsersListHeader" +import { ListViewProvider, useListView } from "../user-rejected/users-list/core/ListViewProvider" +import { QueryRequestProvider } from "../user-rejected/users-list/core/QueryRequestProvider" +import { QueryResponseProvider } from "../user-rejected/users-list/core/QueryResponseProvider" +import { UsersTable } from "../user-rejected/users-list/table/UsersTable" +import { UserEditModal } from "../user-rejected/users-list/user-edit-modal/UserEditModal" + +const UsersList = () => { + const {itemIdForUpdate} = useListView() + return ( + <> + + + + + {itemIdForUpdate !== undefined && } + + ) +} + +const UserRejectedList = () => ( + + + + + + + + + + +) + +export {UserRejectedList} diff --git a/src/app/modules/process/components/UserStartedList.tsx b/src/app/modules/process/components/UserStartedList.tsx new file mode 100644 index 0000000..c7fbc85 --- /dev/null +++ b/src/app/modules/process/components/UserStartedList.tsx @@ -0,0 +1,37 @@ +import { KTCard } from "../../../../_digifi/helpers" +import { Content } from "../../../../_digifi/layout/components/content" +import { ToolbarWrapper } from "../../../../_digifi/layout/components/toolbar" +import { UsersListHeader } from "../user-started/users-list/components/header/UsersListHeader" +import { ListViewProvider, useListView } from "../user-started/users-list/core/ListViewProvider" +import { QueryRequestProvider } from "../user-started/users-list/core/QueryRequestProvider" +import { QueryResponseProvider } from "../user-started/users-list/core/QueryResponseProvider" +import { UsersTable } from "../user-started/users-list/table/UsersTable" +import { UserEditModal } from "../user-started/users-list/user-edit-modal/UserEditModal" + +const UsersList = () => { + const {itemIdForUpdate} = useListView() + return ( + <> + + + + + {itemIdForUpdate !== undefined && } + + ) +} + +const UserStartedList = () => ( + + + + + + + + + + +) + +export {UserStartedList} diff --git a/src/app/modules/process/user-approved/UsersPage.tsx b/src/app/modules/process/user-approved/UsersPage.tsx new file mode 100644 index 0000000..ec70d89 --- /dev/null +++ b/src/app/modules/process/user-approved/UsersPage.tsx @@ -0,0 +1,39 @@ +import {Route, Routes, Outlet, Navigate} from 'react-router-dom' +import {PageLink, PageTitle} from '../../../../_digifi/layout/core' +import {UsersListWrapper} from './users-list/UsersList' + +const usersBreadcrumbs: Array = [ + { + title: 'User Management', + path: '/apps/user-management/users', + isSeparator: false, + isActive: false, + }, + { + title: '', + path: '', + isSeparator: true, + isActive: false, + }, +] + +const UsersPage = () => { + return ( + + }> + + Users list + + + } + /> + + } /> + + ) +} + +export default UsersPage diff --git a/src/app/modules/process/user-approved/users-list/UsersList.tsx b/src/app/modules/process/user-approved/users-list/UsersList.tsx new file mode 100644 index 0000000..d8f1f10 --- /dev/null +++ b/src/app/modules/process/user-approved/users-list/UsersList.tsx @@ -0,0 +1,37 @@ +import {ListViewProvider, useListView} from './core/ListViewProvider' +import {QueryRequestProvider} from './core/QueryRequestProvider' +import {QueryResponseProvider} from './core/QueryResponseProvider' +import {UsersListHeader} from './components/header/UsersListHeader' +import {UsersTable} from './table/UsersTable' +import {UserEditModal} from './user-edit-modal/UserEditModal' +import {KTCard} from '../../../../../_digifi/helpers' +import { ToolbarWrapper } from '../../../../../_digifi/layout/components/toolbar' +import { Content } from '../../../../../_digifi/layout/components/content' + +const UsersList = () => { + const {itemIdForUpdate} = useListView() + return ( + <> + + + + + {itemIdForUpdate !== undefined && } + + ) +} + +const UsersListWrapper = () => ( + + + + + + + + + + +) + +export {UsersListWrapper} diff --git a/src/app/modules/process/user-approved/users-list/components/header/UserListToolbar.tsx b/src/app/modules/process/user-approved/users-list/components/header/UserListToolbar.tsx new file mode 100644 index 0000000..e86343b --- /dev/null +++ b/src/app/modules/process/user-approved/users-list/components/header/UserListToolbar.tsx @@ -0,0 +1,32 @@ +import {KTIcon} from '../../../../../../../_digifi/helpers' +import {useListView} from '../../core/ListViewProvider' +import {UsersListFilter} from './UsersListFilter' + +const UsersListToolbar = () => { + const {setItemIdForUpdate} = useListView() + const openAddUserModal = () => { + setItemIdForUpdate(null) + } + + return ( +
+ + + {/* begin::Export */} + {/* */} + {/* end::Export */} + + {/* begin::Add user */} + {/* */} + {/* end::Add user */} +
+ ) +} + +export {UsersListToolbar} diff --git a/src/app/modules/process/user-approved/users-list/components/header/UsersListFilter.tsx b/src/app/modules/process/user-approved/users-list/components/header/UsersListFilter.tsx new file mode 100644 index 0000000..4ac79a9 --- /dev/null +++ b/src/app/modules/process/user-approved/users-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/process/user-approved/users-list/components/header/UsersListGrouping.tsx b/src/app/modules/process/user-approved/users-list/components/header/UsersListGrouping.tsx new file mode 100644 index 0000000..6400fd7 --- /dev/null +++ b/src/app/modules/process/user-approved/users-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/process/user-approved/users-list/components/header/UsersListHeader.tsx b/src/app/modules/process/user-approved/users-list/components/header/UsersListHeader.tsx new file mode 100644 index 0000000..9d2a3a0 --- /dev/null +++ b/src/app/modules/process/user-approved/users-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/process/user-approved/users-list/components/header/UsersListSearchComponent.tsx b/src/app/modules/process/user-approved/users-list/components/header/UsersListSearchComponent.tsx new file mode 100644 index 0000000..75cba1c --- /dev/null +++ b/src/app/modules/process/user-approved/users-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/process/user-approved/users-list/components/loading/UsersListLoading.tsx b/src/app/modules/process/user-approved/users-list/components/loading/UsersListLoading.tsx new file mode 100644 index 0000000..2278f87 --- /dev/null +++ b/src/app/modules/process/user-approved/users-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/process/user-approved/users-list/components/pagination/UsersListPagination.tsx b/src/app/modules/process/user-approved/users-list/components/pagination/UsersListPagination.tsx new file mode 100644 index 0000000..2879f48 --- /dev/null +++ b/src/app/modules/process/user-approved/users-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/process/user-approved/users-list/core/ListViewProvider.tsx b/src/app/modules/process/user-approved/users-list/core/ListViewProvider.tsx new file mode 100644 index 0000000..91cc2cf --- /dev/null +++ b/src/app/modules/process/user-approved/users-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/process/user-approved/users-list/core/QueryRequestProvider.tsx b/src/app/modules/process/user-approved/users-list/core/QueryRequestProvider.tsx new file mode 100644 index 0000000..95f31cf --- /dev/null +++ b/src/app/modules/process/user-approved/users-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/process/user-approved/users-list/core/QueryResponseProvider.tsx b/src/app/modules/process/user-approved/users-list/core/QueryResponseProvider.tsx new file mode 100644 index 0000000..3f7cffe --- /dev/null +++ b/src/app/modules/process/user-approved/users-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 {getStartedUsers} 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.USERS_LIST}-${query}`, + () => { + return getStartedUsers(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/process/user-approved/users-list/core/_models.ts b/src/app/modules/process/user-approved/users-list/core/_models.ts new file mode 100644 index 0000000..b9b06ec --- /dev/null +++ b/src/app/modules/process/user-approved/users-list/core/_models.ts @@ -0,0 +1,42 @@ +import {ID, Response} from '../../../../../../_digifi/helpers' +export type User = { + id?: ID + name?: string + avatar?: string + // email?: string + position?: string + role?: string + last_login?: string + two_steps?: boolean + joined_day?: string + online?: boolean + initials?: { + label: string + state: string + } + firstname?: string, + lastname?: string + uid?: string + loan_amount?: string + payment_month?: string + sales_agent?: string + gender?: string | null + marital_status?: string + email?: string + address?: string + state?: string + country?: string + status?: string + added?: string + updated?: string +} + +export type UsersQueryResponse = Response> + +export const initialUser: User = { + avatar: 'avatars/300-6.jpg', + position: 'Art Director', + role: 'Administrator', + name: '', + email: '', +} diff --git a/src/app/modules/process/user-approved/users-list/core/_requests.ts b/src/app/modules/process/user-approved/users-list/core/_requests.ts new file mode 100644 index 0000000..a585521 --- /dev/null +++ b/src/app/modules/process/user-approved/users-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 getStartedUsers = (query: string): Promise => { // FUNCTION TO GET USERS THAT HAVE STARTED LOAN APPLICATION + return axios + .get(`${NEW_USER_ENDPOINT}/loan/started`) + .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 { + getStartedUsers, + deleteUser, + deleteSelectedUsers, + getUserById, + createUser, + updateUser, +}; diff --git a/src/app/modules/process/user-approved/users-list/table/UsersTable.tsx b/src/app/modules/process/user-approved/users-list/table/UsersTable.tsx new file mode 100644 index 0000000..f48b4b4 --- /dev/null +++ b/src/app/modules/process/user-approved/users-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/process/user-approved/users-list/table/columns/AddedCell.tsx b/src/app/modules/process/user-approved/users-list/table/columns/AddedCell.tsx new file mode 100644 index 0000000..6f5bc2b --- /dev/null +++ b/src/app/modules/process/user-approved/users-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/process/user-approved/users-list/table/columns/AgentCell.tsx b/src/app/modules/process/user-approved/users-list/table/columns/AgentCell.tsx new file mode 100644 index 0000000..063ade5 --- /dev/null +++ b/src/app/modules/process/user-approved/users-list/table/columns/AgentCell.tsx @@ -0,0 +1,11 @@ +import {FC} from 'react' + +type Props = { + agent?: string +} + +const AgentCell: FC = ({agent}) => ( + <> {agent &&
{agent}
} +) + +export {AgentCell} diff --git a/src/app/modules/process/user-approved/users-list/table/columns/CustomHeaderColumn.tsx b/src/app/modules/process/user-approved/users-list/table/columns/CustomHeaderColumn.tsx new file mode 100644 index 0000000..838a12e --- /dev/null +++ b/src/app/modules/process/user-approved/users-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/process/user-approved/users-list/table/columns/CustomRow.tsx b/src/app/modules/process/user-approved/users-list/table/columns/CustomRow.tsx new file mode 100644 index 0000000..a869cfe --- /dev/null +++ b/src/app/modules/process/user-approved/users-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/process/user-approved/users-list/table/columns/UserActionsCell.tsx b/src/app/modules/process/user-approved/users-list/table/columns/UserActionsCell.tsx new file mode 100644 index 0000000..9a91389 --- /dev/null +++ b/src/app/modules/process/user-approved/users-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/process/user-approved/users-list/table/columns/UserCustomHeader.tsx b/src/app/modules/process/user-approved/users-list/table/columns/UserCustomHeader.tsx new file mode 100644 index 0000000..3d0b58a --- /dev/null +++ b/src/app/modules/process/user-approved/users-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/process/user-approved/users-list/table/columns/UserInfoCell.tsx b/src/app/modules/process/user-approved/users-list/table/columns/UserInfoCell.tsx new file mode 100644 index 0000000..1840fef --- /dev/null +++ b/src/app/modules/process/user-approved/users-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/process/user-approved/users-list/table/columns/UserLastLoginCell.tsx b/src/app/modules/process/user-approved/users-list/table/columns/UserLastLoginCell.tsx new file mode 100644 index 0000000..a8a0ebe --- /dev/null +++ b/src/app/modules/process/user-approved/users-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/process/user-approved/users-list/table/columns/UserSelectionCell.tsx b/src/app/modules/process/user-approved/users-list/table/columns/UserSelectionCell.tsx new file mode 100644 index 0000000..dcd2bfb --- /dev/null +++ b/src/app/modules/process/user-approved/users-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/process/user-approved/users-list/table/columns/UserSelectionHeader.tsx b/src/app/modules/process/user-approved/users-list/table/columns/UserSelectionHeader.tsx new file mode 100644 index 0000000..bbb1eb0 --- /dev/null +++ b/src/app/modules/process/user-approved/users-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/process/user-approved/users-list/table/columns/_columns.tsx b/src/app/modules/process/user-approved/users-list/table/columns/_columns.tsx new file mode 100644 index 0000000..897f37a --- /dev/null +++ b/src/app/modules/process/user-approved/users-list/table/columns/_columns.tsx @@ -0,0 +1,57 @@ +import {Column} from 'react-table' +import {UserInfoCell} from './UserInfoCell' +import { PaymentMonthCell } from './UserLastLoginCell' +import {AgentCell} from './AgentCell' +import {UserActionsCell} from './UserActionsCell' +import {UserSelectionCell} from './UserSelectionCell' +import {UserCustomHeader} from './UserCustomHeader' +import {UserSelectionHeader} from './UserSelectionHeader' +import {User} from '../../core/_models' +import { AddedCell } from './AddedCell' + +const usersColumns: ReadonlyArray> = [ + { + Header: (props) => , + id: 'selection', + Cell: ({...props}) => , + }, + { + Header: (props) => , + id: 'firstname', + Cell: ({...props}) => , + }, + { + Header: (props) => , + accessor: 'loan_amount', + }, + { + Header: (props) => ( + + ), + id: 'payment_month', + Cell: ({...props}) => , + }, + { + Header: (props) => ( + + ), + id: 'sales_agent', + 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/process/user-approved/users-list/user-edit-modal/UserEditModal.tsx b/src/app/modules/process/user-approved/users-list/user-edit-modal/UserEditModal.tsx new file mode 100644 index 0000000..9bf605f --- /dev/null +++ b/src/app/modules/process/user-approved/users-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/process/user-approved/users-list/user-edit-modal/UserEditModalForm.tsx b/src/app/modules/process/user-approved/users-list/user-edit-modal/UserEditModalForm.tsx new file mode 100644 index 0000000..6e8b0ec --- /dev/null +++ b/src/app/modules/process/user-approved/users-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/process/user-approved/users-list/user-edit-modal/UserEditModalFormWrapper.tsx b/src/app/modules/process/user-approved/users-list/user-edit-modal/UserEditModalFormWrapper.tsx new file mode 100644 index 0000000..d60d7e3 --- /dev/null +++ b/src/app/modules/process/user-approved/users-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/process/user-approved/users-list/user-edit-modal/UserEditModalHeader.tsx b/src/app/modules/process/user-approved/users-list/user-edit-modal/UserEditModalHeader.tsx new file mode 100644 index 0000000..cb0f5b6 --- /dev/null +++ b/src/app/modules/process/user-approved/users-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/modules/process/user-pending/UsersPage.tsx b/src/app/modules/process/user-pending/UsersPage.tsx new file mode 100644 index 0000000..ec70d89 --- /dev/null +++ b/src/app/modules/process/user-pending/UsersPage.tsx @@ -0,0 +1,39 @@ +import {Route, Routes, Outlet, Navigate} from 'react-router-dom' +import {PageLink, PageTitle} from '../../../../_digifi/layout/core' +import {UsersListWrapper} from './users-list/UsersList' + +const usersBreadcrumbs: Array = [ + { + title: 'User Management', + path: '/apps/user-management/users', + isSeparator: false, + isActive: false, + }, + { + title: '', + path: '', + isSeparator: true, + isActive: false, + }, +] + +const UsersPage = () => { + return ( + + }> + + Users list + + + } + /> + + } /> + + ) +} + +export default UsersPage diff --git a/src/app/modules/process/user-pending/users-list/UsersList.tsx b/src/app/modules/process/user-pending/users-list/UsersList.tsx new file mode 100644 index 0000000..d8f1f10 --- /dev/null +++ b/src/app/modules/process/user-pending/users-list/UsersList.tsx @@ -0,0 +1,37 @@ +import {ListViewProvider, useListView} from './core/ListViewProvider' +import {QueryRequestProvider} from './core/QueryRequestProvider' +import {QueryResponseProvider} from './core/QueryResponseProvider' +import {UsersListHeader} from './components/header/UsersListHeader' +import {UsersTable} from './table/UsersTable' +import {UserEditModal} from './user-edit-modal/UserEditModal' +import {KTCard} from '../../../../../_digifi/helpers' +import { ToolbarWrapper } from '../../../../../_digifi/layout/components/toolbar' +import { Content } from '../../../../../_digifi/layout/components/content' + +const UsersList = () => { + const {itemIdForUpdate} = useListView() + return ( + <> + + + + + {itemIdForUpdate !== undefined && } + + ) +} + +const UsersListWrapper = () => ( + + + + + + + + + + +) + +export {UsersListWrapper} diff --git a/src/app/modules/process/user-pending/users-list/components/header/UserListToolbar.tsx b/src/app/modules/process/user-pending/users-list/components/header/UserListToolbar.tsx new file mode 100644 index 0000000..e86343b --- /dev/null +++ b/src/app/modules/process/user-pending/users-list/components/header/UserListToolbar.tsx @@ -0,0 +1,32 @@ +import {KTIcon} from '../../../../../../../_digifi/helpers' +import {useListView} from '../../core/ListViewProvider' +import {UsersListFilter} from './UsersListFilter' + +const UsersListToolbar = () => { + const {setItemIdForUpdate} = useListView() + const openAddUserModal = () => { + setItemIdForUpdate(null) + } + + return ( +
+ + + {/* begin::Export */} + {/* */} + {/* end::Export */} + + {/* begin::Add user */} + {/* */} + {/* end::Add user */} +
+ ) +} + +export {UsersListToolbar} diff --git a/src/app/modules/process/user-pending/users-list/components/header/UsersListFilter.tsx b/src/app/modules/process/user-pending/users-list/components/header/UsersListFilter.tsx new file mode 100644 index 0000000..4ac79a9 --- /dev/null +++ b/src/app/modules/process/user-pending/users-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/process/user-pending/users-list/components/header/UsersListGrouping.tsx b/src/app/modules/process/user-pending/users-list/components/header/UsersListGrouping.tsx new file mode 100644 index 0000000..6400fd7 --- /dev/null +++ b/src/app/modules/process/user-pending/users-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/process/user-pending/users-list/components/header/UsersListHeader.tsx b/src/app/modules/process/user-pending/users-list/components/header/UsersListHeader.tsx new file mode 100644 index 0000000..9d2a3a0 --- /dev/null +++ b/src/app/modules/process/user-pending/users-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/process/user-pending/users-list/components/header/UsersListSearchComponent.tsx b/src/app/modules/process/user-pending/users-list/components/header/UsersListSearchComponent.tsx new file mode 100644 index 0000000..75cba1c --- /dev/null +++ b/src/app/modules/process/user-pending/users-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/process/user-pending/users-list/components/loading/UsersListLoading.tsx b/src/app/modules/process/user-pending/users-list/components/loading/UsersListLoading.tsx new file mode 100644 index 0000000..2278f87 --- /dev/null +++ b/src/app/modules/process/user-pending/users-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/process/user-pending/users-list/components/pagination/UsersListPagination.tsx b/src/app/modules/process/user-pending/users-list/components/pagination/UsersListPagination.tsx new file mode 100644 index 0000000..2879f48 --- /dev/null +++ b/src/app/modules/process/user-pending/users-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/process/user-pending/users-list/core/ListViewProvider.tsx b/src/app/modules/process/user-pending/users-list/core/ListViewProvider.tsx new file mode 100644 index 0000000..91cc2cf --- /dev/null +++ b/src/app/modules/process/user-pending/users-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/process/user-pending/users-list/core/QueryRequestProvider.tsx b/src/app/modules/process/user-pending/users-list/core/QueryRequestProvider.tsx new file mode 100644 index 0000000..95f31cf --- /dev/null +++ b/src/app/modules/process/user-pending/users-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/process/user-pending/users-list/core/QueryResponseProvider.tsx b/src/app/modules/process/user-pending/users-list/core/QueryResponseProvider.tsx new file mode 100644 index 0000000..3f7cffe --- /dev/null +++ b/src/app/modules/process/user-pending/users-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 {getStartedUsers} 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.USERS_LIST}-${query}`, + () => { + return getStartedUsers(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/process/user-pending/users-list/core/_models.ts b/src/app/modules/process/user-pending/users-list/core/_models.ts new file mode 100644 index 0000000..b9b06ec --- /dev/null +++ b/src/app/modules/process/user-pending/users-list/core/_models.ts @@ -0,0 +1,42 @@ +import {ID, Response} from '../../../../../../_digifi/helpers' +export type User = { + id?: ID + name?: string + avatar?: string + // email?: string + position?: string + role?: string + last_login?: string + two_steps?: boolean + joined_day?: string + online?: boolean + initials?: { + label: string + state: string + } + firstname?: string, + lastname?: string + uid?: string + loan_amount?: string + payment_month?: string + sales_agent?: string + gender?: string | null + marital_status?: string + email?: string + address?: string + state?: string + country?: string + status?: string + added?: string + updated?: string +} + +export type UsersQueryResponse = Response> + +export const initialUser: User = { + avatar: 'avatars/300-6.jpg', + position: 'Art Director', + role: 'Administrator', + name: '', + email: '', +} diff --git a/src/app/modules/process/user-pending/users-list/core/_requests.ts b/src/app/modules/process/user-pending/users-list/core/_requests.ts new file mode 100644 index 0000000..a585521 --- /dev/null +++ b/src/app/modules/process/user-pending/users-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 getStartedUsers = (query: string): Promise => { // FUNCTION TO GET USERS THAT HAVE STARTED LOAN APPLICATION + return axios + .get(`${NEW_USER_ENDPOINT}/loan/started`) + .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 { + getStartedUsers, + deleteUser, + deleteSelectedUsers, + getUserById, + createUser, + updateUser, +}; diff --git a/src/app/modules/process/user-pending/users-list/table/UsersTable.tsx b/src/app/modules/process/user-pending/users-list/table/UsersTable.tsx new file mode 100644 index 0000000..f48b4b4 --- /dev/null +++ b/src/app/modules/process/user-pending/users-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/process/user-pending/users-list/table/columns/AddedCell.tsx b/src/app/modules/process/user-pending/users-list/table/columns/AddedCell.tsx new file mode 100644 index 0000000..6f5bc2b --- /dev/null +++ b/src/app/modules/process/user-pending/users-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/process/user-pending/users-list/table/columns/AgentCell.tsx b/src/app/modules/process/user-pending/users-list/table/columns/AgentCell.tsx new file mode 100644 index 0000000..063ade5 --- /dev/null +++ b/src/app/modules/process/user-pending/users-list/table/columns/AgentCell.tsx @@ -0,0 +1,11 @@ +import {FC} from 'react' + +type Props = { + agent?: string +} + +const AgentCell: FC = ({agent}) => ( + <> {agent &&
{agent}
} +) + +export {AgentCell} diff --git a/src/app/modules/process/user-pending/users-list/table/columns/CustomHeaderColumn.tsx b/src/app/modules/process/user-pending/users-list/table/columns/CustomHeaderColumn.tsx new file mode 100644 index 0000000..838a12e --- /dev/null +++ b/src/app/modules/process/user-pending/users-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/process/user-pending/users-list/table/columns/CustomRow.tsx b/src/app/modules/process/user-pending/users-list/table/columns/CustomRow.tsx new file mode 100644 index 0000000..a869cfe --- /dev/null +++ b/src/app/modules/process/user-pending/users-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/process/user-pending/users-list/table/columns/UserActionsCell.tsx b/src/app/modules/process/user-pending/users-list/table/columns/UserActionsCell.tsx new file mode 100644 index 0000000..9a91389 --- /dev/null +++ b/src/app/modules/process/user-pending/users-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/process/user-pending/users-list/table/columns/UserCustomHeader.tsx b/src/app/modules/process/user-pending/users-list/table/columns/UserCustomHeader.tsx new file mode 100644 index 0000000..3d0b58a --- /dev/null +++ b/src/app/modules/process/user-pending/users-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/process/user-pending/users-list/table/columns/UserInfoCell.tsx b/src/app/modules/process/user-pending/users-list/table/columns/UserInfoCell.tsx new file mode 100644 index 0000000..1840fef --- /dev/null +++ b/src/app/modules/process/user-pending/users-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/process/user-pending/users-list/table/columns/UserLastLoginCell.tsx b/src/app/modules/process/user-pending/users-list/table/columns/UserLastLoginCell.tsx new file mode 100644 index 0000000..a8a0ebe --- /dev/null +++ b/src/app/modules/process/user-pending/users-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/process/user-pending/users-list/table/columns/UserSelectionCell.tsx b/src/app/modules/process/user-pending/users-list/table/columns/UserSelectionCell.tsx new file mode 100644 index 0000000..dcd2bfb --- /dev/null +++ b/src/app/modules/process/user-pending/users-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/process/user-pending/users-list/table/columns/UserSelectionHeader.tsx b/src/app/modules/process/user-pending/users-list/table/columns/UserSelectionHeader.tsx new file mode 100644 index 0000000..bbb1eb0 --- /dev/null +++ b/src/app/modules/process/user-pending/users-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/process/user-pending/users-list/table/columns/_columns.tsx b/src/app/modules/process/user-pending/users-list/table/columns/_columns.tsx new file mode 100644 index 0000000..897f37a --- /dev/null +++ b/src/app/modules/process/user-pending/users-list/table/columns/_columns.tsx @@ -0,0 +1,57 @@ +import {Column} from 'react-table' +import {UserInfoCell} from './UserInfoCell' +import { PaymentMonthCell } from './UserLastLoginCell' +import {AgentCell} from './AgentCell' +import {UserActionsCell} from './UserActionsCell' +import {UserSelectionCell} from './UserSelectionCell' +import {UserCustomHeader} from './UserCustomHeader' +import {UserSelectionHeader} from './UserSelectionHeader' +import {User} from '../../core/_models' +import { AddedCell } from './AddedCell' + +const usersColumns: ReadonlyArray> = [ + { + Header: (props) => , + id: 'selection', + Cell: ({...props}) => , + }, + { + Header: (props) => , + id: 'firstname', + Cell: ({...props}) => , + }, + { + Header: (props) => , + accessor: 'loan_amount', + }, + { + Header: (props) => ( + + ), + id: 'payment_month', + Cell: ({...props}) => , + }, + { + Header: (props) => ( + + ), + id: 'sales_agent', + 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/process/user-pending/users-list/user-edit-modal/UserEditModal.tsx b/src/app/modules/process/user-pending/users-list/user-edit-modal/UserEditModal.tsx new file mode 100644 index 0000000..9bf605f --- /dev/null +++ b/src/app/modules/process/user-pending/users-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/process/user-pending/users-list/user-edit-modal/UserEditModalForm.tsx b/src/app/modules/process/user-pending/users-list/user-edit-modal/UserEditModalForm.tsx new file mode 100644 index 0000000..6e8b0ec --- /dev/null +++ b/src/app/modules/process/user-pending/users-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/process/user-pending/users-list/user-edit-modal/UserEditModalFormWrapper.tsx b/src/app/modules/process/user-pending/users-list/user-edit-modal/UserEditModalFormWrapper.tsx new file mode 100644 index 0000000..d60d7e3 --- /dev/null +++ b/src/app/modules/process/user-pending/users-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/process/user-pending/users-list/user-edit-modal/UserEditModalHeader.tsx b/src/app/modules/process/user-pending/users-list/user-edit-modal/UserEditModalHeader.tsx new file mode 100644 index 0000000..cb0f5b6 --- /dev/null +++ b/src/app/modules/process/user-pending/users-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/modules/process/user-ready/UsersPage.tsx b/src/app/modules/process/user-ready/UsersPage.tsx new file mode 100644 index 0000000..ec70d89 --- /dev/null +++ b/src/app/modules/process/user-ready/UsersPage.tsx @@ -0,0 +1,39 @@ +import {Route, Routes, Outlet, Navigate} from 'react-router-dom' +import {PageLink, PageTitle} from '../../../../_digifi/layout/core' +import {UsersListWrapper} from './users-list/UsersList' + +const usersBreadcrumbs: Array = [ + { + title: 'User Management', + path: '/apps/user-management/users', + isSeparator: false, + isActive: false, + }, + { + title: '', + path: '', + isSeparator: true, + isActive: false, + }, +] + +const UsersPage = () => { + return ( + + }> + + Users list + + + } + /> + + } /> + + ) +} + +export default UsersPage diff --git a/src/app/modules/process/user-ready/users-list/UsersList.tsx b/src/app/modules/process/user-ready/users-list/UsersList.tsx new file mode 100644 index 0000000..d8f1f10 --- /dev/null +++ b/src/app/modules/process/user-ready/users-list/UsersList.tsx @@ -0,0 +1,37 @@ +import {ListViewProvider, useListView} from './core/ListViewProvider' +import {QueryRequestProvider} from './core/QueryRequestProvider' +import {QueryResponseProvider} from './core/QueryResponseProvider' +import {UsersListHeader} from './components/header/UsersListHeader' +import {UsersTable} from './table/UsersTable' +import {UserEditModal} from './user-edit-modal/UserEditModal' +import {KTCard} from '../../../../../_digifi/helpers' +import { ToolbarWrapper } from '../../../../../_digifi/layout/components/toolbar' +import { Content } from '../../../../../_digifi/layout/components/content' + +const UsersList = () => { + const {itemIdForUpdate} = useListView() + return ( + <> + + + + + {itemIdForUpdate !== undefined && } + + ) +} + +const UsersListWrapper = () => ( + + + + + + + + + + +) + +export {UsersListWrapper} diff --git a/src/app/modules/process/user-ready/users-list/components/header/UserListToolbar.tsx b/src/app/modules/process/user-ready/users-list/components/header/UserListToolbar.tsx new file mode 100644 index 0000000..e86343b --- /dev/null +++ b/src/app/modules/process/user-ready/users-list/components/header/UserListToolbar.tsx @@ -0,0 +1,32 @@ +import {KTIcon} from '../../../../../../../_digifi/helpers' +import {useListView} from '../../core/ListViewProvider' +import {UsersListFilter} from './UsersListFilter' + +const UsersListToolbar = () => { + const {setItemIdForUpdate} = useListView() + const openAddUserModal = () => { + setItemIdForUpdate(null) + } + + return ( +
+ + + {/* begin::Export */} + {/* */} + {/* end::Export */} + + {/* begin::Add user */} + {/* */} + {/* end::Add user */} +
+ ) +} + +export {UsersListToolbar} diff --git a/src/app/modules/process/user-ready/users-list/components/header/UsersListFilter.tsx b/src/app/modules/process/user-ready/users-list/components/header/UsersListFilter.tsx new file mode 100644 index 0000000..4ac79a9 --- /dev/null +++ b/src/app/modules/process/user-ready/users-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/process/user-ready/users-list/components/header/UsersListGrouping.tsx b/src/app/modules/process/user-ready/users-list/components/header/UsersListGrouping.tsx new file mode 100644 index 0000000..6400fd7 --- /dev/null +++ b/src/app/modules/process/user-ready/users-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/process/user-ready/users-list/components/header/UsersListHeader.tsx b/src/app/modules/process/user-ready/users-list/components/header/UsersListHeader.tsx new file mode 100644 index 0000000..9d2a3a0 --- /dev/null +++ b/src/app/modules/process/user-ready/users-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/process/user-ready/users-list/components/header/UsersListSearchComponent.tsx b/src/app/modules/process/user-ready/users-list/components/header/UsersListSearchComponent.tsx new file mode 100644 index 0000000..75cba1c --- /dev/null +++ b/src/app/modules/process/user-ready/users-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/process/user-ready/users-list/components/loading/UsersListLoading.tsx b/src/app/modules/process/user-ready/users-list/components/loading/UsersListLoading.tsx new file mode 100644 index 0000000..2278f87 --- /dev/null +++ b/src/app/modules/process/user-ready/users-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/process/user-ready/users-list/components/pagination/UsersListPagination.tsx b/src/app/modules/process/user-ready/users-list/components/pagination/UsersListPagination.tsx new file mode 100644 index 0000000..2879f48 --- /dev/null +++ b/src/app/modules/process/user-ready/users-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/process/user-ready/users-list/core/ListViewProvider.tsx b/src/app/modules/process/user-ready/users-list/core/ListViewProvider.tsx new file mode 100644 index 0000000..91cc2cf --- /dev/null +++ b/src/app/modules/process/user-ready/users-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/process/user-ready/users-list/core/QueryRequestProvider.tsx b/src/app/modules/process/user-ready/users-list/core/QueryRequestProvider.tsx new file mode 100644 index 0000000..95f31cf --- /dev/null +++ b/src/app/modules/process/user-ready/users-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/process/user-ready/users-list/core/QueryResponseProvider.tsx b/src/app/modules/process/user-ready/users-list/core/QueryResponseProvider.tsx new file mode 100644 index 0000000..3f7cffe --- /dev/null +++ b/src/app/modules/process/user-ready/users-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 {getStartedUsers} 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.USERS_LIST}-${query}`, + () => { + return getStartedUsers(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/process/user-ready/users-list/core/_models.ts b/src/app/modules/process/user-ready/users-list/core/_models.ts new file mode 100644 index 0000000..b9b06ec --- /dev/null +++ b/src/app/modules/process/user-ready/users-list/core/_models.ts @@ -0,0 +1,42 @@ +import {ID, Response} from '../../../../../../_digifi/helpers' +export type User = { + id?: ID + name?: string + avatar?: string + // email?: string + position?: string + role?: string + last_login?: string + two_steps?: boolean + joined_day?: string + online?: boolean + initials?: { + label: string + state: string + } + firstname?: string, + lastname?: string + uid?: string + loan_amount?: string + payment_month?: string + sales_agent?: string + gender?: string | null + marital_status?: string + email?: string + address?: string + state?: string + country?: string + status?: string + added?: string + updated?: string +} + +export type UsersQueryResponse = Response> + +export const initialUser: User = { + avatar: 'avatars/300-6.jpg', + position: 'Art Director', + role: 'Administrator', + name: '', + email: '', +} diff --git a/src/app/modules/process/user-ready/users-list/core/_requests.ts b/src/app/modules/process/user-ready/users-list/core/_requests.ts new file mode 100644 index 0000000..a585521 --- /dev/null +++ b/src/app/modules/process/user-ready/users-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 getStartedUsers = (query: string): Promise => { // FUNCTION TO GET USERS THAT HAVE STARTED LOAN APPLICATION + return axios + .get(`${NEW_USER_ENDPOINT}/loan/started`) + .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 { + getStartedUsers, + deleteUser, + deleteSelectedUsers, + getUserById, + createUser, + updateUser, +}; diff --git a/src/app/modules/process/user-ready/users-list/table/UsersTable.tsx b/src/app/modules/process/user-ready/users-list/table/UsersTable.tsx new file mode 100644 index 0000000..f48b4b4 --- /dev/null +++ b/src/app/modules/process/user-ready/users-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/process/user-ready/users-list/table/columns/AddedCell.tsx b/src/app/modules/process/user-ready/users-list/table/columns/AddedCell.tsx new file mode 100644 index 0000000..6f5bc2b --- /dev/null +++ b/src/app/modules/process/user-ready/users-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/process/user-ready/users-list/table/columns/AgentCell.tsx b/src/app/modules/process/user-ready/users-list/table/columns/AgentCell.tsx new file mode 100644 index 0000000..063ade5 --- /dev/null +++ b/src/app/modules/process/user-ready/users-list/table/columns/AgentCell.tsx @@ -0,0 +1,11 @@ +import {FC} from 'react' + +type Props = { + agent?: string +} + +const AgentCell: FC = ({agent}) => ( + <> {agent &&
{agent}
} +) + +export {AgentCell} diff --git a/src/app/modules/process/user-ready/users-list/table/columns/CustomHeaderColumn.tsx b/src/app/modules/process/user-ready/users-list/table/columns/CustomHeaderColumn.tsx new file mode 100644 index 0000000..838a12e --- /dev/null +++ b/src/app/modules/process/user-ready/users-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/process/user-ready/users-list/table/columns/CustomRow.tsx b/src/app/modules/process/user-ready/users-list/table/columns/CustomRow.tsx new file mode 100644 index 0000000..a869cfe --- /dev/null +++ b/src/app/modules/process/user-ready/users-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/process/user-ready/users-list/table/columns/UserActionsCell.tsx b/src/app/modules/process/user-ready/users-list/table/columns/UserActionsCell.tsx new file mode 100644 index 0000000..9a91389 --- /dev/null +++ b/src/app/modules/process/user-ready/users-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/process/user-ready/users-list/table/columns/UserCustomHeader.tsx b/src/app/modules/process/user-ready/users-list/table/columns/UserCustomHeader.tsx new file mode 100644 index 0000000..3d0b58a --- /dev/null +++ b/src/app/modules/process/user-ready/users-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/process/user-ready/users-list/table/columns/UserInfoCell.tsx b/src/app/modules/process/user-ready/users-list/table/columns/UserInfoCell.tsx new file mode 100644 index 0000000..1840fef --- /dev/null +++ b/src/app/modules/process/user-ready/users-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/process/user-ready/users-list/table/columns/UserLastLoginCell.tsx b/src/app/modules/process/user-ready/users-list/table/columns/UserLastLoginCell.tsx new file mode 100644 index 0000000..a8a0ebe --- /dev/null +++ b/src/app/modules/process/user-ready/users-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/process/user-ready/users-list/table/columns/UserSelectionCell.tsx b/src/app/modules/process/user-ready/users-list/table/columns/UserSelectionCell.tsx new file mode 100644 index 0000000..dcd2bfb --- /dev/null +++ b/src/app/modules/process/user-ready/users-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/process/user-ready/users-list/table/columns/UserSelectionHeader.tsx b/src/app/modules/process/user-ready/users-list/table/columns/UserSelectionHeader.tsx new file mode 100644 index 0000000..bbb1eb0 --- /dev/null +++ b/src/app/modules/process/user-ready/users-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/process/user-ready/users-list/table/columns/_columns.tsx b/src/app/modules/process/user-ready/users-list/table/columns/_columns.tsx new file mode 100644 index 0000000..897f37a --- /dev/null +++ b/src/app/modules/process/user-ready/users-list/table/columns/_columns.tsx @@ -0,0 +1,57 @@ +import {Column} from 'react-table' +import {UserInfoCell} from './UserInfoCell' +import { PaymentMonthCell } from './UserLastLoginCell' +import {AgentCell} from './AgentCell' +import {UserActionsCell} from './UserActionsCell' +import {UserSelectionCell} from './UserSelectionCell' +import {UserCustomHeader} from './UserCustomHeader' +import {UserSelectionHeader} from './UserSelectionHeader' +import {User} from '../../core/_models' +import { AddedCell } from './AddedCell' + +const usersColumns: ReadonlyArray> = [ + { + Header: (props) => , + id: 'selection', + Cell: ({...props}) => , + }, + { + Header: (props) => , + id: 'firstname', + Cell: ({...props}) => , + }, + { + Header: (props) => , + accessor: 'loan_amount', + }, + { + Header: (props) => ( + + ), + id: 'payment_month', + Cell: ({...props}) => , + }, + { + Header: (props) => ( + + ), + id: 'sales_agent', + 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/process/user-ready/users-list/user-edit-modal/UserEditModal.tsx b/src/app/modules/process/user-ready/users-list/user-edit-modal/UserEditModal.tsx new file mode 100644 index 0000000..9bf605f --- /dev/null +++ b/src/app/modules/process/user-ready/users-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/process/user-ready/users-list/user-edit-modal/UserEditModalForm.tsx b/src/app/modules/process/user-ready/users-list/user-edit-modal/UserEditModalForm.tsx new file mode 100644 index 0000000..6e8b0ec --- /dev/null +++ b/src/app/modules/process/user-ready/users-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/process/user-ready/users-list/user-edit-modal/UserEditModalFormWrapper.tsx b/src/app/modules/process/user-ready/users-list/user-edit-modal/UserEditModalFormWrapper.tsx new file mode 100644 index 0000000..d60d7e3 --- /dev/null +++ b/src/app/modules/process/user-ready/users-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/process/user-ready/users-list/user-edit-modal/UserEditModalHeader.tsx b/src/app/modules/process/user-ready/users-list/user-edit-modal/UserEditModalHeader.tsx new file mode 100644 index 0000000..cb0f5b6 --- /dev/null +++ b/src/app/modules/process/user-ready/users-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/modules/process/user-rejected/UsersPage.tsx b/src/app/modules/process/user-rejected/UsersPage.tsx new file mode 100644 index 0000000..ec70d89 --- /dev/null +++ b/src/app/modules/process/user-rejected/UsersPage.tsx @@ -0,0 +1,39 @@ +import {Route, Routes, Outlet, Navigate} from 'react-router-dom' +import {PageLink, PageTitle} from '../../../../_digifi/layout/core' +import {UsersListWrapper} from './users-list/UsersList' + +const usersBreadcrumbs: Array = [ + { + title: 'User Management', + path: '/apps/user-management/users', + isSeparator: false, + isActive: false, + }, + { + title: '', + path: '', + isSeparator: true, + isActive: false, + }, +] + +const UsersPage = () => { + return ( + + }> + + Users list + + + } + /> + + } /> + + ) +} + +export default UsersPage diff --git a/src/app/modules/process/user-rejected/users-list/UsersList.tsx b/src/app/modules/process/user-rejected/users-list/UsersList.tsx new file mode 100644 index 0000000..d8f1f10 --- /dev/null +++ b/src/app/modules/process/user-rejected/users-list/UsersList.tsx @@ -0,0 +1,37 @@ +import {ListViewProvider, useListView} from './core/ListViewProvider' +import {QueryRequestProvider} from './core/QueryRequestProvider' +import {QueryResponseProvider} from './core/QueryResponseProvider' +import {UsersListHeader} from './components/header/UsersListHeader' +import {UsersTable} from './table/UsersTable' +import {UserEditModal} from './user-edit-modal/UserEditModal' +import {KTCard} from '../../../../../_digifi/helpers' +import { ToolbarWrapper } from '../../../../../_digifi/layout/components/toolbar' +import { Content } from '../../../../../_digifi/layout/components/content' + +const UsersList = () => { + const {itemIdForUpdate} = useListView() + return ( + <> + + + + + {itemIdForUpdate !== undefined && } + + ) +} + +const UsersListWrapper = () => ( + + + + + + + + + + +) + +export {UsersListWrapper} diff --git a/src/app/modules/process/user-rejected/users-list/components/header/UserListToolbar.tsx b/src/app/modules/process/user-rejected/users-list/components/header/UserListToolbar.tsx new file mode 100644 index 0000000..e86343b --- /dev/null +++ b/src/app/modules/process/user-rejected/users-list/components/header/UserListToolbar.tsx @@ -0,0 +1,32 @@ +import {KTIcon} from '../../../../../../../_digifi/helpers' +import {useListView} from '../../core/ListViewProvider' +import {UsersListFilter} from './UsersListFilter' + +const UsersListToolbar = () => { + const {setItemIdForUpdate} = useListView() + const openAddUserModal = () => { + setItemIdForUpdate(null) + } + + return ( +
+ + + {/* begin::Export */} + {/* */} + {/* end::Export */} + + {/* begin::Add user */} + {/* */} + {/* end::Add user */} +
+ ) +} + +export {UsersListToolbar} diff --git a/src/app/modules/process/user-rejected/users-list/components/header/UsersListFilter.tsx b/src/app/modules/process/user-rejected/users-list/components/header/UsersListFilter.tsx new file mode 100644 index 0000000..4ac79a9 --- /dev/null +++ b/src/app/modules/process/user-rejected/users-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/process/user-rejected/users-list/components/header/UsersListGrouping.tsx b/src/app/modules/process/user-rejected/users-list/components/header/UsersListGrouping.tsx new file mode 100644 index 0000000..6400fd7 --- /dev/null +++ b/src/app/modules/process/user-rejected/users-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/process/user-rejected/users-list/components/header/UsersListHeader.tsx b/src/app/modules/process/user-rejected/users-list/components/header/UsersListHeader.tsx new file mode 100644 index 0000000..9d2a3a0 --- /dev/null +++ b/src/app/modules/process/user-rejected/users-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/process/user-rejected/users-list/components/header/UsersListSearchComponent.tsx b/src/app/modules/process/user-rejected/users-list/components/header/UsersListSearchComponent.tsx new file mode 100644 index 0000000..75cba1c --- /dev/null +++ b/src/app/modules/process/user-rejected/users-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/process/user-rejected/users-list/components/loading/UsersListLoading.tsx b/src/app/modules/process/user-rejected/users-list/components/loading/UsersListLoading.tsx new file mode 100644 index 0000000..2278f87 --- /dev/null +++ b/src/app/modules/process/user-rejected/users-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/process/user-rejected/users-list/components/pagination/UsersListPagination.tsx b/src/app/modules/process/user-rejected/users-list/components/pagination/UsersListPagination.tsx new file mode 100644 index 0000000..2879f48 --- /dev/null +++ b/src/app/modules/process/user-rejected/users-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/process/user-rejected/users-list/core/ListViewProvider.tsx b/src/app/modules/process/user-rejected/users-list/core/ListViewProvider.tsx new file mode 100644 index 0000000..91cc2cf --- /dev/null +++ b/src/app/modules/process/user-rejected/users-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/process/user-rejected/users-list/core/QueryRequestProvider.tsx b/src/app/modules/process/user-rejected/users-list/core/QueryRequestProvider.tsx new file mode 100644 index 0000000..95f31cf --- /dev/null +++ b/src/app/modules/process/user-rejected/users-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/process/user-rejected/users-list/core/QueryResponseProvider.tsx b/src/app/modules/process/user-rejected/users-list/core/QueryResponseProvider.tsx new file mode 100644 index 0000000..3f7cffe --- /dev/null +++ b/src/app/modules/process/user-rejected/users-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 {getStartedUsers} 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.USERS_LIST}-${query}`, + () => { + return getStartedUsers(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/process/user-rejected/users-list/core/_models.ts b/src/app/modules/process/user-rejected/users-list/core/_models.ts new file mode 100644 index 0000000..b9b06ec --- /dev/null +++ b/src/app/modules/process/user-rejected/users-list/core/_models.ts @@ -0,0 +1,42 @@ +import {ID, Response} from '../../../../../../_digifi/helpers' +export type User = { + id?: ID + name?: string + avatar?: string + // email?: string + position?: string + role?: string + last_login?: string + two_steps?: boolean + joined_day?: string + online?: boolean + initials?: { + label: string + state: string + } + firstname?: string, + lastname?: string + uid?: string + loan_amount?: string + payment_month?: string + sales_agent?: string + gender?: string | null + marital_status?: string + email?: string + address?: string + state?: string + country?: string + status?: string + added?: string + updated?: string +} + +export type UsersQueryResponse = Response> + +export const initialUser: User = { + avatar: 'avatars/300-6.jpg', + position: 'Art Director', + role: 'Administrator', + name: '', + email: '', +} diff --git a/src/app/modules/process/user-rejected/users-list/core/_requests.ts b/src/app/modules/process/user-rejected/users-list/core/_requests.ts new file mode 100644 index 0000000..a585521 --- /dev/null +++ b/src/app/modules/process/user-rejected/users-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 getStartedUsers = (query: string): Promise => { // FUNCTION TO GET USERS THAT HAVE STARTED LOAN APPLICATION + return axios + .get(`${NEW_USER_ENDPOINT}/loan/started`) + .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 { + getStartedUsers, + deleteUser, + deleteSelectedUsers, + getUserById, + createUser, + updateUser, +}; diff --git a/src/app/modules/process/user-rejected/users-list/table/UsersTable.tsx b/src/app/modules/process/user-rejected/users-list/table/UsersTable.tsx new file mode 100644 index 0000000..f48b4b4 --- /dev/null +++ b/src/app/modules/process/user-rejected/users-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/process/user-rejected/users-list/table/columns/AddedCell.tsx b/src/app/modules/process/user-rejected/users-list/table/columns/AddedCell.tsx new file mode 100644 index 0000000..6f5bc2b --- /dev/null +++ b/src/app/modules/process/user-rejected/users-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/process/user-rejected/users-list/table/columns/AgentCell.tsx b/src/app/modules/process/user-rejected/users-list/table/columns/AgentCell.tsx new file mode 100644 index 0000000..063ade5 --- /dev/null +++ b/src/app/modules/process/user-rejected/users-list/table/columns/AgentCell.tsx @@ -0,0 +1,11 @@ +import {FC} from 'react' + +type Props = { + agent?: string +} + +const AgentCell: FC = ({agent}) => ( + <> {agent &&
{agent}
} +) + +export {AgentCell} diff --git a/src/app/modules/process/user-rejected/users-list/table/columns/CustomHeaderColumn.tsx b/src/app/modules/process/user-rejected/users-list/table/columns/CustomHeaderColumn.tsx new file mode 100644 index 0000000..838a12e --- /dev/null +++ b/src/app/modules/process/user-rejected/users-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/process/user-rejected/users-list/table/columns/CustomRow.tsx b/src/app/modules/process/user-rejected/users-list/table/columns/CustomRow.tsx new file mode 100644 index 0000000..a869cfe --- /dev/null +++ b/src/app/modules/process/user-rejected/users-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/process/user-rejected/users-list/table/columns/UserActionsCell.tsx b/src/app/modules/process/user-rejected/users-list/table/columns/UserActionsCell.tsx new file mode 100644 index 0000000..9a91389 --- /dev/null +++ b/src/app/modules/process/user-rejected/users-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/process/user-rejected/users-list/table/columns/UserCustomHeader.tsx b/src/app/modules/process/user-rejected/users-list/table/columns/UserCustomHeader.tsx new file mode 100644 index 0000000..3d0b58a --- /dev/null +++ b/src/app/modules/process/user-rejected/users-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/process/user-rejected/users-list/table/columns/UserInfoCell.tsx b/src/app/modules/process/user-rejected/users-list/table/columns/UserInfoCell.tsx new file mode 100644 index 0000000..1840fef --- /dev/null +++ b/src/app/modules/process/user-rejected/users-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/process/user-rejected/users-list/table/columns/UserLastLoginCell.tsx b/src/app/modules/process/user-rejected/users-list/table/columns/UserLastLoginCell.tsx new file mode 100644 index 0000000..a8a0ebe --- /dev/null +++ b/src/app/modules/process/user-rejected/users-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/process/user-rejected/users-list/table/columns/UserSelectionCell.tsx b/src/app/modules/process/user-rejected/users-list/table/columns/UserSelectionCell.tsx new file mode 100644 index 0000000..dcd2bfb --- /dev/null +++ b/src/app/modules/process/user-rejected/users-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/process/user-rejected/users-list/table/columns/UserSelectionHeader.tsx b/src/app/modules/process/user-rejected/users-list/table/columns/UserSelectionHeader.tsx new file mode 100644 index 0000000..bbb1eb0 --- /dev/null +++ b/src/app/modules/process/user-rejected/users-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/process/user-rejected/users-list/table/columns/_columns.tsx b/src/app/modules/process/user-rejected/users-list/table/columns/_columns.tsx new file mode 100644 index 0000000..897f37a --- /dev/null +++ b/src/app/modules/process/user-rejected/users-list/table/columns/_columns.tsx @@ -0,0 +1,57 @@ +import {Column} from 'react-table' +import {UserInfoCell} from './UserInfoCell' +import { PaymentMonthCell } from './UserLastLoginCell' +import {AgentCell} from './AgentCell' +import {UserActionsCell} from './UserActionsCell' +import {UserSelectionCell} from './UserSelectionCell' +import {UserCustomHeader} from './UserCustomHeader' +import {UserSelectionHeader} from './UserSelectionHeader' +import {User} from '../../core/_models' +import { AddedCell } from './AddedCell' + +const usersColumns: ReadonlyArray> = [ + { + Header: (props) => , + id: 'selection', + Cell: ({...props}) => , + }, + { + Header: (props) => , + id: 'firstname', + Cell: ({...props}) => , + }, + { + Header: (props) => , + accessor: 'loan_amount', + }, + { + Header: (props) => ( + + ), + id: 'payment_month', + Cell: ({...props}) => , + }, + { + Header: (props) => ( + + ), + id: 'sales_agent', + 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/process/user-rejected/users-list/user-edit-modal/UserEditModal.tsx b/src/app/modules/process/user-rejected/users-list/user-edit-modal/UserEditModal.tsx new file mode 100644 index 0000000..9bf605f --- /dev/null +++ b/src/app/modules/process/user-rejected/users-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/process/user-rejected/users-list/user-edit-modal/UserEditModalForm.tsx b/src/app/modules/process/user-rejected/users-list/user-edit-modal/UserEditModalForm.tsx new file mode 100644 index 0000000..6e8b0ec --- /dev/null +++ b/src/app/modules/process/user-rejected/users-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/process/user-rejected/users-list/user-edit-modal/UserEditModalFormWrapper.tsx b/src/app/modules/process/user-rejected/users-list/user-edit-modal/UserEditModalFormWrapper.tsx new file mode 100644 index 0000000..d60d7e3 --- /dev/null +++ b/src/app/modules/process/user-rejected/users-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/process/user-rejected/users-list/user-edit-modal/UserEditModalHeader.tsx b/src/app/modules/process/user-rejected/users-list/user-edit-modal/UserEditModalHeader.tsx new file mode 100644 index 0000000..cb0f5b6 --- /dev/null +++ b/src/app/modules/process/user-rejected/users-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/modules/process/user-started/UsersPage.tsx b/src/app/modules/process/user-started/UsersPage.tsx new file mode 100644 index 0000000..ec70d89 --- /dev/null +++ b/src/app/modules/process/user-started/UsersPage.tsx @@ -0,0 +1,39 @@ +import {Route, Routes, Outlet, Navigate} from 'react-router-dom' +import {PageLink, PageTitle} from '../../../../_digifi/layout/core' +import {UsersListWrapper} from './users-list/UsersList' + +const usersBreadcrumbs: Array = [ + { + title: 'User Management', + path: '/apps/user-management/users', + isSeparator: false, + isActive: false, + }, + { + title: '', + path: '', + isSeparator: true, + isActive: false, + }, +] + +const UsersPage = () => { + return ( + + }> + + Users list + + + } + /> + + } /> + + ) +} + +export default UsersPage diff --git a/src/app/modules/process/user-started/users-list/UsersList.tsx b/src/app/modules/process/user-started/users-list/UsersList.tsx new file mode 100644 index 0000000..d8f1f10 --- /dev/null +++ b/src/app/modules/process/user-started/users-list/UsersList.tsx @@ -0,0 +1,37 @@ +import {ListViewProvider, useListView} from './core/ListViewProvider' +import {QueryRequestProvider} from './core/QueryRequestProvider' +import {QueryResponseProvider} from './core/QueryResponseProvider' +import {UsersListHeader} from './components/header/UsersListHeader' +import {UsersTable} from './table/UsersTable' +import {UserEditModal} from './user-edit-modal/UserEditModal' +import {KTCard} from '../../../../../_digifi/helpers' +import { ToolbarWrapper } from '../../../../../_digifi/layout/components/toolbar' +import { Content } from '../../../../../_digifi/layout/components/content' + +const UsersList = () => { + const {itemIdForUpdate} = useListView() + return ( + <> + + + + + {itemIdForUpdate !== undefined && } + + ) +} + +const UsersListWrapper = () => ( + + + + + + + + + + +) + +export {UsersListWrapper} diff --git a/src/app/modules/process/user-started/users-list/components/header/UserListToolbar.tsx b/src/app/modules/process/user-started/users-list/components/header/UserListToolbar.tsx new file mode 100644 index 0000000..e86343b --- /dev/null +++ b/src/app/modules/process/user-started/users-list/components/header/UserListToolbar.tsx @@ -0,0 +1,32 @@ +import {KTIcon} from '../../../../../../../_digifi/helpers' +import {useListView} from '../../core/ListViewProvider' +import {UsersListFilter} from './UsersListFilter' + +const UsersListToolbar = () => { + const {setItemIdForUpdate} = useListView() + const openAddUserModal = () => { + setItemIdForUpdate(null) + } + + return ( +
+ + + {/* begin::Export */} + {/* */} + {/* end::Export */} + + {/* begin::Add user */} + {/* */} + {/* end::Add user */} +
+ ) +} + +export {UsersListToolbar} diff --git a/src/app/modules/process/user-started/users-list/components/header/UsersListFilter.tsx b/src/app/modules/process/user-started/users-list/components/header/UsersListFilter.tsx new file mode 100644 index 0000000..4ac79a9 --- /dev/null +++ b/src/app/modules/process/user-started/users-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/process/user-started/users-list/components/header/UsersListGrouping.tsx b/src/app/modules/process/user-started/users-list/components/header/UsersListGrouping.tsx new file mode 100644 index 0000000..6400fd7 --- /dev/null +++ b/src/app/modules/process/user-started/users-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/process/user-started/users-list/components/header/UsersListHeader.tsx b/src/app/modules/process/user-started/users-list/components/header/UsersListHeader.tsx new file mode 100644 index 0000000..9d2a3a0 --- /dev/null +++ b/src/app/modules/process/user-started/users-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/process/user-started/users-list/components/header/UsersListSearchComponent.tsx b/src/app/modules/process/user-started/users-list/components/header/UsersListSearchComponent.tsx new file mode 100644 index 0000000..75cba1c --- /dev/null +++ b/src/app/modules/process/user-started/users-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/process/user-started/users-list/components/loading/UsersListLoading.tsx b/src/app/modules/process/user-started/users-list/components/loading/UsersListLoading.tsx new file mode 100644 index 0000000..2278f87 --- /dev/null +++ b/src/app/modules/process/user-started/users-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/process/user-started/users-list/components/pagination/UsersListPagination.tsx b/src/app/modules/process/user-started/users-list/components/pagination/UsersListPagination.tsx new file mode 100644 index 0000000..2879f48 --- /dev/null +++ b/src/app/modules/process/user-started/users-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/process/user-started/users-list/core/ListViewProvider.tsx b/src/app/modules/process/user-started/users-list/core/ListViewProvider.tsx new file mode 100644 index 0000000..91cc2cf --- /dev/null +++ b/src/app/modules/process/user-started/users-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/process/user-started/users-list/core/QueryRequestProvider.tsx b/src/app/modules/process/user-started/users-list/core/QueryRequestProvider.tsx new file mode 100644 index 0000000..95f31cf --- /dev/null +++ b/src/app/modules/process/user-started/users-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/process/user-started/users-list/core/QueryResponseProvider.tsx b/src/app/modules/process/user-started/users-list/core/QueryResponseProvider.tsx new file mode 100644 index 0000000..3f7cffe --- /dev/null +++ b/src/app/modules/process/user-started/users-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 {getStartedUsers} 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.USERS_LIST}-${query}`, + () => { + return getStartedUsers(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/process/user-started/users-list/core/_models.ts b/src/app/modules/process/user-started/users-list/core/_models.ts new file mode 100644 index 0000000..b9b06ec --- /dev/null +++ b/src/app/modules/process/user-started/users-list/core/_models.ts @@ -0,0 +1,42 @@ +import {ID, Response} from '../../../../../../_digifi/helpers' +export type User = { + id?: ID + name?: string + avatar?: string + // email?: string + position?: string + role?: string + last_login?: string + two_steps?: boolean + joined_day?: string + online?: boolean + initials?: { + label: string + state: string + } + firstname?: string, + lastname?: string + uid?: string + loan_amount?: string + payment_month?: string + sales_agent?: string + gender?: string | null + marital_status?: string + email?: string + address?: string + state?: string + country?: string + status?: string + added?: string + updated?: string +} + +export type UsersQueryResponse = Response> + +export const initialUser: User = { + avatar: 'avatars/300-6.jpg', + position: 'Art Director', + role: 'Administrator', + name: '', + email: '', +} diff --git a/src/app/modules/process/user-started/users-list/core/_requests.ts b/src/app/modules/process/user-started/users-list/core/_requests.ts new file mode 100644 index 0000000..a585521 --- /dev/null +++ b/src/app/modules/process/user-started/users-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 getStartedUsers = (query: string): Promise => { // FUNCTION TO GET USERS THAT HAVE STARTED LOAN APPLICATION + return axios + .get(`${NEW_USER_ENDPOINT}/loan/started`) + .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 { + getStartedUsers, + deleteUser, + deleteSelectedUsers, + getUserById, + createUser, + updateUser, +}; diff --git a/src/app/modules/process/user-started/users-list/table/UsersTable.tsx b/src/app/modules/process/user-started/users-list/table/UsersTable.tsx new file mode 100644 index 0000000..f48b4b4 --- /dev/null +++ b/src/app/modules/process/user-started/users-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/process/user-started/users-list/table/columns/AddedCell.tsx b/src/app/modules/process/user-started/users-list/table/columns/AddedCell.tsx new file mode 100644 index 0000000..6f5bc2b --- /dev/null +++ b/src/app/modules/process/user-started/users-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/process/user-started/users-list/table/columns/AgentCell.tsx b/src/app/modules/process/user-started/users-list/table/columns/AgentCell.tsx new file mode 100644 index 0000000..063ade5 --- /dev/null +++ b/src/app/modules/process/user-started/users-list/table/columns/AgentCell.tsx @@ -0,0 +1,11 @@ +import {FC} from 'react' + +type Props = { + agent?: string +} + +const AgentCell: FC = ({agent}) => ( + <> {agent &&
{agent}
} +) + +export {AgentCell} diff --git a/src/app/modules/process/user-started/users-list/table/columns/CustomHeaderColumn.tsx b/src/app/modules/process/user-started/users-list/table/columns/CustomHeaderColumn.tsx new file mode 100644 index 0000000..838a12e --- /dev/null +++ b/src/app/modules/process/user-started/users-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/process/user-started/users-list/table/columns/CustomRow.tsx b/src/app/modules/process/user-started/users-list/table/columns/CustomRow.tsx new file mode 100644 index 0000000..a869cfe --- /dev/null +++ b/src/app/modules/process/user-started/users-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/process/user-started/users-list/table/columns/UserActionsCell.tsx b/src/app/modules/process/user-started/users-list/table/columns/UserActionsCell.tsx new file mode 100644 index 0000000..9a91389 --- /dev/null +++ b/src/app/modules/process/user-started/users-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/process/user-started/users-list/table/columns/UserCustomHeader.tsx b/src/app/modules/process/user-started/users-list/table/columns/UserCustomHeader.tsx new file mode 100644 index 0000000..3d0b58a --- /dev/null +++ b/src/app/modules/process/user-started/users-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/process/user-started/users-list/table/columns/UserInfoCell.tsx b/src/app/modules/process/user-started/users-list/table/columns/UserInfoCell.tsx new file mode 100644 index 0000000..1840fef --- /dev/null +++ b/src/app/modules/process/user-started/users-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/process/user-started/users-list/table/columns/UserLastLoginCell.tsx b/src/app/modules/process/user-started/users-list/table/columns/UserLastLoginCell.tsx new file mode 100644 index 0000000..a8a0ebe --- /dev/null +++ b/src/app/modules/process/user-started/users-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/process/user-started/users-list/table/columns/UserSelectionCell.tsx b/src/app/modules/process/user-started/users-list/table/columns/UserSelectionCell.tsx new file mode 100644 index 0000000..dcd2bfb --- /dev/null +++ b/src/app/modules/process/user-started/users-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/process/user-started/users-list/table/columns/UserSelectionHeader.tsx b/src/app/modules/process/user-started/users-list/table/columns/UserSelectionHeader.tsx new file mode 100644 index 0000000..bbb1eb0 --- /dev/null +++ b/src/app/modules/process/user-started/users-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/process/user-started/users-list/table/columns/_columns.tsx b/src/app/modules/process/user-started/users-list/table/columns/_columns.tsx new file mode 100644 index 0000000..897f37a --- /dev/null +++ b/src/app/modules/process/user-started/users-list/table/columns/_columns.tsx @@ -0,0 +1,57 @@ +import {Column} from 'react-table' +import {UserInfoCell} from './UserInfoCell' +import { PaymentMonthCell } from './UserLastLoginCell' +import {AgentCell} from './AgentCell' +import {UserActionsCell} from './UserActionsCell' +import {UserSelectionCell} from './UserSelectionCell' +import {UserCustomHeader} from './UserCustomHeader' +import {UserSelectionHeader} from './UserSelectionHeader' +import {User} from '../../core/_models' +import { AddedCell } from './AddedCell' + +const usersColumns: ReadonlyArray> = [ + { + Header: (props) => , + id: 'selection', + Cell: ({...props}) => , + }, + { + Header: (props) => , + id: 'firstname', + Cell: ({...props}) => , + }, + { + Header: (props) => , + accessor: 'loan_amount', + }, + { + Header: (props) => ( + + ), + id: 'payment_month', + Cell: ({...props}) => , + }, + { + Header: (props) => ( + + ), + id: 'sales_agent', + 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/process/user-started/users-list/user-edit-modal/UserEditModal.tsx b/src/app/modules/process/user-started/users-list/user-edit-modal/UserEditModal.tsx new file mode 100644 index 0000000..9bf605f --- /dev/null +++ b/src/app/modules/process/user-started/users-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/process/user-started/users-list/user-edit-modal/UserEditModalForm.tsx b/src/app/modules/process/user-started/users-list/user-edit-modal/UserEditModalForm.tsx new file mode 100644 index 0000000..6e8b0ec --- /dev/null +++ b/src/app/modules/process/user-started/users-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/process/user-started/users-list/user-edit-modal/UserEditModalFormWrapper.tsx b/src/app/modules/process/user-started/users-list/user-edit-modal/UserEditModalFormWrapper.tsx new file mode 100644 index 0000000..d60d7e3 --- /dev/null +++ b/src/app/modules/process/user-started/users-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/process/user-started/users-list/user-edit-modal/UserEditModalHeader.tsx b/src/app/modules/process/user-started/users-list/user-edit-modal/UserEditModalHeader.tsx new file mode 100644 index 0000000..cb0f5b6 --- /dev/null +++ b/src/app/modules/process/user-started/users-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}