mirror of
https://github.com/bigcapitalhq/bigcapital.git
synced 2026-02-21 07:10:33 +00:00
feat: wip categorized transactions
This commit is contained in:
@@ -7,6 +7,7 @@ exports.up = function (knex) {
|
|||||||
table.decimal('amount');
|
table.decimal('amount');
|
||||||
table.string('currency_code');
|
table.string('currency_code');
|
||||||
table.string('reference_no').index();
|
table.string('reference_no').index();
|
||||||
|
table.string('payee');
|
||||||
table
|
table
|
||||||
.integer('account_id')
|
.integer('account_id')
|
||||||
.unsigned()
|
.unsigned()
|
||||||
|
|||||||
@@ -0,0 +1,7 @@
|
|||||||
|
exports.up = function (knex) {
|
||||||
|
return knex.schema.table('accounts', (table) => {
|
||||||
|
table.integer('uncategorized_transactions').defaultTo(0);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
exports.down = function (knex) {};
|
||||||
@@ -264,6 +264,7 @@ export interface CreateUncategorizedTransactionDTO {
|
|||||||
accountId: number;
|
accountId: number;
|
||||||
amount: number;
|
amount: number;
|
||||||
currencyCode: string;
|
currencyCode: string;
|
||||||
|
payee?: string;
|
||||||
description?: string;
|
description?: string;
|
||||||
referenceNo?: string | null;
|
referenceNo?: string | null;
|
||||||
plaidTransactionId?: string | null;
|
plaidTransactionId?: string | null;
|
||||||
|
|||||||
@@ -38,7 +38,7 @@ export interface PlaidTransaction {
|
|||||||
iso_currency_code: string;
|
iso_currency_code: string;
|
||||||
transaction_id: string;
|
transaction_id: string;
|
||||||
transaction_type: string;
|
transaction_type: string;
|
||||||
payment_meta: { reference_number: string | null };
|
payment_meta: { reference_number: string | null; payee: string | null };
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface PlaidFetchedTransactionsUpdates {
|
export interface PlaidFetchedTransactionsUpdates {
|
||||||
|
|||||||
@@ -196,6 +196,7 @@ export default class Account extends mixin(TenantModel, [
|
|||||||
const Expense = require('models/Expense');
|
const Expense = require('models/Expense');
|
||||||
const ExpenseEntry = require('models/ExpenseCategory');
|
const ExpenseEntry = require('models/ExpenseCategory');
|
||||||
const ItemEntry = require('models/ItemEntry');
|
const ItemEntry = require('models/ItemEntry');
|
||||||
|
const UncategorizedTransaction = require('models/UncategorizedCashflowTransaction');
|
||||||
|
|
||||||
return {
|
return {
|
||||||
/**
|
/**
|
||||||
@@ -305,6 +306,21 @@ export default class Account extends mixin(TenantModel, [
|
|||||||
to: 'items_entries.sellAccountId',
|
to: 'items_entries.sellAccountId',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Associated uncategorized transactions.
|
||||||
|
*/
|
||||||
|
uncategorizedTransactions: {
|
||||||
|
relation: Model.HasManyRelation,
|
||||||
|
modelClass: UncategorizedTransaction.default,
|
||||||
|
join: {
|
||||||
|
from: 'accounts.id',
|
||||||
|
to: 'uncategorized_cashflow_transactions.accountId',
|
||||||
|
},
|
||||||
|
filter: (query) => {
|
||||||
|
query.filter('categorized', false);
|
||||||
|
},
|
||||||
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
/* eslint-disable global-require */
|
/* eslint-disable global-require */
|
||||||
import TenantModel from 'models/TenantModel';
|
import TenantModel from 'models/TenantModel';
|
||||||
import { Model } from 'objection';
|
import { Model } from 'objection';
|
||||||
|
import Account from './Account';
|
||||||
|
|
||||||
export default class UncategorizedCashflowTransaction extends TenantModel {
|
export default class UncategorizedCashflowTransaction extends TenantModel {
|
||||||
amount: number;
|
amount: number;
|
||||||
@@ -80,4 +81,29 @@ export default class UncategorizedCashflowTransaction extends TenantModel {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @param queryContext
|
||||||
|
*/
|
||||||
|
public async $afterInsert(queryContext) {
|
||||||
|
await super.$afterInsert(queryContext);
|
||||||
|
|
||||||
|
// Increments the uncategorized transactions count of the associated account.
|
||||||
|
await Account.query(queryContext.transaction)
|
||||||
|
.findById(this.accountId)
|
||||||
|
.increment('uncategorized_transactions', 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @param queryContext
|
||||||
|
*/
|
||||||
|
public async $afterDelete(queryContext) {
|
||||||
|
await super.$afterDelete(queryContext);
|
||||||
|
|
||||||
|
await Account.query()
|
||||||
|
.findById(this.accountId)
|
||||||
|
.decrement('uncategorized_transactions', 1);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -90,7 +90,7 @@ export class PlaidSyncDb {
|
|||||||
tenantId,
|
tenantId,
|
||||||
uncategoriedDTO
|
uncategoriedDTO
|
||||||
),
|
),
|
||||||
{ concurrency: CONCURRENCY_ASYNC }
|
{ concurrency: 1 }
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -44,8 +44,9 @@ export const transformPlaidTrxsToCashflowCreate = R.curry(
|
|||||||
): CreateUncategorizedTransactionDTO => {
|
): CreateUncategorizedTransactionDTO => {
|
||||||
return {
|
return {
|
||||||
date: plaidTranasction.date,
|
date: plaidTranasction.date,
|
||||||
description: plaidTranasction.name,
|
|
||||||
amount: plaidTranasction.amount,
|
amount: plaidTranasction.amount,
|
||||||
|
description: plaidTranasction.name,
|
||||||
|
payee: plaidTranasction.payment_meta?.payee,
|
||||||
currencyCode: plaidTranasction.iso_currency_code,
|
currencyCode: plaidTranasction.iso_currency_code,
|
||||||
accountId: cashflowAccountId,
|
accountId: cashflowAccountId,
|
||||||
referenceNo: plaidTranasction.payment_meta?.reference_number,
|
referenceNo: plaidTranasction.payment_meta?.reference_number,
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { Inject, Service } from 'typedi';
|
import { Inject, Service } from 'typedi';
|
||||||
import HasTenancyService from '../Tenancy/TenancyService';
|
import HasTenancyService from '../Tenancy/TenancyService';
|
||||||
import UnitOfWork from '../UnitOfWork';
|
import UnitOfWork, { IsolationLevel } from '../UnitOfWork';
|
||||||
import { Knex } from 'knex';
|
import { Knex } from 'knex';
|
||||||
import { CreateUncategorizedTransactionDTO } from '@/interfaces';
|
import { CreateUncategorizedTransactionDTO } from '@/interfaces';
|
||||||
|
|
||||||
@@ -21,15 +21,20 @@ export class CreateUncategorizedTransaction {
|
|||||||
tenantId: number,
|
tenantId: number,
|
||||||
createDTO: CreateUncategorizedTransactionDTO
|
createDTO: CreateUncategorizedTransactionDTO
|
||||||
) {
|
) {
|
||||||
const { UncategorizedCashflowTransaction } = this.tenancy.models(tenantId);
|
const { UncategorizedCashflowTransaction, Account } =
|
||||||
|
this.tenancy.models(tenantId);
|
||||||
|
|
||||||
return this.uow.withTransaction(tenantId, async (trx: Knex.Transaction) => {
|
return this.uow.withTransaction(
|
||||||
const transaction = await UncategorizedCashflowTransaction.query(
|
tenantId,
|
||||||
trx
|
async (trx: Knex.Transaction) => {
|
||||||
).insertAndFetch({
|
const transaction = await UncategorizedCashflowTransaction.query(
|
||||||
...createDTO,
|
trx
|
||||||
});
|
).insertAndFetch({
|
||||||
return transaction;
|
...createDTO,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
return transaction;
|
||||||
|
},
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -29,9 +29,12 @@ export class UncategorizedTransactionTransformer extends Transformer {
|
|||||||
* @returns {string}
|
* @returns {string}
|
||||||
*/
|
*/
|
||||||
protected formattetDepositAmount(transaction) {
|
protected formattetDepositAmount(transaction) {
|
||||||
return formatNumber(transaction.deposit, {
|
if (transaction.isDepositTransaction) {
|
||||||
currencyCode: transaction.currencyCode,
|
return formatNumber(transaction.deposit, {
|
||||||
});
|
currencyCode: transaction.currencyCode,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return '';
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -40,8 +43,11 @@ export class UncategorizedTransactionTransformer extends Transformer {
|
|||||||
* @returns {string}
|
* @returns {string}
|
||||||
*/
|
*/
|
||||||
protected formattedWithdrawalAmount(transaction) {
|
protected formattedWithdrawalAmount(transaction) {
|
||||||
return formatNumber(transaction.withdrawal, {
|
if (transaction.isWithdrawalTransaction) {
|
||||||
currencyCode: transaction.currencyCode,
|
return formatNumber(transaction.withdrawal, {
|
||||||
});
|
currencyCode: transaction.currencyCode,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return '';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -84,6 +84,18 @@ function AccountBankBalanceItem() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function AccountNumberItem() {
|
||||||
|
const { currentAccount } = useAccountTransactionsContext();
|
||||||
|
|
||||||
|
if (!currentAccount.account_mask) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<AccountBalanceItemWrap>
|
||||||
|
Account Number: xxx{currentAccount.account_mask}
|
||||||
|
</AccountBalanceItemWrap>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
function AccountTransactionsDetailsBarSkeleton() {
|
function AccountTransactionsDetailsBarSkeleton() {
|
||||||
return (
|
return (
|
||||||
<React.Fragment>
|
<React.Fragment>
|
||||||
@@ -101,6 +113,7 @@ function AccountTransactionsDetailsContent() {
|
|||||||
return (
|
return (
|
||||||
<React.Fragment>
|
<React.Fragment>
|
||||||
<AccountSwitchItem />
|
<AccountSwitchItem />
|
||||||
|
<AccountNumberItem />
|
||||||
<AccountBalanceItem />
|
<AccountBalanceItem />
|
||||||
<AccountBankBalanceItem />
|
<AccountBankBalanceItem />
|
||||||
</React.Fragment>
|
</React.Fragment>
|
||||||
|
|||||||
@@ -8,12 +8,17 @@ const AccountContentTabs = styled(ContentTabs)`
|
|||||||
`;
|
`;
|
||||||
|
|
||||||
export function AccountTransactionsFilterTabs() {
|
export function AccountTransactionsFilterTabs() {
|
||||||
const { filterTab, setFilterTab } = useAccountTransactionsContext();
|
const { filterTab, setFilterTab, currentAccount } =
|
||||||
|
useAccountTransactionsContext();
|
||||||
|
|
||||||
const handleChange = (value) => {
|
const handleChange = (value) => {
|
||||||
setFilterTab(value);
|
setFilterTab(value);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const hasUncategorizedTransx = Boolean(
|
||||||
|
currentAccount.uncategorized_transactions,
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<AccountContentTabs value={filterTab} onChange={handleChange}>
|
<AccountContentTabs value={filterTab} onChange={handleChange}>
|
||||||
<ContentTabs.Tab
|
<ContentTabs.Tab
|
||||||
@@ -21,16 +26,20 @@ export function AccountTransactionsFilterTabs() {
|
|||||||
title={'Dashboard'}
|
title={'Dashboard'}
|
||||||
description={'Account Summary'}
|
description={'Account Summary'}
|
||||||
/>
|
/>
|
||||||
<ContentTabs.Tab
|
{hasUncategorizedTransx && (
|
||||||
id={'uncategorized'}
|
<ContentTabs.Tab
|
||||||
title={
|
id={'uncategorized'}
|
||||||
<>
|
title={
|
||||||
<span style={{ color: '#ff0000' }}>20</span> Uncategorized
|
<>
|
||||||
Transactions
|
<span style={{ color: '#ff0000' }}>
|
||||||
</>
|
{currentAccount.uncategorized_transactions}
|
||||||
}
|
</span>{' '}
|
||||||
description={'For Bank Statement'}
|
Uncategorized Transactions
|
||||||
/>
|
</>
|
||||||
|
}
|
||||||
|
description={'For Bank Statement'}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
<ContentTabs.Tab
|
<ContentTabs.Tab
|
||||||
id="all"
|
id="all"
|
||||||
title={'All Transactions'}
|
title={'All Transactions'}
|
||||||
|
|||||||
Reference in New Issue
Block a user