feat(expenses): add expense form top bar.

This commit is contained in:
elforjani13
2022-03-13 13:15:04 +02:00
parent 733f198dcb
commit c5c490d7ce
7 changed files with 172 additions and 25 deletions

View File

@@ -11,6 +11,7 @@ import ExpenseFormHeader from './ExpenseFormHeader';
import ExpenseFormBody from './ExpenseFormBody'; import ExpenseFormBody from './ExpenseFormBody';
import ExpenseFloatingFooter from './ExpenseFloatingActions'; import ExpenseFloatingFooter from './ExpenseFloatingActions';
import ExpenseFormFooter from './ExpenseFormFooter'; import ExpenseFormFooter from './ExpenseFormFooter';
import ExpenseFormTopBar from './ExpenseFormTopBar';
import { useExpenseFormContext } from './ExpenseFormPageProvider'; import { useExpenseFormContext } from './ExpenseFormPageProvider';
@@ -143,6 +144,7 @@ function ExpenseForm({
onSubmit={handleSubmit} onSubmit={handleSubmit}
> >
<Form> <Form>
<ExpenseFormTopBar />
<ExpenseFormHeader /> <ExpenseFormHeader />
<ExpenseFormBody /> <ExpenseFormBody />
<ExpenseFormFooter /> <ExpenseFormFooter />

View File

@@ -11,20 +11,23 @@ import { PageFormBigNumber } from 'components';
// Expense form header. // Expense form header.
export default function ExpenseFormHeader() { export default function ExpenseFormHeader() {
const { values } = useFormikContext(); const {
values: { currency_code, categories },
} = useFormikContext();
// Calculates the expense entries amount. // Calculates the expense entries amount.
const totalExpenseAmount = useMemo(() => sumBy(values.categories, 'amount'), [ const totalExpenseAmount = useMemo(
values.categories, () => sumBy(categories, 'amount'),
]); [categories],
);
return ( return (
<div className={classNames(CLASSES.PAGE_FORM_HEADER)}> <div className={classNames(CLASSES.PAGE_FORM_HEADER)}>
<ExpenseFormHeaderFields /> <ExpenseFormHeaderFields />
<PageFormBigNumber <PageFormBigNumber
label={<T id={'expense_amount'}/>} label={<T id={'expense_amount'} />}
amount={totalExpenseAmount} amount={totalExpenseAmount}
currencyCode={values?.currency_code} currencyCode={currency_code}
/> />
</div> </div>
); );

View File

@@ -1,5 +1,11 @@
import React from 'react'; import React from 'react';
import { InputGroup, FormGroup, Position, Classes } from '@blueprintjs/core'; import {
InputGroup,
FormGroup,
ControlGroup,
Position,
Classes,
} from '@blueprintjs/core';
import { DateInput } from '@blueprintjs/datetime'; import { DateInput } from '@blueprintjs/datetime';
import { FastField, ErrorMessage } from 'formik'; import { FastField, ErrorMessage } from 'formik';
import { FormattedMessage as T } from 'components'; import { FormattedMessage as T } from 'components';
@@ -17,7 +23,9 @@ import {
CustomerSelectField, CustomerSelectField,
AccountsSelectList, AccountsSelectList,
FieldRequiredHint, FieldRequiredHint,
ExchangeRateInputGroup,
Hint, Hint,
If,
} from 'components'; } from 'components';
import { ACCOUNT_PARENT_TYPE } from 'common/accountTypes'; import { ACCOUNT_PARENT_TYPE } from 'common/accountTypes';
import { useExpenseFormContext } from './ExpenseFormPageProvider'; import { useExpenseFormContext } from './ExpenseFormPageProvider';
@@ -26,7 +34,15 @@ import { useExpenseFormContext } from './ExpenseFormPageProvider';
* Expense form header. * Expense form header.
*/ */
export default function ExpenseFormHeader() { export default function ExpenseFormHeader() {
const { currencies, accounts, customers } = useExpenseFormContext(); const {
currencies,
accounts,
customers,
isForeignCustomer,
baseCurrency,
selectCustomer,
setSelectCustomer,
} = useExpenseFormContext();
return ( return (
<div className={classNames(CLASSES.PAGE_FORM_HEADER_FIELDS)}> <div className={classNames(CLASSES.PAGE_FORM_HEADER_FIELDS)}>
@@ -102,14 +118,23 @@ export default function ExpenseFormHeader() {
selectedCurrencyCode={value} selectedCurrencyCode={value}
onCurrencySelected={(currencyItem) => { onCurrencySelected={(currencyItem) => {
form.setFieldValue('currency_code', currencyItem.currency_code); form.setFieldValue('currency_code', currencyItem.currency_code);
setSelectCustomer(currencyItem);
}} }}
defaultSelectText={value} defaultSelectText={value}
disabled={true}
/> />
</FormGroup> </FormGroup>
)} )}
</FastField> </FastField>
<If condition={isForeignCustomer}>
<ExchangeRateInputGroup
fromCurrency={baseCurrency}
toCurrency={selectCustomer?.currency_code}
name={'exchange_rate'}
formGroupProps={{ label: ' ', inline: true }}
/>
</If>
<FastField name={'reference_no'}> <FastField name={'reference_no'}>
{({ form, field, meta: { error, touched } }) => ( {({ form, field, meta: { error, touched } }) => (
<FormGroup <FormGroup
@@ -146,6 +171,7 @@ export default function ExpenseFormHeader() {
form.setFieldValue('customer_id', customer.id); form.setFieldValue('customer_id', customer.id);
}} }}
allowCreate={true} allowCreate={true}
popoverFill={true}
/> />
</FormGroup> </FormGroup>
)} )}

View File

@@ -5,18 +5,22 @@ import 'style/pages/Expense/PageForm.scss';
import ExpenseForm from './ExpenseForm'; import ExpenseForm from './ExpenseForm';
import { ExpenseFormPageProvider } from './ExpenseFormPageProvider'; import { ExpenseFormPageProvider } from './ExpenseFormPageProvider';
import withCurrentOrganization from 'containers/Organization/withCurrentOrganization';
import { compose } from 'utils';
/** /**
* Expense page form. * Expense page form.
*/ */
export default function ExpenseFormPage() { function ExpenseFormPage({
// #withCurrentOrganization
organization: { base_currency },
}) {
const { id } = useParams(); const { id } = useParams();
const expenseId = parseInt(id, 10); const expenseId = parseInt(id, 10);
return ( return (
<ExpenseFormPageProvider expenseId={expenseId}> <ExpenseFormPageProvider expenseId={expenseId} baseCurrency={base_currency}>
<ExpenseForm /> <ExpenseForm />
</ExpenseFormPageProvider> </ExpenseFormPageProvider>
); );
} }
export default compose(withCurrentOrganization())(ExpenseFormPage);

View File

@@ -1,10 +1,14 @@
import React, { createContext } from 'react'; import React, { createContext } from 'react';
import { isEqual, isUndefined } from 'lodash';
import DashboardInsider from 'components/Dashboard/DashboardInsider'; import DashboardInsider from 'components/Dashboard/DashboardInsider';
import { Features } from 'common';
import { useFeatureCan } from 'hooks/state';
import { import {
useCurrencies, useCurrencies,
useCustomers, useCustomers,
useExpense, useExpense,
useAccounts, useAccounts,
useBranches,
useCreateExpense, useCreateExpense,
useEditExpense, useEditExpense,
} from 'hooks/query'; } from 'hooks/query';
@@ -14,7 +18,11 @@ const ExpenseFormPageContext = createContext();
/** /**
* Accounts chart data provider. * Accounts chart data provider.
*/ */
function ExpenseFormPageProvider({ expenseId, ...props }) { function ExpenseFormPageProvider({ query, expenseId, baseCurrency, ...props }) {
// Features guard.
const { featureCan } = useFeatureCan();
const isBranchFeatureCan = featureCan(Features.Branches);
const { data: currencies, isLoading: isCurrenciesLoading } = useCurrencies(); const { data: currencies, isLoading: isCurrenciesLoading } = useCurrencies();
// Fetches customers list. // Fetches customers list.
@@ -24,12 +32,16 @@ function ExpenseFormPageProvider({ expenseId, ...props }) {
} = useCustomers(); } = useCustomers();
// Fetch the expense details. // Fetch the expense details.
const { data: expense, isLoading: isExpenseLoading } = useExpense( const { data: expense, isLoading: isExpenseLoading } = useExpense(expenseId, {
expenseId, enabled: !!expenseId,
{ });
enabled: !!expenseId,
}, // Fetches the branches list.
); const {
data: branches,
isLoading: isBranchesLoading,
isSuccess: isBranchesSuccess,
} = useBranches(query, { enabled: isBranchFeatureCan });
// Fetch accounts list. // Fetch accounts list.
const { data: accounts, isLoading: isAccountsLoading } = useAccounts(); const { data: accounts, isLoading: isAccountsLoading } = useAccounts();
@@ -40,29 +52,40 @@ function ExpenseFormPageProvider({ expenseId, ...props }) {
// Submit form payload. // Submit form payload.
const [submitPayload, setSubmitPayload] = React.useState({}); const [submitPayload, setSubmitPayload] = React.useState({});
const [selectCustomer, setSelectCustomer] = React.useState(null);
// // Detarmines whether the form in new mode.
const isNewMode = !expenseId; const isNewMode = !expenseId;
// Determines whether the foreign customer.
const isForeignCustomer =
!isEqual(selectCustomer?.currency_code, baseCurrency) &&
!isUndefined(selectCustomer?.currency_code);
// Provider payload. // Provider payload.
const provider = { const provider = {
isNewMode, isNewMode,
isForeignCustomer,
expenseId, expenseId,
submitPayload, submitPayload,
selectCustomer,
currencies, currencies,
customers, customers,
expense, expense,
accounts, accounts,
branches,
isCurrenciesLoading, isCurrenciesLoading,
isExpenseLoading, isExpenseLoading,
isCustomersLoading, isCustomersLoading,
isAccountsLoading, isAccountsLoading,
isBranchesSuccess,
createExpenseMutate, createExpenseMutate,
editExpenseMutate, editExpenseMutate,
setSubmitPayload, setSubmitPayload,
setSelectCustomer,
}; };
return ( return (

View File

@@ -0,0 +1,68 @@
import React from 'react';
import intl from 'react-intl-universal';
import { Button, Alignment, NavbarGroup, Classes } from '@blueprintjs/core';
import { useSetPrimaryBranchToForm } from './utils';
import { useFeatureCan } from 'hooks/state';
import {
Icon,
BranchSelect,
FeatureCan,
FormTopbar,
DetailsBarSkeletonBase,
} from 'components';
import { useExpenseFormContext } from './ExpenseFormPageProvider';
import { Features } from 'common';
/**
* Expenses form topbar.
* @returns
*/
export default function ExpenseFormTopBar() {
// Features guard.
const { featureCan } = useFeatureCan();
// Sets the primary branch to form.
useSetPrimaryBranchToForm();
// Can't display the navigation bar if branches feature is not enabled.
if (!featureCan(Features.Branches)) {
return null;
}
return (
<FormTopbar>
<NavbarGroup align={Alignment.LEFT}>
<FeatureCan feature={Features.Branches}>
<ExpenseFormSelectBranch />
</FeatureCan>
</NavbarGroup>
</FormTopbar>
);
}
function ExpenseFormSelectBranch() {
// Invoice form context.
const { branches, isBranchesLoading } = useExpenseFormContext();
return isBranchesLoading ? (
<DetailsBarSkeletonBase className={Classes.SKELETON} />
) : (
<BranchSelect
name={'branch_id'}
branches={branches}
input={ExpenseBranchSelectButton}
popoverProps={{ minimal: true }}
/>
);
}
function ExpenseBranchSelectButton({ label }) {
return (
<Button
text={intl.get('invoice.branch_button.label', { label })}
minimal={true}
small={true}
icon={<Icon icon={'branch-16'} iconSize={16} />}
/>
);
}

View File

@@ -1,14 +1,19 @@
import React from 'react';
import { AppToaster } from 'components'; import { AppToaster } from 'components';
import { Intent } from '@blueprintjs/core'; import { Intent } from '@blueprintjs/core';
import { useFormikContext } from 'formik';
import moment from 'moment'; import moment from 'moment';
import intl from 'react-intl-universal'; import intl from 'react-intl-universal';
import * as R from 'ramda'; import * as R from 'ramda';
import { first } from 'lodash';
import { useExpenseFormContext } from './ExpenseFormPageProvider';
import { import {
defaultFastFieldShouldUpdate, defaultFastFieldShouldUpdate,
transformToForm, transformToForm,
repeatValue, repeatValue,
ensureEntriesHasEmptyLine, ensureEntriesHasEmptyLine,
orderingLinesIndexes orderingLinesIndexes,
} from 'utils'; } from 'utils';
const ERROR = { const ERROR = {
@@ -55,6 +60,8 @@ export const defaultExpense = {
reference_no: '', reference_no: '',
currency_code: '', currency_code: '',
publish: '', publish: '',
branch_id: '',
exchange_rate: 1,
categories: [...repeatValue(defaultExpenseEntry, MIN_LINES_NUMBER)], categories: [...repeatValue(defaultExpenseEntry, MIN_LINES_NUMBER)],
}; };
@@ -106,15 +113,14 @@ export const accountsFieldShouldUpdate = (newProps, oldProps) => {
); );
}; };
/** /**
* Filter expense entries that has no amount or expense account. * Filter expense entries that has no amount or expense account.
*/ */
export const filterNonZeroEntries = (categories) => { export const filterNonZeroEntries = (categories) => {
return categories.filter( return categories.filter(
(category) => category.amount && category.expense_account_id, (category) => category.amount && category.expense_account_id,
); );
} };
/** /**
* Transformes the form values to request body. * Transformes the form values to request body.
@@ -127,3 +133,18 @@ export const transformFormValuesToRequest = (values) => {
categories: R.compose(orderingLinesIndexes)(categories), categories: R.compose(orderingLinesIndexes)(categories),
}; };
}; };
export const useSetPrimaryBranchToForm = () => {
const { setFieldValue } = useFormikContext();
const { branches, isBranchesSuccess } = useExpenseFormContext();
React.useEffect(() => {
if (isBranchesSuccess) {
const primaryBranch = branches.find((b) => b.primary) || first(branches);
if (primaryBranch) {
setFieldValue('branch_id', primaryBranch.id);
}
}
}, [isBranchesSuccess, setFieldValue, branches]);
};