feat: categorize cashflow transaction drawer

This commit is contained in:
Ahmed Bouhuolia
2024-03-05 22:27:42 +02:00
parent db839137d0
commit a17b4f6a8a
16 changed files with 402 additions and 2 deletions

View File

@@ -22,6 +22,11 @@ export default class NewCashflowTransactionController extends BaseController {
public router() {
const router = Router();
router.get(
'/transactions/uncategorized/:id',
this.asyncMiddleware(this.getUncategorizedCashflowTransaction),
this.catchServiceErrors
);
router.get(
'/transactions/:id/uncategorized',
this.asyncMiddleware(this.getUncategorizedCashflowTransactions),
@@ -225,6 +230,31 @@ export default class NewCashflowTransactionController extends BaseController {
}
};
/**
* Retrieves the uncategorized cashflow transactions.
* @param {Request} req
* @param {Response} res
* @param {NextFunction} next
*/
public getUncategorizedCashflowTransaction = async (
req: Request,
res: Response,
next: NextFunction
) => {
const { tenantId } = req;
const { id: transactionId } = req.params;
try {
const data = await this.cashflowApplication.getUncategorizedTransaction(
tenantId,
transactionId
);
return res.status(200).send({ data });
} catch (error) {
next(error);
}
};
/**
* Retrieves the uncategorized cashflow transactions.
* @param {Request} req

View File

@@ -11,6 +11,7 @@ import {
import { CategorizeTransactionAsExpense } from './CategorizeTransactionAsExpense';
import { GetUncategorizedTransactions } from './GetUncategorizedTransactions';
import { CreateUncategorizedTransaction } from './CreateUncategorizedTransaction';
import { GetUncategorizedTransaction } from './GetUncategorizedTransaction';
@Service()
export class CashflowApplication {
@@ -29,6 +30,9 @@ export class CashflowApplication {
@Inject()
private getUncategorizedTransactionsService: GetUncategorizedTransactions;
@Inject()
private getUncategorizedTransactionService: GetUncategorizedTransaction;
@Inject()
private createUncategorizedTransactionService: CreateUncategorizedTransaction;
@@ -126,4 +130,20 @@ export class CashflowApplication {
accountId
);
}
/**
*
* @param {number} tenantId
* @param {number} uncategorizedTransactionId
* @returns
*/
public getUncategorizedTransaction(
tenantId: number,
uncategorizedTransactionId: number
) {
return this.getUncategorizedTransactionService.getTransaction(
tenantId,
uncategorizedTransactionId
);
}
}

View File

@@ -0,0 +1,36 @@
import { Inject, Service } from 'typedi';
import HasTenancyService from '../Tenancy/TenancyService';
import { TransformerInjectable } from '@/lib/Transformer/TransformerInjectable';
import { UncategorizedTransactionTransformer } from './UncategorizedTransactionTransformer';
@Service()
export class GetUncategorizedTransaction {
@Inject()
private tenancy: HasTenancyService;
@Inject()
private transformer: TransformerInjectable;
/**
* Retrieves specific uncategorized cashflow transaction.
* @param {number} tenantId - Tenant id.
* @param {number} uncategorizedTransactionId - Uncategorized transaction id.
*/
public async getTransaction(
tenantId: number,
uncategorizedTransactionId: number
) {
const { UncategorizedCashflowTransaction } = this.tenancy.models(tenantId);
const transaction = await UncategorizedCashflowTransaction.query()
.findById(uncategorizedTransactionId)
.withGraphFetched('account')
.throwIfNotFound();
return this.transformer.transform(
tenantId,
transaction,
new UncategorizedTransactionTransformer()
);
}
}

View File

@@ -23,6 +23,7 @@ import WarehouseTransferDetailDrawer from '@/containers/Drawers/WarehouseTransfe
import TaxRateDetailsDrawer from '@/containers/TaxRates/drawers/TaxRateDetailsDrawer/TaxRateDetailsDrawer';
import { DRAWERS } from '@/constants/drawers';
import CategorizeTransactionDrawer from '@/containers/CashFlow/CategorizeTransaction/drawers/CategorizeTransactionDrawer/CategorizeTransactionDrawer';
/**
* Drawers container of the dashboard.
@@ -61,6 +62,7 @@ export default function DrawersContainer() {
name={DRAWERS.WAREHOUSE_TRANSFER_DETAILS}
/>
<TaxRateDetailsDrawer name={DRAWERS.TAX_RATE_DETAILS} />
<CategorizeTransactionDrawer name={DRAWERS.CATEGORIZE_TRANSACTION} />
</div>
);
}

View File

@@ -23,4 +23,5 @@ export enum DRAWERS {
REFUND_VENDOR_CREDIT_DETAILS = 'refund-vendor-detail-drawer',
WAREHOUSE_TRANSFER_DETAILS = 'warehouse-transfer-detail-drawer',
TAX_RATE_DETAILS = 'tax-rate-detail-drawer',
CATEGORIZE_TRANSACTION = 'categorize-transaction',
}

View File

@@ -11,6 +11,15 @@ const Root = styled.div`
const FilterTag = styled(Tag)`
min-height: 26px;
&.bp4-minimal:not([class*='bp4-intent-']) {
background: #fff;
border: 1px solid #e1e2e8;
&.bp4-interactive:hover {
background-color: rgba(143, 153, 168, 0.05);
}
}
`;
export function AccountTransactionsUncategorizeFilter() {

View File

@@ -25,6 +25,7 @@ import { useAccountTransactionsContext } from './AccountTransactionsProvider';
import { handleCashFlowTransactionType } from './utils';
import { compose } from '@/utils';
import { DRAWERS } from '@/constants/drawers';
/**
* Account transactions data table.
@@ -56,7 +57,11 @@ function AccountTransactionsDataTable({
const handleViewDetailCashflowTransaction = (referenceType) => {};
// Handle cell click.
const handleCellClick = (cell, event) => {};
const handleCellClick = (cell, event) => {
openDrawer(DRAWERS.CATEGORIZE_TRANSACTION, {
uncategorizedTransactionId: cell.row.original.id,
});
};
return (
<CashflowTransactionsTable
@@ -132,7 +137,7 @@ const CashflowTransactionsTable = styled(DashboardConstrantTable)`
}
.td-description {
color: #5F6B7C;
color: #5f6b7c;
}
}
}

View File

@@ -0,0 +1,38 @@
// @ts-nocheck
import React from 'react';
import { DrawerHeaderContent, DrawerLoading } from '@/components';
import { DRAWERS } from '@/constants/drawers';
import { useUncategorizedTransaction } from '@/hooks/query';
const CategorizeTransactionBootContext = React.createContext();
/**
* Estimate detail provider.
*/
function CategorizeTransactionBoot({ uncategorizedTransactionId, ...props }) {
const {
data: uncategorizedTransaction,
isLoading: isUncategorizedTransactionLoading,
} = useUncategorizedTransaction(uncategorizedTransactionId);
const provider = {
uncategorizedTransaction,
isUncategorizedTransactionLoading,
};
return (
<DrawerLoading loading={isUncategorizedTransactionLoading}>
<DrawerHeaderContent
name={DRAWERS.CATEGORIZE_TRANSACTION}
title={'Categorize Transaction'}
subTitle={''}
/>
<CategorizeTransactionBootContext.Provider value={provider} {...props} />
</DrawerLoading>
);
}
const useCategorizeTransactionBoot = () =>
React.useContext(CategorizeTransactionBootContext);
export { CategorizeTransactionBoot, useCategorizeTransactionBoot };

View File

@@ -0,0 +1,18 @@
// @ts-nocheck
import { DrawerBody } from '@/components';
import { CategorizeTransactionBoot } from './CategorizeTransactionBoot';
import { CategorizeTransactionForm } from './CategorizeTransactionForm';
export default function CategorizeTransactionContent({
uncategorizedTransactionId,
}) {
return (
<CategorizeTransactionBoot
uncategorizedTransactionId={uncategorizedTransactionId}
>
<DrawerBody>
<CategorizeTransactionForm />
</DrawerBody>
</CategorizeTransactionBoot>
);
}

View File

@@ -0,0 +1,37 @@
// @ts-nocheck
import React, { lazy } from 'react';
import { Drawer, DrawerSuspense } from '@/components';
import withDrawers from '@/containers/Drawer/withDrawers';
import { compose } from '@/utils';
const CategorizeTransactionContent = lazy(
() => import('./CategorizeTransactionContent'),
);
/**
* Categorize the uncategorized transaction drawer.
*/
function CategorizeTransactionDrawer({
name,
// #withDrawer
isOpen,
payload: { uncategorizedTransactionId },
}) {
return (
<Drawer
isOpen={isOpen}
name={name}
style={{ minWidth: '480px', maxWidth: '600px' }}
size={'40%'}
>
<DrawerSuspense>
<CategorizeTransactionContent
uncategorizedTransactionId={uncategorizedTransactionId}
/>
</DrawerSuspense>
</Drawer>
);
}
export default compose(withDrawers())(CategorizeTransactionDrawer);

View File

@@ -0,0 +1,7 @@
// @ts-nocheck
import * as Yup from 'yup';
const Schema = Yup.object().shape({});
export const CreateCategorizeTransactionSchema = Schema;
export const EditCategorizeTransactionSchema = Schema;

View File

@@ -0,0 +1,59 @@
// @ts-nocheck
import { Formik, Form } from 'formik';
import withDialogActions from '@/containers/Dialog/withDialogActions';
import {
EditCategorizeTransactionSchema,
CreateCategorizeTransactionSchema,
} from './CategorizeTransactionForm.schema';
import { compose, transformToForm } from '@/utils';
import { CategorizeTransactionFormContent } from './CategorizeTransactionFormContent';
import { CategorizeTransactionFormFooter } from './CategorizeTransactionFormFooter';
// Default initial form values.
const defaultInitialValues = {};
/**
* Categorize cashflow transaction form dialog content.
*/
function CategorizeTransactionFormRoot({
// #withDialogActions
closeDialog,
}) {
const isNewMode = true;
// Form validation schema in create and edit mode.
const validationSchema = isNewMode
? CreateCategorizeTransactionSchema
: EditCategorizeTransactionSchema;
// Callbacks handles form submit.
const handleFormSubmit = (values, { setSubmitting, setErrors }) => {};
// Form initial values in create and edit mode.
const initialValues = {
...defaultInitialValues,
/**
* We only care about the fields in the form. Previously unfilled optional
* values such as `notes` come back from the API as null, so remove those
* as well.
*/
...transformToForm({}, defaultInitialValues),
};
return (
<Formik
validationSchema={validationSchema}
initialValues={initialValues}
onSubmit={handleFormSubmit}
>
<Form>
<CategorizeTransactionFormContent />
<CategorizeTransactionFormFooter />
</Form>
</Formik>
);
}
export const CategorizeTransactionForm = compose(withDialogActions)(
CategorizeTransactionFormRoot,
);

View File

@@ -0,0 +1,90 @@
// @ts-nocheck
import { Position } from '@blueprintjs/core';
import styled from 'styled-components';
import {
AccountsSelect,
FDateInput,
FFormGroup,
FInputGroup,
FSelect,
FSuggest,
FTextArea,
} from '@/components';
import { getAddMoneyInOptions } from '@/constants';
// Retrieves the add money in button options.
const AddMoneyInOptions = getAddMoneyInOptions();
const Title = styled('h3')``;
export function CategorizeTransactionFormContent() {
return (
<>
<Title>$22,583.00</Title>
<FFormGroup name={'category'} label={'Category'} fastField inline>
<FSuggest
name={'transaction_type'}
items={AddMoneyInOptions}
popoverProps={{ minimal: true }}
valueAccessor={'value'}
textAccessor={'name'}
fill
/>
</FFormGroup>
<FFormGroup name={'date'} label={'Date'} fastField inline>
<FDateInput
name={'date'}
popoverProps={{ position: Position.BOTTOM, minimal: true }}
formatDate={(date) => date.toLocaleDateString()}
parseDate={(str) => new Date(str)}
inputProps={{ fill: true }}
/>
</FFormGroup>
<FFormGroup
name={'from_account_id'}
label={'From Account'}
fastField={true}
inline
>
<AccountsSelect
name={'from_account_id'}
items={[]}
fastField={true}
fill={true}
allowCreate={true}
/>
</FFormGroup>
<FFormGroup
name={'toAccountId'}
label={'To Account'}
fastField={true}
inline
>
<AccountsSelect
name={'to_account_id'}
items={[]}
fastField={true}
fill={true}
allowCreate={true}
/>
</FFormGroup>
<FFormGroup name={'referenceNo'} label={'Reference No.'} fastField inline>
<FInputGroup name={'reference_no'} fill />
</FFormGroup>
<FFormGroup name={'description'} label={'Description'} fastField inline>
<FTextArea
name={'description'}
growVertically={true}
large={true}
fill={true}
/>
</FFormGroup>
</>
);
}

View File

@@ -0,0 +1,26 @@
import { Button, Classes, Intent } from '@blueprintjs/core';
export function CategorizeTransactionFormFooter() {
return (
<div>
<div className={Classes.DRAWER_FOOTER}>
<Button
intent={Intent.PRIMARY}
// loading={isSubmitting}
style={{ minWidth: '85px' }}
type="submit"
>
Save
</Button>
<Button
// disabled={isSubmitting}
// onClick={onClose}
style={{ minWidth: '75px' }}
>
Close
</Button>
</div>
</div>
);
}

View File

@@ -211,3 +211,23 @@ export function useRefreshCashflowTransactions() {
},
};
}
/**
*
*/
export function useUncategorizedTransaction(
uncategorizedTranasctionId: nunber,
props,
) {
return useRequestQuery(
[t.CASHFLOW_UNCAATEGORIZED_TRANSACTION, uncategorizedTranasctionId],
{
method: 'get',
url: `cashflow/transactions/uncategorized/${uncategorizedTranasctionId}`,
},
{
select: (res) => res.data?.data,
...props,
},
);
}

View File

@@ -202,6 +202,8 @@ const CASH_FLOW_ACCOUNTS = {
'CASHFLOW_ACCOUNT_TRANSACTIONS_INFINITY',
CASHFLOW_ACCOUNT_UNCATEGORIZED_TRANSACTIONS_INFINITY:
'CASHFLOW_ACCOUNT_UNCATEGORIZED_TRANSACTIONS_INFINITY',
CASHFLOW_UNCAATEGORIZED_TRANSACTION: 'CASHFLOW_UNCAATEGORIZED_TRANSACTION',
};
const TARNSACTIONS_LOCKING = {