From c27458ebcc298e30e1c3ddb6ba5f5d1503fd385b Mon Sep 17 00:00:00 2001 From: Ahmed Bouhuolia Date: Mon, 1 Jul 2024 12:02:59 +0200 Subject: [PATCH] feat: wip matching bank transactions --- .../UncategorizedCashflowTransaction.ts | 21 +++++++++++++++ .../Exclude/UnexcludeBankTransaction.ts | 2 +- .../Matching/GetMatchedTransactionsByBills.ts | 27 +++++++++++++++++-- .../GetMatchedTransactionsByExpenses.ts | 25 ++++++++++++++++- .../GetMatchedTransactionsByInvoices.ts | 4 ++- .../Cashflow/GetUncategorizedTransactions.ts | 1 + .../ExcludedTransactions/_components.tsx | 7 ++++- .../AccountTransactions/components.tsx | 2 +- .../CategorizeTransactionForm.tsx | 13 +++++---- .../MatchingTransaction.tsx | 2 +- packages/webapp/src/hooks/query/bank-rules.ts | 6 +++++ packages/webapp/src/static/json/icons.tsx | 12 +++++++++ 12 files changed, 107 insertions(+), 15 deletions(-) diff --git a/packages/server/src/models/UncategorizedCashflowTransaction.ts b/packages/server/src/models/UncategorizedCashflowTransaction.ts index 3d70ff7d0..f9902e347 100644 --- a/packages/server/src/models/UncategorizedCashflowTransaction.ts +++ b/packages/server/src/models/UncategorizedCashflowTransaction.ts @@ -89,6 +89,27 @@ export default class UncategorizedCashflowTransaction extends mixin( return !!this.recognizedTransactionId; } + /** + * Model modifiers. + */ + static get modifiers() { + return { + /** + * Filters the not excluded transactions. + */ + notExcluded(query) { + query.whereNull('excluded'); + }, + + /** + * Filters the excluded transactions. + */ + excluded(query) { + query.where('excluded', true) + } + }; + }, + /** * Relationship mapping. */ diff --git a/packages/server/src/services/Banking/Exclude/UnexcludeBankTransaction.ts b/packages/server/src/services/Banking/Exclude/UnexcludeBankTransaction.ts index 9dd23c1a6..e92de6c94 100644 --- a/packages/server/src/services/Banking/Exclude/UnexcludeBankTransaction.ts +++ b/packages/server/src/services/Banking/Exclude/UnexcludeBankTransaction.ts @@ -34,7 +34,7 @@ export class UnexcludeBankTransaction { await UncategorizedCashflowTransaction.query(trx) .findById(uncategorizedTransactionId) .patch({ - excluded: false, + excluded: null, }); }); } diff --git a/packages/server/src/services/Banking/Matching/GetMatchedTransactionsByBills.ts b/packages/server/src/services/Banking/Matching/GetMatchedTransactionsByBills.ts index 7ac246bce..4796f7598 100644 --- a/packages/server/src/services/Banking/Matching/GetMatchedTransactionsByBills.ts +++ b/packages/server/src/services/Banking/Matching/GetMatchedTransactionsByBills.ts @@ -1,7 +1,7 @@ import { Inject, Service } from 'typedi'; import { TransformerInjectable } from '@/lib/Transformer/TransformerInjectable'; import { GetMatchedTransactionBillsTransformer } from './GetMatchedTransactionBillsTransformer'; -import { GetMatchedTransactionsFilter } from './types'; +import { GetMatchedTransactionsFilter, MatchedTransactionPOJO } from './types'; import HasTenancyService from '@/services/Tenancy/TenancyService'; import { GetMatchedTransactionsByType } from './GetMatchedTransactionsByType'; @@ -24,7 +24,9 @@ export class GetMatchedTransactionsByBills extends GetMatchedTransactionsByType ) { const { Bill } = this.tenancy.models(tenantId); - const bills = await Bill.query(); + const bills = await Bill.query().onBuild((q) => { + q.whereNotExists(Bill.relatedQuery('matchedBankTransaction')); + }); return this.transformer.transform( tenantId, @@ -32,4 +34,25 @@ export class GetMatchedTransactionsByBills extends GetMatchedTransactionsByType new GetMatchedTransactionBillsTransformer() ); } + + /** + * Retrieves the given bill matched transaction. + * @param {number} tenantId + * @param {number} transactionId + * @returns {Promise} + */ + public async getMatchedTransaction( + tenantId: number, + transactionId: number + ): Promise { + const { Bill } = this.tenancy.models(tenantId); + + const bill = await Bill.query().findById(transactionId).throwIfNotFound(); + + return this.transformer.transform( + tenantId, + bill, + new GetMatchedTransactionBillsTransformer() + ); + } } diff --git a/packages/server/src/services/Banking/Matching/GetMatchedTransactionsByExpenses.ts b/packages/server/src/services/Banking/Matching/GetMatchedTransactionsByExpenses.ts index 9205ce2e8..8996c4b91 100644 --- a/packages/server/src/services/Banking/Matching/GetMatchedTransactionsByExpenses.ts +++ b/packages/server/src/services/Banking/Matching/GetMatchedTransactionsByExpenses.ts @@ -1,5 +1,5 @@ import { Inject, Service } from 'typedi'; -import { GetMatchedTransactionsFilter } from './types'; +import { GetMatchedTransactionsFilter, MatchedTransactionPOJO } from './types'; import { TransformerInjectable } from '@/lib/Transformer/TransformerInjectable'; import HasTenancyService from '@/services/Tenancy/TenancyService'; import { GetMatchedTransactionsByType } from './GetMatchedTransactionsByType'; @@ -46,4 +46,27 @@ export class GetMatchedTransactionsByExpenses extends GetMatchedTransactionsByTy new GetMatchedTransactionExpensesTransformer() ); } + + /** + * Retrieves the given matched expense transaction. + * @param {number} tenantId + * @param {number} transactionId + * @returns {GetMatchedTransactionExpensesTransformer-} + */ + public async getMatchedTransaction( + tenantId: number, + transactionId: number + ): Promise { + const { Expense } = this.tenancy.models(tenantId); + + const expense = await Expense.query() + .findById(transactionId) + .throwIfNotFound(); + + return this.transformer.transform( + tenantId, + expense, + new GetMatchedTransactionExpensesTransformer() + ); + } } diff --git a/packages/server/src/services/Banking/Matching/GetMatchedTransactionsByInvoices.ts b/packages/server/src/services/Banking/Matching/GetMatchedTransactionsByInvoices.ts index 89f1c3307..141d22fe1 100644 --- a/packages/server/src/services/Banking/Matching/GetMatchedTransactionsByInvoices.ts +++ b/packages/server/src/services/Banking/Matching/GetMatchedTransactionsByInvoices.ts @@ -29,7 +29,9 @@ export class GetMatchedTransactionsByInvoices extends GetMatchedTransactionsByTy ): Promise { const { SaleInvoice } = this.tenancy.models(tenantId); - const invoices = await SaleInvoice.query(); + const invoices = await SaleInvoice.query().onBuild((q) => { + q.whereNotExists(SaleInvoice.relatedQuery('matchedBankTransaction')); + }); return this.transformer.transform( tenantId, diff --git a/packages/server/src/services/Cashflow/GetUncategorizedTransactions.ts b/packages/server/src/services/Cashflow/GetUncategorizedTransactions.ts index 36606f582..5501baf75 100644 --- a/packages/server/src/services/Cashflow/GetUncategorizedTransactions.ts +++ b/packages/server/src/services/Cashflow/GetUncategorizedTransactions.ts @@ -34,6 +34,7 @@ export class GetUncategorizedTransactions { await UncategorizedCashflowTransaction.query() .where('accountId', accountId) .where('categorized', false) + .modify('notExcluded') .withGraphFetched('account') .orderBy('date', 'DESC') .pagination(_query.page - 1, _query.pageSize); diff --git a/packages/webapp/src/containers/CashFlow/AccountTransactions/ExcludedTransactions/_components.tsx b/packages/webapp/src/containers/CashFlow/AccountTransactions/ExcludedTransactions/_components.tsx index 1d622b585..6593ac57e 100644 --- a/packages/webapp/src/containers/CashFlow/AccountTransactions/ExcludedTransactions/_components.tsx +++ b/packages/webapp/src/containers/CashFlow/AccountTransactions/ExcludedTransactions/_components.tsx @@ -1,11 +1,16 @@ // @ts-nocheck import { Menu, MenuItem, MenuDivider } from '@blueprintjs/core'; import { safeCallback } from '@/utils'; +import { Icon } from '@/components'; export function ActionsMenu({ payload: { onRestore }, row: { original } }) { return ( - + } + onClick={safeCallback(onRestore, original)} + /> ); } diff --git a/packages/webapp/src/containers/CashFlow/AccountTransactions/components.tsx b/packages/webapp/src/containers/CashFlow/AccountTransactions/components.tsx index bdd85115f..83307fc33 100644 --- a/packages/webapp/src/containers/CashFlow/AccountTransactions/components.tsx +++ b/packages/webapp/src/containers/CashFlow/AccountTransactions/components.tsx @@ -40,7 +40,7 @@ export function ActionsMenu({ } + icon={} /> ); diff --git a/packages/webapp/src/containers/CashFlow/CategorizeTransaction/drawers/CategorizeTransactionDrawer/CategorizeTransactionForm.tsx b/packages/webapp/src/containers/CashFlow/CategorizeTransaction/drawers/CategorizeTransactionDrawer/CategorizeTransactionForm.tsx index cd44a5450..83ba74c4d 100644 --- a/packages/webapp/src/containers/CashFlow/CategorizeTransaction/drawers/CategorizeTransactionDrawer/CategorizeTransactionForm.tsx +++ b/packages/webapp/src/containers/CashFlow/CategorizeTransaction/drawers/CategorizeTransactionDrawer/CategorizeTransactionForm.tsx @@ -1,29 +1,28 @@ // @ts-nocheck import { Formik, Form } from 'formik'; +import { Intent } from '@blueprintjs/core'; import styled from 'styled-components'; import { CreateCategorizeTransactionSchema } from './CategorizeTransactionForm.schema'; import { CategorizeTransactionFormContent } from './CategorizeTransactionFormContent'; import { CategorizeTransactionFormFooter } from './CategorizeTransactionFormFooter'; import { useCategorizeTransaction } from '@/hooks/query'; import { useCategorizeTransactionBoot } from './CategorizeTransactionBoot'; -import { DRAWERS } from '@/constants/drawers'; import { transformToCategorizeForm, defaultInitialValues, tranformToRequest, } from './_utils'; import { compose } from '@/utils'; -import withDrawerActions from '@/containers/Drawer/withDrawerActions'; +import { withBankingActions } from '@/containers/CashFlow/withBankingActions'; import { AppToaster } from '@/components'; -import { Intent } from '@blueprintjs/core'; import { useCategorizeTransactionTabsBoot } from '@/containers/CashFlow/CategorizeTransactionAside/CategorizeTransactionTabsBoot'; /** * Categorize cashflow transaction form dialog content. */ function CategorizeTransactionFormRoot({ - // #withDrawerActions - closeDrawer, + // #withBankingActions + closeMatchingTransactionAside, }) { const { uncategorizedTransactionId, uncategorizedTransaction } = useCategorizeTransactionTabsBoot(); @@ -38,12 +37,12 @@ function CategorizeTransactionFormRoot({ categorizeTransaction([uncategorizedTransactionId, transformedValues]) .then(() => { setSubmitting(false); - closeDrawer(DRAWERS.CATEGORIZE_TRANSACTION); AppToaster.show({ message: 'The uncategorized transaction has been categorized.', intent: Intent.SUCCESS, }); + closeMatchingTransactionAside(); }) .catch((err) => { setSubmitting(false); @@ -93,7 +92,7 @@ function CategorizeTransactionFormRoot({ ); } -export const CategorizeTransactionForm = compose(withDrawerActions)( +export const CategorizeTransactionForm = compose(withBankingActions)( CategorizeTransactionFormRoot, ); diff --git a/packages/webapp/src/containers/CashFlow/CategorizeTransactionAside/MatchingTransaction.tsx b/packages/webapp/src/containers/CashFlow/CategorizeTransactionAside/MatchingTransaction.tsx index 6f4d47e2a..21b84ba4b 100644 --- a/packages/webapp/src/containers/CashFlow/CategorizeTransactionAside/MatchingTransaction.tsx +++ b/packages/webapp/src/containers/CashFlow/CategorizeTransactionAside/MatchingTransaction.tsx @@ -38,7 +38,7 @@ export function MatchingBankTransaction() { }); return; } - matchTransaction({ id: uncategorizedTransactionId, values: _values }) + matchTransaction({ id: uncategorizedTransactionId, value: _values }) .then(() => { AppToaster.show({ intent: Intent.SUCCESS, diff --git a/packages/webapp/src/hooks/query/bank-rules.ts b/packages/webapp/src/hooks/query/bank-rules.ts index 05fdeca29..008b28b56 100644 --- a/packages/webapp/src/hooks/query/bank-rules.ts +++ b/packages/webapp/src/hooks/query/bank-rules.ts @@ -223,6 +223,9 @@ export function useExcludeUncategorizedTransaction( queryClient.invalidateQueries( QUERY_KEY.EXCLUDED_BANK_TRANSACTIONS_INFINITY, ); + queryClient.invalidateQueries( + t.CASHFLOW_ACCOUNT_UNCATEGORIZED_TRANSACTIONS_INFINITY, + ); }, ...options, }, @@ -267,6 +270,9 @@ export function useUnexcludeUncategorizedTransaction( queryClient.invalidateQueries( QUERY_KEY.EXCLUDED_BANK_TRANSACTIONS_INFINITY, ); + queryClient.invalidateQueries( + t.CASHFLOW_ACCOUNT_UNCATEGORIZED_TRANSACTIONS_INFINITY, + ); }, ...options, }, diff --git a/packages/webapp/src/static/json/icons.tsx b/packages/webapp/src/static/json/icons.tsx index a1f8e1d1d..c39e72f30 100644 --- a/packages/webapp/src/static/json/icons.tsx +++ b/packages/webapp/src/static/json/icons.tsx @@ -617,4 +617,16 @@ export default { ], viewBox: '0 0 16 16', }, + disable: { + path: [ + 'M7.99-0.01c-4.42,0-8,3.58-8,8s3.58,8,8,8s8-3.58,8-8S12.41-0.01,7.99-0.01zM1.99,7.99c0-3.31,2.69-6,6-6c1.3,0,2.49,0.42,3.47,1.12l-8.35,8.35C2.41,10.48,1.99,9.29,1.99,7.99z M7.99,13.99c-1.3,0-2.49-0.42-3.47-1.12l8.35-8.35c0.7,0.98,1.12,2.17,1.12,3.47C13.99,11.31,11.31,13.99,7.99,13.99z', + ], + viewBox: '0 0 16 16', + }, + redo: { + path: [ + 'M4,11c-1.1,0-2,0.9-2,2s0.9,2,2,2s2-0.9,2-2S5.1,11,4,11z M11,4H3.41l1.29-1.29C4.89,2.53,5,2.28,5,2c0-0.55-0.45-1-1-1C3.72,1,3.47,1.11,3.29,1.29l-3,3C0.11,4.47,0,4.72,0,5c0,0.28,0.11,0.53,0.29,0.71l3,3C3.47,8.89,3.72,9,4,9c0.55,0,1-0.45,1-1c0-0.28-0.11-0.53-0.29-0.71L3.41,6H11c1.66,0,3,1.34,3,3s-1.34,3-3,3H7v2h4c2.76,0,5-2.24,5-5S13.76,4,11,4z', + ], + viewBox: '0 0 16 16', + }, };