From fa7e6b1fcab7990427a30db8b6eb0e55d01a6300 Mon Sep 17 00:00:00 2001 From: Ahmed Bouhuolia Date: Mon, 15 Jul 2024 23:18:39 +0200 Subject: [PATCH 01/15] feat: disconnect bank account --- .../Banking/BankAccountsController.ts | 35 +++++++++ .../BankAccounts/BankAccountsApplication.tsx | 21 ++++++ .../BankAccounts/DisconnectBankAccount.tsx | 72 +++++++++++++++++++ packages/server/src/subscribers/events.ts | 5 ++ .../AccountTransactionsActionsBar.tsx | 29 ++++++++ packages/webapp/src/hooks/query/bank-rules.ts | 21 +++++- 6 files changed, 182 insertions(+), 1 deletion(-) create mode 100644 packages/server/src/services/Banking/BankAccounts/BankAccountsApplication.tsx create mode 100644 packages/server/src/services/Banking/BankAccounts/DisconnectBankAccount.tsx diff --git a/packages/server/src/api/controllers/Banking/BankAccountsController.ts b/packages/server/src/api/controllers/Banking/BankAccountsController.ts index 4b062768f..c876ba604 100644 --- a/packages/server/src/api/controllers/Banking/BankAccountsController.ts +++ b/packages/server/src/api/controllers/Banking/BankAccountsController.ts @@ -3,12 +3,16 @@ 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'; @Service() export class BankAccountsController extends BaseController { @Inject() private getBankAccountSummaryService: GetBankAccountSummary; + @Inject() + private bankAccountsApp: BankAccountsApplication; + /** * Router constructor. */ @@ -16,6 +20,10 @@ export class BankAccountsController extends BaseController { const router = Router(); router.get('/:bankAccountId/meta', this.getBankAccountSummary.bind(this)); + router.post( + '/:bankAccountId/disconnect', + this.discountBankAccount.bind(this) + ); return router; } @@ -46,4 +54,31 @@ export class BankAccountsController extends BaseController { next(error); } } + + /** + * Disonnect the given bank account. + * @param {Request} req + * @param {Response} res + * @param {NextFunction} next + * @returns {Promise} + */ + async disconnectBankAccount( + req: Request<{ bankAccountId: number }>, + res: Response, + next: NextFunction + ) { + const { bankAccountId } = req.params; + const { tenantId } = req; + + try { + await this.bankAccountsApp.disconnectBankAccount(tenantId, bankAccountId); + + return res.status(200).send({ + id: bankAccountId, + message: 'The bank account has been disconnected.', + }); + } catch (error) { + next(error); + } + } } diff --git a/packages/server/src/services/Banking/BankAccounts/BankAccountsApplication.tsx b/packages/server/src/services/Banking/BankAccounts/BankAccountsApplication.tsx new file mode 100644 index 000000000..c375030ef --- /dev/null +++ b/packages/server/src/services/Banking/BankAccounts/BankAccountsApplication.tsx @@ -0,0 +1,21 @@ +import { Inject, Service } from 'typedi'; +import { DisconnectBankAccount } from './DisconnectBankAccount'; + +@Service() +export class BankAccountsApplication { + @Inject() + private disconnectBankAccountService: DisconnectBankAccount; + + /** + * Disconnects the given bank account. + * @param {number} tenantId + * @param {number} bankAccountId + * @returns {Promise} + */ + async disconnectBankAccount(tenantId: number, bankAccountId: number) { + return this.disconnectBankAccountService.disconnectBankAccount( + tenantId, + bankAccountId + ); + } +} diff --git a/packages/server/src/services/Banking/BankAccounts/DisconnectBankAccount.tsx b/packages/server/src/services/Banking/BankAccounts/DisconnectBankAccount.tsx new file mode 100644 index 000000000..4f9e6106e --- /dev/null +++ b/packages/server/src/services/Banking/BankAccounts/DisconnectBankAccount.tsx @@ -0,0 +1,72 @@ +import { Knex } from 'knex'; +import { Inject, Service } from 'typedi'; +import { ServiceError } from '@/exceptions'; +import { EventPublisher } from '@/lib/EventPublisher/EventPublisher'; +import { PlaidClientWrapper } from '@/lib/Plaid'; +import HasTenancyService from '@/services/Tenancy/TenancyService'; +import UnitOfWork from '@/services/UnitOfWork'; +import events from '@/subscribers/events'; + +@Service() +export class DisconnectBankAccount { + @Inject() + private tenancy: HasTenancyService; + + @Inject() + private eventPublisher: EventPublisher; + + @Inject() + private uow: UnitOfWork; + + /** + * Disconnects the given bank account. + * @param {number} tenantId + * @param {number} bankAccountId + * @returns {Promise} + */ + async disconnectBankAccount(tenantId: number, bankAccountId: number) { + const { Account, PlaidItem } = this.tenancy.models(tenantId); + + const account = await Account.query() + .findById(bankAccountId) + .where('type', ['bank']) + .throwIfNotFound(); + + const plaidItem = await PlaidItem.query().findById(account.plaidAccountId); + + if (!plaidItem) { + throw new ServiceError(ERRORS.BANK_ACCOUNT_NOT_CONNECTED); + } + const request = { + accessToken: plaidItem.plaidAccessToken, + }; + const plaidInstance = new PlaidClientWrapper(); + + // + return this.uow.withTransaction(tenantId, async (trx: Knex.Transaction) => { + await this.eventPublisher.emitAsync(events.bankAccount.onDisconnecting, { + tenantId, + bankAccountId, + }); + // Remove the Plaid item. + const data = await plaidInstance.itemRemove(request); + + // Remove the Plaid item from the system. + await PlaidItem.query().findById(account.plaidAccountId).delete(); + + // Remove the plaid item association to the bank account. + await Account.query().findById(bankAccountId).patch({ + plaidAccountId: null, + isFeedsActive: false, + }); + await this.eventPublisher.emitAsync(events.bankAccount.onDisconnected, { + tenantId, + bankAccountId, + }); + }); + } +} + +const ERRORS = { + BANK_ACCOUNT_NOT_CONNECTED: 'BANK_ACCOUNT_NOT_CONNECTED', +}; diff --git a/packages/server/src/subscribers/events.ts b/packages/server/src/subscribers/events.ts index 711dbce35..b64e398c3 100644 --- a/packages/server/src/subscribers/events.ts +++ b/packages/server/src/subscribers/events.ts @@ -651,6 +651,11 @@ export default { onUnexcluded: 'onBankTransactionUnexcluded', }, + bankAccount: { + onDisconnecting: 'onBankAccountDisconnecting', + onDisconnected: 'onBankAccountDisconnected', + }, + // Import files. import: { onImportCommitted: 'onImportFileCommitted', diff --git a/packages/webapp/src/containers/CashFlow/AccountTransactions/AccountTransactionsActionsBar.tsx b/packages/webapp/src/containers/CashFlow/AccountTransactions/AccountTransactionsActionsBar.tsx index b5db2eb78..850b3fa37 100644 --- a/packages/webapp/src/containers/CashFlow/AccountTransactions/AccountTransactionsActionsBar.tsx +++ b/packages/webapp/src/containers/CashFlow/AccountTransactions/AccountTransactionsActionsBar.tsx @@ -11,6 +11,7 @@ import { MenuItem, PopoverInteractionKind, Position, + Intent, } from '@blueprintjs/core'; import { useHistory } from 'react-router-dom'; import { @@ -18,6 +19,7 @@ import { DashboardActionsBar, DashboardRowsHeightButton, FormattedMessage as T, + AppToaster, } from '@/components'; import { CashFlowMenuItems } from './utils'; @@ -33,6 +35,7 @@ import withSettings from '@/containers/Settings/withSettings'; import withSettingsActions from '@/containers/Settings/withSettingsActions'; import { compose } from '@/utils'; +import { useDisconnectBankAccount } from '@/hooks/query/bank-rules'; function AccountTransactionsActionsBar({ // #withDialogActions @@ -50,6 +53,8 @@ function AccountTransactionsActionsBar({ // Refresh cashflow infinity transactions hook. const { refresh } = useRefreshCashflowTransactionsInfinity(); + const { mutateAsync: disconnectBankAccount } = useDisconnectBankAccount(); + // Retrieves the money in/out buttons options. const addMoneyInOptions = useMemo(() => getAddMoneyInOptions(), []); const addMoneyOutOptions = useMemo(() => getAddMoneyOutOptions(), []); @@ -82,6 +87,26 @@ function AccountTransactionsActionsBar({ const handleBankRulesClick = () => { history.push(`/bank-rules?accountId=${accountId}`); }; + + const isConnected = true; + + // Handles the bank account disconnect click. + const handleDisconnectClick = () => { + disconnectBankAccount(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, + }); + }); + }; + // Handle the refresh button click. const handleRefreshBtnClick = () => { refresh(); @@ -142,6 +167,10 @@ function AccountTransactionsActionsBar({ content={ + + {isConnected && ( + + )} } > diff --git a/packages/webapp/src/hooks/query/bank-rules.ts b/packages/webapp/src/hooks/query/bank-rules.ts index 77a770ff4..a49e4a5b0 100644 --- a/packages/webapp/src/hooks/query/bank-rules.ts +++ b/packages/webapp/src/hooks/query/bank-rules.ts @@ -1,4 +1,3 @@ -// @ts-nocheck import { QueryClient, UseMutationOptions, @@ -61,6 +60,26 @@ export function useCreateBankRule( ); } +interface DisconnectBankAccountRes {} + +export function useDisconnectBankAccount( + options?: UseMutationOptions, +) { + const queryClient = useQueryClient(); + const apiRequest = useApiRequest(); + + return useMutation( + (bankAccountId: number) => + apiRequest + .post(`/banking/bank_accounts/${bankAccountId}`) + .then((res) => res.data), + { + ...options, + onSuccess: () => {}, + }, + ); +} + interface EditBankRuleValues { id: number; value: any; From c2815afbe39b15670fde1d51609828737fec200f Mon Sep 17 00:00:00 2001 From: Ahmed Bouhuolia Date: Tue, 16 Jul 2024 17:09:00 +0200 Subject: [PATCH 02/15] feat: disconnect and update bank account --- .../Banking/BankAccountsController.ts | 30 ++++++++- ...732_add_plaid_item_id_to_accounts_table.js | 11 ++++ packages/server/src/lib/Plaid/Plaid.ts | 1 + packages/server/src/models/Account.ts | 13 ++++ .../BankAccounts/BankAccountsApplication.tsx | 17 +++++ .../BankAccounts/DisconnectBankAccount.tsx | 34 +++++----- .../BankAccounts/RefreshBankAccount.tsx | 39 ++++++++++++ .../services/Banking/BankAccounts/types.ts | 17 +++++ .../AccountTransactionsActionsBar.tsx | 43 ++++++++++++- packages/webapp/src/hooks/query/bank-rules.ts | 63 ++++++++++++++++--- packages/webapp/src/static/json/icons.tsx | 7 +++ 11 files changed, 246 insertions(+), 29 deletions(-) create mode 100644 packages/server/src/database/migrations/20240716114732_add_plaid_item_id_to_accounts_table.js create mode 100644 packages/server/src/services/Banking/BankAccounts/RefreshBankAccount.tsx create mode 100644 packages/server/src/services/Banking/BankAccounts/types.ts diff --git a/packages/server/src/api/controllers/Banking/BankAccountsController.ts b/packages/server/src/api/controllers/Banking/BankAccountsController.ts index c876ba604..f337c0b38 100644 --- a/packages/server/src/api/controllers/Banking/BankAccountsController.ts +++ b/packages/server/src/api/controllers/Banking/BankAccountsController.ts @@ -22,8 +22,9 @@ export class BankAccountsController extends BaseController { router.get('/:bankAccountId/meta', this.getBankAccountSummary.bind(this)); router.post( '/:bankAccountId/disconnect', - this.discountBankAccount.bind(this) + this.disconnectBankAccount.bind(this) ); + router.post('/:bankAccountId/update', this.refreshBankAccount.bind(this)); return router; } @@ -81,4 +82,31 @@ export class BankAccountsController extends BaseController { next(error); } } + + /** + * Refresh the given bank account. + * @param {Request} req + * @param {Response} res + * @param {NextFunction} next + * @returns {Promise} + */ + async refreshBankAccount( + req: Request<{ bankAccountId: number }>, + res: Response, + next: NextFunction + ) { + const { bankAccountId } = req.params; + const { tenantId } = req; + + try { + await this.bankAccountsApp.refreshBankAccount(tenantId, bankAccountId); + + return res.status(200).send({ + id: bankAccountId, + message: 'The bank account has been disconnected.', + }); + } catch (error) { + next(error); + } + } } diff --git a/packages/server/src/database/migrations/20240716114732_add_plaid_item_id_to_accounts_table.js b/packages/server/src/database/migrations/20240716114732_add_plaid_item_id_to_accounts_table.js new file mode 100644 index 000000000..ce084dca8 --- /dev/null +++ b/packages/server/src/database/migrations/20240716114732_add_plaid_item_id_to_accounts_table.js @@ -0,0 +1,11 @@ +exports.up = function (knex) { + return knex.schema.table('accounts', (table) => { + table.string('plaid_item_id').nullable(); + }); +}; + +exports.down = function (knex) { + return knex.schema.table('accounts', (table) => { + table.dropColumn('plaid_item_id'); + }); +}; diff --git a/packages/server/src/lib/Plaid/Plaid.ts b/packages/server/src/lib/Plaid/Plaid.ts index 532d3cfe8..04cc71888 100644 --- a/packages/server/src/lib/Plaid/Plaid.ts +++ b/packages/server/src/lib/Plaid/Plaid.ts @@ -52,6 +52,7 @@ const noAccessTokenLogger = async ( // Plaid client methods used in this app, mapped to their appropriate logging functions. const clientMethodLoggingFns = { + transactionsRefresh: defaultLogger, accountsGet: defaultLogger, institutionsGet: noAccessTokenLogger, institutionsGetById: noAccessTokenLogger, diff --git a/packages/server/src/models/Account.ts b/packages/server/src/models/Account.ts index 7e0d8d6e4..79100d6d6 100644 --- a/packages/server/src/models/Account.ts +++ b/packages/server/src/models/Account.ts @@ -197,6 +197,7 @@ export default class Account extends mixin(TenantModel, [ const ExpenseEntry = require('models/ExpenseCategory'); const ItemEntry = require('models/ItemEntry'); const UncategorizedTransaction = require('models/UncategorizedCashflowTransaction'); + const PlaidItem = require('models/PlaidItem'); return { /** @@ -321,6 +322,18 @@ export default class Account extends mixin(TenantModel, [ query.where('categorized', false); }, }, + + /** + * Account model may belongs to a Plaid item. + */ + plaidItem: { + relation: Model.BelongsToOneRelation, + modelClass: PlaidItem.default, + join: { + from: 'accounts.plaidItemId', + to: 'plaid_items.id', + }, + }, }; } diff --git a/packages/server/src/services/Banking/BankAccounts/BankAccountsApplication.tsx b/packages/server/src/services/Banking/BankAccounts/BankAccountsApplication.tsx index c375030ef..51c12106e 100644 --- a/packages/server/src/services/Banking/BankAccounts/BankAccountsApplication.tsx +++ b/packages/server/src/services/Banking/BankAccounts/BankAccountsApplication.tsx @@ -1,11 +1,15 @@ import { Inject, Service } from 'typedi'; import { DisconnectBankAccount } from './DisconnectBankAccount'; +import { RefreshBankAccountService } from './RefreshBankAccount'; @Service() export class BankAccountsApplication { @Inject() private disconnectBankAccountService: DisconnectBankAccount; + @Inject() + private refreshBankAccountService: RefreshBankAccountService; + /** * Disconnects the given bank account. * @param {number} tenantId @@ -18,4 +22,17 @@ export class BankAccountsApplication { bankAccountId ); } + + /** + * Refresh the bank transactions of the given bank account. + * @param {number} tenantId + * @param {number} bankAccountId + * @returns {Promise} + */ + async refreshBankAccount(tenantId: number, bankAccountId: number) { + return this.refreshBankAccountService.refreshBankAccount( + tenantId, + bankAccountId + ); + } } diff --git a/packages/server/src/services/Banking/BankAccounts/DisconnectBankAccount.tsx b/packages/server/src/services/Banking/BankAccounts/DisconnectBankAccount.tsx index 4f9e6106e..d562b31fc 100644 --- a/packages/server/src/services/Banking/BankAccounts/DisconnectBankAccount.tsx +++ b/packages/server/src/services/Banking/BankAccounts/DisconnectBankAccount.tsx @@ -6,6 +6,7 @@ import { PlaidClientWrapper } from '@/lib/Plaid'; import HasTenancyService from '@/services/Tenancy/TenancyService'; import UnitOfWork from '@/services/UnitOfWork'; import events from '@/subscribers/events'; +import { ERRORS } from './types'; @Service() export class DisconnectBankAccount { @@ -20,45 +21,46 @@ export class DisconnectBankAccount { /** * Disconnects the given bank account. - * @param {number} tenantId - * @param {number} bankAccountId + * @param {number} tenantId + * @param {number} bankAccountId * @returns {Promise} */ async disconnectBankAccount(tenantId: number, bankAccountId: number) { const { Account, PlaidItem } = this.tenancy.models(tenantId); + // Retrieve the bank account or throw not found error. const account = await Account.query() .findById(bankAccountId) - .where('type', ['bank']) + .whereIn('account_type', ['bank', 'cash']) .throwIfNotFound(); - const plaidItem = await PlaidItem.query().findById(account.plaidAccountId); + const oldPlaidItem = await PlaidItem.query().findById(account.plaidItemId); - if (!plaidItem) { + if (!oldPlaidItem) { throw new ServiceError(ERRORS.BANK_ACCOUNT_NOT_CONNECTED); } - const request = { - accessToken: plaidItem.plaidAccessToken, - }; const plaidInstance = new PlaidClientWrapper(); - // return this.uow.withTransaction(tenantId, async (trx: Knex.Transaction) => { + // Triggers `onBankAccountDisconnecting` event. await this.eventPublisher.emitAsync(events.bankAccount.onDisconnecting, { tenantId, bankAccountId, }); - // Remove the Plaid item. - const data = await plaidInstance.itemRemove(request); - // Remove the Plaid item from the system. - await PlaidItem.query().findById(account.plaidAccountId).delete(); + await PlaidItem.query(trx).findById(account.plaidItemId).delete(); // Remove the plaid item association to the bank account. - await Account.query().findById(bankAccountId).patch({ + await Account.query(trx).findById(bankAccountId).patch({ plaidAccountId: null, + plaidItemId: null, isFeedsActive: false, }); + // Remove the Plaid item. + const data = await plaidInstance.itemRemove({ + access_token: oldPlaidItem.plaidAccessToken, + }); + // Triggers `onBankAccountDisconnected` event. await this.eventPublisher.emitAsync(events.bankAccount.onDisconnected, { tenantId, bankAccountId, @@ -66,7 +68,3 @@ export class DisconnectBankAccount { }); } } - -const ERRORS = { - BANK_ACCOUNT_NOT_CONNECTED: 'BANK_ACCOUNT_NOT_CONNECTED', -}; diff --git a/packages/server/src/services/Banking/BankAccounts/RefreshBankAccount.tsx b/packages/server/src/services/Banking/BankAccounts/RefreshBankAccount.tsx new file mode 100644 index 000000000..814d00c75 --- /dev/null +++ b/packages/server/src/services/Banking/BankAccounts/RefreshBankAccount.tsx @@ -0,0 +1,39 @@ +import { ServiceError } from '@/exceptions'; +import { PlaidClientWrapper } from '@/lib/Plaid'; +import HasTenancyService from '@/services/Tenancy/TenancyService'; +import UnitOfWork from '@/services/UnitOfWork'; +import { Inject } from 'typedi'; +export class RefreshBankAccountService { + @Inject() + private tenancy: HasTenancyService; + + @Inject() + private uow: UnitOfWork; + + /** + * + * @param {number} tenantId + * @param {number} bankAccountId + */ + public async refreshBankAccount(tenantId: number, bankAccountId: number) { + const { Account } = this.tenancy.models(tenantId); + + const bankAccount = await Account.query() + .findById(bankAccountId) + .withGraphFetched('plaidItem') + .throwIfNotFound(); + + if (!bankAccount.plaidItem) { + throw new ServiceError(''); + } + const plaidInstance = new PlaidClientWrapper(); + + const data = await plaidInstance.transactionsRefresh({ + access_token: bankAccount.plaidItem.plaidAccessToken, + }); + await Account.query().findById(bankAccountId).patch({ + isFeedsActive: true, + lastFeedsUpdatedAt: new Date(), + }); + } +} diff --git a/packages/server/src/services/Banking/BankAccounts/types.ts b/packages/server/src/services/Banking/BankAccounts/types.ts new file mode 100644 index 000000000..d3198cc5c --- /dev/null +++ b/packages/server/src/services/Banking/BankAccounts/types.ts @@ -0,0 +1,17 @@ +import { Knex } from 'knex'; + +export interface IBankAccountDisconnectingEventPayload { + tenantId: number; + bankAccountId: number; + trx: Knex.Transaction; +} + +export interface IBankAccountDisconnectedEventPayload { + tenantId: number; + bankAccountId: number; + trx: Knex.Transaction; +} + +export const ERRORS = { + BANK_ACCOUNT_NOT_CONNECTED: 'BANK_ACCOUNT_NOT_CONNECTED', +}; diff --git a/packages/webapp/src/containers/CashFlow/AccountTransactions/AccountTransactionsActionsBar.tsx b/packages/webapp/src/containers/CashFlow/AccountTransactions/AccountTransactionsActionsBar.tsx index 850b3fa37..e9635524c 100644 --- a/packages/webapp/src/containers/CashFlow/AccountTransactions/AccountTransactionsActionsBar.tsx +++ b/packages/webapp/src/containers/CashFlow/AccountTransactions/AccountTransactionsActionsBar.tsx @@ -12,6 +12,8 @@ import { PopoverInteractionKind, Position, Intent, + Tooltip, + MenuDivider, } from '@blueprintjs/core'; import { useHistory } from 'react-router-dom'; import { @@ -35,7 +37,10 @@ import withSettings from '@/containers/Settings/withSettings'; import withSettingsActions from '@/containers/Settings/withSettingsActions'; import { compose } from '@/utils'; -import { useDisconnectBankAccount } from '@/hooks/query/bank-rules'; +import { + useDisconnectBankAccount, + useUpdateBankAccount, +} from '@/hooks/query/bank-rules'; function AccountTransactionsActionsBar({ // #withDialogActions @@ -54,6 +59,7 @@ function AccountTransactionsActionsBar({ const { refresh } = useRefreshCashflowTransactionsInfinity(); const { mutateAsync: disconnectBankAccount } = useDisconnectBankAccount(); + const { mutateAsync: updateBankAccount } = useUpdateBankAccount(); // Retrieves the money in/out buttons options. const addMoneyInOptions = useMemo(() => getAddMoneyInOptions(), []); @@ -92,7 +98,7 @@ function AccountTransactionsActionsBar({ // Handles the bank account disconnect click. const handleDisconnectClick = () => { - disconnectBankAccount(accountId) + disconnectBankAccount({ bankAccountId: accountId }) .then(() => { AppToaster.show({ message: 'The bank account has been disconnected.', @@ -106,7 +112,22 @@ function AccountTransactionsActionsBar({ }); }); }; - + // handles the bank update button click. + const handleBankUpdateClick = () => { + updateBankAccount({ bankAccountId: accountId }) + .then(() => { + AppToaster.show({ + message: 'The transactions of the bank account has been updated.', + intent: Intent.SUCCESS, + }); + }) + .catch(() => { + AppToaster.show({ + message: 'Something went wrong.', + intent: Intent.DANGER, + }); + }); + }; // Handle the refresh button click. const handleRefreshBtnClick = () => { refresh(); @@ -154,6 +175,18 @@ function AccountTransactionsActionsBar({ onChange={handleTableRowSizeChange} /> + + + + {!isEmpty(entries) && ( + + )} )} diff --git a/packages/webapp/src/containers/Purchases/PaymentMades/PaymentForm/PaymentMadeFormProvider.tsx b/packages/webapp/src/containers/Purchases/PaymentMades/PaymentForm/PaymentMadeFormProvider.tsx index f5d0c50d3..6a33713a7 100644 --- a/packages/webapp/src/containers/Purchases/PaymentMades/PaymentForm/PaymentMadeFormProvider.tsx +++ b/packages/webapp/src/containers/Purchases/PaymentMades/PaymentForm/PaymentMadeFormProvider.tsx @@ -1,5 +1,5 @@ // @ts-nocheck -import React, { createContext, useContext } from 'react'; +import React, { createContext, useContext, useState } from 'react'; import { Features } from '@/constants'; import { useFeatureCan } from '@/hooks/state'; import { @@ -71,6 +71,8 @@ function PaymentMadeFormProvider({ query, paymentMadeId, ...props }) { const isFeatureLoading = isBranchesLoading; + const [isExcessConfirmed, setIsExcessConfirmed] = useState(false); + // Provider payload. const provider = { paymentMadeId, @@ -98,6 +100,9 @@ function PaymentMadeFormProvider({ query, paymentMadeId, ...props }) { setSubmitPayload, setPaymentVendorId, + + isExcessConfirmed, + setIsExcessConfirmed, }; return ( diff --git a/packages/webapp/src/containers/Purchases/PaymentMades/PaymentForm/dialogs/PaymentMadeExcessDialog/PaymentMadeExcessDialog.tsx b/packages/webapp/src/containers/Purchases/PaymentMades/PaymentForm/dialogs/PaymentMadeExcessDialog/PaymentMadeExcessDialog.tsx new file mode 100644 index 000000000..c93c85a17 --- /dev/null +++ b/packages/webapp/src/containers/Purchases/PaymentMades/PaymentForm/dialogs/PaymentMadeExcessDialog/PaymentMadeExcessDialog.tsx @@ -0,0 +1,37 @@ +// @ts-nocheck +import React from 'react'; +import { Dialog, DialogSuspense } from '@/components'; +import withDialogRedux from '@/components/DialogReduxConnect'; +import { compose } from '@/utils'; + +const ExcessPaymentDialogContent = React.lazy(() => + import('./PaymentMadeExcessDialogContent').then((module) => ({ + default: module.ExcessPaymentDialogContent, + })), +); + +/** + * Exess payment dialog of the payment made form. + */ +function ExcessPaymentDialogRoot({ dialogName, isOpen }) { + return ( + + + + + + ); +} + +export const ExcessPaymentDialog = compose(withDialogRedux())( + ExcessPaymentDialogRoot, +); + +ExcessPaymentDialog.displayName = 'ExcessPaymentDialog'; diff --git a/packages/webapp/src/containers/Purchases/PaymentMades/PaymentForm/dialogs/PaymentMadeExcessDialog/PaymentMadeExcessDialogContent.tsx b/packages/webapp/src/containers/Purchases/PaymentMades/PaymentForm/dialogs/PaymentMadeExcessDialog/PaymentMadeExcessDialogContent.tsx new file mode 100644 index 000000000..c57f67131 --- /dev/null +++ b/packages/webapp/src/containers/Purchases/PaymentMades/PaymentForm/dialogs/PaymentMadeExcessDialog/PaymentMadeExcessDialogContent.tsx @@ -0,0 +1,93 @@ +// @ts-nocheck +import * as R from 'ramda'; +import React from 'react'; +import { Button, Classes, Intent } from '@blueprintjs/core'; +import { Form, Formik, FormikHelpers, useFormikContext } from 'formik'; +import { FormatNumber } from '@/components'; +import { usePaymentMadeFormContext } from '../../PaymentMadeFormProvider'; +import withDialogActions from '@/containers/Dialog/withDialogActions'; +import { usePaymentMadeExcessAmount } from '../../utils'; + +interface ExcessPaymentValues {} +function ExcessPaymentDialogContentRoot({ dialogName, closeDialog }) { + const { + submitForm, + values: { currency_code: currencyCode }, + } = useFormikContext(); + const { setIsExcessConfirmed } = usePaymentMadeFormContext(); + + // Handles the form submitting. + const handleSubmit = ( + values: ExcessPaymentValues, + { setSubmitting }: FormikHelpers, + ) => { + setSubmitting(true); + setIsExcessConfirmed(true); + + return submitForm().then(() => { + setSubmitting(false); + closeDialog(dialogName); + }); + }; + // Handle close button click. + const handleCloseBtn = () => { + closeDialog(dialogName); + }; + const excessAmount = usePaymentMadeExcessAmount(); + + return ( + +
+ + } + onClose={handleCloseBtn} + /> + +
+ ); +} + +export const ExcessPaymentDialogContent = R.compose(withDialogActions)( + ExcessPaymentDialogContentRoot, +); + +interface ExcessPaymentDialogContentFormProps { + excessAmount: string | number | React.ReactNode; + onClose?: () => void; +} + +function ExcessPaymentDialogContentForm({ + excessAmount, + onClose, +}: ExcessPaymentDialogContentFormProps) { + const { submitForm, isSubmitting } = useFormikContext(); + + const handleCloseBtn = () => { + onClose && onClose(); + }; + return ( + <> +
+

+ Would you like to record the excess amount of{' '} + {excessAmount} as credit payment from the vendor. +

+
+ +
+
+ + +
+
+ + ); +} diff --git a/packages/webapp/src/containers/Purchases/PaymentMades/PaymentForm/dialogs/PaymentMadeExcessDialog/index.ts b/packages/webapp/src/containers/Purchases/PaymentMades/PaymentForm/dialogs/PaymentMadeExcessDialog/index.ts new file mode 100644 index 000000000..dae9903b5 --- /dev/null +++ b/packages/webapp/src/containers/Purchases/PaymentMades/PaymentForm/dialogs/PaymentMadeExcessDialog/index.ts @@ -0,0 +1 @@ +export * from './PaymentMadeExcessDialog'; \ No newline at end of file diff --git a/packages/webapp/src/containers/Purchases/PaymentMades/PaymentForm/utils.tsx b/packages/webapp/src/containers/Purchases/PaymentMades/PaymentForm/utils.tsx index fd469ce88..8b8323939 100644 --- a/packages/webapp/src/containers/Purchases/PaymentMades/PaymentForm/utils.tsx +++ b/packages/webapp/src/containers/Purchases/PaymentMades/PaymentForm/utils.tsx @@ -37,7 +37,7 @@ export const defaultPaymentMadeEntry = { // Default initial values of payment made. export const defaultPaymentMade = { - full_amount: '', + amount: '', vendor_id: '', payment_account_id: '', payment_date: moment(new Date()).format('YYYY-MM-DD'), @@ -53,10 +53,10 @@ export const defaultPaymentMade = { export const transformToEditForm = (paymentMade, paymentMadeEntries) => { const attachments = transformAttachmentsToForm(paymentMade); + const appliedAmount = safeSumBy(paymentMadeEntries, 'payment_amount'); return { ...transformToForm(paymentMade, defaultPaymentMade), - full_amount: safeSumBy(paymentMadeEntries, 'payment_amount'), entries: [ ...paymentMadeEntries.map((paymentMadeEntry) => ({ ...transformToForm(paymentMadeEntry, defaultPaymentMadeEntry), @@ -177,6 +177,30 @@ export const usePaymentMadeTotals = () => { }; }; +export const usePaymentmadeTotalAmount = () => { + const { + values: { amount }, + } = useFormikContext(); + + return amount; +}; + +export const usePaymentMadeAppliedAmount = () => { + const { + values: { entries }, + } = useFormikContext(); + + // Retrieves the invoice entries total. + return React.useMemo(() => sumBy(entries, 'payment_amount'), [entries]); +}; + +export const usePaymentMadeExcessAmount = () => { + const appliedAmount = usePaymentMadeAppliedAmount(); + const totalAmount = usePaymentmadeTotalAmount(); + + return Math.abs(totalAmount - appliedAmount); +}; + /** * Detarmines whether the bill has foreign customer. * @returns {boolean} @@ -191,3 +215,10 @@ export const usePaymentMadeIsForeignCustomer = () => { ); return isForeignCustomer; }; + +export const getPaymentExcessAmountFromValues = (values) => { + const appliedAmount = sumBy(values.entries, 'payment_amount'); + const totalAmount = values.amount; + + return Math.abs(totalAmount - appliedAmount); +}; diff --git a/packages/webapp/src/containers/Sales/PaymentReceives/PaymentReceiveForm/PaymentReceiveForm.tsx b/packages/webapp/src/containers/Sales/PaymentReceives/PaymentReceiveForm/PaymentReceiveForm.tsx index a0c84bacb..be69cee4d 100644 --- a/packages/webapp/src/containers/Sales/PaymentReceives/PaymentReceiveForm/PaymentReceiveForm.tsx +++ b/packages/webapp/src/containers/Sales/PaymentReceives/PaymentReceiveForm/PaymentReceiveForm.tsx @@ -1,5 +1,5 @@ // @ts-nocheck -import React, { useMemo } from 'react'; +import React, { useMemo, useRef } from 'react'; import { sumBy, isEmpty, defaultTo } from 'lodash'; import intl from 'react-intl-universal'; import classNames from 'classnames'; @@ -21,6 +21,7 @@ import { PaymentReceiveInnerProvider } from './PaymentReceiveInnerProvider'; import withSettings from '@/containers/Settings/withSettings'; import withCurrentOrganization from '@/containers/Organization/withCurrentOrganization'; +import withDialogActions from '@/containers/Dialog/withDialogActions'; import { EditPaymentReceiveFormSchema, @@ -36,6 +37,7 @@ import { transformFormToRequest, transformErrors, resetFormState, + getExceededAmountFromValues, } from './utils'; import { PaymentReceiveSyncIncrementSettingsToForm } from './components'; @@ -51,6 +53,9 @@ function PaymentReceiveForm({ // #withCurrentOrganization organization: { base_currency }, + + // #withDialogActions + openDialog, }) { const history = useHistory(); @@ -63,6 +68,7 @@ function PaymentReceiveForm({ submitPayload, editPaymentReceiveMutate, createPaymentReceiveMutate, + isExcessConfirmed, } = usePaymentReceiveFormContext(); // Payment receive number. @@ -94,18 +100,16 @@ function PaymentReceiveForm({ preferredDepositAccount, ], ); - // Handle form submit. const handleSubmitForm = ( values, { setSubmitting, resetForm, setFieldError }, ) => { setSubmitting(true); + const exceededAmount = getExceededAmountFromValues(values); - // Calculates the total payment amount of entries. - const totalPaymentAmount = sumBy(values.entries, 'payment_amount'); - - if (totalPaymentAmount <= 0) { + // Validates the amount should be bigger than zero. + if (values.amount <= 0) { AppToaster.show({ message: intl.get('you_cannot_make_payment_with_zero_total_amount'), intent: Intent.DANGER, @@ -113,6 +117,13 @@ function PaymentReceiveForm({ setSubmitting(false); return; } + // Show the confirm popup if the excessed amount bigger than zero and + // excess confirmation has not been confirmed yet. + if (exceededAmount > 0 && !isExcessConfirmed) { + setSubmitting(false); + openDialog('payment-received-excessed-payment'); + return; + } // Transformes the form values to request body. const form = transformFormToRequest(values); @@ -148,11 +159,11 @@ function PaymentReceiveForm({ }; if (paymentReceiveId) { - editPaymentReceiveMutate([paymentReceiveId, form]) + return editPaymentReceiveMutate([paymentReceiveId, form]) .then(onSaved) .catch(onError); } else { - createPaymentReceiveMutate(form).then(onSaved).catch(onError); + return createPaymentReceiveMutate(form).then(onSaved).catch(onError); } }; return ( @@ -202,4 +213,5 @@ export default compose( preferredDepositAccount: paymentReceiveSettings?.preferredDepositAccount, })), withCurrentOrganization(), + withDialogActions, )(PaymentReceiveForm); diff --git a/packages/webapp/src/containers/Sales/PaymentReceives/PaymentReceiveForm/PaymentReceiveFormDialogs.tsx b/packages/webapp/src/containers/Sales/PaymentReceives/PaymentReceiveForm/PaymentReceiveFormDialogs.tsx index e60b27142..76ccb72a4 100644 --- a/packages/webapp/src/containers/Sales/PaymentReceives/PaymentReceiveForm/PaymentReceiveFormDialogs.tsx +++ b/packages/webapp/src/containers/Sales/PaymentReceives/PaymentReceiveForm/PaymentReceiveFormDialogs.tsx @@ -2,6 +2,7 @@ import React from 'react'; import { useFormikContext } from 'formik'; import PaymentReceiveNumberDialog from '@/containers/Dialogs/PaymentReceiveNumberDialog'; +import { ExcessPaymentDialog } from './dialogs/ExcessPaymentDialog'; /** * Payment receive form dialogs. @@ -21,9 +22,12 @@ export default function PaymentReceiveFormDialogs() { }; return ( - + <> + + + ); } diff --git a/packages/webapp/src/containers/Sales/PaymentReceives/PaymentReceiveForm/PaymentReceiveFormFootetRight.tsx b/packages/webapp/src/containers/Sales/PaymentReceives/PaymentReceiveForm/PaymentReceiveFormFootetRight.tsx index d35d7c6a7..2cee6c9f7 100644 --- a/packages/webapp/src/containers/Sales/PaymentReceives/PaymentReceiveForm/PaymentReceiveFormFootetRight.tsx +++ b/packages/webapp/src/containers/Sales/PaymentReceives/PaymentReceiveForm/PaymentReceiveFormFootetRight.tsx @@ -7,11 +7,16 @@ import { TotalLine, TotalLineBorderStyle, TotalLineTextStyle, + FormatNumber, } from '@/components'; -import { usePaymentReceiveTotals } from './utils'; +import { + usePaymentReceiveTotals, + usePaymentReceivedTotalExceededAmount, +} from './utils'; export function PaymentReceiveFormFootetRight() { const { formattedSubtotal, formattedTotal } = usePaymentReceiveTotals(); + const exceededAmount = usePaymentReceivedTotalExceededAmount(); return ( @@ -25,6 +30,11 @@ export function PaymentReceiveFormFootetRight() { value={formattedTotal} textStyle={TotalLineTextStyle.Bold} /> + } + textStyle={TotalLineTextStyle.Regular} + /> ); } diff --git a/packages/webapp/src/containers/Sales/PaymentReceives/PaymentReceiveForm/PaymentReceiveFormHeader.tsx b/packages/webapp/src/containers/Sales/PaymentReceives/PaymentReceiveForm/PaymentReceiveFormHeader.tsx index d7e44fadf..c6d08f074 100644 --- a/packages/webapp/src/containers/Sales/PaymentReceives/PaymentReceiveForm/PaymentReceiveFormHeader.tsx +++ b/packages/webapp/src/containers/Sales/PaymentReceives/PaymentReceiveForm/PaymentReceiveFormHeader.tsx @@ -30,15 +30,9 @@ function PaymentReceiveFormHeader() { function PaymentReceiveFormBigTotal() { // Formik form context. const { - values: { currency_code, entries }, + values: { currency_code, amount }, } = useFormikContext(); - // Calculates the total payment amount from due amount. - const paymentFullAmount = useMemo( - () => sumBy(entries, 'payment_amount'), - [entries], - ); - return (
@@ -46,7 +40,7 @@ function PaymentReceiveFormBigTotal() {

- +

diff --git a/packages/webapp/src/containers/Sales/PaymentReceives/PaymentReceiveForm/PaymentReceiveFormProvider.tsx b/packages/webapp/src/containers/Sales/PaymentReceives/PaymentReceiveForm/PaymentReceiveFormProvider.tsx index 8c08abd3e..c6f5a4729 100644 --- a/packages/webapp/src/containers/Sales/PaymentReceives/PaymentReceiveForm/PaymentReceiveFormProvider.tsx +++ b/packages/webapp/src/containers/Sales/PaymentReceives/PaymentReceiveForm/PaymentReceiveFormProvider.tsx @@ -1,5 +1,5 @@ // @ts-nocheck -import React, { createContext, useContext } from 'react'; +import React, { createContext, useContext, useState } from 'react'; import { Features } from '@/constants'; import { useFeatureCan } from '@/hooks/state'; import { DashboardInsider } from '@/components'; @@ -74,6 +74,8 @@ function PaymentReceiveFormProvider({ query, paymentReceiveId, ...props }) { const { mutateAsync: editPaymentReceiveMutate } = useEditPaymentReceive(); const { mutateAsync: createPaymentReceiveMutate } = useCreatePaymentReceive(); + const [isExcessConfirmed, setIsExcessConfirmed] = useState(false); + // Provider payload. const provider = { paymentReceiveId, @@ -97,6 +99,9 @@ function PaymentReceiveFormProvider({ query, paymentReceiveId, ...props }) { editPaymentReceiveMutate, createPaymentReceiveMutate, + + isExcessConfirmed, + setIsExcessConfirmed, }; return ( diff --git a/packages/webapp/src/containers/Sales/PaymentReceives/PaymentReceiveForm/PaymentReceiveHeaderFields.tsx b/packages/webapp/src/containers/Sales/PaymentReceives/PaymentReceiveForm/PaymentReceiveHeaderFields.tsx index 9ed1e5503..98b9fe4e2 100644 --- a/packages/webapp/src/containers/Sales/PaymentReceives/PaymentReceiveForm/PaymentReceiveHeaderFields.tsx +++ b/packages/webapp/src/containers/Sales/PaymentReceives/PaymentReceiveForm/PaymentReceiveHeaderFields.tsx @@ -11,7 +11,7 @@ import { Button, } from '@blueprintjs/core'; import { DateInput } from '@blueprintjs/datetime'; -import { toSafeInteger } from 'lodash'; +import { isEmpty, toSafeInteger } from 'lodash'; import { FastField, Field, useFormikContext, ErrorMessage } from 'formik'; import { @@ -124,11 +124,11 @@ export default function PaymentReceiveHeaderFields() { {/* ------------ Full amount ------------ */} - + {({ form: { setFieldValue, - values: { currency_code }, + values: { currency_code, entries }, }, field: { value, onChange }, meta: { error, touched }, @@ -146,21 +146,23 @@ export default function PaymentReceiveHeaderFields() { { - setFieldValue('full_amount', value); + setFieldValue('amount', value); }} onBlurValue={onFullAmountBlur} /> - + {!isEmpty(entries) && ( + + )} )} diff --git a/packages/webapp/src/containers/Sales/PaymentReceives/PaymentReceiveForm/dialogs/ExcessPaymentDialog/ExcessPaymentDialog.tsx b/packages/webapp/src/containers/Sales/PaymentReceives/PaymentReceiveForm/dialogs/ExcessPaymentDialog/ExcessPaymentDialog.tsx new file mode 100644 index 000000000..44e660e76 --- /dev/null +++ b/packages/webapp/src/containers/Sales/PaymentReceives/PaymentReceiveForm/dialogs/ExcessPaymentDialog/ExcessPaymentDialog.tsx @@ -0,0 +1,37 @@ +// @ts-nocheck +import React from 'react'; +import { Dialog, DialogSuspense } from '@/components'; +import withDialogRedux from '@/components/DialogReduxConnect'; +import { compose } from '@/utils'; + +const ExcessPaymentDialogContent = React.lazy(() => + import('./ExcessPaymentDialogContent').then((module) => ({ + default: module.ExcessPaymentDialogContent, + })), +); + +/** + * Excess payment dialog of the payment received form. + */ +function ExcessPaymentDialogRoot({ dialogName, isOpen }) { + return ( + + + + + + ); +} + +export const ExcessPaymentDialog = compose(withDialogRedux())( + ExcessPaymentDialogRoot, +); + +ExcessPaymentDialog.displayName = 'ExcessPaymentDialog'; diff --git a/packages/webapp/src/containers/Sales/PaymentReceives/PaymentReceiveForm/dialogs/ExcessPaymentDialog/ExcessPaymentDialogContent.tsx b/packages/webapp/src/containers/Sales/PaymentReceives/PaymentReceiveForm/dialogs/ExcessPaymentDialog/ExcessPaymentDialogContent.tsx new file mode 100644 index 000000000..868a75bdf --- /dev/null +++ b/packages/webapp/src/containers/Sales/PaymentReceives/PaymentReceiveForm/dialogs/ExcessPaymentDialog/ExcessPaymentDialogContent.tsx @@ -0,0 +1,86 @@ +// @ts-nocheck +import * as Yup from 'yup'; +import * as R from 'ramda'; +import { Button, Classes, Intent } from '@blueprintjs/core'; +import { Form, Formik, FormikHelpers, useFormikContext } from 'formik'; +import { FormatNumber } from '@/components'; +import { usePaymentReceiveFormContext } from '../../PaymentReceiveFormProvider'; +import withDialogActions from '@/containers/Dialog/withDialogActions'; +import { usePaymentReceivedTotalExceededAmount } from '../../utils'; + +interface ExcessPaymentValues {} + +export function ExcessPaymentDialogContentRoot({ dialogName, closeDialog }) { + const { + submitForm, + values: { currency_code: currencyCode }, + } = useFormikContext(); + const { setIsExcessConfirmed } = usePaymentReceiveFormContext(); + const exceededAmount = usePaymentReceivedTotalExceededAmount(); + + const handleSubmit = ( + values: ExcessPaymentValues, + { setSubmitting }: FormikHelpers, + ) => { + setSubmitting(true); + setIsExcessConfirmed(true); + + submitForm().then(() => { + closeDialog(dialogName); + setSubmitting(false); + }); + }; + const handleClose = () => { + closeDialog(dialogName); + }; + + return ( + +
+ + } + onClose={handleClose} + /> + +
+ ); +} + +export const ExcessPaymentDialogContent = R.compose(withDialogActions)( + ExcessPaymentDialogContentRoot, +); + +function ExcessPaymentDialogContentForm({ onClose, exceededAmount }) { + const { submitForm, isSubmitting } = useFormikContext(); + + const handleCloseBtn = () => { + onClose && onClose(); + }; + + return ( + <> +
+

+ Would you like to record the excess amount of{' '} + {exceededAmount} as credit payment from the customer. +

+
+ +
+
+ + +
+
+ + ); +} diff --git a/packages/webapp/src/containers/Sales/PaymentReceives/PaymentReceiveForm/dialogs/ExcessPaymentDialog/index.ts b/packages/webapp/src/containers/Sales/PaymentReceives/PaymentReceiveForm/dialogs/ExcessPaymentDialog/index.ts new file mode 100644 index 000000000..9be100852 --- /dev/null +++ b/packages/webapp/src/containers/Sales/PaymentReceives/PaymentReceiveForm/dialogs/ExcessPaymentDialog/index.ts @@ -0,0 +1 @@ +export * from './ExcessPaymentDialog'; \ No newline at end of file diff --git a/packages/webapp/src/containers/Sales/PaymentReceives/PaymentReceiveForm/utils.tsx b/packages/webapp/src/containers/Sales/PaymentReceives/PaymentReceiveForm/utils.tsx index 71f75280a..3be7c0ea4 100644 --- a/packages/webapp/src/containers/Sales/PaymentReceives/PaymentReceiveForm/utils.tsx +++ b/packages/webapp/src/containers/Sales/PaymentReceives/PaymentReceiveForm/utils.tsx @@ -42,12 +42,12 @@ export const defaultPaymentReceive = { // Holds the payment number that entered manually only. payment_receive_no_manually: '', statement: '', - full_amount: '', + amount: '', currency_code: '', branch_id: '', exchange_rate: 1, entries: [], - attachments: [] + attachments: [], }; export const defaultRequestPaymentEntry = { @@ -249,6 +249,30 @@ export const usePaymentReceiveTotals = () => { }; }; +export const usePaymentReceivedTotalAppliedAmount = () => { + const { + values: { entries }, + } = useFormikContext(); + + // Retrieves the invoice entries total. + return React.useMemo(() => sumBy(entries, 'payment_amount'), [entries]); +}; + +export const usePaymentReceivedTotalAmount = () => { + const { + values: { amount }, + } = useFormikContext(); + + return amount; +}; + +export const usePaymentReceivedTotalExceededAmount = () => { + const totalAmount = usePaymentReceivedTotalAmount(); + const totalApplied = usePaymentReceivedTotalAppliedAmount(); + + return Math.abs(totalAmount - totalApplied); +}; + /** * Detarmines whether the payment has foreign customer. * @returns {boolean} @@ -273,3 +297,10 @@ export const resetFormState = ({ initialValues, values, resetForm }) => { }, }); }; + +export const getExceededAmountFromValues = (values) => { + const totalApplied = sumBy(values.entries, 'payment_amount'); + const totalAmount = values.amount; + + return totalAmount - totalApplied; +}; From 9e6500ac79af59e09f11d2199ef0cd07089a83c6 Mon Sep 17 00:00:00 2001 From: Ahmed Bouhuolia Date: Thu, 25 Jul 2024 19:17:54 +0200 Subject: [PATCH 06/15] feat: remove the views tabs bar from all tables --- .../Accounting/JournalsLanding/ManualJournalsList.tsx | 2 -- packages/webapp/src/containers/Accounts/AccountsChart.tsx | 6 ++---- .../containers/Customers/CustomersLanding/CustomersList.tsx | 2 -- .../containers/Expenses/ExpensesLanding/ExpensesList.tsx | 2 -- packages/webapp/src/containers/Items/ItemsList.tsx | 2 -- .../containers/Purchases/Bills/BillsLanding/BillsList.tsx | 2 -- .../CreditNotesLanding/VendorsCreditNotesList.tsx | 2 -- .../PaymentMades/PaymentsLanding/PaymentMadeList.tsx | 2 -- .../CreditNotes/CreditNotesLanding/CreditNotesList.tsx | 3 +-- .../Sales/Estimates/EstimatesLanding/EstimatesList.tsx | 4 +--- .../Sales/Invoices/InvoicesLanding/InvoicesList.tsx | 2 -- .../PaymentReceives/PaymentsLanding/PaymentReceivesList.tsx | 1 - .../src/containers/Vendors/VendorsLanding/VendorsList.tsx | 2 -- .../WarehouseTransfersLanding/WarehouseTransfersList.tsx | 3 +-- 14 files changed, 5 insertions(+), 30 deletions(-) diff --git a/packages/webapp/src/containers/Accounting/JournalsLanding/ManualJournalsList.tsx b/packages/webapp/src/containers/Accounting/JournalsLanding/ManualJournalsList.tsx index 749ac981f..cd0135010 100644 --- a/packages/webapp/src/containers/Accounting/JournalsLanding/ManualJournalsList.tsx +++ b/packages/webapp/src/containers/Accounting/JournalsLanding/ManualJournalsList.tsx @@ -7,7 +7,6 @@ import { DashboardPageContent } from '@/components'; import { transformTableStateToQuery, compose } from '@/utils'; import { ManualJournalsListProvider } from './ManualJournalsListProvider'; -import ManualJournalsViewTabs from './ManualJournalsViewTabs'; import ManualJournalsDataTable from './ManualJournalsDataTable'; import ManualJournalsActionsBar from './ManualJournalActionsBar'; import withManualJournals from './withManualJournals'; @@ -29,7 +28,6 @@ function ManualJournalsTable({ - diff --git a/packages/webapp/src/containers/Accounts/AccountsChart.tsx b/packages/webapp/src/containers/Accounts/AccountsChart.tsx index 271e3260d..4be0c9c6c 100644 --- a/packages/webapp/src/containers/Accounts/AccountsChart.tsx +++ b/packages/webapp/src/containers/Accounts/AccountsChart.tsx @@ -2,15 +2,15 @@ import React, { useEffect } from 'react'; import '@/style/pages/Accounts/List.scss'; -import { DashboardPageContent, DashboardContentTable } from '@/components'; +import { DashboardPageContent, DashboardContentTable } from '@/components'; import { AccountsChartProvider } from './AccountsChartProvider'; -import AccountsViewsTabs from './AccountsViewsTabs'; import AccountsActionsBar from './AccountsActionsBar'; import AccountsDataTable from './AccountsDataTable'; import withAccounts from '@/containers/Accounts/withAccounts'; import withAccountsTableActions from './withAccountsTableActions'; + import { transformAccountsStateToQuery } from './utils'; import { compose } from '@/utils'; @@ -41,8 +41,6 @@ function AccountsChart({ - - diff --git a/packages/webapp/src/containers/Customers/CustomersLanding/CustomersList.tsx b/packages/webapp/src/containers/Customers/CustomersLanding/CustomersList.tsx index cdf293905..3f9cddef2 100644 --- a/packages/webapp/src/containers/Customers/CustomersLanding/CustomersList.tsx +++ b/packages/webapp/src/containers/Customers/CustomersLanding/CustomersList.tsx @@ -6,7 +6,6 @@ import '@/style/pages/Customers/List.scss'; import { DashboardPageContent } from '@/components'; import CustomersActionsBar from './CustomersActionsBar'; -import CustomersViewsTabs from './CustomersViewsTabs'; import CustomersTable from './CustomersTable'; import { CustomersListProvider } from './CustomersListProvider'; @@ -42,7 +41,6 @@ function CustomersList({ - diff --git a/packages/webapp/src/containers/Expenses/ExpensesLanding/ExpensesList.tsx b/packages/webapp/src/containers/Expenses/ExpensesLanding/ExpensesList.tsx index 65e9e69fa..c92ca6cdb 100644 --- a/packages/webapp/src/containers/Expenses/ExpensesLanding/ExpensesList.tsx +++ b/packages/webapp/src/containers/Expenses/ExpensesLanding/ExpensesList.tsx @@ -6,7 +6,6 @@ import '@/style/pages/Expense/List.scss'; import { DashboardPageContent } from '@/components'; import ExpenseActionsBar from './ExpenseActionsBar'; -import ExpenseViewTabs from './ExpenseViewTabs'; import ExpenseDataTable from './ExpenseDataTable'; import withExpenses from './withExpenses'; @@ -42,7 +41,6 @@ function ExpensesList({ - diff --git a/packages/webapp/src/containers/Items/ItemsList.tsx b/packages/webapp/src/containers/Items/ItemsList.tsx index 017a5302c..de6c10ea1 100644 --- a/packages/webapp/src/containers/Items/ItemsList.tsx +++ b/packages/webapp/src/containers/Items/ItemsList.tsx @@ -8,7 +8,6 @@ import { DashboardPageContent } from '@/components'; import { ItemsListProvider } from './ItemsListProvider'; import ItemsActionsBar from './ItemsActionsBar'; -import ItemsViewsTabs from './ItemsViewsTabs'; import ItemsDataTable from './ItemsDataTable'; import withItems from './withItems'; @@ -41,7 +40,6 @@ function ItemsList({ - diff --git a/packages/webapp/src/containers/Purchases/Bills/BillsLanding/BillsList.tsx b/packages/webapp/src/containers/Purchases/Bills/BillsLanding/BillsList.tsx index 7e6bb795d..5afd9ffa3 100644 --- a/packages/webapp/src/containers/Purchases/Bills/BillsLanding/BillsList.tsx +++ b/packages/webapp/src/containers/Purchases/Bills/BillsLanding/BillsList.tsx @@ -7,7 +7,6 @@ import '@/style/pages/Bills/List.scss'; import { BillsListProvider } from './BillsListProvider'; import BillsActionsBar from './BillsActionsBar'; -import BillsViewsTabs from './BillsViewsTabs'; import BillsTable from './BillsTable'; import withBills from './withBills'; @@ -42,7 +41,6 @@ function BillsList({ - diff --git a/packages/webapp/src/containers/Purchases/CreditNotes/CreditNotesLanding/VendorsCreditNotesList.tsx b/packages/webapp/src/containers/Purchases/CreditNotes/CreditNotesLanding/VendorsCreditNotesList.tsx index 02f3345b5..0ac4158fe 100644 --- a/packages/webapp/src/containers/Purchases/CreditNotes/CreditNotesLanding/VendorsCreditNotesList.tsx +++ b/packages/webapp/src/containers/Purchases/CreditNotes/CreditNotesLanding/VendorsCreditNotesList.tsx @@ -5,7 +5,6 @@ import '@/style/pages/VendorsCreditNote/List.scss'; import { DashboardPageContent } from '@/components'; import VendorsCreditNoteActionsBar from './VendorsCreditNoteActionsBar'; -import VendorsCreditNoteViewTabs from './VendorsCreditNoteViewTabs'; import VendorsCreditNoteDataTable from './VendorsCreditNoteDataTable'; import withVendorsCreditNotes from './withVendorsCreditNotes'; @@ -37,7 +36,6 @@ function VendorsCreditNotesList({ > - diff --git a/packages/webapp/src/containers/Purchases/PaymentMades/PaymentsLanding/PaymentMadeList.tsx b/packages/webapp/src/containers/Purchases/PaymentMades/PaymentsLanding/PaymentMadeList.tsx index f2fa51028..45427c1cc 100644 --- a/packages/webapp/src/containers/Purchases/PaymentMades/PaymentsLanding/PaymentMadeList.tsx +++ b/packages/webapp/src/containers/Purchases/PaymentMades/PaymentsLanding/PaymentMadeList.tsx @@ -7,7 +7,6 @@ import { DashboardPageContent } from '@/components'; import { PaymentMadesListProvider } from './PaymentMadesListProvider'; import PaymentMadeActionsBar from './PaymentMadeActionsBar'; import PaymentMadesTable from './PaymentMadesTable'; -import PaymentMadeViewTabs from './PaymentMadeViewTabs'; import withPaymentMades from './withPaymentMade'; import withPaymentMadeActions from './withPaymentMadeActions'; @@ -41,7 +40,6 @@ function PaymentMadeList({ - diff --git a/packages/webapp/src/containers/Sales/CreditNotes/CreditNotesLanding/CreditNotesList.tsx b/packages/webapp/src/containers/Sales/CreditNotes/CreditNotesLanding/CreditNotesList.tsx index 13873aacc..360c5a6a0 100644 --- a/packages/webapp/src/containers/Sales/CreditNotes/CreditNotesLanding/CreditNotesList.tsx +++ b/packages/webapp/src/containers/Sales/CreditNotes/CreditNotesLanding/CreditNotesList.tsx @@ -5,7 +5,6 @@ import '@/style/pages/CreditNote/List.scss'; import { DashboardPageContent } from '@/components'; import CreditNotesActionsBar from './CreditNotesActionsBar'; -import CreditNotesViewTabs from './CreditNotesViewTabs'; import CreditNotesDataTable from './CreditNotesDataTable'; import withCreditNotes from './withCreditNotes'; @@ -36,8 +35,8 @@ function CreditNotesList({ tableStateChanged={creditNoteTableStateChanged} > + - diff --git a/packages/webapp/src/containers/Sales/Estimates/EstimatesLanding/EstimatesList.tsx b/packages/webapp/src/containers/Sales/Estimates/EstimatesLanding/EstimatesList.tsx index 39c33cfc4..866871f4b 100644 --- a/packages/webapp/src/containers/Sales/Estimates/EstimatesLanding/EstimatesList.tsx +++ b/packages/webapp/src/containers/Sales/Estimates/EstimatesLanding/EstimatesList.tsx @@ -1,11 +1,10 @@ // @ts-nocheck import React from 'react'; -import { DashboardContentTable, DashboardPageContent } from '@/components'; +import { DashboardPageContent } from '@/components'; import '@/style/pages/SaleEstimate/List.scss'; import EstimatesActionsBar from './EstimatesActionsBar'; -import EstimatesViewTabs from './EstimatesViewTabs'; import EstimatesDataTable from './EstimatesDataTable'; import withEstimates from './withEstimates'; @@ -41,7 +40,6 @@ function EstimatesList({ - diff --git a/packages/webapp/src/containers/Sales/Invoices/InvoicesLanding/InvoicesList.tsx b/packages/webapp/src/containers/Sales/Invoices/InvoicesLanding/InvoicesList.tsx index dc4577d0d..e846dc86a 100644 --- a/packages/webapp/src/containers/Sales/Invoices/InvoicesLanding/InvoicesList.tsx +++ b/packages/webapp/src/containers/Sales/Invoices/InvoicesLanding/InvoicesList.tsx @@ -6,7 +6,6 @@ import '@/style/pages/SaleInvoice/List.scss'; import { DashboardPageContent } from '@/components'; import { InvoicesListProvider } from './InvoicesListProvider'; -import InvoiceViewTabs from './InvoiceViewTabs'; import InvoicesDataTable from './InvoicesDataTable'; import InvoicesActionsBar from './InvoicesActionsBar'; @@ -43,7 +42,6 @@ function InvoicesList({ - diff --git a/packages/webapp/src/containers/Sales/PaymentReceives/PaymentsLanding/PaymentReceivesList.tsx b/packages/webapp/src/containers/Sales/PaymentReceives/PaymentsLanding/PaymentReceivesList.tsx index f2d8b2bb3..66a7b59d6 100644 --- a/packages/webapp/src/containers/Sales/PaymentReceives/PaymentsLanding/PaymentReceivesList.tsx +++ b/packages/webapp/src/containers/Sales/PaymentReceives/PaymentsLanding/PaymentReceivesList.tsx @@ -41,7 +41,6 @@ function PaymentReceiveList({ - diff --git a/packages/webapp/src/containers/Vendors/VendorsLanding/VendorsList.tsx b/packages/webapp/src/containers/Vendors/VendorsLanding/VendorsList.tsx index c558f6024..edc173dc0 100644 --- a/packages/webapp/src/containers/Vendors/VendorsLanding/VendorsList.tsx +++ b/packages/webapp/src/containers/Vendors/VendorsLanding/VendorsList.tsx @@ -7,7 +7,6 @@ import { DashboardPageContent } from '@/components'; import { VendorsListProvider } from './VendorsListProvider'; import VendorActionsBar from './VendorActionsBar'; -import VendorViewsTabs from './VendorViewsTabs'; import VendorsTable from './VendorsTable'; import withVendors from './withVendors'; @@ -42,7 +41,6 @@ function VendorsList({ - diff --git a/packages/webapp/src/containers/WarehouseTransfers/WarehouseTransfersLanding/WarehouseTransfersList.tsx b/packages/webapp/src/containers/WarehouseTransfers/WarehouseTransfersLanding/WarehouseTransfersList.tsx index 83a4c8935..c57fa2762 100644 --- a/packages/webapp/src/containers/WarehouseTransfers/WarehouseTransfersLanding/WarehouseTransfersList.tsx +++ b/packages/webapp/src/containers/WarehouseTransfers/WarehouseTransfersLanding/WarehouseTransfersList.tsx @@ -3,7 +3,6 @@ import React from 'react'; import { DashboardPageContent } from '@/components'; import WarehouseTransfersActionsBar from './WarehouseTransfersActionsBar'; -import WarehouseTransfersViewTabs from './WarehouseTransfersViewTabs'; import WarehouseTransfersDataTable from './WarehouseTransfersDataTable'; import withWarehouseTransfers from './withWarehouseTransfers'; import withWarehouseTransfersActions from './withWarehouseTransfersActions'; @@ -33,8 +32,8 @@ function WarehouseTransfersList({ tableStateChanged={warehouseTransferTableStateChanged} > + - From 0a7b522b8751ffe104183f6a04e8eeb86db104ac Mon Sep 17 00:00:00 2001 From: Ahmed Bouhuolia Date: Thu, 25 Jul 2024 19:21:16 +0200 Subject: [PATCH 07/15] chore: remove unused import --- .../PaymentReceives/PaymentsLanding/PaymentReceivesList.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/webapp/src/containers/Sales/PaymentReceives/PaymentsLanding/PaymentReceivesList.tsx b/packages/webapp/src/containers/Sales/PaymentReceives/PaymentsLanding/PaymentReceivesList.tsx index 66a7b59d6..38a04e170 100644 --- a/packages/webapp/src/containers/Sales/PaymentReceives/PaymentsLanding/PaymentReceivesList.tsx +++ b/packages/webapp/src/containers/Sales/PaymentReceives/PaymentsLanding/PaymentReceivesList.tsx @@ -5,7 +5,6 @@ import '@/style/pages/PaymentReceive/List.scss'; import { DashboardPageContent } from '@/components'; import { PaymentReceivesListProvider } from './PaymentReceiptsListProvider'; -import PaymentReceiveViewTabs from './PaymentReceiveViewTabs'; import PaymentReceivesTable from './PaymentReceivesTable'; import PaymentReceiveActionsBar from './PaymentReceiveActionsBar'; From 14d5e82b4aa4394d877435898c7ae69310e2d66f Mon Sep 17 00:00:00 2001 From: Ahmed Bouhuolia Date: Mon, 29 Jul 2024 12:00:34 +0200 Subject: [PATCH 08/15] fix: style of database checkbox --- .../src/style/components/DataTable/DataTable.scss | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/packages/webapp/src/style/components/DataTable/DataTable.scss b/packages/webapp/src/style/components/DataTable/DataTable.scss index cf4018489..1c1b138cd 100644 --- a/packages/webapp/src/style/components/DataTable/DataTable.scss +++ b/packages/webapp/src/style/components/DataTable/DataTable.scss @@ -124,22 +124,18 @@ } } - .bp4-control.bp4-checkbox .bp4-control-indicator { + .bp4-control.bp4-checkbox .bp4-control-indicator { cursor: auto; &, - &:hover { + &::before { height: 15px; width: 15px; } } - .bp4-control.bp4-checkbox { - - input:checked~.bp4-control-indicator, - input:indeterminate~.bp4-control-indicator { - border-color: #0052ff; - } + .bp4-control.bp4-checkbox input:not(:checked):not(:indeterminate) ~ .bp4-control-indicator{ + box-shadow: inset 0 0 0 1px #C5CBD3; } .skeleton { From 4345623ea9c512b0210f22d0682b5c04ce1a77dd Mon Sep 17 00:00:00 2001 From: Ahmed Bouhuolia Date: Mon, 29 Jul 2024 13:00:50 +0200 Subject: [PATCH 09/15] feat: document functions --- .../Banking/Exclude/ExcludeBankTransactions.ts | 5 +++-- .../Exclude/UnexcludeBankTransactions.ts | 2 +- .../AccountTransactionsActionsBar.tsx | 8 +++----- .../containers/CashFlow/withBankingActions.ts | 16 ++++++++++++++++ .../src/store/banking/banking.reducer.ts | 18 ++++++++++++++++++ .../src/style/pages/Dashboard/Dashboard.scss | 6 +++++- 6 files changed, 46 insertions(+), 9 deletions(-) diff --git a/packages/server/src/services/Banking/Exclude/ExcludeBankTransactions.ts b/packages/server/src/services/Banking/Exclude/ExcludeBankTransactions.ts index abf6bd434..65d65a7c1 100644 --- a/packages/server/src/services/Banking/Exclude/ExcludeBankTransactions.ts +++ b/packages/server/src/services/Banking/Exclude/ExcludeBankTransactions.ts @@ -1,7 +1,7 @@ import { Inject, Service } from 'typedi'; -import { ExcludeBankTransaction } from './ExcludeBankTransaction'; import PromisePool from '@supercharge/promise-pool'; import { castArray } from 'lodash'; +import { ExcludeBankTransaction } from './ExcludeBankTransaction'; @Service() export class ExcludeBankTransactions { @@ -12,6 +12,7 @@ export class ExcludeBankTransactions { * Exclude bank transactions in bulk. * @param {number} tenantId * @param {number} bankTransactionIds + * @returns {Promise} */ public async excludeBankTransactions( tenantId: number, @@ -21,7 +22,7 @@ export class ExcludeBankTransactions { await PromisePool.withConcurrency(1) .for(_bankTransactionIds) - .process(async (bankTransactionId: number) => { + .process((bankTransactionId: number) => { return this.excludeBankTransaction.excludeBankTransaction( tenantId, bankTransactionId diff --git a/packages/server/src/services/Banking/Exclude/UnexcludeBankTransactions.ts b/packages/server/src/services/Banking/Exclude/UnexcludeBankTransactions.ts index 840eb6259..846ea1fd8 100644 --- a/packages/server/src/services/Banking/Exclude/UnexcludeBankTransactions.ts +++ b/packages/server/src/services/Banking/Exclude/UnexcludeBankTransactions.ts @@ -21,7 +21,7 @@ export class UnexcludeBankTransactions { await PromisePool.withConcurrency(1) .for(_bankTransactionIds) - .process(async (bankTransactionId: number) => { + .process((bankTransactionId: number) => { return this.unexcludeBankTransaction.unexcludeBankTransaction( tenantId, bankTransactionId diff --git a/packages/webapp/src/containers/CashFlow/AccountTransactions/AccountTransactionsActionsBar.tsx b/packages/webapp/src/containers/CashFlow/AccountTransactions/AccountTransactionsActionsBar.tsx index 7ca9abc37..53ef5a786 100644 --- a/packages/webapp/src/containers/CashFlow/AccountTransactions/AccountTransactionsActionsBar.tsx +++ b/packages/webapp/src/containers/CashFlow/AccountTransactions/AccountTransactionsActionsBar.tsx @@ -39,7 +39,6 @@ import { withBanking } from '../withBanking'; import { isEmpty } from 'lodash'; import { useExcludeUncategorizedTransactions, - useUnexcludeUncategorizedTransaction, useUnexcludeUncategorizedTransactions, } from '@/hooks/query/bank-rules'; @@ -136,7 +135,7 @@ function AccountTransactionsActionsBar({ }) .then(() => { AppToaster.show({ - message: 'The selected transactions have been unexcluded.', + message: 'The selected excluded transactions have been unexcluded.', intent: Intent.SUCCESS, }); }) @@ -198,10 +197,9 @@ function AccountTransactionsActionsBar({ onClick={handleExcludeUncategorizedBtnClick} className={Classes.MINIMAL} intent={Intent.DANGER} - disable={isExcludingLoading} + disabled={isExcludingLoading} /> )} - {!isEmpty(excludedTransactionsIdsSelected) && (