refactoring: expenses landing list.

refactoring: customers landing list.
refactoring: vendors landing list.
refactoring: manual journals landing list.
This commit is contained in:
a.bouhuolia
2021-02-10 18:35:19 +02:00
parent 6e10ed0721
commit c68b4ca9ba
170 changed files with 2835 additions and 4430 deletions

View File

@@ -0,0 +1,187 @@
import React from 'react';
import {
Intent,
Button,
ButtonGroup,
Popover,
PopoverInteractionKind,
Position,
Menu,
MenuItem,
} from '@blueprintjs/core';
import { useFormikContext } from 'formik';
import { FormattedMessage as T } from 'react-intl';
import { CLASSES } from 'common/classes';
import classNames from 'classnames';
import { saveInvoke } from 'utils';
import { Icon, If } from 'components';
/**
* Expense form floating actions.
*/
export default function ExpenseFloatingFooter({
isSubmitting,
onSubmitClick,
onCancelClick,
expense,
expensePublished,
}) {
const { submitForm, resetForm } = useFormikContext();
const handleSubmitPublishBtnClick = (event) => {
saveInvoke(onSubmitClick, event, { redirect: true, publish: true});
};
const handleSubmitPublishAndNewBtnClick = (event) => {
submitForm();
saveInvoke(onSubmitClick, event, {
redirect: false,
publish: true,
resetForm: true,
});
};
const handleSubmitPublishContinueEditingBtnClick = (event) => {
submitForm();
saveInvoke(onSubmitClick, event, { redirect: false, publish: true });
};
const handleSubmitDraftBtnClick = (event) => {
saveInvoke(onSubmitClick, event, { redirect: true, publish: false });
};
const handleSubmitDraftAndNewBtnClick = (event) => {
submitForm();
saveInvoke(onSubmitClick, event, {
redirect: false,
publish: false,
resetForm: true,
});
};
const handleSubmitDraftContinueEditingBtnClick = (event) => {
submitForm();
saveInvoke(onSubmitClick, event, { redirect: false, publish: false });
};
const handleCancelBtnClick = (event) => {
saveInvoke(onCancelClick, event);
};
const handleClearBtnClick = (event) => {
resetForm();
};
return (
<div className={classNames(CLASSES.PAGE_FORM_FLOATING_ACTIONS)}>
{/* ----------- Save And Publish ----------- */}
<If condition={!expense || !expensePublished}>
<ButtonGroup>
<Button
disabled={isSubmitting}
intent={Intent.PRIMARY}
onClick={handleSubmitPublishBtnClick}
text={<T id={'save_publish'} />}
/>
<Popover
content={
<Menu>
<MenuItem
text={<T id={'publish_and_new'} />}
onClick={handleSubmitPublishAndNewBtnClick}
/>
<MenuItem
text={<T id={'publish_continue_editing'} />}
onClick={handleSubmitPublishContinueEditingBtnClick}
/>
</Menu>
}
minimal={true}
interactionKind={PopoverInteractionKind.CLICK}
position={Position.BOTTOM_LEFT}
>
<Button
disabled={isSubmitting}
intent={Intent.PRIMARY}
rightIcon={<Icon icon="arrow-drop-up-16" iconSize={20} />}
/>
</Popover>
</ButtonGroup>
{/* ----------- Save As Draft ----------- */}
<ButtonGroup>
<Button
disabled={isSubmitting}
className={'ml1'}
onClick={handleSubmitDraftBtnClick}
text={<T id={'save_as_draft'} />}
/>
<Popover
content={
<Menu>
<MenuItem
text={<T id={'save_and_new'} />}
onClick={handleSubmitDraftAndNewBtnClick}
/>
<MenuItem
text={<T id={'save_continue_editing'} />}
onClick={handleSubmitDraftContinueEditingBtnClick}
/>
</Menu>
}
minimal={true}
interactionKind={PopoverInteractionKind.CLICK}
position={Position.BOTTOM_LEFT}
>
<Button
disabled={isSubmitting}
rightIcon={<Icon icon="arrow-drop-up-16" iconSize={20} />}
/>
</Popover>
</ButtonGroup>
</If>
{/* ----------- Save and New ----------- */}
<If condition={expense && expensePublished}>
<ButtonGroup>
<Button
disabled={isSubmitting}
intent={Intent.PRIMARY}
onClick={handleSubmitPublishBtnClick}
text={<T id={'save'} />}
/>
<Popover
content={
<Menu>
<MenuItem
text={<T id={'save_and_new'} />}
onClick={handleSubmitPublishAndNewBtnClick}
/>
</Menu>
}
minimal={true}
interactionKind={PopoverInteractionKind.CLICK}
position={Position.BOTTOM_LEFT}
>
<Button
disabled={isSubmitting}
intent={Intent.PRIMARY}
rightIcon={<Icon icon="arrow-drop-up-16" iconSize={20} />}
/>
</Popover>
</ButtonGroup>
</If>
{/* ----------- Clear & Reset----------- */}
<Button
className={'ml1'}
disabled={isSubmitting}
onClick={handleClearBtnClick}
text={expense ? <T id={'reset'} /> : <T id={'clear'} />}
/>
{/* ----------- Cancel ----------- */}
<Button
className={'ml1'}
onClick={handleCancelBtnClick}
text={<T id={'cancel'} />}
/>
</div>
);
}

View File

@@ -0,0 +1,218 @@
import React, { useMemo, useEffect, useState, useCallback } from 'react';
import { Intent } from '@blueprintjs/core';
import { useIntl } from 'react-intl';
import { defaultTo, pick, sumBy } from 'lodash';
import { Formik, Form } from 'formik';
import moment from 'moment';
import classNames from 'classnames';
import { useHistory } from 'react-router-dom';
import { CLASSES } from 'common/classes';
import ExpenseFormHeader from './ExpenseFormHeader';
import ExpenseFormBody from './ExpenseFormBody';
import ExpenseFloatingFooter from './ExpenseFloatingActions';
import ExpenseFormFooter from './ExpenseFormFooter';
import { useExpenseFormContext } from './ExpenseFormPageProvider';
import withDashboardActions from 'containers/Dashboard/withDashboardActions';
import withMediaActions from 'containers/Media/withMediaActions';
import withSettings from 'containers/Settings/withSettings';
import AppToaster from 'components/AppToaster';
import {
CreateExpenseFormSchema,
EditExpenseFormSchema,
} from './ExpenseForm.schema';
import { transformErrors } from './utils';
import { compose, repeatValue, orderingLinesIndexes } from 'utils';
const MIN_LINES_NUMBER = 4;
const defaultCategory = {
index: 0,
amount: '',
expense_account_id: '',
description: '',
};
const defaultInitialValues = {
payment_account_id: '',
beneficiary: '',
payment_date: moment(new Date()).format('YYYY-MM-DD'),
description: '',
reference_no: '',
currency_code: '',
publish: '',
categories: [...repeatValue(defaultCategory, MIN_LINES_NUMBER)],
};
/**
* Expense form.
*/
function ExpenseForm({
// #withDashboard
changePageTitle,
// #withSettings
baseCurrency,
preferredPaymentAccount,
}) {
const {
editExpenseMutate,
createExpenseMutate,
expense,
expenseId,
} = useExpenseFormContext();
const isNewMode = !expenseId;
const [submitPayload, setSubmitPayload] = useState({});
const { formatMessage } = useIntl();
const history = useHistory();
useEffect(() => {
if (isNewMode) {
changePageTitle(formatMessage({ id: 'new_expense' }));
} else {
changePageTitle(formatMessage({ id: 'edit_expense' }));
}
}, [changePageTitle, isNewMode, formatMessage]);
const initialValues = useMemo(
() => ({
...(expense
? {
...pick(expense, Object.keys(defaultInitialValues)),
categories: [
...expense.categories.map((category) => ({
...pick(category, Object.keys(defaultCategory)),
})),
],
}
: {
...defaultInitialValues,
currency_code: baseCurrency,
payment_account_id: defaultTo(preferredPaymentAccount, ''),
categories: orderingLinesIndexes(defaultInitialValues.categories),
}),
}),
[expense, baseCurrency, preferredPaymentAccount],
);
// Handle form submit.
const handleSubmit = (values, { setSubmitting, setErrors, resetForm }) => {
setSubmitting(true);
const totalAmount = sumBy(values.categories, 'amount');
if (totalAmount <= 0) {
AppToaster.show({
message: formatMessage({
id: 'amount_cannot_be_zero_or_empty',
}),
intent: Intent.DANGER,
});
return;
}
const categories = values.categories.filter(
(category) =>
category.amount && category.index && category.expense_account_id,
);
const form = {
...values,
publish: submitPayload.publish,
categories,
};
// Handle request success.
const handleSuccess = (response) => {
AppToaster.show({
message: formatMessage(
{
id: isNewMode
? 'the_expense_has_been_created_successfully'
: 'the_expense_has_been_edited_successfully',
},
{ number: values.payment_account_id },
),
intent: Intent.SUCCESS,
});
setSubmitting(false);
if (submitPayload.redirect) {
history.push('/expenses');
}
if (submitPayload.resetForm) {
resetForm();
}
};
// Handle request error
const handleError = (error) => {
transformErrors(error, { setErrors });
setSubmitting(false);
};
if (isNewMode) {
createExpenseMutate(form).then(handleSuccess).catch(handleError);
} else {
editExpenseMutate(expense.id, form)
.then(handleSuccess)
.catch(handleError);
}
};
const handleCancelClick = useCallback(() => {
history.goBack();
}, [history]);
const handleSubmitClick = useCallback(
(event, payload) => {
setSubmitPayload({ ...payload });
},
[setSubmitPayload],
);
return (
<div
className={classNames(
CLASSES.PAGE_FORM,
CLASSES.PAGE_FORM_STRIP_STYLE,
CLASSES.PAGE_FORM_EXPENSE,
)}
>
<Formik
validationSchema={isNewMode
? CreateExpenseFormSchema
: EditExpenseFormSchema}
initialValues={initialValues}
onSubmit={handleSubmit}
>
{({ isSubmitting, values }) => (
<Form>
<ExpenseFormHeader />
<ExpenseFormBody />
<ExpenseFormFooter />
<ExpenseFloatingFooter
isSubmitting={isSubmitting}
expense={expenseId}
expensePublished={values.publish}
onCancelClick={handleCancelClick}
onSubmitClick={handleSubmitClick}
/>
</Form>
)}
</Formik>
</div>
);
}
export default compose(
withDashboardActions,
withMediaActions,
withSettings(({ organizationSettings, expenseSettings }) => ({
baseCurrency: organizationSettings?.baseCurrency,
preferredPaymentAccount: parseInt(
expenseSettings?.preferredPaymentAccount,
10,
),
})),
)(ExpenseForm);

View File

@@ -0,0 +1,42 @@
import * as Yup from 'yup';
import { formatMessage } from 'services/intl';
import { DATATYPES_LENGTH } from 'common/dataTypes';
import { isBlank } from 'utils';
const Schema = Yup.object().shape({
beneficiary: Yup.string().label(formatMessage({ id: 'beneficiary' })),
payment_account_id: Yup.number()
.required()
.label(formatMessage({ id: 'payment_account_' })),
payment_date: Yup.date()
.required()
.label(formatMessage({ id: 'payment_date_' })),
reference_no: Yup.string().min(1).max(DATATYPES_LENGTH.STRING).nullable(),
currency_code: Yup.string()
.nullable()
.max(3)
.label(formatMessage({ id: 'currency_code' })),
description: Yup.string()
.trim()
.min(1)
.max(DATATYPES_LENGTH.TEXT)
.nullable()
.label(formatMessage({ id: 'description' })),
publish: Yup.boolean(),
categories: Yup.array().of(
Yup.object().shape({
index: Yup.number().min(1).max(DATATYPES_LENGTH.INT_10).nullable(),
amount: Yup.number().nullable(),
expense_account_id: Yup.number()
.nullable()
.when(['amount'], {
is: (amount) => !isBlank(amount),
then: Yup.number().required(),
}),
description: Yup.string().max(DATATYPES_LENGTH.TEXT).nullable(),
}),
),
});
export const CreateExpenseFormSchema = Schema;
export const EditExpenseFormSchema = Schema;

View File

@@ -0,0 +1,12 @@
import React from 'react';
import classNames from 'classnames';
import { CLASSES } from 'common/classes';
import ExpenseFormEntriesField from './ExpenseFormEntriesField';
export default function ExpenseFormBody() {
return (
<div className={classNames(CLASSES.PAGE_FORM_BODY)}>
<ExpenseFormEntriesField />
</div>
)
}

View File

@@ -0,0 +1,143 @@
import React, { useState, useMemo, useEffect, useCallback } from 'react';
import { Button, Intent, Position, Tooltip } from '@blueprintjs/core';
import { FormattedMessage as T, useIntl } from 'react-intl';
import { omit } from 'lodash';
import { DataTableEditable, Icon } from 'components';
import { Hint } from 'components';
import {
formattedAmount,
transformUpdatedRows,
saveInvoke,
} from 'utils';
import {
AccountsListFieldCell,
MoneyFieldCell,
InputGroupCell,
} from 'components/DataTableCells';
import { useExpenseFormContext } from './ExpenseFormPageProvider';
import { useExpenseFormTableColumns } from './components';
export default function ExpenseTable({
// #ownPorps
onClickRemoveRow,
onClickAddNewRow,
onClickClearAllLines,
entries,
error,
onChange,
}) {
const [rows, setRows] = useState([]);
const { formatMessage } = useIntl();
const { accounts } = useExpenseFormContext();
useEffect(() => {
setRows([...entries.map((e) => ({ ...e, rowType: 'editor' }))]);
}, [entries]);
// Final table rows editor rows and total and final blank row.
const tableRows = useMemo(() => [...rows, { rowType: 'total' }], [rows]);
// Memorized data table columns.
const columns = useExpenseFormTableColumns();
// Handles update datatable data.
const handleUpdateData = useCallback(
(rowIndex, columnIdOrObj, value) => {
const newRows = transformUpdatedRows(
rows,
rowIndex,
columnIdOrObj,
value,
);
saveInvoke(
onChange,
newRows
.filter((row) => row.rowType === 'editor')
.map((row) => ({
...omit(row, ['rowType']),
})),
);
},
[rows, onChange],
);
// Handles click remove datatable row.
const handleRemoveRow = useCallback(
(rowIndex) => {
// Can't continue if there is just one row line or less.
if (rows.length <= 1) {
return;
}
const removeIndex = parseInt(rowIndex, 10);
const newRows = rows.filter((row, index) => index !== removeIndex);
saveInvoke(
onChange,
newRows
.filter((row) => row.rowType === 'editor')
.map((row, index) => ({
...omit(row, ['rowType']),
index: index + 1,
})),
);
saveInvoke(onClickRemoveRow, removeIndex);
},
[rows, onChange, onClickRemoveRow],
);
// Invoke when click on add new line button.
const onClickNewRow = () => {
saveInvoke(onClickAddNewRow);
};
// Invoke when click on clear all lines button.
const handleClickClearAllLines = () => {
saveInvoke(onClickClearAllLines);
};
// Rows classnames callback.
const rowClassNames = useCallback(
(row) => ({
'row--total': rows.length === row.index + 1,
}),
[rows],
);
return (
<DataTableEditable
columns={columns}
data={tableRows}
rowClassNames={rowClassNames}
sticky={true}
payload={{
accounts: accounts,
errors: error,
updateData: handleUpdateData,
removeRow: handleRemoveRow,
autoFocus: ['expense_account_id', 0],
}}
actions={
<>
<Button
small={true}
className={'button--secondary button--new-line'}
onClick={onClickNewRow}
>
<T id={'new_lines'} />
</Button>
<Button
small={true}
className={'button--secondary button--clear-lines ml1'}
onClick={handleClickClearAllLines}
>
<T id={'clear_all_lines'} />
</Button>
</>
}
totalRow={true}
/>
);
}

View File

@@ -0,0 +1,32 @@
import { FastField } from 'formik';
import React from 'react';
import ExpenseFormEntries from './ExpenseFormEntries';
import { orderingLinesIndexes, repeatValue } from 'utils';
export default function ExpenseFormEntriesField({
defaultRow,
linesNumber = 4,
}) {
return (
<FastField name={'categories'}>
{({ form, field: { value }, meta: { error, touched } }) => (
<ExpenseFormEntries
entries={value}
error={error}
onChange={(entries) => {
form.setFieldValue('categories', entries);
}}
onClickAddNewRow={() => {
form.setFieldValue('categories', [...value, defaultRow]);
}}
onClickClearAllLines={() => {
form.setFieldValue(
'categories',
orderingLinesIndexes([...repeatValue(defaultRow, linesNumber)])
);
}}
/>
)}
</FastField>
);
}

View File

@@ -0,0 +1,39 @@
import React from 'react';
import { FastField } from 'formik';
import { FormGroup, TextArea } from '@blueprintjs/core';
import { FormattedMessage as T } from 'react-intl';
import classNames from 'classnames';
import { inputIntent } from 'utils';
import { Row, Dragzone, Col } from 'components';
import { CLASSES } from 'common/classes';
export default function ExpenseFormFooter({}) {
return (
<div className={classNames(CLASSES.PAGE_FORM_FOOTER)}>
<Row>
<Col md={8}>
<FastField name={'description'}>
{({ field, meta: { error, touched } }) => (
<FormGroup
label={<T id={'description'} />}
className={'form-group--description'}
intent={inputIntent({ error, touched })}
>
<TextArea growVertically={true} {...field} />
</FormGroup>
)}
</FastField>
</Col>
<Col md={4}>
<Dragzone
initialFiles={[]}
// onDrop={handleDropFiles}
// onDeleteFile={handleDeleteFile}
hint={'Attachments: Maxiumum size: 20MB'}
/>
</Col>
</Row>
</div>
);
}

View File

@@ -0,0 +1,30 @@
import React, { useMemo } from 'react';
import classNames from 'classnames';
import { sumBy } from 'lodash';
import { useFormikContext } from 'formik';
import { CLASSES } from 'common/classes';
import ExpenseFormHeaderFields from './ExpenseFormHeaderFields';
import { PageFormBigNumber } from 'components';
// Expense form header.
export default function ExpenseFormHeader() {
const { values } = useFormikContext();
// Calculates the expense entries amount.
const totalExpenseAmount = useMemo(() => sumBy(values.categories, 'amount'), [
values.categories,
]);
return (
<div className={classNames(CLASSES.PAGE_FORM_HEADER)}>
<ExpenseFormHeaderFields />
<PageFormBigNumber
label={'Expense Amount'}
amount={totalExpenseAmount}
currencyCode={values?.currency_code}
/>
</div>
);
}

View File

@@ -0,0 +1,146 @@
import React from 'react';
import { InputGroup, FormGroup, Position, Classes } from '@blueprintjs/core';
import { DateInput } from '@blueprintjs/datetime';
import { FastField } from 'formik';
import { FormattedMessage as T } from 'react-intl';
import { CLASSES } from 'common/classes';
import {
momentFormatter,
compose,
tansformDateValue,
inputIntent,
handleDateChange,
} from 'utils';
import classNames from 'classnames';
import {
CurrencySelectList,
ContactSelecetList,
ErrorMessage,
AccountsSelectList,
FieldRequiredHint,
Hint,
} from 'components';
import { ACCOUNT_PARENT_TYPE } from 'common/accountTypes';
import { useExpenseFormContext } from './ExpenseFormPageProvider';
/**
* Expense form header.
*/
export default function ExpenseFormHeader({}) {
const { currencies, accounts, customers } = useExpenseFormContext();
return (
<div className={classNames(CLASSES.PAGE_FORM_HEADER_FIELDS)}>
<FastField name={'payment_date'}>
{({ form, field: { value }, meta: { error, touched } }) => (
<FormGroup
label={<T id={'payment_date'} />}
labelInfo={<Hint />}
className={classNames('form-group--select-list', Classes.FILL)}
intent={inputIntent({ error, touched })}
helperText={<ErrorMessage name="payment_date" />}
inline={true}
>
<DateInput
{...momentFormatter('YYYY/MM/DD')}
value={tansformDateValue(value)}
onChange={handleDateChange((formattedDate) => {
form.setFieldValue('payment_date', formattedDate);
})}
popoverProps={{ position: Position.BOTTOM, minimal: true }}
/>
</FormGroup>
)}
</FastField>
<FastField name={'payment_account_id'}>
{({ form, field: { value }, meta: { error, touched } }) => (
<FormGroup
label={<T id={'payment_account'} />}
className={classNames(
'form-group--payment_account',
'form-group--select-list',
Classes.FILL,
)}
labelInfo={<FieldRequiredHint />}
intent={inputIntent({ error, touched })}
helperText={<ErrorMessage name={'payment_account_id'} />}
inline={true}
>
<AccountsSelectList
accounts={accounts}
onAccountSelected={(account) => {
form.setFieldValue('payment_account_id', account.id);
}}
defaultSelectText={<T id={'select_payment_account'} />}
selectedAccountId={value}
filterByParentTypes={[ACCOUNT_PARENT_TYPE.CURRENT_ASSET]}
/>
</FormGroup>
)}
</FastField>
<FastField name={'currency_code'}>
{({ form, field: { value }, meta: { error, touched } }) => (
<FormGroup
label={<T id={'currency'} />}
className={classNames(
'form-group--select-list',
'form-group--currency',
Classes.FILL,
)}
intent={inputIntent({ error, touched })}
helperText={<ErrorMessage name="currency_code" />}
inline={true}
>
<CurrencySelectList
currenciesList={currencies}
selectedCurrencyCode={value}
onCurrencySelected={(currencyItem) => {
form.setFieldValue('currency_code', currencyItem.currency_code);
}}
defaultSelectText={value}
/>
</FormGroup>
)}
</FastField>
<FastField name={'reference_no'}>
{({ form, field, meta: { error, touched } }) => (
<FormGroup
label={<T id={'reference_no'} />}
className={classNames('form-group--ref_no', Classes.FILL)}
intent={inputIntent({ error, touched })}
helperText={<ErrorMessage name="reference_no" />}
inline={true}
>
<InputGroup minimal={true} {...field} />
</FormGroup>
)}
</FastField>
<FastField name={'customer_id'}>
{({ form, field: { value }, meta: { error, touched } }) => (
<FormGroup
label={<T id={'customer'} />}
className={classNames('form-group--select-list', Classes.FILL)}
labelInfo={<Hint />}
intent={inputIntent({ error, touched })}
helperText={<ErrorMessage name={'assign_to_customer'} />}
inline={true}
>
<ContactSelecetList
contactsList={customers}
selectedContactId={value}
defaultSelectText={<T id={'select_customer_account'} />}
onContactSelected={(customer) => {
form.setFieldValue('customer_id', customer.id);
}}
/>
</FormGroup>
)}
</FastField>
</div>
);
}

View File

@@ -0,0 +1,48 @@
import React, { useCallback, useEffect } from 'react';
import { useParams, useHistory } from 'react-router-dom';
import ExpenseForm from './ExpenseForm';
import { ExpenseFormPageProvider } from './ExpenseFormPageProvider';
import withDashboardActions from 'containers/Dashboard/withDashboardActions';
import { compose } from 'utils';
import 'style/pages/Expense/PageForm.scss';
/**
* Expense page form.
*/
function ExpenseFormPage({
// #withDashboardActions
setSidebarShrink,
resetSidebarPreviousExpand,
setDashboardBackLink,
}) {
const history = useHistory();
const { id } = useParams();
useEffect(() => {
// Shrink the sidebar by foce.
setSidebarShrink();
// Show the back link on dashboard topbar.
setDashboardBackLink(true);
return () => {
// Reset the sidebar to the previous status.
resetSidebarPreviousExpand();
// Hide the back link on dashboard topbar.
setDashboardBackLink(false);
};
}, [resetSidebarPreviousExpand, setSidebarShrink, setDashboardBackLink]);
return (
<ExpenseFormPageProvider expenseId={id}>
<ExpenseForm />
</ExpenseFormPageProvider>
);
}
export default compose(
withDashboardActions,
)(ExpenseFormPage);

View File

@@ -0,0 +1,71 @@
import React, { createContext } from 'react';
import DashboardInsider from 'components/Dashboard/DashboardInsider';
import {
useCurrencies,
useCustomers,
useExpense,
useAccounts,
useCreateExpense,
useEditExpense,
} from 'hooks/query';
const ExpenseFormPageContext = createContext();
/**
* Accounts chart data provider.
*/
function ExpenseFormPageProvider({ expenseId, ...props }) {
const { data: currencies, isFetching: isCurrenciesLoading } = useCurrencies();
// Fetches customers list.
const {
data: { customers },
isFetching: isFieldsLoading,
} = useCustomers();
// Fetch the expense details.
const { data: expense, isFetching: isExpenseLoading } = useExpense(expenseId);
// Fetch accounts list.
const { data: accounts, isFetching: isAccountsLoading } = useAccounts();
// Create and edit expense mutate.
const { mutateAsync: createExpenseMutate } = useCreateExpense();
const { mutateAsync: editExpenseMutate } = useEditExpense();
// Provider payload.
const provider = {
expenseId,
currencies,
customers,
expense,
accounts,
isCurrenciesLoading,
isExpenseLoading,
isFieldsLoading,
isAccountsLoading,
createExpenseMutate,
editExpenseMutate,
};
return (
<DashboardInsider
loading={
isCurrenciesLoading ||
isExpenseLoading ||
isFieldsLoading ||
isAccountsLoading
}
name={'expense-form'}
>
<ExpenseFormPageContext.Provider value={provider} {...props} />
</DashboardInsider>
);
}
const useExpenseFormContext = () => React.useContext(ExpenseFormPageContext);
export { ExpenseFormPageProvider, useExpenseFormContext };

View File

@@ -0,0 +1,132 @@
const ExpenseCategoryHeaderCell = () => {
return (
<>
<T id={'expense_category'} />
<Hint />
</>
);
};
// Actions cell renderer.
const ActionsCellRenderer = ({
row: { index },
column: { id },
cell: { value: initialValue },
data,
payload,
}) => {
if (data.length <= index + 1) {
return '';
}
const onClickRemoveRole = () => {
payload.removeRow(index);
};
return (
<Tooltip content={<T id={'remove_the_line'} />} position={Position.LEFT}>
<Button
icon={<Icon icon="times-circle" iconSize={14} />}
iconSize={14}
className="ml2"
minimal={true}
intent={Intent.DANGER}
onClick={onClickRemoveRole}
/>
</Tooltip>
);
};
// Total text cell renderer.
const TotalExpenseCellRenderer = (chainedComponent) => (props) => {
if (props.data.length <= props.row.index + 1) {
return (
<span>
<T id={'total_currency'} values={{ currency: 'USD' }} />
</span>
);
}
return chainedComponent(props);
};
/**
* Note cell renderer.
*/
const NoteCellRenderer = (chainedComponent) => (props) => {
if (props.data.length === props.row.index + 1) {
return '';
}
return chainedComponent(props);
};
/**
* Total amount cell renderer.
*/
const TotalAmountCellRenderer = (chainedComponent, type) => (props) => {
if (props.data.length === props.row.index + 1) {
const total = props.data.reduce((total, entry) => {
const amount = parseInt(entry[type], 10);
const computed = amount ? total + amount : total;
return computed;
}, 0);
return <span>{formattedAmount(total, 'USD')}</span>;
}
return chainedComponent(props);
};
export function useExpenseFormTableColumns() {
return React.useMemo(
() => [
{
Header: '#',
accessor: 'index',
Cell: ({ row: { index } }) => <span>{index + 1}</span>,
className: 'index',
width: 40,
disableResizing: true,
disableSortBy: true,
},
{
Header: ExpenseCategoryHeaderCell,
id: 'expense_account_id',
accessor: 'expense_account_id',
Cell: TotalExpenseCellRenderer(AccountsListFieldCell),
className: 'expense_account_id',
disableSortBy: true,
width: 40,
filterAccountsByRootType: ['expense'],
},
{
Header: formatMessage({ id: 'amount_currency' }, { currency: 'USD' }),
accessor: 'amount',
Cell: TotalAmountCellRenderer(MoneyFieldCell, 'amount'),
disableSortBy: true,
width: 40,
className: 'amount',
},
{
Header: formatMessage({ id: 'description' }),
accessor: 'description',
Cell: NoteCellRenderer(InputGroupCell),
disableSortBy: true,
className: 'description',
width: 100,
},
{
Header: '',
accessor: 'action',
Cell: ActionsCellRenderer,
className: 'actions',
disableSortBy: true,
disableResizing: true,
width: 45,
},
],
[formatMessage],
)
}

View File

@@ -0,0 +1,21 @@
import { AppToaster } from 'components';
import { formatMessage } from 'services/intl';
const ERROR = {
EXPENSE_ALREADY_PUBLISHED: 'EXPENSE.ALREADY.PUBLISHED',
};
// Transform API errors in toasts messages.
export const transformErrors = (errors, { setErrors }) => {
const hasError = (errorType) => errors.some((e) => e.type === errorType);
if (hasError(ERROR.EXPENSE_ALREADY_PUBLISHED)) {
setErrors(
AppToaster.show({
message: formatMessage({
id: 'the_expense_is_already_published',
}),
}),
);
}
};