diff --git a/.circleci/config.yml b/.circleci/config.yml index eb97a7ce7..d9b8f6456 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -257,7 +257,7 @@ workflows: branches: only: - dev - - feat/wallet-admin + - LVT-256 - 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 a26d94225..73d694e1d 100644 --- a/src/apps/wallet-admin/src/home/tabs/WalletAdminTabs.tsx +++ b/src/apps/wallet-admin/src/home/tabs/WalletAdminTabs.tsx @@ -7,6 +7,8 @@ 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 { @@ -44,6 +46,10 @@ 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 afeb83052..b29c75195 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,8 @@ import { TabsNavItem } from '~/libs/ui' export enum WalletAdminTabViews { home = '0', payments = '1', - // taxforms = '2', - // withdrawalmethods = '3', + taxforms = '2', + withdrawalmethods = '3', } export const WalletAdminTabsConfig: TabsNavItem[] = [ @@ -16,14 +16,14 @@ export const WalletAdminTabsConfig: TabsNavItem[] = [ id: WalletAdminTabViews.payments, title: 'Payments', }, - // { - // id: WalletAdminTabViews.withdrawalmethods, - // title: 'Withdrawal Methods', - // }, - // { - // id: WalletAdminTabViews.taxforms, - // title: 'Tax Forms', - // }, + { + id: WalletAdminTabViews.withdrawalmethods, + title: 'Payment Providers', + }, + { + id: WalletAdminTabViews.taxforms, + title: 'Tax Forms', + }, ] export function getHashFromTabId(tabId: string): string { @@ -32,10 +32,10 @@ export function getHashFromTabId(tabId: string): string { return '#home' case WalletAdminTabViews.payments: return '#payments' - // case WalletAdminTabViews.taxforms: - // return '#tax-forms' - // case WalletAdminTabViews.withdrawalmethods: - // return '#withdrawal-methods' + case WalletAdminTabViews.taxforms: + return '#tax-forms' + case WalletAdminTabViews.withdrawalmethods: + return '#payment-providers' default: return '#home' } @@ -43,12 +43,12 @@ export function getHashFromTabId(tabId: string): string { export function getTabIdFromHash(hash: string): string { switch (hash) { - case '#winnings': + case '#payments': return WalletAdminTabViews.payments - // case '#tax-forms': - // return WalletAdminTabViews.taxforms - // case '#withdrawal-methods': - // return WalletAdminTabViews.withdrawalmethods + 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 new file mode 100644 index 000000000..4d5773ab0 --- /dev/null +++ b/src/apps/wallet-admin/src/home/tabs/payment-methods/PaymentMethodsTab.module.scss @@ -0,0 +1,35 @@ +@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 new file mode 100644 index 000000000..9539b9f7a --- /dev/null +++ b/src/apps/wallet-admin/src/home/tabs/payment-methods/PaymentMethodsTab.tsx @@ -0,0 +1,209 @@ +/* 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 new file mode 100644 index 000000000..ae6c8e814 --- /dev/null +++ b/src/apps/wallet-admin/src/home/tabs/payment-methods/index.ts @@ -0,0 +1 @@ +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 ed3167d6b..c26ad9302 100644 --- a/src/apps/wallet-admin/src/home/tabs/payments/PaymentsTab.tsx +++ b/src/apps/wallet-admin/src/home/tabs/payments/PaymentsTab.tsx @@ -1,16 +1,20 @@ /* 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 { editPayment, getMemberHandle, getPayments } from '../../../lib/services/wallet' +import { editPayment, exportSearchResults, getMemberHandle, getPaymentMethods, getPayments, getTaxForms } from '../../../lib/services/wallet' import { Winning, WinningDetail } from '../../../lib/models/WinningDetail' -import { FilterBar, PaymentView } from '../../../lib' +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' @@ -21,34 +25,18 @@ interface ListViewProps { profile: UserProfile } -function formatIOSDateString(iosDateString: string): string { - const date = new Date(iosDateString) - - if (Number.isNaN(date.getTime())) { - throw new Error('Invalid date string') - } - - const options: Intl.DateTimeFormatOptions = { - day: '2-digit', - month: '2-digit', - year: 'numeric', - } - - return date.toLocaleDateString('en-GB', options) -} - function formatStatus(status: string): string { switch (status) { case 'ON_HOLD': - return 'Owed' + return 'ON_HOLD' case 'ON_HOLD_ADMIN': - return 'On Hold' + return 'On Hold (Admin)' case 'OWED': return 'Owed' case 'PAID': return 'Paid' case 'CANCELLED': - return 'Cancelled' + return 'Cancel' default: return status.replaceAll('_', ' ') } @@ -91,6 +79,7 @@ const ListView: FC = (props: ListViewProps) => { paymentStatus?: string; auditNote?: string; }>({}) + const [apiErrorMsg, setApiErrorMsg] = React.useState('Member earnings will appear here.') const editStateRef = React.useRef(editState) @@ -111,7 +100,7 @@ const ListView: FC = (props: ListViewProps) => { }, []) const convertToWinnings = useCallback( - (payments: WinningDetail[], handleMap: Map) => payments.map(payment => { + (payments: WinningDetail[], handleMap: Map, userHasTaxFormSetup: Map, userHasPaymentProvider: Map): ReadonlyArray => payments.map(payment => { const now = new Date() const releaseDate = new Date(payment.releaseDate) const diffMs = releaseDate.getTime() - now.getTime() @@ -132,19 +121,35 @@ const ListView: FC = (props: ListViewProps) => { formattedReleaseDate = formatIOSDateString(payment.releaseDate) } + let status = formatStatus(payment.details[0].status) + if (status === 'Cancel') { + status = 'Cancelled' + } + + if (status === 'ON_HOLD') { + if (!userHasTaxFormSetup.get(payment.winnerId)) { + status = 'On Hold (Tax Form)' + } else if (!userHasPaymentProvider.get(payment.winnerId)) { + status = 'On Hold (Payment Provider)' + } else { + status = 'On Hold (Member)' + } + } + return { createDate: formatIOSDateString(payment.createdAt), currency: payment.details[0].currency, datePaid: payment.details[0].datePaid ? formatIOSDateString(payment.details[0].datePaid) : '-', description: payment.description, details: payment.details, + externalId: payment.externalId, 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: formatStatus(payment.details[0].status), + status, type: payment.category.replaceAll('_', ' ') .toLowerCase(), } @@ -161,12 +166,39 @@ const ListView: FC = (props: ListViewProps) => { try { 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) + const winningsData = convertToWinnings(payments.winnings, handleMap, userHasTaxFormSetup, userHasPaymentProvider) setWinnings(winningsData) setPagination(payments.pagination) } catch (apiError) { - console.error('Failed to fetch winnings:', 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) } @@ -195,12 +227,20 @@ const ListView: FC = (props: ListViewProps) => { releaseDate: currentEditState.releaseDate !== undefined ? currentEditState.releaseDate : undefined, } - let paymentStatus : 'ON_HOLD_ADMIN' | 'OWED' | undefined - if (updateObj.paymentStatus !== undefined) paymentStatus = updateObj.paymentStatus.indexOf('Owed') > -1 ? 'OWED' : 'ON_HOLD_ADMIN' + let paymentStatus : 'ON_HOLD_ADMIN' | 'OWED' | 'CANCELLED' | undefined + if (updateObj.paymentStatus !== undefined) { + if (updateObj.paymentStatus === 'Owed') { + paymentStatus = 'OWED' + } else if (updateObj.paymentStatus === 'On Hold') { + paymentStatus = 'ON_HOLD_ADMIN' + } else if (updateObj.paymentStatus === 'Cancel') { + paymentStatus = 'CANCELLED' + } + } const updates: { auditNote?: string - paymentStatus?: 'ON_HOLD_ADMIN' | 'OWED' + paymentStatus?: 'ON_HOLD_ADMIN' | 'OWED' | 'CANCELLED' releaseDate?: string paymentAmount?: number winningsId: string @@ -210,8 +250,10 @@ const ListView: FC = (props: ListViewProps) => { } if (paymentStatus) updates.paymentStatus = paymentStatus - if (updateObj.releaseDate !== undefined) updates.releaseDate = updateObj.releaseDate.toISOString() - if (updateObj.netAmount !== undefined) updates.paymentAmount = updateObj.netAmount + if (paymentStatus !== 'CANCELLED') { + if (updateObj.releaseDate !== undefined) updates.releaseDate = updateObj.releaseDate.toISOString() + if (updateObj.netAmount !== undefined) updates.paymentAmount = updateObj.netAmount + } toast.success('Updating payment', { position: toast.POSITION.BOTTOM_RIGHT }) try { @@ -232,6 +274,13 @@ const ListView: FC = (props: ListViewProps) => { }, [fetchWinnings]) const onPaymentEditCallback = useCallback((payment: Winning) => { + let status = payment.status + if (status === 'On Hold (Admin)') { + status = 'On Hold' + } else if (['On Hold (Member)', 'On Hold (Tax Form)', 'On Hold (Payment Provider)'].indexOf(status) !== -1) { + status = 'Owed' + } + setConfirmFlow({ action: 'Save', callback: async () => { @@ -239,7 +288,10 @@ const ListView: FC = (props: ListViewProps) => { }, content: ( @@ -260,77 +312,68 @@ const ListView: FC = (props: ListViewProps) => {
Payment Listing}> { + toast.success('Downloading payments report. This may take a few moments.', { position: toast.POSITION.BOTTOM_RIGHT }) + downloadBlob( + await exportSearchResults(filters), + `payments-${new Date() + .getTime()}.csv`, + ) + toast.success('Download complete', { position: toast.POSITION.BOTTOM_RIGHT }) + }} filters={[ { key: 'winnerIds', - label: 'User ID', + label: 'Username/Handle', type: 'member_autocomplete', }, - // { - // key: 'date', - // label: 'Date', - // options: [ - // { - // label: 'Last 7 days', - // value: 'last7days', - // }, - // { - // label: 'Last 30 days', - // value: 'last30days', - // }, - // { - // label: 'All', - // value: 'all', - // }, - // ], - // type: 'dropdown', - // }, - // { - // key: 'type', - // label: 'Type', - // options: [ - // { - // label: 'Task Payment', - // value: 'TASK_PAYMENT', - // }, - // { - // label: 'Contest Payment', - // value: 'CONTEST_PAYMENT', - // }, - // { - // label: 'Copilot Payment', - // value: 'COPILOT_PAYMENT', - // }, - // { - // label: 'Review Board Payment', - // value: 'REVIEW_BOARD_PAYMENT', - // }, - // ], - // type: 'dropdown', - // }, - // { - // key: 'status', - // label: 'Status', - // options: [ - // { - // label: 'Available', - // value: 'OWED', - // }, - // { - // label: 'On Hold', - // value: 'ON_HOLD', - // }, - // { - // label: 'Paid', - // value: 'PAID', - // }, - // { - // label: 'Cancelled', - // value: 'CANCELLED', - // }, - // ], - // type: 'dropdown', - // }, + { + key: 'status', + label: 'Status', + options: [ + { + label: 'Owed', + value: 'OWED', + }, + { + label: 'On Hold (Admin)', + value: 'ON_HOLD_ADMIN', + }, + { + label: 'On Hold (Member)', + value: 'ON_HOLD', + }, + { + label: 'Paid', + value: 'PAID', + }, + { + label: 'Cancelled', + value: 'CANCELLED', + }, + ], + type: 'dropdown', + }, + { + key: 'date', + label: 'Date', + options: [ + { + label: 'Last 7 days', + value: 'last7days', + }, + { + label: 'Last 30 days', + value: 'last30days', + }, + { + label: 'All', + value: 'all', + }, + ], + type: 'dropdown', + }, { key: 'pageSize', label: 'Payments per page', @@ -436,7 +479,7 @@ const ListView: FC = (props: ListViewProps) => {

{Object.keys(filters).length === 0 - ? 'Member earnings will appear here.' + ? apiErrorMsg : 'No payments match your filters.'}

@@ -446,6 +489,8 @@ const ListView: FC = (props: ListViewProps) => {
{confirmFlow && ( = (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 new file mode 100644 index 000000000..08eb9c245 --- /dev/null +++ b/src/apps/wallet-admin/src/home/tabs/tax-forms/index.ts @@ -0,0 +1 @@ +export { default as TaxFormsTab } from './TaxFormsTab' 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 d601f3c9c..154292610 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,7 +1,7 @@ /* eslint-disable react/jsx-no-bind */ import React, { ChangeEvent } from 'react' -import { Button, InputSelect, InputText } from '~/libs/ui' +import { Button, IconOutline, InputSelect, InputText } from '~/libs/ui' import { InputHandleAutocomplete, MembersAutocompeteResult } from '~/apps/gamification-admin/src/game-lib' import styles from './FilterBar.module.scss' @@ -20,8 +20,10 @@ type Filter = { interface FilterBarProps { filters: Filter[]; + showExportButton?: boolean; onFilterChange: (key: string, value: string[]) => void; onResetFilters?: () => void; + onExport?: () => void; } const FilterBar: React.FC = (props: FilterBarProps) => { @@ -104,6 +106,14 @@ const FilterBar: React.FC = (props: FilterBarProps) => { ))} + {props.showExportButton && ( +