diff --git a/.circleci/config.yml b/.circleci/config.yml index 93ebaa7d6..68adf3aa3 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -223,6 +223,7 @@ workflows: - CORE-635 - feat/system-admin - pm-1365_1 + - PM-959_tc-finance-integration - deployQa: context: org-global diff --git a/src/apps/wallet-admin/src/home/tabs/WalletAdminTabs.tsx b/src/apps/wallet-admin/src/home/tabs/WalletAdminTabs.tsx index 73d694e1d..a26d94225 100644 --- a/src/apps/wallet-admin/src/home/tabs/WalletAdminTabs.tsx +++ b/src/apps/wallet-admin/src/home/tabs/WalletAdminTabs.tsx @@ -7,8 +7,6 @@ import { PageTitle, TabsNavbar, TabsNavItem } from '~/libs/ui' import { getHashFromTabId, getTabIdFromHash, WalletAdminTabsConfig, WalletAdminTabViews } from './config' import { PaymentsTab } from './payments' import { HomeTab } from './home' -import { TaxFormsTab } from './tax-forms' -import { PaymentMethodsTab } from './payment-methods' import styles from './WalletAdminTabs.module.scss' interface WalletHomeProps { @@ -46,10 +44,6 @@ const WalletAdminTabs: FC = (props: WalletHomeProps) => { {activeTab === WalletAdminTabViews.home && } {activeTab === WalletAdminTabViews.payments && } - - {activeTab === WalletAdminTabViews.taxforms && } - - {activeTab === WalletAdminTabViews.withdrawalmethods && } ) } diff --git a/src/apps/wallet-admin/src/home/tabs/config/wallet-tabs-config.ts b/src/apps/wallet-admin/src/home/tabs/config/wallet-tabs-config.ts index b29c75195..b127d5f74 100644 --- a/src/apps/wallet-admin/src/home/tabs/config/wallet-tabs-config.ts +++ b/src/apps/wallet-admin/src/home/tabs/config/wallet-tabs-config.ts @@ -3,8 +3,6 @@ import { TabsNavItem } from '~/libs/ui' export enum WalletAdminTabViews { home = '0', payments = '1', - taxforms = '2', - withdrawalmethods = '3', } export const WalletAdminTabsConfig: TabsNavItem[] = [ @@ -16,14 +14,6 @@ export const WalletAdminTabsConfig: TabsNavItem[] = [ id: WalletAdminTabViews.payments, title: 'Payments', }, - { - id: WalletAdminTabViews.withdrawalmethods, - title: 'Payment Providers', - }, - { - id: WalletAdminTabViews.taxforms, - title: 'Tax Forms', - }, ] export function getHashFromTabId(tabId: string): string { @@ -32,10 +22,6 @@ export function getHashFromTabId(tabId: string): string { return '#home' case WalletAdminTabViews.payments: return '#payments' - case WalletAdminTabViews.taxforms: - return '#tax-forms' - case WalletAdminTabViews.withdrawalmethods: - return '#payment-providers' default: return '#home' } @@ -45,10 +31,6 @@ export function getTabIdFromHash(hash: string): string { switch (hash) { case '#payments': return WalletAdminTabViews.payments - case '#tax-forms': - return WalletAdminTabViews.taxforms - case '#payment-providers': - return WalletAdminTabViews.withdrawalmethods default: return WalletAdminTabViews.home } diff --git a/src/apps/wallet-admin/src/home/tabs/payment-methods/PaymentMethodsTab.module.scss b/src/apps/wallet-admin/src/home/tabs/payment-methods/PaymentMethodsTab.module.scss deleted file mode 100644 index 4d5773ab0..000000000 --- a/src/apps/wallet-admin/src/home/tabs/payment-methods/PaymentMethodsTab.module.scss +++ /dev/null @@ -1,35 +0,0 @@ -@import '@libs/ui/styles/includes'; - -.container { - background-color: $black-5; - padding: $sp-6; - margin: $sp-8 0; - border-radius: 6px; - - @include ltelg { - padding: $sp-4; - } - - .header { - display: flex; - justify-content: flex-start; - gap: 5px; - align-items: center; - - @include ltelg { - flex-direction: column; - } - } - - .content { - background-color: $tc-white; - border-radius: 4px; - margin-top: $sp-4; - .centered { - height: 200px; - display: flex; - justify-content: space-around; - align-items: center; - } - } -} diff --git a/src/apps/wallet-admin/src/home/tabs/payment-methods/PaymentMethodsTab.tsx b/src/apps/wallet-admin/src/home/tabs/payment-methods/PaymentMethodsTab.tsx deleted file mode 100644 index 9539b9f7a..000000000 --- a/src/apps/wallet-admin/src/home/tabs/payment-methods/PaymentMethodsTab.tsx +++ /dev/null @@ -1,209 +0,0 @@ -/* eslint-disable max-len */ -/* eslint-disable react/jsx-no-bind */ -import { toast } from 'react-toastify' -import React, { FC, useCallback, useEffect } from 'react' - -import { Collapsible, ConfirmModal, LoadingCircles } from '~/libs/ui' -import { UserProfile } from '~/libs/core' - -import { PaymentProvider } from '../../../lib/models/PaymentProvider' -import { deletePaymentProvider, getMemberHandle, getPaymentMethods } from '../../../lib/services/wallet' -import { FilterBar, PaymentMethodTable } from '../../../lib' -import { PaginationInfo } from '../../../lib/models/PaginationInfo' - -import styles from './PaymentMethodsTab.module.scss' - -interface ListViewProps { - // eslint-disable-next-line react/no-unused-prop-types - profile: UserProfile -} - -// eslint-disable-next-line @typescript-eslint/no-unused-vars -const ListView: FC = (props: ListViewProps) => { - const [confirmFlow, setConfirmFlow] = React.useState<{ - provider: PaymentProvider - } | undefined>(undefined) - const [isLoading, setIsLoading] = React.useState(false) - const [filters, setFilters] = React.useState>({}) - const [paymentMethods, setPaymentMethods] = React.useState([]) - const [userIds, setUserIds] = React.useState([]) - const [pagination, setPagination] = React.useState({ - currentPage: 1, - pageSize: 10, - totalItems: 0, - totalPages: 0, - }) - - const fetchPaymentProviders = useCallback(async () => { - if (isLoading) { - return - } - - setIsLoading(true) - try { - - const paymentMethodsResponse = await getPaymentMethods(pagination.pageSize, (pagination.currentPage - 1) * pagination.pageSize, userIds) - const tmpUserIds = paymentMethodsResponse.paymentMethods.map(provider => provider.userId) - const handleMap = await getMemberHandle(tmpUserIds) - - const userPaymentMethods = paymentMethodsResponse.paymentMethods.map((provider: PaymentProvider) => ({ ...provider, handle: handleMap.get(parseInt(provider.userId, 10)) ?? provider.userId })) - - setPaymentMethods(userPaymentMethods) - setPagination(paymentMethodsResponse.pagination) - } catch (apiError) { - console.error('Failed to fetch winnings:', apiError) - } finally { - setIsLoading(false) - } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [pagination.pageSize, pagination.currentPage, userIds]) - - useEffect(() => { - fetchPaymentProviders() - }, [fetchPaymentProviders]) - - return ( - <> -
-
-

Member Payment Providers

-
-
- Member Payment Providers Listing}> - { - const newPagination = { - ...pagination, - currentPage: 1, - } - if (key === 'pageSize') { - newPagination.pageSize = parseInt(value[0], 10) - } - - if (key === 'userIds') { - setUserIds(value) - } - - setPagination(newPagination) - setFilters({ - ...filters, - [key]: value, - }) - }} - onResetFilters={() => { - setPagination({ - ...pagination, - currentPage: 1, - pageSize: 10, - }) - setFilters({}) - }} - /> - {isLoading && } - {!isLoading && paymentMethods.length > 0 && ( - { - setPagination({ - ...pagination, - currentPage: pagination.currentPage - 1, - }) - }} - onNextPageClick={() => { - setPagination({ - ...pagination, - currentPage: pagination.currentPage + 1, - }) - }} - onPageClick={(pageNumber: number) => { - setPagination({ - ...pagination, - currentPage: pageNumber, - }) - }} - onDeleteClick={async (provider: PaymentProvider) => { - setConfirmFlow({ provider }) - }} - /> - )} - {!isLoading && paymentMethods.length === 0 && ( -
-

- {Object.keys(filters).length === 0 - ? 'Member payment-providers will appear here.' - : 'No payment-provider found for the selected member(s).'} -

-
- )} -
-
-
- {confirmFlow && ( - { - setConfirmFlow(undefined) - }} - onConfirm={async () => { - const userId = confirmFlow.provider.userId - const providerId = confirmFlow.provider.id! - setConfirmFlow(undefined) - - toast.success('Deleting payment provider. Please wait...', { position: 'bottom-right' }) - try { - await deletePaymentProvider(userId, providerId) - toast.success('Successfully deleted payment provider.', { position: 'bottom-right' }) - } catch (err) { - toast.error('Failed to delete users payment provider. Please try again later', { position: 'bottom-right' }) - } - - fetchPaymentProviders() - }} - open={confirmFlow !== undefined} - > -
-

- Are you sure you want to reset the payment provider of the member - {' '} - {confirmFlow.provider.handle} - ? -

-
-

This action cannot be undone.

-
-
- )} - - ) -} - -export default ListView diff --git a/src/apps/wallet-admin/src/home/tabs/payment-methods/index.ts b/src/apps/wallet-admin/src/home/tabs/payment-methods/index.ts deleted file mode 100644 index ae6c8e814..000000000 --- a/src/apps/wallet-admin/src/home/tabs/payment-methods/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { default as PaymentMethodsTab } from './PaymentMethodsTab' diff --git a/src/apps/wallet-admin/src/home/tabs/payments/PaymentsTab.tsx b/src/apps/wallet-admin/src/home/tabs/payments/PaymentsTab.tsx index 9a4bdd02e..1158713fa 100644 --- a/src/apps/wallet-admin/src/home/tabs/payments/PaymentsTab.tsx +++ b/src/apps/wallet-admin/src/home/tabs/payments/PaymentsTab.tsx @@ -8,13 +8,11 @@ import { Collapsible, ConfirmModal, LoadingCircles } from '~/libs/ui' import { UserProfile } from '~/libs/core' import { downloadBlob } from '~/libs/shared' -import { editPayment, exportSearchResults, getMemberHandle, getPaymentMethods, getPayments, getTaxForms } from '../../../lib/services/wallet' +import { editPayment, exportSearchResults, getMemberHandle, getPayments } from '../../../lib/services/wallet' import { Winning, WinningDetail } from '../../../lib/models/WinningDetail' import { FilterBar, formatIOSDateString, PaymentView } from '../../../lib' import { ConfirmFlowData } from '../../../lib/models/ConfirmFlowData' import { PaginationInfo } from '../../../lib/models/PaginationInfo' -import { TaxForm } from '../../../lib/models/TaxForm' -import { PaymentProvider } from '../../../lib/models/PaymentProvider' import PaymentEditForm from '../../../lib/components/payment-edit/PaymentEdit' import PaymentsTable from '../../../lib/components/payments-table/PaymentTable' @@ -39,6 +37,10 @@ function formatStatus(status: string): string { return 'Cancel' case 'PROCESSING': return 'Processing' + case 'FAILED': + return 'Failed' + case 'RETURNED': + return 'Returned' default: return status.replaceAll('_', ' ') } @@ -76,7 +78,8 @@ const ListView: FC = (props: ListViewProps) => { totalPages: 0, }) const [editState, setEditState] = React.useState<{ - netAmount?: number; + grossAmount?: number; + description?: string; releaseDate?: Date; paymentStatus?: string; auditNote?: string; @@ -91,7 +94,8 @@ const ListView: FC = (props: ListViewProps) => { const handleValueUpdated = useCallback((updates: { auditNote?: string, - netAmount?: number, + description?: string, + grossAmount?: number, paymentStatus?: string, releaseDate?: Date, }) => { @@ -102,7 +106,7 @@ const ListView: FC = (props: ListViewProps) => { }, []) const convertToWinnings = useCallback( - (payments: WinningDetail[], handleMap: Map, userHasTaxFormSetup: Map, userHasPaymentProvider: Map): ReadonlyArray => payments.map(payment => { + (payments: WinningDetail[], handleMap: Map): ReadonlyArray => payments.map(payment => { const now = new Date() const releaseDate = new Date(payment.releaseDate) const diffMs = releaseDate.getTime() - now.getTime() @@ -129,10 +133,10 @@ const ListView: FC = (props: ListViewProps) => { } if (status === 'ON_HOLD') { - if (!userHasTaxFormSetup.get(payment.winnerId)) { - status = 'On Hold (Tax Form)' - } else if (!userHasPaymentProvider.get(payment.winnerId)) { + if (!payment.paymentStatus?.payoutSetupComplete) { status = 'On Hold (Payment Provider)' + } else if (!payment.paymentStatus?.taxFormSetupComplete) { + status = 'On Hold (Tax Form)' } else { status = 'On Hold (Member)' } @@ -145,10 +149,10 @@ const ListView: FC = (props: ListViewProps) => { description: payment.description, details: payment.details, externalId: payment.externalId, + grossAmount: formatCurrency(payment.details[0].grossAmount, payment.details[0].currency), + grossAmountNumber: parseFloat(payment.details[0].grossAmount), handle: handleMap.get(parseInt(payment.winnerId, 10)) ?? payment.winnerId, id: payment.id, - netPayment: formatCurrency(payment.details[0].totalAmount, payment.details[0].currency), - netPaymentNumber: parseFloat(payment.details[0].totalAmount), releaseDate: formattedReleaseDate, releaseDateObj: releaseDate, status, @@ -169,30 +173,8 @@ const ListView: FC = (props: ListViewProps) => { const payments = await getPayments(pagination.pageSize, (pagination.currentPage - 1) * pagination.pageSize, filters) const winnerIds = payments.winnings.map(winning => winning.winnerId) - const onHoldUserIds = payments.winnings - .filter(winning => winning.details[0].status === 'ON_HOLD') - .map(winning => winning.winnerId) - - const userHasTaxFormSetup: Map = new Map() - const userHasPaymentProvider: Map = new Map() - - try { - const missingTaxForms = await getTaxForms(100, 0, onHoldUserIds) - const missingPaymentProviders = await getPaymentMethods(100, 0, onHoldUserIds) - - missingTaxForms.forms.forEach((form: TaxForm) => { - userHasTaxFormSetup.set(form.userId, form.status === 'ACTIVE') - }) - - missingPaymentProviders.paymentMethods.forEach((method: PaymentProvider) => { - userHasPaymentProvider.set(method.userId, method.status === 'CONNECTED') - }) - } catch (err) { - // Ignore errors - } - const handleMap = await getMemberHandle(winnerIds) - const winningsData = convertToWinnings(payments.winnings, handleMap, userHasTaxFormSetup, userHasPaymentProvider) + const winningsData = convertToWinnings(payments.winnings, handleMap) setWinnings(winningsData) setPagination(payments.pagination) } catch (apiError) { @@ -225,7 +207,8 @@ const ListView: FC = (props: ListViewProps) => { // Send to server only the fields that have changed const updateObj = { auditNote: currentEditState.auditNote !== undefined ? currentEditState.auditNote : undefined, - netAmount: currentEditState.netAmount !== undefined ? currentEditState.netAmount : undefined, + description: currentEditState.description !== undefined ? currentEditState.description : undefined, + grossAmount: currentEditState.grossAmount !== undefined ? currentEditState.grossAmount : undefined, paymentStatus: currentEditState.paymentStatus !== undefined ? currentEditState.paymentStatus : undefined, releaseDate: currentEditState.releaseDate !== undefined ? currentEditState.releaseDate : undefined, } @@ -243,6 +226,7 @@ const ListView: FC = (props: ListViewProps) => { const updates: { auditNote?: string + description?: string paymentStatus?: 'ON_HOLD_ADMIN' | 'OWED' | 'CANCELLED' releaseDate?: string paymentAmount?: number @@ -252,10 +236,11 @@ const ListView: FC = (props: ListViewProps) => { winningsId: paymentId, } + if (updateObj.description) updates.description = updateObj.description if (paymentStatus) updates.paymentStatus = paymentStatus if (paymentStatus !== 'CANCELLED') { if (updateObj.releaseDate !== undefined) updates.releaseDate = updateObj.releaseDate.toISOString() - if (updateObj.netAmount !== undefined) updates.paymentAmount = updateObj.netAmount + if (updateObj.grossAmount !== undefined) updates.paymentAmount = updateObj.grossAmount } toast.success('Updating payment', { position: toast.POSITION.BOTTOM_RIGHT }) @@ -307,7 +292,7 @@ const ListView: FC = (props: ListViewProps) => { title: 'Edit Payment', }) // eslint-disable-next-line react-hooks/exhaustive-deps - }, [handleValueUpdated, editState]) + }, [handleValueUpdated, editState, fetchWinnings]) const isEditingAllowed = (): boolean => props.profile.roles.includes('Payment Admin') || props.profile.roles.includes('Payment Editor') @@ -364,6 +349,14 @@ const ListView: FC = (props: ListViewProps) => { label: 'Processing', value: 'PROCESSING', }, + { + label: 'Failed', + value: 'FAILED', + }, + { + label: 'Returned', + value: 'RETURNED', + }, ], type: 'dropdown', }, diff --git a/src/apps/wallet-admin/src/home/tabs/tax-forms/TaxFormsTab.module.scss b/src/apps/wallet-admin/src/home/tabs/tax-forms/TaxFormsTab.module.scss deleted file mode 100644 index 4d5773ab0..000000000 --- a/src/apps/wallet-admin/src/home/tabs/tax-forms/TaxFormsTab.module.scss +++ /dev/null @@ -1,35 +0,0 @@ -@import '@libs/ui/styles/includes'; - -.container { - background-color: $black-5; - padding: $sp-6; - margin: $sp-8 0; - border-radius: 6px; - - @include ltelg { - padding: $sp-4; - } - - .header { - display: flex; - justify-content: flex-start; - gap: 5px; - align-items: center; - - @include ltelg { - flex-direction: column; - } - } - - .content { - background-color: $tc-white; - border-radius: 4px; - margin-top: $sp-4; - .centered { - height: 200px; - display: flex; - justify-content: space-around; - align-items: center; - } - } -} diff --git a/src/apps/wallet-admin/src/home/tabs/tax-forms/TaxFormsTab.tsx b/src/apps/wallet-admin/src/home/tabs/tax-forms/TaxFormsTab.tsx deleted file mode 100644 index 1fd6e1bb0..000000000 --- a/src/apps/wallet-admin/src/home/tabs/tax-forms/TaxFormsTab.tsx +++ /dev/null @@ -1,228 +0,0 @@ -/* eslint-disable max-len */ -/* eslint-disable react/jsx-no-bind */ -import { toast } from 'react-toastify' -import { AxiosError } from 'axios' -import React, { FC, useCallback, useEffect } from 'react' - -import { Collapsible, ConfirmModal, LoadingCircles } from '~/libs/ui' -import { UserProfile } from '~/libs/core' -import { downloadBlob } from '~/libs/shared' - -import { deleteTaxForm, downloadTaxForm, getMemberHandle, getTaxForms } from '../../../lib/services/wallet' -import { TaxForm } from '../../../lib/models/TaxForm' -import { FilterBar, formatIOSDateString, TaxFormTable } from '../../../lib' -import { PaginationInfo } from '../../../lib/models/PaginationInfo' - -import styles from './TaxFormsTab.module.scss' - -interface ListViewProps { - // eslint-disable-next-line react/no-unused-prop-types - profile: UserProfile -} - -// eslint-disable-next-line @typescript-eslint/no-unused-vars -const ListView: FC = (props: ListViewProps) => { - const [confirmFlow, setConfirmFlow] = React.useState<{ - form: TaxForm - } | undefined>(undefined) - const [isLoading, setIsLoading] = React.useState(false) - const [filters, setFilters] = React.useState>({}) - const [forms, setForms] = React.useState([]) - const [userIds, setUserIds] = React.useState([]) - const [pagination, setPagination] = React.useState({ - currentPage: 1, - pageSize: 10, - totalItems: 0, - totalPages: 0, - }) - const [apiErrorMsg, setApiErrorMsg] = React.useState('Member earnings will appear here.') - - const fetchTaxForms = useCallback(async () => { - if (isLoading) { - return - } - - setIsLoading(true) - try { - - const taxFormsResponse = await getTaxForms(pagination.pageSize, (pagination.currentPage - 1) * pagination.pageSize, userIds) - const tmpUserIds = taxFormsResponse.forms.map(form => form.userId) - const handleMap = await getMemberHandle(tmpUserIds) - - const taxForms = taxFormsResponse.forms.map((form: TaxForm) => ({ ...form, dateFiled: form.dateFiled ? formatIOSDateString(form.dateFiled) : '-', handle: handleMap.get(parseInt(form.userId, 10)) ?? form.userId })) - - setForms(taxForms) - setPagination(taxFormsResponse.pagination) - } catch (apiError) { - if (apiError instanceof AxiosError && apiError?.response?.status === 403) { - setApiErrorMsg(apiError.response.data.message) - } else { - setApiErrorMsg('Failed to fetch winnings. Please try again later.') - } - } finally { - setIsLoading(false) - } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [pagination.pageSize, pagination.currentPage, userIds]) - - useEffect(() => { - fetchTaxForms() - }, [fetchTaxForms]) - - return ( - <> -
-
-

Member Tax Forms

-
-
- Tax Forms Listing}> - { - const newPagination = { - ...pagination, - currentPage: 1, - } - if (key === 'pageSize') { - newPagination.pageSize = parseInt(value[0], 10) - } - - if (key === 'userIds') { - setUserIds(value) - } - - setPagination(newPagination) - setFilters({ - ...filters, - [key]: value, - }) - }} - onResetFilters={() => { - setPagination({ - ...pagination, - currentPage: 1, - pageSize: 10, - }) - setFilters({}) - }} - /> - {isLoading && } - {!isLoading && forms.length > 0 && ( - { - setPagination({ - ...pagination, - currentPage: pagination.currentPage - 1, - }) - }} - onNextPageClick={() => { - setPagination({ - ...pagination, - currentPage: pagination.currentPage + 1, - }) - }} - onPageClick={(pageNumber: number) => { - setPagination({ - ...pagination, - currentPage: pageNumber, - }) - }} - onDownloadClick={async (form: TaxForm) => { - toast.success('Downloading tax form. Please wait...', { position: 'bottom-right' }) - try { - downloadBlob( - await downloadTaxForm(form.userId, form.id), - `tax-form-${form.userId}-${new Date() - .getTime()}.pdf`, - ) - } catch (err) { - toast.error('Failed to download tax form. Please try again later', { position: 'bottom-right' }) - } - }} - onDeleteClick={async (form: TaxForm) => { - setConfirmFlow({ form }) - }} - /> - )} - {!isLoading && forms.length === 0 && ( -
-

- {Object.keys(filters).length === 0 - ? apiErrorMsg - : 'No tax-forms found for the selected member(s).'} -

-
- )} -
-
-
- {confirmFlow && ( - { - setConfirmFlow(undefined) - }} - onConfirm={async () => { - const userId = confirmFlow.form.userId - const formId = confirmFlow.form.id - setConfirmFlow(undefined) - - toast.success('Deleting tax form. Please wait...', { position: 'bottom-right' }) - try { - await deleteTaxForm(userId, formId) - toast.success('Successfully deleted tax-form.', { position: 'bottom-right' }) - } catch (err) { - toast.error('Failed to delete users tax-form. Please try again later', { position: 'bottom-right' }) - } - - fetchTaxForms() - }} - open={confirmFlow !== undefined} - > -
-

- Are you sure you want to reset the tax-form of the member - {' '} - {confirmFlow.form.handle} - ? -

-
-

This action cannot be undone.

-
-
- )} - - ) -} - -export default ListView diff --git a/src/apps/wallet-admin/src/home/tabs/tax-forms/index.ts b/src/apps/wallet-admin/src/home/tabs/tax-forms/index.ts deleted file mode 100644 index 08eb9c245..000000000 --- a/src/apps/wallet-admin/src/home/tabs/tax-forms/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { default as TaxFormsTab } from './TaxFormsTab' diff --git a/src/apps/wallet-admin/src/lib/components/filter-bar/FilterBar.module.scss b/src/apps/wallet-admin/src/lib/components/filter-bar/FilterBar.module.scss index 647bd3765..a6ee2ace1 100644 --- a/src/apps/wallet-admin/src/lib/components/filter-bar/FilterBar.module.scss +++ b/src/apps/wallet-admin/src/lib/components/filter-bar/FilterBar.module.scss @@ -25,7 +25,7 @@ } .filter { - width: 150px; + width: 165px; height: 47px; } diff --git a/src/apps/wallet-admin/src/lib/components/filter-bar/FilterBar.tsx b/src/apps/wallet-admin/src/lib/components/filter-bar/FilterBar.tsx index 5b53ba647..da5b4b023 100644 --- a/src/apps/wallet-admin/src/lib/components/filter-bar/FilterBar.tsx +++ b/src/apps/wallet-admin/src/lib/components/filter-bar/FilterBar.tsx @@ -1,5 +1,5 @@ /* eslint-disable react/jsx-no-bind */ -import React, { ChangeEvent } from 'react' +import React, { ChangeEvent, useRef } from 'react' import { Button, IconOutline, InputSelect, InputText } from '~/libs/ui' import { @@ -31,6 +31,7 @@ interface FilterBarProps { const FilterBar: React.FC = (props: FilterBarProps) => { const [selectedValue, setSelectedValue] = React.useState>(new Map()) + const selectedMembers = useRef([]) const renderDropdown = (index: number, filter: Filter): JSX.Element => ( = (props: FilterBarProps) => { className={styles.filterInput} placeholder={filter.label} onChange={(event: Array) => { + selectedMembers.current = event setSelectedValue(new Map(selectedValue.set(filter.key, event))) props.onFilterChange(filter.key, event.map(member => member.userId)) }} tabIndex={index} + value={selectedMembers.current} /> ) @@ -124,6 +127,7 @@ const FilterBar: React.FC = (props: FilterBarProps) => { size='lg' disabled={selectedValue.size === 0} onClick={() => { + selectedMembers.current = [] setSelectedValue(new Map()) props.onResetFilters?.() }} diff --git a/src/apps/wallet-admin/src/lib/components/index.ts b/src/apps/wallet-admin/src/lib/components/index.ts index 23457ecab..820a0c53c 100644 --- a/src/apps/wallet-admin/src/lib/components/index.ts +++ b/src/apps/wallet-admin/src/lib/components/index.ts @@ -3,5 +3,3 @@ export * from './chip' export * from './filter-bar' export * from './payment-edit' export * from './payment-view' -export * from './tax-forms-table' -export * from './payment-method-table' diff --git a/src/apps/wallet-admin/src/lib/components/payment-edit/PaymentEdit.tsx b/src/apps/wallet-admin/src/lib/components/payment-edit/PaymentEdit.tsx index d8241b859..d06ebe2f6 100644 --- a/src/apps/wallet-admin/src/lib/components/payment-edit/PaymentEdit.tsx +++ b/src/apps/wallet-admin/src/lib/components/payment-edit/PaymentEdit.tsx @@ -15,44 +15,47 @@ interface PaymentEditFormProps { payment: Winning canSave?: (canSave: boolean) => void onValueUpdated?: ({ - releaseDate, netAmount, paymentStatus, auditNote, + releaseDate, grossAmount, paymentStatus, auditNote, }: { releaseDate?: Date - netAmount?: number + description?: string + grossAmount?: number paymentStatus?: string auditNote?: string }) => void } const PaymentEdit: React.FC = (props: PaymentEditFormProps) => { + const [description, setDescription] = useState('') const [paymentStatus, setPaymentStatus] = useState('') const [releaseDate, setReleaseDate] = useState(new Date()) - const [netAmount, setNetAmount] = useState(0) - const [netAmountErrorString, setNetAmountErrorString] = useState('') + const [grossAmount, setGrossAmount] = useState(0) + const [grossAmountErrorString, setGrossAmountErrorString] = useState('') const [auditNote, setAuditNote] = useState('') const [dirty, setDirty] = useState(false) const [disableEdits, setDisableEdits] = useState(false) const initialValues = useMemo(() => ({ auditNote: '', - netPayment: props.payment.netPaymentNumber, + description: props.payment.description, + grossAmount: props.payment.grossAmountNumber, paymentStatus: props.payment.status, releaseDate: props.payment.releaseDateObj, }), [props.payment]) - const validateNetAmount = (value: number): boolean => { + const validateGrossAmount = (value: number): boolean => { if (Number.isNaN(value)) { - setNetAmountErrorString('A valid number is required') + setGrossAmountErrorString('A valid number is required') return false } if (value < 0) { - setNetAmountErrorString('Net Payment must be greater than 0') + setGrossAmountErrorString('Payment must be greater than 0') return false } if (!/^\d+(\.\d{0,2})?$/.test(value.toString())) { - setNetAmountErrorString('Amount can only have 2 decimal places at most') + setGrossAmountErrorString('Amount can only have 2 decimal places at most') return false } @@ -63,17 +66,17 @@ const PaymentEdit: React.FC = (props: PaymentEditFormProps let isValid = true switch (name) { - case 'netPayment': - isValid = validateNetAmount(value as number) + case 'grossPayment': + isValid = validateGrossAmount(value as number) if (isValid) { - setNetAmount(value as number) + setGrossAmount(value as number) if (props.onValueUpdated) { props.onValueUpdated({ - netAmount: value as number, + grossAmount: value as number, }) } - setNetAmountErrorString('') + setGrossAmountErrorString('') } break @@ -85,6 +88,15 @@ const PaymentEdit: React.FC = (props: PaymentEditFormProps }) } + break + case 'description': + setDescription(value as string) + if (props.onValueUpdated) { + props.onValueUpdated({ + description: value as string, + }) + } + break case 'releaseDate': setReleaseDate(value as Date) @@ -110,15 +122,19 @@ const PaymentEdit: React.FC = (props: PaymentEditFormProps } useEffect(() => { + setDescription(props.payment.description) setPaymentStatus(props.payment.status) setReleaseDate(props.payment.releaseDateObj) - setNetAmount(props.payment.netPaymentNumber) + setGrossAmount(props.payment.grossAmountNumber) }, [props.payment]) useEffect(() => { const valuesToCheck = [{ - key: 'netPayment', - value: netAmount, + key: 'description', + value: description, + }, { + key: 'grossPayment', + value: grossAmount, }, { key: 'paymentStatus', value: paymentStatus, @@ -132,7 +148,7 @@ const PaymentEdit: React.FC = (props: PaymentEditFormProps const isDirty = valuesToCheck.some(x => x.value !== initialValues[x.key as keyof typeof initialValues]) setDirty(isDirty) - }, [netAmount, paymentStatus, releaseDate, auditNote, initialValues]) + }, [description, grossAmount, paymentStatus, releaseDate, auditNote, initialValues]) useEffect(() => { if (props.canSave) { @@ -140,8 +156,11 @@ const PaymentEdit: React.FC = (props: PaymentEditFormProps props.canSave(false) } else { const valuesToCheck = [{ - key: 'netPayment', - value: netAmount, + key: 'description', + value: description, + }, { + key: 'grossPayment', + value: grossAmount, }, { key: 'paymentStatus', value: paymentStatus, @@ -151,10 +170,10 @@ const PaymentEdit: React.FC = (props: PaymentEditFormProps }] const haveChange = valuesToCheck.some(x => x.value !== initialValues[x.key as keyof typeof initialValues]) // check if any value has changed that's not the audit note - props.canSave(haveChange && netAmountErrorString.length === 0 && auditNote.length > 0) + props.canSave(haveChange && grossAmountErrorString.length === 0 && auditNote.length > 0) } } - }, [dirty, auditNote, props, netAmountErrorString.length, netAmount, paymentStatus, releaseDate, initialValues]) + }, [dirty, auditNote, props, grossAmountErrorString.length, description, grossAmount, paymentStatus, releaseDate, initialValues]) const getLink = (externalId: string): string => `${TOPCODER_URL}/challenges/${externalId}` @@ -198,18 +217,31 @@ const PaymentEdit: React.FC = (props: PaymentEditFormProps handleInputChange('netPayment', parseFloat(e.target.value))} + error={grossAmountErrorString} + value={props.payment.grossAmountNumber.toString()} + onChange={e => handleInputChange('grossPayment', parseFloat(e.target.value))} + /> + handleInputChange('description', e.target.value)} /> + ; - currentPage: number; - numPages: number; - onNextPageClick: () => void; - onPreviousPageClick: () => void; - onPageClick: (pageNumber: number) => void; - onDeleteClick?: (form: PaymentProvider) => void; -} - -const PaymentProviderTable: React.FC = (props: PaymentMethodTableProps) => ( - <> -
- - - - - - - - {/* eslint-disable-next-line jsx-a11y/control-has-associated-label */} - - - - - {props.paymentMethods.map(provider => ( - - - - - - - - ))} - -
HANDLECONNECTED PROVIDERPROVIDER IDSTATUS
{provider.handle}{provider.type}{provider.providerId === 'Legacy' ? provider.userId : provider.providerId}{provider.status} - -
-
- - {props.numPages > 1 && ( -
-
-
- {props.currentPage < props.numPages - 2 && ...} -
- - )} - -) - -export default PaymentProviderTable diff --git a/src/apps/wallet-admin/src/lib/components/payment-method-table/index.ts b/src/apps/wallet-admin/src/lib/components/payment-method-table/index.ts deleted file mode 100644 index 6fa841064..000000000 --- a/src/apps/wallet-admin/src/lib/components/payment-method-table/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { default as PaymentMethodTable } from './PaymentMethodTable' diff --git a/src/apps/wallet-admin/src/lib/components/payment-view/PaymentView.tsx b/src/apps/wallet-admin/src/lib/components/payment-view/PaymentView.tsx index 75cec34bb..dc3294698 100644 --- a/src/apps/wallet-admin/src/lib/components/payment-view/PaymentView.tsx +++ b/src/apps/wallet-admin/src/lib/components/payment-view/PaymentView.tsx @@ -109,9 +109,9 @@ const PaymentView: React.FC = (props: PaymentViewProps) => {
- Net Payment + Payment

- {props.payment.netPaymentNumber.toLocaleString(undefined, { + {props.payment.grossAmountNumber.toLocaleString(undefined, { currency: 'USD', style: 'currency', })} diff --git a/src/apps/wallet-admin/src/lib/components/payments-table/PaymentTable.module.scss b/src/apps/wallet-admin/src/lib/components/payments-table/PaymentTable.module.scss index 648854b99..bf4f9d3af 100644 --- a/src/apps/wallet-admin/src/lib/components/payments-table/PaymentTable.module.scss +++ b/src/apps/wallet-admin/src/lib/components/payments-table/PaymentTable.module.scss @@ -41,6 +41,11 @@ table { top: 0; background-color: white !important; text-transform: uppercase; + + &.description { + width: 360px; + max-width: 360px; + } } tbody tr td:first-child { @@ -97,7 +102,7 @@ table { .actionButtons { padding-left: 8px; display: flex; - justify-content: center; + justify-content: flex-end; align-items: center; } diff --git a/src/apps/wallet-admin/src/lib/components/payments-table/PaymentTable.tsx b/src/apps/wallet-admin/src/lib/components/payments-table/PaymentTable.tsx index 9b7f419de..207fef3ba 100644 --- a/src/apps/wallet-admin/src/lib/components/payments-table/PaymentTable.tsx +++ b/src/apps/wallet-admin/src/lib/components/payments-table/PaymentTable.tsx @@ -38,12 +38,13 @@ const PaymentsTable: React.FC = (props: PaymentTableProps) => HANDLE - DESCRIPTION + DESCRIPTION CREATE DATE - NET PAYMENT + PAYMENT STATUS RELEASE DATE DATE PAID + @@ -55,7 +56,7 @@ const PaymentsTable: React.FC = (props: PaymentTableProps) => {payment.handle} {payment.description} {payment.createDate} - {payment.netPayment} + {payment.grossAmount} {payment.status} {payment.releaseDate} {payment.datePaid} diff --git a/src/apps/wallet-admin/src/lib/components/tax-forms-table/TaxFormTable.module.scss b/src/apps/wallet-admin/src/lib/components/tax-forms-table/TaxFormTable.module.scss deleted file mode 100644 index 5e4ea7c5a..000000000 --- a/src/apps/wallet-admin/src/lib/components/tax-forms-table/TaxFormTable.module.scss +++ /dev/null @@ -1,109 +0,0 @@ -@import '@libs/ui/styles/includes'; - -.tableContainer { - width: 100%; - overflow-x: auto; - margin-top: $sp-2; -} - -table { - width: 100%; - border-collapse: separate; - border-spacing: 0; - margin: 16px 0; - - th, - td { - text-align: left; - padding: 8px; - - &:last-child { - width: 50px; - } - } - - tbody { - tr { - &:nth-child(odd) { - background-color: #f4f4f4; - } - &:nth-child(even) { - background-color: #ffffff; - } - - .capitalize { - text-transform: capitalize; - } - } - } - - th { - top: 0; - background-color: white !important; - text-transform: uppercase; - } - - tbody tr td:first-child { - border-top-left-radius: 8px; - border-bottom-left-radius: 8px; - } - - tbody tr td:last-child { - border-top-right-radius: 8px; - border-bottom-right-radius: 8px; - } - -} - -.selected { - background-color: #e7f4ff; -} - -.pageButtons { - display: flex; - justify-content: flex-end; - align-items: center; - margin-top: 16px; - gap: $sp-4; - - .pageNumbers { - display: flex; - justify-content: center; - align-items: center; - gap: $sp-1; - } - -} - -.footer { - display: flex; - flex-direction: column; - justify-content: space-between; - align-items: center; - margin-top: 16px; - padding-bottom: 16px; - border-radius: 8px; - background-color: white; - border: 1px solid #eaeaea; - - .total { - font-size: 18px; - font-weight: bold; - - color: #333; - } -} - -.actionButtons { - padding-right: 50px; - display: flex; - justify-content: center; - align-items: center; -} - -@media (max-width: 768px) { - .footer { - flex-direction: column; - align-items: center; - } -} diff --git a/src/apps/wallet-admin/src/lib/components/tax-forms-table/TaxFormTable.tsx b/src/apps/wallet-admin/src/lib/components/tax-forms-table/TaxFormTable.tsx deleted file mode 100644 index 6df26da92..000000000 --- a/src/apps/wallet-admin/src/lib/components/tax-forms-table/TaxFormTable.tsx +++ /dev/null @@ -1,124 +0,0 @@ -/* eslint-disable max-len */ -/* eslint-disable react/jsx-no-bind */ -/* eslint-disable @typescript-eslint/explicit-function-return-type */ -import React from 'react' - -import { Button, IconOutline, Tooltip } from '~/libs/ui' - -import { TaxForm } from '../../models/TaxForm' - -import styles from './TaxFormTable.module.scss' - -interface TaxFormTableProps { - taxForms: ReadonlyArray; - currentPage: number; - numPages: number; - onNextPageClick: () => void; - onPreviousPageClick: () => void; - onPageClick: (pageNumber: number) => void; - onDownloadClick?: (form: TaxForm) => void; - onDeleteClick?: (form: TaxForm) => void; -} - -const TaxFormTable: React.FC = (props: TaxFormTableProps) => ( - <> -

- - - - - - - - {/* eslint-disable-next-line jsx-a11y/control-has-associated-label */} - - - - - {props.taxForms.map(form => ( - - - - - - - - ))} - -
HANDLEFORMDATE FILEDSTATUS
{form.handle}{form.taxForm.name}{form.dateFiled}{form.status} - -
-
- - {props.numPages > 1 && ( -
-
-
- {props.currentPage < props.numPages - 2 && ...} -
-
- )} - -) - -export default TaxFormTable diff --git a/src/apps/wallet-admin/src/lib/components/tax-forms-table/index.ts b/src/apps/wallet-admin/src/lib/components/tax-forms-table/index.ts deleted file mode 100644 index f526c746a..000000000 --- a/src/apps/wallet-admin/src/lib/components/tax-forms-table/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { default as TaxFormTable } from './TaxFormTable' diff --git a/src/apps/wallet-admin/src/lib/models/PaymentProvider.ts b/src/apps/wallet-admin/src/lib/models/PaymentProvider.ts deleted file mode 100644 index 41a9ba5b1..000000000 --- a/src/apps/wallet-admin/src/lib/models/PaymentProvider.ts +++ /dev/null @@ -1,10 +0,0 @@ -export interface PaymentProvider { - id?: number - upmId?: string - userId: string - type: 'Payoneer' | 'Paypal' - name: 'Payoneer' | 'Paypal' - status: string - handle?: string - providerId: string -} diff --git a/src/apps/wallet-admin/src/lib/models/TaxForm.ts b/src/apps/wallet-admin/src/lib/models/TaxForm.ts deleted file mode 100644 index 679f5a833..000000000 --- a/src/apps/wallet-admin/src/lib/models/TaxForm.ts +++ /dev/null @@ -1,12 +0,0 @@ -export interface TaxForm { - id: string; - taxFormId: string; - dateFiled: string; - userId: string; - handle?: string; - taxForm: { - taxFormId: string; - name: string; - } - status: string; -} diff --git a/src/apps/wallet-admin/src/lib/models/WinningDetail.ts b/src/apps/wallet-admin/src/lib/models/WinningDetail.ts index 5719c06c3..8d62e1cf1 100644 --- a/src/apps/wallet-admin/src/lib/models/WinningDetail.ts +++ b/src/apps/wallet-admin/src/lib/models/WinningDetail.ts @@ -1,6 +1,5 @@ export interface PaymentDetail { id: string - netAmount: string grossAmount: string totalAmount: string installmentNumber: number @@ -9,6 +8,11 @@ export interface PaymentDetail { datePaid: string } +export interface PayoutStatus { + payoutSetupComplete: boolean; + taxFormSetupComplete: boolean; +} + export interface Winning { id: string description: string @@ -16,8 +20,8 @@ export interface Winning { type: string handle: string; createDate: string - netPayment: string - netPaymentNumber: number + grossAmount: string + grossAmountNumber: number status: string releaseDate: string releaseDateObj: Date @@ -43,4 +47,5 @@ export interface WinningDetail { createdAt: string releaseDate: string datePaid: string + paymentStatus?: PayoutStatus } diff --git a/src/apps/wallet-admin/src/lib/services/wallet.ts b/src/apps/wallet-admin/src/lib/services/wallet.ts index 3475cb4e4..8d21bf23b 100644 --- a/src/apps/wallet-admin/src/lib/services/wallet.ts +++ b/src/apps/wallet-admin/src/lib/services/wallet.ts @@ -1,18 +1,17 @@ +/* eslint-disable camelcase */ import { EnvironmentConfig } from '~/config' import { xhrDeleteAsync, xhrGetAsync, xhrPatchAsync, xhrPostAsync } from '~/libs/core' -import { getAsyncWithBlobHandling, postAsyncWithBlobHandling } from '~/libs/core/lib/xhr/xhr-functions/xhr.functions' +import { postAsyncWithBlobHandling } from '~/libs/core/lib/xhr/xhr-functions/xhr.functions' import { WalletDetails } from '../models/WalletDetails' -import { PaymentProvider } from '../models/PaymentProvider' import { WinningDetail } from '../models/WinningDetail' -import { TaxForm } from '../models/TaxForm' import { TransactionResponse } from '../models/TransactionId' import { PaginationInfo } from '../models/PaginationInfo' import { WinningsAudit } from '../models/WinningsAudit' import { PayoutAudit } from '../models/PayoutAudit' import ApiResponse from '../models/ApiResponse' -const baseUrl = `${EnvironmentConfig.API.V5}/payments` +const baseUrl = `${EnvironmentConfig.TC_FINANCE_API}` const memberApiBaseUrl = `${EnvironmentConfig.API.V5}/members` export async function getWalletDetails(): Promise { @@ -25,25 +24,6 @@ export async function getWalletDetails(): Promise { return response.data } -export async function getUserPaymentProviders(): Promise { - const response = await xhrGetAsync>(`${baseUrl}/user/payment-methods`) - - if (response.status === 'error') { - throw new Error('Error fetching user payment providers') - } - - return response.data -} - -export async function getUserTaxFormDetails(): Promise { - const response = await xhrGetAsync>(`${baseUrl}/user/tax-forms`) - if (response.status === 'error') { - throw new Error('Error fetching user tax form details') - } - - return response.data -} - export async function fetchAuditLogs(paymentId: string): Promise { const response = await xhrGetAsync>(`${baseUrl}/admin/winnings/${paymentId}/audit`) @@ -93,61 +73,6 @@ export async function editPayment(updates: { return response.data } -export async function getTaxForms(limit: number, offset: number, userIds: string[]): Promise<{ - forms: TaxForm[], - pagination: PaginationInfo -}> { - const body = JSON.stringify({ - limit, - offset, - userIds, - }) - - const url = `${baseUrl}/admin/tax-forms` - const response = await xhrPostAsync>(url, body) - - if (response.status === 'error') { - throw new Error('Error fetching tax forms') - } - - if (response.data.forms === null || response.data.forms === undefined) { - response.data.forms = [] - } - - return response.data -} - -export async function getPaymentMethods(limit: number, offset: number, userIds: string[]): Promise<{ - paymentMethods: PaymentProvider[], - pagination: PaginationInfo -}> { - const body = JSON.stringify({ - limit, - offset, - userIds, - }) - - const url = `${baseUrl}/admin/payment-methods` - const response = await xhrPostAsync>(url, body) - - if (response.status === 'error') { - throw new Error('Error fetching payment methods') - } - - if (response.data.paymentMethods === null || response.data.paymentMethods === undefined) { - response.data.paymentMethods = [] - } - - return response.data - -} - export async function exportSearchResults(filters: Record): Promise { const url = `${baseUrl}/admin/winnings/export` @@ -180,33 +105,6 @@ export async function exportSearchResults(filters: Record): Pr } } -export async function downloadTaxForm(userId: string, taxFormId: string): Promise { - const url = `${baseUrl}/admin/tax-forms/${userId}/${taxFormId}/download` - try { - return await getAsyncWithBlobHandling(url) - } catch (err) { - throw new Error('Failed to download users tax-form.') - } -} - -export async function deleteTaxForm(userId: string, taxFormId: string): Promise { - const url = `${baseUrl}/admin/tax-forms/${userId}/${taxFormId}` - try { - return await xhrDeleteAsync(url) - } catch (err) { - throw new Error('Failed to delete users tax-form') - } -} - -export async function deletePaymentProvider(userId: string, providerId: number): Promise { - const url = `${baseUrl}/admin/payment-methods/${userId}/${providerId}` - try { - return await xhrDeleteAsync(url) - } catch (err) { - throw new Error('Failed to delete users payment provider') - } -} - // eslint-disable-next-line max-len export async function getPayments(limit: number, offset: number, filters: Record): Promise<{ winnings: WinningDetail[], @@ -309,44 +207,6 @@ export async function removePaymentProvider(type: string): Promise { - const body = JSON.stringify({ - taxForm, - userId, - }) - - const url = `${baseUrl}/user/tax-form` - const response = await xhrPostAsync>(url, body) - - if (response.status === 'error') { - throw new Error('Error setting tax form') - } - - return response.data -} - -export async function removeTaxForm(taxFormId: string): Promise { - const url = `${baseUrl}/user/tax-forms/${taxFormId}` - const response = await xhrDeleteAsync>(url) - - if (response.status === 'error') { - throw new Error('Error removing tax form') - } - - return response.data -} - -export async function getRecipientViewURL(): Promise { - const url = `${baseUrl}/user/tax-form/view` - const response = await xhrGetAsync>(url) - - if (response.status === 'error') { - throw new Error('Error removing tax form') - } - - return response.data -} - export async function getMemberHandle(userIds: string[]): Promise> { const BATCH_SIZE = 50 diff --git a/src/apps/wallet/src/home/tabs/WalletTabs.tsx b/src/apps/wallet/src/home/tabs/WalletTabs.tsx index 17b0db0bb..d731c1b20 100644 --- a/src/apps/wallet/src/home/tabs/WalletTabs.tsx +++ b/src/apps/wallet/src/home/tabs/WalletTabs.tsx @@ -4,11 +4,13 @@ import { useLocation } from 'react-router-dom' import { UserProfile } from '~/libs/core' import { PageTitle, TabsNavbar, TabsNavItem } from '~/libs/ui' +import { PayoutGuard } from '../../lib' +import { useCanViewPayout } from '../../lib/hooks/use-can-view-payout' + import { getHashFromTabId, getTabIdFromHash, WalletTabsConfig, WalletTabViews } from './config' -import { PaymentsTab } from './payments' import { WinningsTab } from './winnings' import { HomeTab } from './home' -import { TaxFormsTab } from './tax-forms' +import { PayoutTab } from './payout' import styles from './WalletTabs.module.scss' interface WalletHomeProps { @@ -17,37 +19,45 @@ interface WalletHomeProps { const WalletTabs: FC = (props: WalletHomeProps) => { const { hash }: { hash: string } = useLocation() + const canViewPayout = useCanViewPayout(props.profile) + + const activeTabHash: WalletTabViews = useMemo(() => getTabIdFromHash(hash), [hash]) - const activeTabHash: string = useMemo(() => getTabIdFromHash(hash), [hash]) + const [activeTab, setActiveTab]: [WalletTabViews, Dispatch>] + = useState(activeTabHash) - const [activeTab, setActiveTab]: [string, Dispatch>] = useState(activeTabHash) + const tabsConfig = useMemo(() => WalletTabsConfig.filter(tab => ( + tab.id !== WalletTabViews.payout || canViewPayout + )), [canViewPayout]) useEffect(() => { setActiveTab(activeTabHash) }, [activeTabHash]) - function handleTabChange(tabId: string): void { + function handleTabChange(tabId: WalletTabViews): void { setActiveTab(tabId) window.location.hash = getHashFromTabId(tabId) } return (
- + - {[WalletTabsConfig.find((tab: TabsNavItem) => tab.id === activeTab)?.title, 'Wallet', 'Topcoder'].join( - ' | ', - )} + {[ + WalletTabsConfig.find((tab: TabsNavItem) => tab.id === activeTab)?.title, + 'Wallet', + 'Topcoder', + ].join(' | ')} - {activeTab === WalletTabViews.withdrawalmethods && } - {activeTab === WalletTabViews.winnings && } {activeTab === WalletTabViews.home && } - {activeTab === WalletTabViews.taxforms && } + + {activeTab === WalletTabViews.payout && } +
) } diff --git a/src/apps/wallet/src/home/tabs/config/wallet-tabs-config.ts b/src/apps/wallet/src/home/tabs/config/wallet-tabs-config.ts index 4a31f3f23..eb0eac4e9 100644 --- a/src/apps/wallet/src/home/tabs/config/wallet-tabs-config.ts +++ b/src/apps/wallet/src/home/tabs/config/wallet-tabs-config.ts @@ -1,13 +1,12 @@ import { TabsNavItem } from '~/libs/ui' export enum WalletTabViews { - home = '0', - winnings = '1', - taxforms = '2', - withdrawalmethods = '3', + home, + winnings, + payout, } -export const WalletTabsConfig: TabsNavItem[] = [ +export const WalletTabsConfig: TabsNavItem[] = [ { id: WalletTabViews.home, title: 'Wallet', @@ -17,38 +16,28 @@ export const WalletTabsConfig: TabsNavItem[] = [ title: 'Winnings', }, { - id: WalletTabViews.withdrawalmethods, - title: 'Withdrawal Methods', - }, - { - id: WalletTabViews.taxforms, - title: 'Tax Forms', + id: WalletTabViews.payout, + title: 'Payout', }, ] -export function getHashFromTabId(tabId: string): string { +export function getHashFromTabId(tabId: WalletTabViews): string { switch (tabId) { - case WalletTabViews.home: - return '#home' case WalletTabViews.winnings: return '#winnings' - case WalletTabViews.taxforms: - return '#tax-forms' - case WalletTabViews.withdrawalmethods: - return '#withdrawal-methods' + case WalletTabViews.payout: + return '#payout' default: return '#home' } } -export function getTabIdFromHash(hash: string): string { +export function getTabIdFromHash(hash: string): WalletTabViews { switch (hash) { case '#winnings': return WalletTabViews.winnings - case '#tax-forms': - return WalletTabViews.taxforms - case '#withdrawal-methods': - return WalletTabViews.withdrawalmethods + case '#payout': + return WalletTabViews.payout default: return WalletTabViews.home } diff --git a/src/apps/wallet/src/home/tabs/home/HomeTab.tsx b/src/apps/wallet/src/home/tabs/home/HomeTab.tsx index e37eefb93..78619a12f 100644 --- a/src/apps/wallet/src/home/tabs/home/HomeTab.tsx +++ b/src/apps/wallet/src/home/tabs/home/HomeTab.tsx @@ -4,10 +4,11 @@ import { FC, useEffect, useState } from 'react' import { UserProfile } from '~/libs/core' import { IconOutline, LinkButton, LoadingCircles } from '~/libs/ui' -import { Balance, WalletDetails } from '../../../lib/models/WalletDetails' -import { getWalletDetails } from '../../../lib/services/wallet' -import { InfoRow } from '../../../lib' +import { Balance } from '../../../lib/models/WalletDetails' +import { InfoRow, PayoutGuard } from '../../../lib' import { BannerImage, BannerText } from '../../../lib/assets/home' +import { nullToZero } from '../../../lib/util' +import { useWalletDetails, WalletDetailsResponse } from '../../../lib/hooks/use-wallet-details' import Chip from '../../../lib/components/chip/Chip' import styles from './Home.module.scss' @@ -16,28 +17,10 @@ interface HomeTabProps { profile: UserProfile } -const HomeTab: FC = () => { - const [walletDetails, setWalletDetails] = useState(undefined) - const [isLoading, setIsLoading] = useState(false) - const [balanceSum, setBalanceSum] = useState(0) - const [error, setError] = useState(undefined) - - useEffect(() => { - // eslint-disable-next-line @typescript-eslint/explicit-function-return-type - const fetchWalletDetails = async () => { - setIsLoading(true) - try { - const details = await getWalletDetails() - setWalletDetails(details) - } catch (apiError) { - setError('Error fetching wallet details') - } +const HomeTab: FC = props => { - setIsLoading(false) - } - - fetchWalletDetails() - }, []) + const { data: walletDetails, isLoading, error }: WalletDetailsResponse = useWalletDetails() + const [balanceSum, setBalanceSum] = useState(0) useEffect(() => { if (walletDetails) { @@ -58,7 +41,7 @@ const HomeTab: FC = () => { {isLoading && } - {!isLoading && ( + {!isLoading && walletDetails && (
= () => { /> } /> - {!walletDetails?.withdrawalMethod.isSetupComplete && ( - - ) - } - action={ - - } - /> - )} - {!walletDetails?.taxForm.isSetupComplete && ( - } - action={ - - } - /> - )} + + {walletDetails.withdrawalMethod.isSetupComplete && ( + + } + /> + )} + {walletDetails.taxForm.isSetupComplete && ( + + } + /> + )} + + {!walletDetails?.withdrawalMethod.isSetupComplete && ( + + ) + } + action={ + + } + /> + )} + + {!walletDetails?.taxForm.isSetupComplete && ( + + } + action={ + + } + /> + )} + {!walletDetails?.identityVerification.isSetupComplete && ( + } + action={ + + } + /> + )} +
)} diff --git a/src/apps/wallet/src/home/tabs/payments/PaymentsTab.module.scss b/src/apps/wallet/src/home/tabs/payments/PaymentsTab.module.scss deleted file mode 100644 index c6d8d6f81..000000000 --- a/src/apps/wallet/src/home/tabs/payments/PaymentsTab.module.scss +++ /dev/null @@ -1,125 +0,0 @@ -@import '@libs/ui/styles/includes'; - -.container { - background-color: $black-5; - padding: $sp-6; - margin: $sp-8 0; - border-radius: 6px; - - @include ltelg { - padding: $sp-4; - } - - .paymentsHeader { - display: flex; - justify-content: flex-start; - gap: 5px; - align-items: center; - - @include ltelg { - flex-direction: column; - } - - .managePaymentsLink { - font-weight: $font-weight-bold; - color: $turq-160; - display: flex; - align-items: center; - - @include ltelg { - margin-top: $sp-4; - } - - svg { - margin-left: $sp-2; - max-width: 100%; - } - } - } - - .content { - background-color: $tc-white; - border-radius: 4px; - margin-top: $sp-8; - - .confirmSelectionReset { - display: flex; - justify-content: space-between; - align-items: center; - margin-top: $sp-8; - - @include ltelg { - flex-direction: column; - align-items: flex-start; - - button { - margin-top: $sp-4; - } - } - } - - .providerContainer { - display: flex; - flex-direction: column; - align-items: flex-start; - margin-top: $sp-8; - margin-bottom: $sp-4; - - .alternateProviderButton { - margin-top: $sp-4; - padding-left: 0px !important; - padding-right: 0px !important; - } - } - - .providersSingleRow { - margin-top: $sp-4; - display: grid; - grid-template-columns: repeat(1, 1fr); - width: 100%; - } - - .providersStacked { - display: grid; - gap: $sp-4; - grid-template-columns: repeat(1, 1fr); - margin-top: 24px; - - @media (min-width: '768px') { - grid-template-columns: repeat(2, 1fr); - grid-template-rows: auto auto; - } - } - - .providerSubmitted { - margin-top: $sp-8; - - @include ltelg { - flex-direction: column; - } - - .providerSubmittedIcon { - background: linear-gradient(264.69deg, #198807 2.17%, #017c6d 97.49%); - padding: $sp-4; - border-radius: 4px; - width: 64px; - height: 64px; - color: $tc-white; - margin-right: $sp-4; - - @include ltelg { - margin-bottom: $sp-4; - } - } - - button { - align-self: center; - - @include ltelg { - align-self: flex-start; - margin-top: $sp-4; - } - } - } - } -} diff --git a/src/apps/wallet/src/home/tabs/payments/PaymentsTab.tsx b/src/apps/wallet/src/home/tabs/payments/PaymentsTab.tsx deleted file mode 100644 index ef426d99c..000000000 --- a/src/apps/wallet/src/home/tabs/payments/PaymentsTab.tsx +++ /dev/null @@ -1,338 +0,0 @@ -/* eslint-disable @typescript-eslint/explicit-function-return-type */ -/* eslint-disable react/jsx-no-bind */ -import { FC, useEffect, useState } from 'react' -import { toast } from 'react-toastify' - -import { Button, Collapsible, LoadingCircles } from '~/libs/ui' -import { ArrowDownIcon, ArrowUpIcon } from '@heroicons/react/solid' - -import { Chip, IconDollar, IconSpeed, IconWorld, PayoneerLogo, PayPalLogo } from '../../../lib' -import { PaymentProvider } from '../../../lib/models/PaymentProvider' -import { PaymentProviderCard } from '../../../lib/components/payment-provider-card' -import { OtpModal } from '../../../lib/components/otp-modal' -import { TransactionResponse } from '../../../lib/models/TransactionId' -import { - confirmPaymentProvider, - getPaymentProviderRegistrationLink, - getUserPaymentProviders, removePaymentProvider, resendOtp, setPaymentProvider, -} from '../../../lib/services/wallet' - -import { PaymentInfoModal } from './payment-info-modal' -import styles from './PaymentsTab.module.scss' - -const PAYMENT_PROVIDER_DETAILS = { - Payoneer: { - details: [ - { - icon: , - label: 'FEES', - value: '$0-$3 + Currency Conversion Rates May Apply', - }, - { - icon: , - label: 'COUNTRIES', - value: 'Available in 150+ countries', - }, - { - icon: , - label: 'SPEED', - value: 'Up to 1 Business Day', - }, - ], - logo: , - }, - Paypal: { - details: [ - { - icon: , - label: 'FEES', - value: '3.49% + an international fee (non US) + a fixed fee depending upon currency', - }, - { - icon: , - label: 'COUNTRIES', - value: 'Available in 200+ countries', - }, - { - icon: , - label: 'SPEED', - value: 'Up to 1 Business Day', - }, - ], - logo: , - }, -} - -const PaymentsTab: FC = () => { - const [selectedPaymentProvider, setSelectedPaymentProvider] = useState(undefined) - const [setupRequired, setSetupRequired] = useState(false) - const [isLoading, setIsLoading] = useState(false) - const [showAlternateProvider, setShowAlternateProvider] = useState(false) - - const [paymentInfoModalFlow, setPaymentInfoModalFlow] = useState(undefined) - const [otpFlow, setOtpFlow] = useState(undefined) - - const fetchPaymentProviders = async (refresh: boolean = true) => { - setIsLoading(refresh) - - try { - const providers = await getUserPaymentProviders() - if (providers.length === 0) { - setSetupRequired(true) - } else { - setSetupRequired(false) - setSelectedPaymentProvider(providers[0]) - } - - } catch (apiError) { - setSelectedPaymentProvider(undefined) - } - - setIsLoading(false) - } - - useEffect(() => { - fetchPaymentProviders() - }, []) - - useEffect(() => { - if (selectedPaymentProvider?.status === 'OTP_VERIFIED') { - const queryParams = new URLSearchParams(window.location.search) - const code = queryParams.get('code') - - if (code) { - if (selectedPaymentProvider.type === 'Paypal' && selectedPaymentProvider.transactionId) { - confirmPaymentProvider('Paypal', code, selectedPaymentProvider.transactionId) - .then((response: any) => { - fetchPaymentProviders() - toast.success( - response.message ?? 'Payment provider added successfully.', - { position: toast.POSITION.BOTTOM_RIGHT }, - ) - }) - .catch((err: any) => { - toast.error( - err.message ?? 'Something went wrong. Please try again.', - { position: toast.POSITION.BOTTOM_RIGHT }, - ) - }) - } - } - } - }, [selectedPaymentProvider?.status, selectedPaymentProvider?.type, selectedPaymentProvider?.transactionId]) - - function renderProviders(): JSX.Element { - return ( -
- - -
- ) - } - - function renderConnectedProvider(): JSX.Element | undefined { - if (selectedPaymentProvider === undefined) return undefined - - return ( -
-

Chosen Payment Provider

-
- -
-
- ) - } - - return ( -
-
-

WITHDRAWAL METHODS

- {!isLoading && setupRequired && } -
- -
- PAYMENT PROVIDER}> -

- Topcoder is partnered with several payment providers to send payments to our community members. - Once a provider is set up, payments will be routed to your selected payment provider at the - completion of work. Currently, members can be paid through one of the following providers: - Payoneer® or PayPal®. -

- - {isLoading && } - - {!isLoading && selectedPaymentProvider === undefined && renderProviders()} - {!isLoading && selectedPaymentProvider !== undefined && renderConnectedProvider()} - -

- Provider details are based on the latest information from their official sites; we advise - confirming the current terms directly before finalizing your payment option. -

-
-
- - {paymentInfoModalFlow && ( - { - setOtpFlow({ - ...response, - type: 'SETUP_PAYMENT_PROVIDER', - }) - fetchPaymentProviders(false) - }) - .catch((err: Error) => { - toast.error( - err.message ?? 'Something went wrong. Please try again.', - { position: toast.POSITION.BOTTOM_RIGHT }, - ) - }) - }} - /> - )} - {otpFlow && ( - - )} -
- ) -} - -export default PaymentsTab diff --git a/src/apps/wallet/src/home/tabs/payments/index.ts b/src/apps/wallet/src/home/tabs/payments/index.ts deleted file mode 100644 index 8dc41bc69..000000000 --- a/src/apps/wallet/src/home/tabs/payments/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { default as PaymentsTab } from './PaymentsTab' diff --git a/src/apps/wallet/src/home/tabs/payments/payment-info-modal/PaymentInfoModal.module.scss b/src/apps/wallet/src/home/tabs/payments/payment-info-modal/PaymentInfoModal.module.scss deleted file mode 100644 index 3917b7e01..000000000 --- a/src/apps/wallet/src/home/tabs/payments/payment-info-modal/PaymentInfoModal.module.scss +++ /dev/null @@ -1,13 +0,0 @@ -@import '@libs/ui/styles/includes'; - -.infoModal { - :global(.react-responsive-modal-closeButton) { - display: flex; - } - - .modalContent { - display: flex; - flex-direction: column; - gap: 25px; - } -} diff --git a/src/apps/wallet/src/home/tabs/payments/payment-info-modal/PaymentInfoModal.tsx b/src/apps/wallet/src/home/tabs/payments/payment-info-modal/PaymentInfoModal.tsx deleted file mode 100644 index cdb6df8be..000000000 --- a/src/apps/wallet/src/home/tabs/payments/payment-info-modal/PaymentInfoModal.tsx +++ /dev/null @@ -1,85 +0,0 @@ -/* eslint-disable react/jsx-wrap-multilines */ -/* eslint-disable react/jsx-no-bind */ -import { FC } from 'react' - -import { BaseModal, Button, IconOutline, LinkButton } from '~/libs/ui' - -import { PayoneerLogo, PayPalLogo } from '../../../../lib' - -import styles from './PaymentInfoModal.module.scss' - -interface PaymentInfoModalProps { - selectedPaymentProvider: string - handlePaymentSelection: (provider: string) => void - handleModalClose: () => void -} - -function renderPayoneer(): JSX.Element { - return ( - <> - -

- You can elect to receive payments through Payoneer either to your Payoneer prepaid MasterCard or by - using their Global Bank Transfer service. The Payoneer Bank Transfer Service offers a local bank - transfer option (where available) and a wire transfer option. Certain fees may apply. -

-

- You will be directed to Payoneer's website in a new tab to complete your connection. Please make - sure your account is fully verified to ensure withdrawal success. - - You can return here after finishing up on Payoneer's site. - -

- - ) -} - -function renderPaypal(): JSX.Element { - return ( - <> - -

You can elect to receive payments deposited directly to your PayPal account. Certain fees may apply.

-

- You will be directed to PayPal's website in a new tab to complete your connection. Please make - sure your account is fully verified to ensure withdrawal success. - {' '} - - You can return here after finishing up - on PayPal's site. - -

- - ) -} - -const PaymentInfoModal: FC = (props: PaymentInfoModalProps) => ( - - -