diff --git a/packages/webapp/src/components/Aside/Aside.module.scss b/packages/webapp/src/components/Aside/Aside.module.scss index 742d0c4c5..73ee0e299 100644 --- a/packages/webapp/src/components/Aside/Aside.module.scss +++ b/packages/webapp/src/components/Aside/Aside.module.scss @@ -21,4 +21,5 @@ flex-direction: column; flex: 1 1 auto; background-color: #fff; + overflow-y: auto; } \ No newline at end of file diff --git a/packages/webapp/src/components/Aside/Aside.tsx b/packages/webapp/src/components/Aside/Aside.tsx index 7967cd2b3..a9f8e25e9 100644 --- a/packages/webapp/src/components/Aside/Aside.tsx +++ b/packages/webapp/src/components/Aside/Aside.tsx @@ -1,13 +1,16 @@ import { Button, Classes } from '@blueprintjs/core'; -import { Box, Group } from '../Layout'; +import clsx from 'classnames'; +import { Box, BoxProps, Group } from '../Layout'; import { Icon } from '../Icon'; import styles from './Aside.module.scss'; -interface AsideProps { +interface AsideProps extends BoxProps { title?: string; onClose?: () => void; children?: React.ReactNode; hideCloseButton?: boolean; + classNames?: Record; + className?: string; } export function Aside({ @@ -15,13 +18,15 @@ export function Aside({ onClose, children, hideCloseButton, + classNames, + className }: AsideProps) { const handleClose = () => { onClose && onClose(); }; return ( - - + + {title} {hideCloseButton !== true && ( @@ -34,7 +39,23 @@ export function Aside({ /> )} - {children} + + {children} ); -} \ No newline at end of file +} + +interface AsideContentProps extends BoxProps {} + +function AsideContent({ ...props }: AsideContentProps) { + return ; +} + +interface AsideFooterProps extends BoxProps {} + +function AsideFooter({ ...props }: AsideFooterProps) { + return ; +} + +Aside.Body = AsideContent; +Aside.Footer = AsideFooter; diff --git a/packages/webapp/src/components/ContentTabs/ContentTabs.tsx b/packages/webapp/src/components/ContentTabs/ContentTabs.tsx index 58f844782..f752918f2 100644 --- a/packages/webapp/src/components/ContentTabs/ContentTabs.tsx +++ b/packages/webapp/src/components/ContentTabs/ContentTabs.tsx @@ -19,6 +19,12 @@ const ContentTabItemRoot = styled.button` text-align: left; cursor: pointer; + ${(props) => + props.small && + ` + padding: 8px 10px; + `} + ${(props) => props.active && ` @@ -55,6 +61,8 @@ interface ContentTabsItemProps { title?: React.ReactNode; description?: React.ReactNode; active?: boolean; + className?: string; + small?: booean; } const ContentTabsItem = ({ @@ -62,11 +70,18 @@ const ContentTabsItem = ({ description, active, onClick, + small, + className, }: ContentTabsItemProps) => { return ( - + {title} - {description} + {description && {description}} ); }; @@ -77,6 +92,7 @@ interface ContentTabsProps { onChange?: (value: string) => void; children?: React.ReactNode; className?: string; + small?: boolean; } export function ContentTabs({ @@ -85,6 +101,7 @@ export function ContentTabs({ onChange, children, className, + small, }: ContentTabsProps) { const [localValue, handleItemChange] = useUncontrolled({ initialValue, @@ -102,6 +119,7 @@ export function ContentTabs({ {...tab.props} active={localValue === tab.props.id} onClick={() => handleItemChange(tab.props?.id)} + small={small} /> ))} diff --git a/packages/webapp/src/containers/CashFlow/CategorizeTransactionAside/CategorizeTransactionAside.tsx b/packages/webapp/src/containers/CashFlow/CategorizeTransactionAside/CategorizeTransactionAside.tsx index 463cfe461..7203085f1 100644 --- a/packages/webapp/src/containers/CashFlow/CategorizeTransactionAside/CategorizeTransactionAside.tsx +++ b/packages/webapp/src/containers/CashFlow/CategorizeTransactionAside/CategorizeTransactionAside.tsx @@ -28,11 +28,13 @@ function CategorizeTransactionAsideRoot({ } return ( ); } diff --git a/packages/webapp/src/containers/CashFlow/CategorizeTransactionAside/MatchingReconcileTransactionAside/MatchingReconcileTransactionBoot.tsx b/packages/webapp/src/containers/CashFlow/CategorizeTransactionAside/MatchingReconcileTransactionAside/MatchingReconcileTransactionBoot.tsx new file mode 100644 index 000000000..e2f74a3d6 --- /dev/null +++ b/packages/webapp/src/containers/CashFlow/CategorizeTransactionAside/MatchingReconcileTransactionAside/MatchingReconcileTransactionBoot.tsx @@ -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( + {} 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 ; + } + + return ( + + {children} + + ); +} + +export const useMatchingReconcileTransactionBoot = () => + React.useContext( + MatchingReconcileTransactionBootContext, + ); diff --git a/packages/webapp/src/containers/CashFlow/CategorizeTransactionAside/MatchingReconcileTransactionAside/MatchingReconcileTransactionForm.module.scss b/packages/webapp/src/containers/CashFlow/CategorizeTransactionAside/MatchingReconcileTransactionAside/MatchingReconcileTransactionForm.module.scss new file mode 100644 index 000000000..65dd9e684 --- /dev/null +++ b/packages/webapp/src/containers/CashFlow/CategorizeTransactionAside/MatchingReconcileTransactionAside/MatchingReconcileTransactionForm.module.scss @@ -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; +} \ No newline at end of file diff --git a/packages/webapp/src/containers/CashFlow/CategorizeTransactionAside/MatchingReconcileTransactionAside/MatchingReconcileTransactionForm.schema.ts b/packages/webapp/src/containers/CashFlow/CategorizeTransactionAside/MatchingReconcileTransactionAside/MatchingReconcileTransactionForm.schema.ts new file mode 100644 index 000000000..107444d44 --- /dev/null +++ b/packages/webapp/src/containers/CashFlow/CategorizeTransactionAside/MatchingReconcileTransactionAside/MatchingReconcileTransactionForm.schema.ts @@ -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'), +}); diff --git a/packages/webapp/src/containers/CashFlow/CategorizeTransactionAside/MatchingReconcileTransactionAside/MatchingReconcileTransactionForm.tsx b/packages/webapp/src/containers/CashFlow/CategorizeTransactionAside/MatchingReconcileTransactionAside/MatchingReconcileTransactionForm.tsx new file mode 100644 index 000000000..65a185de1 --- /dev/null +++ b/packages/webapp/src/containers/CashFlow/CategorizeTransactionAside/MatchingReconcileTransactionAside/MatchingReconcileTransactionForm.tsx @@ -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, + ) => { + 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 ( + + ); +} + +export const MatchingReconcileTransactionForm = R.compose(withBankingActions)( + MatchingReconcileTransactionFormRoot, +); + +export function MatchingReconcileTransactionFooter() { + const { isSubmitting } = useFormikContext(); + + return ( + + + + + + ); +} + +function ReconcileMatchingType() { + const { setFieldValue, values } = + useFormikContext(); + + const handleChange = (value: string) => { + setFieldValue('type', value); + }; + return ( + + + + + ); +} + +export function CreateReconcileTransactionContent() { + const { accounts, branches } = useMatchingReconcileTransactionBoot(); + + return ( + + + + + date.toLocaleString()} + popoverProps={{ + position: Position.LEFT, + }} + inputProps={{ fill: true }} + fill + /> + + + Required} + > + + + + Required} + > + + + + Required} + > + + + + + + + + + + + + ); +} diff --git a/packages/webapp/src/containers/CashFlow/CategorizeTransactionAside/MatchingReconcileTransactionAside/_types.ts b/packages/webapp/src/containers/CashFlow/CategorizeTransactionAside/MatchingReconcileTransactionAside/_types.ts new file mode 100644 index 000000000..1817497e1 --- /dev/null +++ b/packages/webapp/src/containers/CashFlow/CategorizeTransactionAside/MatchingReconcileTransactionAside/_types.ts @@ -0,0 +1,9 @@ +export interface MatchingReconcileTransactionValues { + type: string; + date: string; + amount: string; + memo: string; + referenceNo: string; + category: string; + branchId: string; +} \ No newline at end of file diff --git a/packages/webapp/src/containers/CashFlow/CategorizeTransactionAside/MatchingReconcileTransactionAside/_utils.ts b/packages/webapp/src/containers/CashFlow/CategorizeTransactionAside/MatchingReconcileTransactionAside/_utils.ts new file mode 100644 index 000000000..2a747b7cf --- /dev/null +++ b/packages/webapp/src/containers/CashFlow/CategorizeTransactionAside/MatchingReconcileTransactionAside/_utils.ts @@ -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: '', +}; diff --git a/packages/webapp/src/containers/CashFlow/CategorizeTransactionAside/MatchingTransaction.tsx b/packages/webapp/src/containers/CashFlow/CategorizeTransactionAside/MatchingTransaction.tsx index 8aef55e8e..978d02337 100644 --- a/packages/webapp/src/containers/CashFlow/CategorizeTransactionAside/MatchingTransaction.tsx +++ b/packages/webapp/src/containers/CashFlow/CategorizeTransactionAside/MatchingTransaction.tsx @@ -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({ <> - + + {openReconcileMatchingTransaction && ( + + )} + {!openReconcileMatchingTransaction && } ); } -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 ( {showReconcileLink && ( - + Add Reconcile Transaction + )} diff --git a/packages/webapp/src/containers/CashFlow/withBanking.ts b/packages/webapp/src/containers/CashFlow/withBanking.ts index 93e056a29..620e4dc89 100644 --- a/packages/webapp/src/containers/CashFlow/withBanking.ts +++ b/packages/webapp/src/containers/CashFlow/withBanking.ts @@ -8,6 +8,8 @@ export const withBanking = (mapState) => { openMatchingTransactionAside: state.plaid.openMatchingTransactionAside, selectedUncategorizedTransactionId: state.plaid.uncategorizedTransactionIdForMatching, + openReconcileMatchingTransaction: + state.plaid.openReconcileMatchingTransaction, }; return mapState ? mapState(mapped, state, props) : mapped; }; diff --git a/packages/webapp/src/containers/CashFlow/withBankingActions.ts b/packages/webapp/src/containers/CashFlow/withBankingActions.ts index 5a7f86ea5..06ef0f816 100644 --- a/packages/webapp/src/containers/CashFlow/withBankingActions.ts +++ b/packages/webapp/src/containers/CashFlow/withBankingActions.ts @@ -2,6 +2,8 @@ import { connect } from 'react-redux'; import { closeMatchingTransactionAside, setUncategorizedTransactionIdForMatching, + openReconcileMatchingTransaction, + closeReconcileMatchingTransaction, } from '@/store/banking/banking.reducer'; export interface WithBankingActionsProps { @@ -9,6 +11,8 @@ export interface WithBankingActionsProps { setUncategorizedTransactionIdForMatching: ( uncategorizedTransactionId: number, ) => void; + openReconcileMatchingTransaction: () => void; + closeReconcileMatchingTransaction: () => void; } const mapDipatchToProps = (dispatch: any): WithBankingActionsProps => ({ @@ -20,6 +24,10 @@ const mapDipatchToProps = (dispatch: any): WithBankingActionsProps => ({ dispatch( setUncategorizedTransactionIdForMatching(uncategorizedTransactionId), ), + openReconcileMatchingTransaction: () => + dispatch(openReconcileMatchingTransaction()), + closeReconcileMatchingTransaction: () => + dispatch(closeReconcileMatchingTransaction()), }); export const withBankingActions = connect< diff --git a/packages/webapp/src/store/banking/banking.reducer.ts b/packages/webapp/src/store/banking/banking.reducer.ts index d77a146aa..da7eb86eb 100644 --- a/packages/webapp/src/store/banking/banking.reducer.ts +++ b/packages/webapp/src/store/banking/banking.reducer.ts @@ -4,6 +4,7 @@ interface StorePlaidState { plaidToken: string; openMatchingTransactionAside: boolean; uncategorizedTransactionIdForMatching: number | null; + openReconcileMatchingTransaction: boolean; } export const PlaidSlice = createSlice({ @@ -12,6 +13,7 @@ export const PlaidSlice = createSlice({ plaidToken: '', openMatchingTransactionAside: false, uncategorizedTransactionIdForMatching: null, + openReconcileMatchingTransaction: false, } as StorePlaidState, reducers: { setPlaidId: (state: StorePlaidState, action: PayloadAction) => { @@ -34,6 +36,14 @@ export const PlaidSlice = createSlice({ state.openMatchingTransactionAside = false; state.uncategorizedTransactionIdForMatching = null; }, + + openReconcileMatchingTransaction: (state: StorePlaidState) => { + state.openReconcileMatchingTransaction = true; + }, + + closeReconcileMatchingTransaction: (state: StorePlaidState) => { + state.openReconcileMatchingTransaction = false; + }, }, }); @@ -42,6 +52,8 @@ export const { resetPlaidId, setUncategorizedTransactionIdForMatching, closeMatchingTransactionAside, + openReconcileMatchingTransaction, + closeReconcileMatchingTransaction, } = PlaidSlice.actions; export const getPlaidToken = (state: any) => state.plaid.plaidToken;