feat: reconcile matching transactions

This commit is contained in:
Ahmed Bouhuolia
2024-07-04 19:21:05 +02:00
parent 168883a933
commit 202179ec0b
14 changed files with 440 additions and 19 deletions

View File

@@ -28,11 +28,13 @@ function CategorizeTransactionAsideRoot({
}
return (
<Aside title={'Categorize Bank Transaction'} onClose={handleClose}>
<CategorizeTransactionTabsBoot
uncategorizedTransactionId={uncategorizedTransactionId}
>
<CategorizeTransactionTabs />
</CategorizeTransactionTabsBoot>
<Aside.Body>
<CategorizeTransactionTabsBoot
uncategorizedTransactionId={uncategorizedTransactionId}
>
<CategorizeTransactionTabs />
</CategorizeTransactionTabsBoot>
</Aside.Body>
</Aside>
);
}

View File

@@ -0,0 +1,43 @@
import { useAccounts, useBranches } from '@/hooks/query';
import { Spinner } from '@blueprintjs/core';
import React from 'react';
interface MatchingReconcileTransactionBootProps {
children: React.ReactNode;
}
interface MatchingReconcileTransactionBootValue {}
const MatchingReconcileTransactionBootContext =
React.createContext<MatchingReconcileTransactionBootValue>(
{} as MatchingReconcileTransactionBootValue,
);
export function MatchingReconcileTransactionBoot({
children,
}: MatchingReconcileTransactionBootProps) {
const { data: accounts, isLoading: isAccountsLoading } = useAccounts({}, {});
const { data: branches, isLoading: isBranchesLoading } = useBranches({}, {});
const provider = {
accounts,
branches,
isAccountsLoading,
isBranchesLoading,
};
const isLoading = isAccountsLoading || isBranchesLoading;
if (isLoading) {
return <Spinner size={20} />;
}
return (
<MatchingReconcileTransactionBootContext.Provider value={provider}>
{children}
</MatchingReconcileTransactionBootContext.Provider>
);
}
export const useMatchingReconcileTransactionBoot = () =>
React.useContext<MatchingReconcileTransactionBootValue>(
MatchingReconcileTransactionBootContext,
);

View File

@@ -0,0 +1,43 @@
.content{
padding: 18px;
display: flex;
flex-direction: column;
gap: 12px;
flex: 1;
}
.footer {
padding: 11px 20px;
border-top: 1px solid #ced4db;
}
.form{
display: flex;
flex-direction: column;
flex: 1 1 0;
:global .bp4-form-group{
margin-bottom: 0;
}
:global .bp4-input {
line-height: 30px;
height: 30px;
}
}
.asideContent{
background: #F6F7F9;
height: 335px;
}
.asideRoot {
flex: 1 1 0;
box-shadow: 0 0 0 1px rgba(17,20,24,.1),0 1px 1px rgba(17,20,24,.2),0 2px 6px rgba(17,20,24,.2);
}
.asideFooter {
background: #F6F7F9;
}

View File

@@ -0,0 +1,10 @@
import * as Yup from 'yup';
export const MatchingReconcileFormSchema = Yup.object().shape({
type: Yup.string().required().label('Type'),
date: Yup.string().required().label('Date'),
amount: Yup.string().required().label('Amount'),
memo: Yup.string().required().label('Memo'),
referenceNo: Yup.string().label('Refernece #'),
category: Yup.string().required().label('Categogry'),
});

View File

@@ -0,0 +1,199 @@
// @ts-nocheck
import * as R from 'ramda';
import { Button, Intent, Position, Tag } from '@blueprintjs/core';
import { Form, Formik, FormikValues, useFormikContext } from 'formik';
import {
AccountsSelect,
AppToaster,
Box,
BranchSelect,
FDateInput,
FFormGroup,
FInputGroup,
FMoneyInputGroup,
Group,
} from '@/components';
import { Aside } from '@/components/Aside/Aside';
import { momentFormatter } from '@/utils';
import styles from './MatchingReconcileTransactionForm.module.scss';
import { ContentTabs } from '@/components/ContentTabs';
import { withBankingActions } from '../../withBankingActions';
import {
MatchingReconcileTransactionBoot,
useMatchingReconcileTransactionBoot,
} from './MatchingReconcileTransactionBoot';
import { useCreateCashflowTransaction } from '@/hooks/query';
import { useAccountTransactionsContext } from '../../AccountTransactions/AccountTransactionsProvider';
import { MatchingReconcileFormSchema } from './MatchingReconcileTransactionForm.schema';
import { initialValues } from './_utils';
function MatchingReconcileTransactionFormRoot({
closeReconcileMatchingTransaction,
}) {
// Mutation create cashflow transaction.
const { mutateAsync: createCashflowTransactionMutate } =
useCreateCashflowTransaction();
const { accountId } = useAccountTransactionsContext();
const handleAsideClose = () => {
closeReconcileMatchingTransaction();
};
const handleSubmit = (
values: MatchingReconcileTransactionValues,
{ setSubmitting }: FormikValues<MatchingReconcileTransactionValues>,
) => {
setSubmitting(true);
const _values = transformToReq(values, accountId);
createCashflowTransactionMutate(_values)
.then(() => {
setSubmitting(false);
AppToaster.show({
message: 'The transaction has been created.',
intent: Intent.SUCCESS,
});
closeReconcileMatchingTransaction();
})
.catch((error) => {
setSubmitting(false);
AppToaster.show({
message: 'Something went wrong.',
intent: Intent.DANGER,
});
});
};
return (
<Aside
title={'Create Reconcile Transactions'}
className={styles.asideRoot}
onClose={handleAsideClose}
>
<MatchingReconcileTransactionBoot>
<Formik
onSubmit={handleSubmit}
initialValues={initialValues}
validationSchema={MatchingReconcileFormSchema}
>
<Form className={styles.form}>
<Aside.Body className={styles.asideContent}>
<CreateReconcileTransactionContent />
</Aside.Body>
<Aside.Footer className={styles.asideFooter}>
<MatchingReconcileTransactionFooter />
</Aside.Footer>
</Form>
</Formik>
</MatchingReconcileTransactionBoot>
</Aside>
);
}
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>();
const handleChange = (value: string) => {
setFieldValue('type', value);
};
return (
<ContentTabs
value={values?.type || 'deposit'}
onChange={handleChange}
small
>
<ContentTabs.Tab id={'deposit'} title={'Deposit'} />
<ContentTabs.Tab id={'withdrawal'} title={'Withdrawal'} />
</ContentTabs>
);
}
export function CreateReconcileTransactionContent() {
const { accounts, branches } = useMatchingReconcileTransactionBoot();
return (
<Box className={styles.content}>
<ReconcileMatchingType />
<FFormGroup label={'Date'} name={'date'}>
<FDateInput
{...momentFormatter('YYYY/MM/DD')}
name={'date'}
formatDate={(date) => date.toLocaleString()}
popoverProps={{
position: Position.LEFT,
}}
inputProps={{ fill: true }}
fill
/>
</FFormGroup>
<FFormGroup
label={'Amount'}
name={'amount'}
labelInfo={<Tag minimal>Required</Tag>}
>
<FMoneyInputGroup name={'amount'} />
</FFormGroup>
<FFormGroup
label={'Category'}
name={'category'}
labelInfo={<Tag minimal>Required</Tag>}
>
<AccountsSelect
name={'category'}
items={accounts}
popoverProps={{ minimal: false, position: Position.LEFT }}
/>
</FFormGroup>
<FFormGroup
label={'Memo'}
name={'memo'}
labelInfo={<Tag minimal>Required</Tag>}
>
<FInputGroup name={'memo'} />
</FFormGroup>
<FFormGroup label={'Reference No.'} name={'reference_no'}>
<FInputGroup name={'reference_no'} />
</FFormGroup>
<FFormGroup name={'branchId'} label={'Branch'}>
<BranchSelect
name={'branchId'}
branches={branches}
popoverProps={{ minimal: true }}
/>
</FFormGroup>
</Box>
);
}

View File

@@ -0,0 +1,9 @@
export interface MatchingReconcileTransactionValues {
type: string;
date: string;
amount: string;
memo: string;
referenceNo: string;
category: string;
branchId: string;
}

View File

@@ -0,0 +1,30 @@
import { MatchingReconcileTransactionValues } from './_types';
export const transformToReq = (
values: MatchingReconcileTransactionValues,
bankAccountId: number,
) => {
return {
date: values.date,
reference_no: values.referenceNo,
transaction_type:
values.type === 'deposit' ? 'other_income' : 'other_expense',
description: values.memo,
amount: values.amount,
credit_account_id: values.category,
cashflow_account_id: bankAccountId,
branch_id: values.branchId,
publish: true,
};
};
export const initialValues = {
type: 'deposit',
date: '',
amount: '',
memo: '',
referenceNo: '',
category: '',
branchId: '',
};

View File

@@ -25,6 +25,8 @@ import {
withBankingActions,
} from '../withBankingActions';
import styles from './CategorizeTransactionAside.module.scss';
import { MatchingReconcileTransactionForm } from './MatchingReconcileTransactionAside/MatchingReconcileTransactionForm';
import { withBanking } from '../withBanking';
const initialValues = {
matched: {},
@@ -37,6 +39,9 @@ const initialValues = {
function MatchingBankTransactionRoot({
// #withBankingActions
closeMatchingTransactionAside,
// #withBanking
openReconcileMatchingTransaction,
}) {
const { uncategorizedTransactionId } = useCategorizeTransactionTabsBoot();
const { mutateAsync: matchTransaction } = useMatchUncategorizedTransaction();
@@ -81,16 +86,23 @@ function MatchingBankTransactionRoot({
<Formik initialValues={initialValues} onSubmit={handleSubmit}>
<>
<MatchingBankTransactionContent />
<MatchTransactionFooter />
{openReconcileMatchingTransaction && (
<MatchingReconcileTransactionForm />
)}
{!openReconcileMatchingTransaction && <MatchTransactionFooter />}
</>
</Formik>
</MatchingTransactionBoot>
);
}
export const MatchingBankTransaction = R.compose(withBankingActions)(
MatchingBankTransactionRoot,
);
export const MatchingBankTransaction = R.compose(
withBankingActions,
withBanking(({ openReconcileMatchingTransaction }) => ({
openReconcileMatchingTransaction,
})),
)(MatchingBankTransactionRoot);
function MatchingBankTransactionContent() {
return (
@@ -212,7 +224,10 @@ interface MatchTransctionFooterProps extends WithBankingActionsProps {}
* @returns {React.ReactNode}
*/
const MatchTransactionFooter = R.compose(withBankingActions)(
({ closeMatchingTransactionAside }: MatchTransctionFooterProps) => {
({
closeMatchingTransactionAside,
openReconcileMatchingTransaction,
}: MatchTransctionFooterProps) => {
const { submitForm, isSubmitting } = useFormikContext();
const totalPending = useGetPendingAmountMatched();
const showReconcileLink = useIsShowReconcileTransactionLink();
@@ -224,13 +239,21 @@ const MatchTransactionFooter = R.compose(withBankingActions)(
const handleSubmitBtnClick = () => {
submitForm();
};
const handleReconcileTransaction = () => {
openReconcileMatchingTransaction();
};
return (
<Box className={styles.footer}>
<Box className={styles.footerTotal}>
<Group position={'apart'}>
{showReconcileLink && (
<AnchorButton small minimal intent={Intent.PRIMARY}>
<AnchorButton
small
minimal
intent={Intent.PRIMARY}
onClick={handleReconcileTransaction}
>
Add Reconcile Transaction +
</AnchorButton>
)}