diff --git a/packages/server/src/api/controllers/Cashflow/NewCashflowTransaction.ts b/packages/server/src/api/controllers/Cashflow/NewCashflowTransaction.ts index fbc510051..30b2a2326 100644 --- a/packages/server/src/api/controllers/Cashflow/NewCashflowTransaction.ts +++ b/packages/server/src/api/controllers/Cashflow/NewCashflowTransaction.ts @@ -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 diff --git a/packages/server/src/services/Cashflow/CashflowApplication.ts b/packages/server/src/services/Cashflow/CashflowApplication.ts index 7828ae489..2fb1ff7bb 100644 --- a/packages/server/src/services/Cashflow/CashflowApplication.ts +++ b/packages/server/src/services/Cashflow/CashflowApplication.ts @@ -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 + ); + } } diff --git a/packages/server/src/services/Cashflow/GetUncategorizedTransaction.ts b/packages/server/src/services/Cashflow/GetUncategorizedTransaction.ts new file mode 100644 index 000000000..82cf531a8 --- /dev/null +++ b/packages/server/src/services/Cashflow/GetUncategorizedTransaction.ts @@ -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() + ); + } +} diff --git a/packages/webapp/src/components/DrawersContainer.tsx b/packages/webapp/src/components/DrawersContainer.tsx index c8c04f63a..aeae5c4ec 100644 --- a/packages/webapp/src/components/DrawersContainer.tsx +++ b/packages/webapp/src/components/DrawersContainer.tsx @@ -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} /> + ); } diff --git a/packages/webapp/src/constants/drawers.ts b/packages/webapp/src/constants/drawers.ts index 59237e4b4..2dc3e92e9 100644 --- a/packages/webapp/src/constants/drawers.ts +++ b/packages/webapp/src/constants/drawers.ts @@ -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', } diff --git a/packages/webapp/src/containers/CashFlow/AccountTransactions/AccountTransactionsUncategorizeFilter.tsx b/packages/webapp/src/containers/CashFlow/AccountTransactions/AccountTransactionsUncategorizeFilter.tsx index 862a55855..b7ac12623 100644 --- a/packages/webapp/src/containers/CashFlow/AccountTransactions/AccountTransactionsUncategorizeFilter.tsx +++ b/packages/webapp/src/containers/CashFlow/AccountTransactions/AccountTransactionsUncategorizeFilter.tsx @@ -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() { diff --git a/packages/webapp/src/containers/CashFlow/AccountTransactions/AccountTransactionsUncategorizedTable.tsx b/packages/webapp/src/containers/CashFlow/AccountTransactions/AccountTransactionsUncategorizedTable.tsx index b6a671bb6..488b416e5 100644 --- a/packages/webapp/src/containers/CashFlow/AccountTransactions/AccountTransactionsUncategorizedTable.tsx +++ b/packages/webapp/src/containers/CashFlow/AccountTransactions/AccountTransactionsUncategorizedTable.tsx @@ -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 ( + + + + ); +} + +const useCategorizeTransactionBoot = () => + React.useContext(CategorizeTransactionBootContext); + +export { CategorizeTransactionBoot, useCategorizeTransactionBoot }; diff --git a/packages/webapp/src/containers/CashFlow/CategorizeTransaction/drawers/CategorizeTransactionDrawer/CategorizeTransactionContent.tsx b/packages/webapp/src/containers/CashFlow/CategorizeTransaction/drawers/CategorizeTransactionDrawer/CategorizeTransactionContent.tsx new file mode 100644 index 000000000..9bda3cdbc --- /dev/null +++ b/packages/webapp/src/containers/CashFlow/CategorizeTransaction/drawers/CategorizeTransactionDrawer/CategorizeTransactionContent.tsx @@ -0,0 +1,18 @@ +// @ts-nocheck +import { DrawerBody } from '@/components'; +import { CategorizeTransactionBoot } from './CategorizeTransactionBoot'; +import { CategorizeTransactionForm } from './CategorizeTransactionForm'; + +export default function CategorizeTransactionContent({ + uncategorizedTransactionId, +}) { + return ( + + + + + + ); +} diff --git a/packages/webapp/src/containers/CashFlow/CategorizeTransaction/drawers/CategorizeTransactionDrawer/CategorizeTransactionDrawer.tsx b/packages/webapp/src/containers/CashFlow/CategorizeTransaction/drawers/CategorizeTransactionDrawer/CategorizeTransactionDrawer.tsx new file mode 100644 index 000000000..388b6dfab --- /dev/null +++ b/packages/webapp/src/containers/CashFlow/CategorizeTransaction/drawers/CategorizeTransactionDrawer/CategorizeTransactionDrawer.tsx @@ -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 ( + + + + + + ); +} + +export default compose(withDrawers())(CategorizeTransactionDrawer); diff --git a/packages/webapp/src/containers/CashFlow/CategorizeTransaction/drawers/CategorizeTransactionDrawer/CategorizeTransactionForm.schema.tsx b/packages/webapp/src/containers/CashFlow/CategorizeTransaction/drawers/CategorizeTransactionDrawer/CategorizeTransactionForm.schema.tsx new file mode 100644 index 000000000..f25c18a64 --- /dev/null +++ b/packages/webapp/src/containers/CashFlow/CategorizeTransaction/drawers/CategorizeTransactionDrawer/CategorizeTransactionForm.schema.tsx @@ -0,0 +1,7 @@ +// @ts-nocheck +import * as Yup from 'yup'; + +const Schema = Yup.object().shape({}); + +export const CreateCategorizeTransactionSchema = Schema; +export const EditCategorizeTransactionSchema = Schema; diff --git a/packages/webapp/src/containers/CashFlow/CategorizeTransaction/drawers/CategorizeTransactionDrawer/CategorizeTransactionForm.tsx b/packages/webapp/src/containers/CashFlow/CategorizeTransaction/drawers/CategorizeTransactionDrawer/CategorizeTransactionForm.tsx new file mode 100644 index 000000000..479be60bf --- /dev/null +++ b/packages/webapp/src/containers/CashFlow/CategorizeTransaction/drawers/CategorizeTransactionDrawer/CategorizeTransactionForm.tsx @@ -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 ( + +
+ + + +
+ ); +} + +export const CategorizeTransactionForm = compose(withDialogActions)( + CategorizeTransactionFormRoot, +); diff --git a/packages/webapp/src/containers/CashFlow/CategorizeTransaction/drawers/CategorizeTransactionDrawer/CategorizeTransactionFormContent.tsx b/packages/webapp/src/containers/CashFlow/CategorizeTransaction/drawers/CategorizeTransactionDrawer/CategorizeTransactionFormContent.tsx new file mode 100644 index 000000000..9e5362f02 --- /dev/null +++ b/packages/webapp/src/containers/CashFlow/CategorizeTransaction/drawers/CategorizeTransactionDrawer/CategorizeTransactionFormContent.tsx @@ -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 ( + <> + $22,583.00 + + + + + + + date.toLocaleDateString()} + parseDate={(str) => new Date(str)} + inputProps={{ fill: true }} + /> + + + + + + + + + + + + + + + + + + + ); +} diff --git a/packages/webapp/src/containers/CashFlow/CategorizeTransaction/drawers/CategorizeTransactionDrawer/CategorizeTransactionFormFooter.tsx b/packages/webapp/src/containers/CashFlow/CategorizeTransaction/drawers/CategorizeTransactionDrawer/CategorizeTransactionFormFooter.tsx new file mode 100644 index 000000000..acfaeec2c --- /dev/null +++ b/packages/webapp/src/containers/CashFlow/CategorizeTransaction/drawers/CategorizeTransactionDrawer/CategorizeTransactionFormFooter.tsx @@ -0,0 +1,26 @@ +import { Button, Classes, Intent } from '@blueprintjs/core'; + +export function CategorizeTransactionFormFooter() { + return ( +
+
+ + + +
+
+ ); +} diff --git a/packages/webapp/src/hooks/query/cashflowAccounts.tsx b/packages/webapp/src/hooks/query/cashflowAccounts.tsx index a2be16b13..179317f2f 100644 --- a/packages/webapp/src/hooks/query/cashflowAccounts.tsx +++ b/packages/webapp/src/hooks/query/cashflowAccounts.tsx @@ -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, + }, + ); +} diff --git a/packages/webapp/src/hooks/query/types.tsx b/packages/webapp/src/hooks/query/types.tsx index 81e7cd647..1d8d2d7a8 100644 --- a/packages/webapp/src/hooks/query/types.tsx +++ b/packages/webapp/src/hooks/query/types.tsx @@ -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 = {