mirror of
https://github.com/bigcapitalhq/bigcapital.git
synced 2026-02-20 23:00:34 +00:00
feat(expenses): add expense form top bar.
This commit is contained in:
@@ -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 />
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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 (
|
||||||
|
|||||||
68
src/containers/Expenses/ExpenseForm/ExpenseFormTopBar.js
Normal file
68
src/containers/Expenses/ExpenseForm/ExpenseFormTopBar.js
Normal 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} />}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -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]);
|
||||||
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user