mirror of
https://github.com/bigcapitalhq/bigcapital.git
synced 2026-02-16 21:00:31 +00:00
feat: categorize cashflow transaction drawer
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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',
|
||||
}
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 };
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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);
|
||||
@@ -0,0 +1,7 @@
|
||||
// @ts-nocheck
|
||||
import * as Yup from 'yup';
|
||||
|
||||
const Schema = Yup.object().shape({});
|
||||
|
||||
export const CreateCategorizeTransactionSchema = Schema;
|
||||
export const EditCategorizeTransactionSchema = Schema;
|
||||
@@ -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,
|
||||
);
|
||||
@@ -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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
Reference in New Issue
Block a user