mirror of
https://github.com/bigcapitalhq/bigcapital.git
synced 2026-02-15 20:30:33 +00:00
feat: cashflow transaction matching
This commit is contained in:
@@ -25,11 +25,18 @@ import {
|
||||
import { useCreateCashflowTransaction } from '@/hooks/query';
|
||||
import { useAccountTransactionsContext } from '../../AccountTransactions/AccountTransactionsProvider';
|
||||
import { MatchingReconcileFormSchema } from './MatchingReconcileTransactionForm.schema';
|
||||
import { initialValues } from './_utils';
|
||||
import { initialValues, transformToReq } from './_utils';
|
||||
|
||||
interface MatchingReconcileTransactionFormProps {
|
||||
onSubmitSuccess?: (values: any) => void;
|
||||
}
|
||||
|
||||
function MatchingReconcileTransactionFormRoot({
|
||||
closeReconcileMatchingTransaction,
|
||||
}) {
|
||||
|
||||
// #props¿
|
||||
onSubmitSuccess,
|
||||
}: MatchingReconcileTransactionFormProps) {
|
||||
// Mutation create cashflow transaction.
|
||||
const { mutateAsync: createCashflowTransactionMutate } =
|
||||
useCreateCashflowTransaction();
|
||||
@@ -47,7 +54,7 @@ function MatchingReconcileTransactionFormRoot({
|
||||
const _values = transformToReq(values, accountId);
|
||||
|
||||
createCashflowTransactionMutate(_values)
|
||||
.then(() => {
|
||||
.then((res) => {
|
||||
setSubmitting(false);
|
||||
|
||||
AppToaster.show({
|
||||
@@ -55,6 +62,8 @@ function MatchingReconcileTransactionFormRoot({
|
||||
intent: Intent.SUCCESS,
|
||||
});
|
||||
closeReconcileMatchingTransaction();
|
||||
onSubmitSuccess &&
|
||||
onSubmitSuccess({ id: res.data.id, type: 'CashflowTransaction' });
|
||||
})
|
||||
.catch((error) => {
|
||||
setSubmitting(false);
|
||||
@@ -97,25 +106,6 @@ export const MatchingReconcileTransactionForm = R.compose(withBankingActions)(
|
||||
MatchingReconcileTransactionFormRoot,
|
||||
);
|
||||
|
||||
export function MatchingReconcileTransactionFooter() {
|
||||
const { isSubmitting } = useFormikContext();
|
||||
|
||||
return (
|
||||
<Box className={styles.footer}>
|
||||
<Group>
|
||||
<Button
|
||||
fill
|
||||
type={'submit'}
|
||||
intent={Intent.PRIMARY}
|
||||
loading={isSubmitting}
|
||||
>
|
||||
Submit
|
||||
</Button>
|
||||
</Group>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
function ReconcileMatchingType() {
|
||||
const { setFieldValue, values } =
|
||||
useFormikContext<MatchingReconcileFormValues>();
|
||||
@@ -135,7 +125,7 @@ function ReconcileMatchingType() {
|
||||
);
|
||||
}
|
||||
|
||||
export function CreateReconcileTransactionContent() {
|
||||
function CreateReconcileTransactionContent() {
|
||||
const { accounts, branches } = useMatchingReconcileTransactionBoot();
|
||||
|
||||
return (
|
||||
@@ -197,3 +187,22 @@ export function CreateReconcileTransactionContent() {
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
function MatchingReconcileTransactionFooter() {
|
||||
const { isSubmitting } = useFormikContext();
|
||||
|
||||
return (
|
||||
<Box className={styles.footer}>
|
||||
<Group>
|
||||
<Button
|
||||
fill
|
||||
type={'submit'}
|
||||
intent={Intent.PRIMARY}
|
||||
loading={isSubmitting}
|
||||
>
|
||||
Submit
|
||||
</Button>
|
||||
</Group>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
// @ts-nocheck
|
||||
import { isEmpty } from 'lodash';
|
||||
import * as R from 'ramda';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { AnchorButton, Button, Intent, Tag, Text } from '@blueprintjs/core';
|
||||
import { FastField, FastFieldProps, Formik, useFormikContext } from 'formik';
|
||||
import { AppToaster, Box, FormatNumber, Group, Stack } from '@/components';
|
||||
@@ -39,9 +40,6 @@ const initialValues = {
|
||||
function MatchingBankTransactionRoot({
|
||||
// #withBankingActions
|
||||
closeMatchingTransactionAside,
|
||||
|
||||
// #withBanking
|
||||
openReconcileMatchingTransaction,
|
||||
}) {
|
||||
const { uncategorizedTransactionId } = useCategorizeTransactionTabsBoot();
|
||||
const { mutateAsync: matchTransaction } = useMatchUncategorizedTransaction();
|
||||
@@ -84,25 +82,89 @@ function MatchingBankTransactionRoot({
|
||||
uncategorizedTransactionId={uncategorizedTransactionId}
|
||||
>
|
||||
<Formik initialValues={initialValues} onSubmit={handleSubmit}>
|
||||
<>
|
||||
<MatchingBankTransactionContent />
|
||||
|
||||
{openReconcileMatchingTransaction && (
|
||||
<MatchingReconcileTransactionForm />
|
||||
)}
|
||||
{!openReconcileMatchingTransaction && <MatchTransactionFooter />}
|
||||
</>
|
||||
<MatchingBankTransactionFormContent />
|
||||
</Formik>
|
||||
</MatchingTransactionBoot>
|
||||
);
|
||||
}
|
||||
|
||||
export const MatchingBankTransaction = R.compose(
|
||||
export const MatchingBankTransaction = R.compose(withBankingActions)(
|
||||
MatchingBankTransactionRoot,
|
||||
);
|
||||
|
||||
/**
|
||||
* Matching bank transaction form content.
|
||||
* @returns {React.ReactNode}
|
||||
*/
|
||||
const MatchingBankTransactionFormContent = R.compose(
|
||||
withBankingActions,
|
||||
withBanking(({ openReconcileMatchingTransaction }) => ({
|
||||
openReconcileMatchingTransaction,
|
||||
})),
|
||||
)(MatchingBankTransactionRoot);
|
||||
)(
|
||||
({
|
||||
// #withBankingActions
|
||||
closeMatchingTransactionAside,
|
||||
|
||||
// #withBanking
|
||||
openReconcileMatchingTransaction,
|
||||
}) => {
|
||||
const {
|
||||
isMatchingTransactionsFetching,
|
||||
isMatchingTransactionsSuccess,
|
||||
matches,
|
||||
} = useMatchingTransactionBoot();
|
||||
const [pending, setPending] = useState<null | {
|
||||
refId: number;
|
||||
refType: string;
|
||||
}>(null);
|
||||
|
||||
const { setFieldValue } = useFormikContext();
|
||||
|
||||
// This effect is responsible for automatically marking a transaction as matched
|
||||
// when the matching process is successful and not currently fetching.
|
||||
useEffect(() => {
|
||||
if (
|
||||
pending &&
|
||||
isMatchingTransactionsSuccess &&
|
||||
!isMatchingTransactionsFetching
|
||||
) {
|
||||
const foundMatch = matches?.find(
|
||||
(m) =>
|
||||
m.referenceType === pending?.refType &&
|
||||
m.referenceId === pending?.refId,
|
||||
);
|
||||
if (foundMatch) {
|
||||
setFieldValue(`matched.${pending.refType}-${pending.refId}`, true);
|
||||
}
|
||||
setPending(null);
|
||||
}
|
||||
}, [
|
||||
isMatchingTransactionsFetching,
|
||||
isMatchingTransactionsSuccess,
|
||||
matches,
|
||||
pending,
|
||||
setFieldValue,
|
||||
]);
|
||||
|
||||
const handleReconcileFormSubmitSuccess = (payload) => {
|
||||
setPending({ refId: payload.id, refType: payload.type });
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<MatchingBankTransactionContent />
|
||||
|
||||
{openReconcileMatchingTransaction && (
|
||||
<MatchingReconcileTransactionForm
|
||||
onSubmitSuccess={handleReconcileFormSubmitSuccess}
|
||||
/>
|
||||
)}
|
||||
{!openReconcileMatchingTransaction && <MatchTransactionFooter />}
|
||||
</>
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
function MatchingBankTransactionContent() {
|
||||
return (
|
||||
@@ -178,8 +240,8 @@ function PossibleMatchingTransactions() {
|
||||
key={index}
|
||||
label={`${match.transsactionTypeFormatted} for ${match.amountFormatted}`}
|
||||
date={match.dateFormatted}
|
||||
transactionId={match.transactionId}
|
||||
transactionType={match.transactionType}
|
||||
transactionId={match.referenceId}
|
||||
transactionType={match.referenceType}
|
||||
/>
|
||||
))}
|
||||
</Stack>
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
import { defaultTo } from 'lodash';
|
||||
import React, { createContext } from 'react';
|
||||
import { defaultTo } from 'lodash';
|
||||
import * as R from 'ramda';
|
||||
import { useGetBankTransactionsMatches } from '@/hooks/query/bank-rules';
|
||||
|
||||
interface MatchingTransactionBootValues {
|
||||
isMatchingTransactionsLoading: boolean;
|
||||
isMatchingTransactionsFetching: boolean;
|
||||
isMatchingTransactionsSuccess: boolean;
|
||||
possibleMatches: Array<any>;
|
||||
perfectMatchesCount: number;
|
||||
perfectMatches: Array<any>;
|
||||
@@ -26,13 +29,24 @@ function MatchingTransactionBoot({
|
||||
const {
|
||||
data: matchingTransactions,
|
||||
isLoading: isMatchingTransactionsLoading,
|
||||
isFetching: isMatchingTransactionsFetching,
|
||||
isSuccess: isMatchingTransactionsSuccess,
|
||||
} = useGetBankTransactionsMatches(uncategorizedTransactionId);
|
||||
|
||||
const possibleMatches = defaultTo(matchingTransactions?.possibleMatches, []);
|
||||
const perfectMatchesCount = matchingTransactions?.perfectMatches?.length || 0;
|
||||
const perfectMatches = defaultTo(matchingTransactions?.perfectMatches, []);
|
||||
|
||||
const matches = R.concat(perfectMatches, possibleMatches);
|
||||
|
||||
const provider = {
|
||||
isMatchingTransactionsLoading,
|
||||
possibleMatches: defaultTo(matchingTransactions?.possibleMatches, []),
|
||||
perfectMatchesCount: matchingTransactions?.perfectMatches?.length || 0,
|
||||
perfectMatches: defaultTo(matchingTransactions?.perfectMatches, []),
|
||||
isMatchingTransactionsFetching,
|
||||
isMatchingTransactionsSuccess,
|
||||
possibleMatches,
|
||||
perfectMatchesCount,
|
||||
perfectMatches,
|
||||
matches,
|
||||
} as MatchingTransactionBootValues;
|
||||
|
||||
return <RuleFormBootContext.Provider value={provider} {...props} />;
|
||||
|
||||
@@ -24,7 +24,7 @@ export const useGetPendingAmountMatched = () => {
|
||||
return useMemo(() => {
|
||||
const matchedItems = [...perfectMatches, ...possibleMatches].filter(
|
||||
(match) => {
|
||||
const key = `${match.transactionType}-${match.transactionId}`;
|
||||
const key = `${match.referenceType}-${match.referenceId}`;
|
||||
return values.matched[key];
|
||||
},
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user