diff --git a/packages/server/src/api/controllers/Banking/BankAccountsController.ts b/packages/server/src/api/controllers/Banking/BankAccountsController.ts index f337c0b38..424c28857 100644 --- a/packages/server/src/api/controllers/Banking/BankAccountsController.ts +++ b/packages/server/src/api/controllers/Banking/BankAccountsController.ts @@ -1,9 +1,9 @@ import { Inject, Service } from 'typedi'; import { NextFunction, Request, Response, Router } from 'express'; import BaseController from '@/api/controllers/BaseController'; -import { CashflowApplication } from '@/services/Cashflow/CashflowApplication'; import { GetBankAccountSummary } from '@/services/Banking/BankAccounts/GetBankAccountSummary'; import { BankAccountsApplication } from '@/services/Banking/BankAccounts/BankAccountsApplication'; +import { param } from 'express-validator'; @Service() export class BankAccountsController extends BaseController { @@ -25,6 +25,22 @@ export class BankAccountsController extends BaseController { this.disconnectBankAccount.bind(this) ); router.post('/:bankAccountId/update', this.refreshBankAccount.bind(this)); + router.post( + '/:bankAccountId/pause_feeds', + [ + param('bankAccountId').exists().isNumeric().toInt(), + ], + this.validationResult, + this.pauseBankAccountFeeds.bind(this) + ); + router.post( + '/:bankAccountId/resume_feeds', + [ + param('bankAccountId').exists().isNumeric().toInt(), + ], + this.validationResult, + this.resumeBankAccountFeeds.bind(this) + ); return router; } @@ -109,4 +125,58 @@ export class BankAccountsController extends BaseController { next(error); } } + + /** + * Resumes the bank account feeds sync. + * @param {Request} req + * @param {Response} res + * @param {NextFunction} next + * @returns {Promise} + */ + async resumeBankAccountFeeds( + req: Request<{ bankAccountId: number }>, + res: Response, + next: NextFunction + ) { + const { bankAccountId } = req.params; + const { tenantId } = req; + + try { + await this.bankAccountsApp.resumeBankAccount(tenantId, bankAccountId); + + return res.status(200).send({ + message: 'The bank account feeds syncing has been resumed.', + id: bankAccountId, + }); + } catch (error) { + next(error); + } + } + + /** + * Pauses the bank account feeds sync. + * @param {Request} req + * @param {Response} res + * @param {NextFunction} next + * @returns {Promise} + */ + async pauseBankAccountFeeds( + req: Request<{ bankAccountId: number }>, + res: Response, + next: NextFunction + ) { + const { bankAccountId } = req.params; + const { tenantId } = req; + + try { + await this.bankAccountsApp.pauseBankAccount(tenantId, bankAccountId); + + return res.status(200).send({ + message: 'The bank account feeds syncing has been paused.', + id: bankAccountId, + }); + } catch (error) { + next(error); + } + } } diff --git a/packages/server/src/database/migrations/20240804084709_create_paused_at_column_to_plaid_items_table.js b/packages/server/src/database/migrations/20240804084709_create_paused_at_column_to_plaid_items_table.js new file mode 100644 index 000000000..2947eb30b --- /dev/null +++ b/packages/server/src/database/migrations/20240804084709_create_paused_at_column_to_plaid_items_table.js @@ -0,0 +1,11 @@ +exports.up = function (knex) { + return knex.schema.table('plaid_items', (table) => { + table.datetime('paused_at'); + }); +}; + +exports.down = function (knex) { + return knex.schema.table('plaid_items', (table) => { + table.dropColumn('paused_at'); + }); +}; diff --git a/packages/server/src/models/PlaidItem.ts b/packages/server/src/models/PlaidItem.ts index 6dc515394..aca189038 100644 --- a/packages/server/src/models/PlaidItem.ts +++ b/packages/server/src/models/PlaidItem.ts @@ -1,6 +1,8 @@ import TenantModel from 'models/TenantModel'; export default class PlaidItem extends TenantModel { + pausedAt: Date; + /** * Table name. */ @@ -21,4 +23,19 @@ export default class PlaidItem extends TenantModel { static get relationMappings() { return {}; } + + /** + * Virtual attributes. + */ + static get virtualAttributes() { + return ['isPaused']; + } + + /** + * Detarmines whether the Plaid item feeds syncing is paused. + * @return {boolean} + */ + get isPaused() { + return !!this.pausedAt; + } } diff --git a/packages/server/src/services/Accounts/AccountTransform.ts b/packages/server/src/services/Accounts/AccountTransform.ts index 28f3b74a5..1fde95fe4 100644 --- a/packages/server/src/services/Accounts/AccountTransform.ts +++ b/packages/server/src/services/Accounts/AccountTransform.ts @@ -18,9 +18,18 @@ export class AccountTransformer extends Transformer { 'flattenName', 'bankBalanceFormatted', 'lastFeedsUpdatedAtFormatted', + 'isFeedsPaused', ]; }; + /** + * Exclude attributes. + * @returns {string[]} + */ + public excludeAttributes = (): string[] => { + return ['plaidItem']; + }; + /** * Retrieves the flatten name with all dependants accounts names. * @param {IAccount} account - @@ -66,6 +75,15 @@ export class AccountTransformer extends Transformer { return this.formatDate(account.lastFeedsUpdatedAt); }; + /** + * Detarmines whether the bank account connection is paused. + * @param account + * @returns {boolean} + */ + protected isFeedsPaused = (account: any): boolean => { + return account.plaidItem?.isPaused || false; + }; + /** * Transformes the accounts collection to flat or nested array. * @param {IAccount[]} diff --git a/packages/server/src/services/Accounts/GetAccount.ts b/packages/server/src/services/Accounts/GetAccount.ts index 7da213328..c16c69459 100644 --- a/packages/server/src/services/Accounts/GetAccount.ts +++ b/packages/server/src/services/Accounts/GetAccount.ts @@ -25,7 +25,10 @@ export class GetAccount { const { accountRepository } = this.tenancy.repositories(tenantId); // Find the given account or throw not found error. - const account = await Account.query().findById(accountId).throwIfNotFound(); + const account = await Account.query() + .findById(accountId) + .withGraphFetched('plaidItem') + .throwIfNotFound(); const accountsGraph = await accountRepository.getDependencyGraph(); diff --git a/packages/server/src/services/Banking/BankAccounts/BankAccountsApplication.tsx b/packages/server/src/services/Banking/BankAccounts/BankAccountsApplication.tsx index 51c12106e..b2fc48775 100644 --- a/packages/server/src/services/Banking/BankAccounts/BankAccountsApplication.tsx +++ b/packages/server/src/services/Banking/BankAccounts/BankAccountsApplication.tsx @@ -1,6 +1,8 @@ import { Inject, Service } from 'typedi'; import { DisconnectBankAccount } from './DisconnectBankAccount'; import { RefreshBankAccountService } from './RefreshBankAccount'; +import { PauseBankAccountFeeds } from './PauseBankAccountFeeds'; +import { ResumeBankAccountFeeds } from './ResumeBankAccountFeeds'; @Service() export class BankAccountsApplication { @@ -10,6 +12,12 @@ export class BankAccountsApplication { @Inject() private refreshBankAccountService: RefreshBankAccountService; + @Inject() + private resumeBankAccountFeedsService: ResumeBankAccountFeeds; + + @Inject() + private pauseBankAccountFeedsService: PauseBankAccountFeeds; + /** * Disconnects the given bank account. * @param {number} tenantId @@ -27,7 +35,7 @@ export class BankAccountsApplication { * Refresh the bank transactions of the given bank account. * @param {number} tenantId * @param {number} bankAccountId - * @returns {Promise} + * @returns {Promise} */ async refreshBankAccount(tenantId: number, bankAccountId: number) { return this.refreshBankAccountService.refreshBankAccount( @@ -35,4 +43,30 @@ export class BankAccountsApplication { bankAccountId ); } + + /** + * Pauses the feeds sync of the given bank account. + * @param {number} tenantId + * @param {number} bankAccountId + * @returns {Promise} + */ + async pauseBankAccount(tenantId: number, bankAccountId: number) { + return this.pauseBankAccountFeedsService.pauseBankAccountFeeds( + tenantId, + bankAccountId + ); + } + + /** + * Resumes the feeds sync of the given bank account. + * @param {number} tenantId + * @param {number} bankAccountId + * @returns {Promise} + */ + async resumeBankAccount(tenantId: number, bankAccountId: number) { + return this.resumeBankAccountFeedsService.resumeBankAccountFeeds( + tenantId, + bankAccountId + ); + } } diff --git a/packages/server/src/services/Banking/BankAccounts/PauseBankAccountFeeds.tsx b/packages/server/src/services/Banking/BankAccounts/PauseBankAccountFeeds.tsx new file mode 100644 index 000000000..3b16ecd76 --- /dev/null +++ b/packages/server/src/services/Banking/BankAccounts/PauseBankAccountFeeds.tsx @@ -0,0 +1,44 @@ +import { Inject, Service } from 'typedi'; +import { Knex } from 'knex'; +import HasTenancyService from '@/services/Tenancy/TenancyService'; +import UnitOfWork from '@/services/UnitOfWork'; +import { ServiceError } from '@/exceptions'; +import { ERRORS } from './types'; + +@Service() +export class PauseBankAccountFeeds { + @Inject() + private tenancy: HasTenancyService; + + @Inject() + private uow: UnitOfWork; + + /** + * Pauses the bankfeed syncing of the given bank account. + * @param {number} tenantId + * @param {number} bankAccountId + * @returns {Promise} + */ + public async pauseBankAccountFeeds(tenantId: number, bankAccountId: number) { + const { Account, PlaidItem } = this.tenancy.models(tenantId); + + const oldAccount = await Account.query() + .findById(bankAccountId) + .withGraphFetched('plaidItem') + .throwIfNotFound(); + + // Can't continue if the bank account is not connected. + if (!oldAccount.plaidItem) { + throw new ServiceError(ERRORS.BANK_ACCOUNT_NOT_CONNECTED); + } + // Cannot continue if the bank account feeds is already paused. + if (oldAccount.plaidItem.isPaused) { + throw new ServiceError(ERRORS.BANK_ACCOUNT_FEEDS_ALREADY_PAUSED); + } + return this.uow.withTransaction(tenantId, async (trx: Knex.Transaction) => { + await PlaidItem.query(trx).findById(oldAccount.plaidItem.id).patch({ + pausedAt: new Date(), + }); + }); + } +} diff --git a/packages/server/src/services/Banking/BankAccounts/ResumeBankAccountFeeds.tsx b/packages/server/src/services/Banking/BankAccounts/ResumeBankAccountFeeds.tsx new file mode 100644 index 000000000..ab630a145 --- /dev/null +++ b/packages/server/src/services/Banking/BankAccounts/ResumeBankAccountFeeds.tsx @@ -0,0 +1,43 @@ +import { Inject, Service } from 'typedi'; +import { Knex } from 'knex'; +import HasTenancyService from '@/services/Tenancy/TenancyService'; +import UnitOfWork from '@/services/UnitOfWork'; +import { ServiceError } from '@/exceptions'; +import { ERRORS } from './types'; + +@Service() +export class ResumeBankAccountFeeds { + @Inject() + private tenancy: HasTenancyService; + + @Inject() + private uow: UnitOfWork; + + /** + * Resumes the bank feeds syncing of the bank account. + * @param {number} tenantId + * @param {number} bankAccountId + * @returns {Promise} + */ + public async resumeBankAccountFeeds(tenantId: number, bankAccountId: number) { + const { Account, PlaidItem } = this.tenancy.models(tenantId); + + const oldAccount = await Account.query() + .findById(bankAccountId) + .withGraphFetched('plaidItem'); + + // Can't continue if the bank account is not connected. + if (!oldAccount.plaidItem) { + throw new ServiceError(ERRORS.BANK_ACCOUNT_NOT_CONNECTED); + } + // Cannot continue if the bank account feeds is already paused. + if (!oldAccount.plaidItem.isPaused) { + throw new ServiceError(ERRORS.BANK_ACCOUNT_FEEDS_ALREADY_RESUMED); + } + return this.uow.withTransaction(tenantId, async (trx: Knex.Transaction) => { + await PlaidItem.query(trx).findById(oldAccount.plaidItem.id).patch({ + pausedAt: null, + }); + }); + } +} diff --git a/packages/server/src/services/Banking/BankAccounts/types.ts b/packages/server/src/services/Banking/BankAccounts/types.ts index d3198cc5c..cd21e490c 100644 --- a/packages/server/src/services/Banking/BankAccounts/types.ts +++ b/packages/server/src/services/Banking/BankAccounts/types.ts @@ -14,4 +14,6 @@ export interface IBankAccountDisconnectedEventPayload { export const ERRORS = { BANK_ACCOUNT_NOT_CONNECTED: 'BANK_ACCOUNT_NOT_CONNECTED', + BANK_ACCOUNT_FEEDS_ALREADY_PAUSED: 'BANK_ACCOUNT_FEEDS_ALREADY_PAUSED', + BANK_ACCOUNT_FEEDS_ALREADY_RESUMED: 'BANK_ACCOUNT_FEEDS_ALREADY_RESUMED', }; diff --git a/packages/server/src/services/Banking/Plaid/PlaidWebhooks.ts b/packages/server/src/services/Banking/Plaid/PlaidWebhooks.ts index 5c3afb1ec..24a8c5d8d 100644 --- a/packages/server/src/services/Banking/Plaid/PlaidWebhooks.ts +++ b/packages/server/src/services/Banking/Plaid/PlaidWebhooks.ts @@ -1,11 +1,15 @@ import { Inject, Service } from 'typedi'; import { PlaidUpdateTransactions } from './PlaidUpdateTransactions'; +import HasTenancyService from '@/services/Tenancy/TenancyService'; @Service() export class PlaidWebooks { @Inject() private updateTransactionsService: PlaidUpdateTransactions; + @Inject() + private tenancy: HasTenancyService; + /** * Listens to Plaid webhooks * @param {number} tenantId - Tenant Id. @@ -61,7 +65,7 @@ export class PlaidWebooks { plaidItemId: string ): void { console.log( - `WEBHOOK: TRANSACTIONS: ${webhookCode}: Plaid_item_id ${plaidItemId}: ${additionalInfo}` + `PLAID WEBHOOK: TRANSACTIONS: ${webhookCode}: Plaid_item_id ${plaidItemId}: ${additionalInfo}` ); } @@ -78,8 +82,21 @@ export class PlaidWebooks { plaidItemId: string, webhookCode: string ): Promise { + const { PlaidItem } = this.tenancy.models(tenantId); + const plaidItem = await PlaidItem.query() + .findById(plaidItemId) + .throwIfNotFound(); + switch (webhookCode) { case 'SYNC_UPDATES_AVAILABLE': { + if (plaidItem.isPaused) { + this.serverLogAndEmitSocket( + 'Plaid item syncing is paused.', + webhookCode, + plaidItemId + ); + return; + } // Fired when new transactions data becomes available. const { addedCount, modifiedCount, removedCount } = await this.updateTransactionsService.updateTransactions( diff --git a/packages/webapp/src/components/DialogsContainer.tsx b/packages/webapp/src/components/DialogsContainer.tsx index 5c753c213..284c3cfbc 100644 --- a/packages/webapp/src/components/DialogsContainer.tsx +++ b/packages/webapp/src/components/DialogsContainer.tsx @@ -52,6 +52,7 @@ import ReceiptMailDialog from '@/containers/Sales/Receipts/ReceiptMailDialog/Rec import PaymentMailDialog from '@/containers/Sales/PaymentReceives/PaymentMailDialog/PaymentMailDialog'; import { ExportDialog } from '@/containers/Dialogs/ExportDialog'; import { RuleFormDialog } from '@/containers/Banking/Rules/RuleFormDialog/RuleFormDialog'; +import { DisconnectBankAccountDialog } from '@/containers/CashFlow/AccountTransactions/dialogs/DisconnectBankAccountDialog/DisconnectBankAccountDialog'; /** * Dialogs container. @@ -148,7 +149,10 @@ export default function DialogsContainer() { - + + ); } diff --git a/packages/webapp/src/constants/dialogs.ts b/packages/webapp/src/constants/dialogs.ts index 07ed83d67..b86755cfb 100644 --- a/packages/webapp/src/constants/dialogs.ts +++ b/packages/webapp/src/constants/dialogs.ts @@ -75,5 +75,6 @@ export enum DialogsName { GeneralLedgerPdfPreview = 'GeneralLedgerPdfPreview', SalesTaxLiabilitySummaryPdfPreview = 'SalesTaxLiabilitySummaryPdfPreview', Export = 'Export', - BankRuleForm = 'BankRuleForm' + BankRuleForm = 'BankRuleForm', + DisconnectBankAccountConfirmation = 'DisconnectBankAccountConfirmation', } diff --git a/packages/webapp/src/containers/AlertsContainer/registered.tsx b/packages/webapp/src/containers/AlertsContainer/registered.tsx index 40724c66c..f585c398f 100644 --- a/packages/webapp/src/containers/AlertsContainer/registered.tsx +++ b/packages/webapp/src/containers/AlertsContainer/registered.tsx @@ -28,6 +28,7 @@ import TaxRatesAlerts from '@/containers/TaxRates/alerts'; import { CashflowAlerts } from '../CashFlow/CashflowAlerts'; import { BankRulesAlerts } from '../Banking/Rules/RulesList/BankRulesAlerts'; import { SubscriptionAlerts } from '../Subscriptions/alerts/alerts'; +import { BankAccountAlerts } from '@/containers/CashFlow/AccountTransactions/alerts'; export default [ ...AccountsAlerts, @@ -58,5 +59,6 @@ export default [ ...TaxRatesAlerts, ...CashflowAlerts, ...BankRulesAlerts, - ...SubscriptionAlerts + ...SubscriptionAlerts, + ...BankAccountAlerts, ]; diff --git a/packages/webapp/src/containers/CashFlow/AccountTransactions/AccountTransactionsActionsBar.tsx b/packages/webapp/src/containers/CashFlow/AccountTransactions/AccountTransactionsActionsBar.tsx index d6ff1b075..994b2c6e0 100644 --- a/packages/webapp/src/containers/CashFlow/AccountTransactions/AccountTransactionsActionsBar.tsx +++ b/packages/webapp/src/containers/CashFlow/AccountTransactions/AccountTransactionsActionsBar.tsx @@ -40,12 +40,13 @@ import withSettingsActions from '@/containers/Settings/withSettingsActions'; import { compose } from '@/utils'; import { - useDisconnectBankAccount, useUpdateBankAccount, useExcludeUncategorizedTransactions, useUnexcludeUncategorizedTransactions, } from '@/hooks/query/bank-rules'; import { withBanking } from '../withBanking'; +import withAlertActions from '@/containers/Alert/withAlertActions'; +import { DialogsName } from '@/constants/dialogs'; function AccountTransactionsActionsBar({ // #withDialogActions @@ -60,6 +61,9 @@ function AccountTransactionsActionsBar({ // #withBanking uncategorizedTransationsIdsSelected, excludedTransactionsIdsSelected, + + // #withAlerts + openAlert, }) { const history = useHistory(); const { accountId, currentAccount } = useAccountTransactionsContext(); @@ -67,7 +71,6 @@ function AccountTransactionsActionsBar({ // Refresh cashflow infinity transactions hook. const { refresh } = useRefreshCashflowTransactionsInfinity(); - const { mutateAsync: disconnectBankAccount } = useDisconnectBankAccount(); const { mutateAsync: updateBankAccount } = useUpdateBankAccount(); // Retrieves the money in/out buttons options. @@ -75,6 +78,7 @@ function AccountTransactionsActionsBar({ const addMoneyOutOptions = useMemo(() => getAddMoneyOutOptions(), []); const isFeedsActive = !!currentAccount.is_feeds_active; + const isFeedsPaused = currentAccount.is_feeds_paused; const isSyncingOwner = currentAccount.is_syncing_owner; // Handle table row size change. @@ -108,19 +112,9 @@ function AccountTransactionsActionsBar({ // Handles the bank account disconnect click. const handleDisconnectClick = () => { - disconnectBankAccount({ bankAccountId: accountId }) - .then(() => { - AppToaster.show({ - message: 'The bank account has been disconnected.', - intent: Intent.SUCCESS, - }); - }) - .catch((error) => { - AppToaster.show({ - message: 'Something went wrong.', - intent: Intent.DANGER, - }); - }); + openDialog(DialogsName.DisconnectBankAccountConfirmation, { + bankAccountId: accountId, + }); }; // handles the bank update button click. const handleBankUpdateClick = () => { @@ -191,6 +185,19 @@ function AccountTransactionsActionsBar({ }); }; + // Handle resume bank feeds syncing. + const handleResumeFeedsSyncing = () => { + openAlert('resume-feeds-syncing-bank-accounnt', { + bankAccountId: accountId, + }); + }; + // Handles pause bank feeds syncing. + const handlePauseFeedsSyncing = () => { + openAlert('pause-feeds-syncing-bank-accounnt', { + bankAccountId: accountId, + }); + }; + return ( @@ -238,7 +245,9 @@ function AccountTransactionsActionsBar({ } - intent={isFeedsActive ? Intent.SUCCESS : Intent.DANGER} + intent={ + isFeedsActive + ? isFeedsPaused + ? Intent.WARNING + : Intent.SUCCESS + : Intent.DANGER + } /> @@ -288,6 +303,23 @@ function AccountTransactionsActionsBar({ + + + + + + + + + + + @@ -311,6 +343,7 @@ function AccountTransactionsActionsBar({ export default compose( withDialogActions, + withAlertActions, withSettingsActions, withSettings(({ cashflowTransactionsSettings }) => ({ cashflowTansactionsTableSize: cashflowTransactionsSettings?.tableSize, diff --git a/packages/webapp/src/containers/CashFlow/AccountTransactions/alerts/PauseFeedsBankAccount.tsx b/packages/webapp/src/containers/CashFlow/AccountTransactions/alerts/PauseFeedsBankAccount.tsx new file mode 100644 index 000000000..d86f875ec --- /dev/null +++ b/packages/webapp/src/containers/CashFlow/AccountTransactions/alerts/PauseFeedsBankAccount.tsx @@ -0,0 +1,68 @@ +// @ts-nocheck +import React from 'react'; +import { Intent, Alert } from '@blueprintjs/core'; + +import { AppToaster, FormattedMessage as T } from '@/components'; +import withAlertStoreConnect from '@/containers/Alert/withAlertStoreConnect'; +import withAlertActions from '@/containers/Alert/withAlertActions'; + +import { usePauseFeedsBankAccount } from '@/hooks/query/bank-accounts'; +import { compose } from '@/utils'; + +/** + * Pause feeds of the bank account alert. + */ +function PauseFeedsBankAccountAlert({ + name, + + // #withAlertStoreConnect + isOpen, + payload: { bankAccountId }, + + // #withAlertActions + closeAlert, +}) { + const { mutateAsync: pauseBankAccountFeeds, isLoading } = + usePauseFeedsBankAccount(); + + // Handle activate item alert cancel. + const handleCancelActivateItem = () => { + closeAlert(name); + }; + // Handle confirm item activated. + const handleConfirmItemActivate = () => { + pauseBankAccountFeeds({ bankAccountId }) + .then(() => { + AppToaster.show({ + message: 'The bank feeds of the bank account has been paused.', + intent: Intent.SUCCESS, + }); + }) + .catch((error) => {}) + .finally(() => { + closeAlert(name); + }); + }; + + return ( + } + confirmButtonText={'Pause bank feeds'} + intent={Intent.WARNING} + isOpen={isOpen} + onCancel={handleCancelActivateItem} + loading={isLoading} + onConfirm={handleConfirmItemActivate} + > +

+ Are you sure want to pause bank feeds syncing of this bank account, you + can always resume it again? +

+
+ ); +} + +export default compose( + withAlertStoreConnect(), + withAlertActions, +)(PauseFeedsBankAccountAlert); diff --git a/packages/webapp/src/containers/CashFlow/AccountTransactions/alerts/ResumeFeedsBankAccount.tsx b/packages/webapp/src/containers/CashFlow/AccountTransactions/alerts/ResumeFeedsBankAccount.tsx new file mode 100644 index 000000000..7d5211a84 --- /dev/null +++ b/packages/webapp/src/containers/CashFlow/AccountTransactions/alerts/ResumeFeedsBankAccount.tsx @@ -0,0 +1,69 @@ +// @ts-nocheck +import React from 'react'; +import { Intent, Alert } from '@blueprintjs/core'; + +import { AppToaster, FormattedMessage as T } from '@/components'; +import withAlertStoreConnect from '@/containers/Alert/withAlertStoreConnect'; +import withAlertActions from '@/containers/Alert/withAlertActions'; + +import { useResumeFeedsBankAccount } from '@/hooks/query/bank-accounts'; +import { compose } from '@/utils'; + +/** + * Resume bank account feeds alert. + */ +function ResumeFeedsBankAccountAlert({ + name, + + // #withAlertStoreConnect + isOpen, + payload: { bankAccountId }, + + // #withAlertActions + closeAlert, +}) { + const { mutateAsync: resumeFeedsBankAccount, isLoading } = + useResumeFeedsBankAccount(); + + // Handle activate item alert cancel. + const handleCancelActivateItem = () => { + closeAlert(name); + }; + + // Handle confirm item activated. + const handleConfirmItemActivate = () => { + resumeFeedsBankAccount({ bankAccountId }) + .then(() => { + AppToaster.show({ + message: 'The bank feeds of the bank account has been resumed.', + intent: Intent.SUCCESS, + }); + }) + .catch((error) => {}) + .finally(() => { + closeAlert(name); + }); + }; + + return ( + } + confirmButtonText={'Resume bank feeds'} + intent={Intent.SUCCESS} + isOpen={isOpen} + onCancel={handleCancelActivateItem} + loading={isLoading} + onConfirm={handleConfirmItemActivate} + > +

+ Are you sure want to resume bank feeds syncing of this bank account, you + can always pause it again? +

+
+ ); +} + +export default compose( + withAlertStoreConnect(), + withAlertActions, +)(ResumeFeedsBankAccountAlert); diff --git a/packages/webapp/src/containers/CashFlow/AccountTransactions/alerts/index.ts b/packages/webapp/src/containers/CashFlow/AccountTransactions/alerts/index.ts new file mode 100644 index 000000000..c8b6e0fcb --- /dev/null +++ b/packages/webapp/src/containers/CashFlow/AccountTransactions/alerts/index.ts @@ -0,0 +1,24 @@ +// @ts-nocheck +import React from 'react'; + +const ResumeFeedsBankAccountAlert = React.lazy( + () => import('./ResumeFeedsBankAccount'), +); + +const PauseFeedsBankAccountAlert = React.lazy( + () => import('./PauseFeedsBankAccount'), +); + +/** + * Bank account alerts. + */ +export const BankAccountAlerts = [ + { + name: 'resume-feeds-syncing-bank-accounnt', + component: ResumeFeedsBankAccountAlert, + }, + { + name: 'pause-feeds-syncing-bank-accounnt', + component: PauseFeedsBankAccountAlert, + }, +]; diff --git a/packages/webapp/src/containers/CashFlow/AccountTransactions/dialogs/DisconnectBankAccountDialog/DisconnectBankAccountDialog.tsx b/packages/webapp/src/containers/CashFlow/AccountTransactions/dialogs/DisconnectBankAccountDialog/DisconnectBankAccountDialog.tsx new file mode 100644 index 000000000..5f07fe70f --- /dev/null +++ b/packages/webapp/src/containers/CashFlow/AccountTransactions/dialogs/DisconnectBankAccountDialog/DisconnectBankAccountDialog.tsx @@ -0,0 +1,42 @@ +// @ts-nocheck +import React from 'react'; +import { Dialog, DialogSuspense } from '@/components'; +import withDialogRedux from '@/components/DialogReduxConnect'; +import { compose } from '@/utils'; + +const DisconnectBankAccountDialogContent = React.lazy( + () => import('./DisconnectBankAccountDialogContent'), +); + +/** + * Disconnect bank account confirmation dialog. + */ +function DisconnectBankAccountDialogRoot({ + dialogName, + payload: { bankAccountId }, + isOpen, +}) { + return ( + + + + + + ); +} + +export const DisconnectBankAccountDialog = compose(withDialogRedux())( + DisconnectBankAccountDialogRoot, +); + +DisconnectBankAccountDialog.displayName = 'DisconnectBankAccountDialog'; diff --git a/packages/webapp/src/containers/CashFlow/AccountTransactions/dialogs/DisconnectBankAccountDialog/DisconnectBankAccountDialogContent.tsx b/packages/webapp/src/containers/CashFlow/AccountTransactions/dialogs/DisconnectBankAccountDialog/DisconnectBankAccountDialogContent.tsx new file mode 100644 index 000000000..0da3dd486 --- /dev/null +++ b/packages/webapp/src/containers/CashFlow/AccountTransactions/dialogs/DisconnectBankAccountDialog/DisconnectBankAccountDialogContent.tsx @@ -0,0 +1,104 @@ +// @ts-nocheck +import * as Yup from 'yup'; +import { Button, Intent, Classes } from '@blueprintjs/core'; +import * as R from 'ramda'; +import { Form, Formik, FormikHelpers } from 'formik'; +import { AppToaster, FFormGroup, FInputGroup } from '@/components'; +import { useDisconnectBankAccount } from '@/hooks/query/bank-rules'; +import withDialogActions from '@/containers/Dialog/withDialogActions'; +import { DialogsName } from '@/constants/dialogs'; + +interface DisconnectFormValues { + label: string; +} + +const initialValues = { + label: '', +}; + +const Schema = Yup.object().shape({ + label: Yup.string().required().label('Confirmation'), +}); + +interface DisconnectBankAccountDialogContentProps { + bankAccountId: number; +} + +function DisconnectBankAccountDialogContent({ + bankAccountId, + + // #withDialogActions + closeDialog, +}: DisconnectBankAccountDialogContentProps) { + const { mutateAsync: disconnectBankAccount } = useDisconnectBankAccount(); + + const handleSubmit = ( + values: DisconnectFormValues, + { setErrors, setSubmitting }: FormikHelpers, + ) => { + debugger; + setSubmitting(true); + + if (values.label !== 'DISCONNECT ACCOUNT') { + setErrors({ + label: 'The entered value is incorrect.', + }); + setSubmitting(false); + return; + } + disconnectBankAccount({ bankAccountId }) + .then(() => { + setSubmitting(false); + AppToaster.show({ + message: 'The bank account has been disconnected.', + intent: Intent.SUCCESS, + }); + closeDialog(DialogsName.DisconnectBankAccountConfirmation); + }) + .catch((error) => { + setSubmitting(false); + AppToaster.show({ + message: 'Something went wrong.', + intent: Intent.DANGER, + }); + }); + }; + + const handleCancelBtnClick = () => { + closeDialog(DialogsName.DisconnectBankAccountConfirmation); + }; + + return ( + +
+
+ + + +
+ +
+
+ + + +
+
+
+
+ ); +} + +export default R.compose(withDialogActions)(DisconnectBankAccountDialogContent); diff --git a/packages/webapp/src/hooks/query/bank-accounts.ts b/packages/webapp/src/hooks/query/bank-accounts.ts new file mode 100644 index 000000000..e5ba6795d --- /dev/null +++ b/packages/webapp/src/hooks/query/bank-accounts.ts @@ -0,0 +1,91 @@ +// @ts-nocheck +import { + UseMutationOptions, + UseMutationResult, + useQueryClient, + useMutation, +} from 'react-query'; +import useApiRequest from '../useRequest'; +import t from './types'; + +type PuaseFeedsBankAccountValues = { bankAccountId: number }; + +interface PuaseFeedsBankAccountResponse {} + +/** + * Resumes the feeds syncing of the bank account. + * @param {UseMutationResult} options + * @returns {UseMutationResult} + */ +export function usePauseFeedsBankAccount( + options?: UseMutationOptions< + PuaseFeedsBankAccountResponse, + Error, + PuaseFeedsBankAccountValues + >, +): UseMutationResult< + PuaseFeedsBankAccountResponse, + Error, + PuaseFeedsBankAccountValues +> { + const queryClient = useQueryClient(); + const apiRequest = useApiRequest(); + + return useMutation< + PuaseFeedsBankAccountResponse, + Error, + PuaseFeedsBankAccountValues + >( + (values) => + apiRequest.post( + `/banking/bank_accounts/${values.bankAccountId}/pause_feeds`, + ), + { + onSuccess: (res, values) => { + queryClient.invalidateQueries([t.ACCOUNT, values.bankAccountId]); + }, + ...options, + }, + ); +} + +type ResumeFeedsBankAccountValues = { bankAccountId: number }; + +interface ResumeFeedsBankAccountResponse {} + +/** + * Resumes the feeds syncing of the bank account. + * @param {UseMutationResult} options + * @returns {UseMutationResult} + */ +export function useResumeFeedsBankAccount( + options?: UseMutationOptions< + ResumeFeedsBankAccountResponse, + Error, + ResumeFeedsBankAccountValues + >, +): UseMutationResult< + ResumeFeedsBankAccountResponse, + Error, + ResumeFeedsBankAccountValues +> { + const queryClient = useQueryClient(); + const apiRequest = useApiRequest(); + + return useMutation< + ResumeFeedsBankAccountResponse, + Error, + ResumeFeedsBankAccountValues + >( + (values) => + apiRequest.post( + `/banking/bank_accounts/${values.bankAccountId}/resume_feeds`, + ), + { + onSuccess: (res, values) => { + queryClient.invalidateQueries([t.ACCOUNT, values.bankAccountId]); + }, + ...options, + }, + ); +} diff --git a/packages/webapp/src/style/pages/Dashboard/Dashboard.scss b/packages/webapp/src/style/pages/Dashboard/Dashboard.scss index da1a3249f..a6ea3968f 100644 --- a/packages/webapp/src/style/pages/Dashboard/Dashboard.scss +++ b/packages/webapp/src/style/pages/Dashboard/Dashboard.scss @@ -229,6 +229,9 @@ $dashboard-views-bar-height: 44px; } } + &.#{$ns}-minimal.#{$ns}-intent-warning{ + color: #cc7e25; + } &.button--blue-highlight { background-color: #ebfaff;