feat: expand sidebar once open form editor page.

feat: rounding money amount.
feat: optimize page form structure.
feat: refactoring make journal and expense form with FastField component.
This commit is contained in:
Ahmed Bouhuolia
2020-11-29 00:06:59 +02:00
parent 53dd447540
commit 011542e2a3
118 changed files with 3883 additions and 2660 deletions

View File

@@ -141,6 +141,7 @@ export default function ExpenseFloatingFooter({
>
<Button rightIcon={<Icon icon="arrow-drop-up-16" iconSize={20} />} />
</Popover>
{/* ----------- Clear ----------- */}
<Button
className={'ml1'}

View File

@@ -1,21 +1,19 @@
import React, {
useMemo,
useState,
useEffect,
useRef,
useCallback,
} from 'react';
import * as Yup from 'yup';
import { useFormik } from 'formik';
import { Intent } from '@blueprintjs/core';
import { useIntl } from 'react-intl';
import { defaultTo, pick } from 'lodash';
import { Formik, Form } from 'formik';
import moment from 'moment';
import { Intent, FormGroup, TextArea } from '@blueprintjs/core';
import { FormattedMessage as T, useIntl } from 'react-intl';
import { pick } from 'lodash';
import { useHistory } from 'react-router-dom';
import classNames from 'classnames';
import { CLASSES } from 'common/classes';
import ExpenseFormHeader from './ExpenseFormHeader';
import ExpenseTable from './ExpenseTable';
import ExpenseFormBody from './ExpenseFormBody';
import ExpenseFloatingFooter from './ExpenseFloatingActions';
import ExpenseFormFooter from './ExpenseFormFooter';
import withExpensesActions from 'containers/Expenses/withExpensesActions';
import withExpenseDetail from 'containers/Expenses/withExpenseDetail';
@@ -30,12 +28,28 @@ import {
CreateExpenseFormSchema,
EditExpenseFormSchema,
} from './ExpenseForm.schema';
import useMedia from 'hooks/useMedia';
import { compose, repeatValue, transformToForm } from 'utils';
import {
transformErrors,
} from './utils';
import { compose, repeatValue, orderingLinesIndexes } from 'utils';
const MIN_LINES_NUMBER = 4;
const ERROR = {
EXPENSE_ALREADY_PUBLISHED: 'EXPENSE.ALREADY.PUBLISHED',
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: '',
categories: [...repeatValue(defaultCategory, MIN_LINES_NUMBER)],
};
/**
@@ -46,106 +60,49 @@ function ExpenseForm({
requestSubmitMedia,
requestDeleteMedia,
//#withExpensesActions
// #withExpensesActions
requestSubmitExpense,
requestEditExpense,
requestFetchExpensesTable,
// #withDashboard
changePageTitle,
changePageSubtitle,
//#withExpenseDetail
// #withExpenseDetail
expense,
// #withSettings
baseCurrency,
preferredPaymentAccount,
// #own Props
// #ownProps
expenseId,
onFormSubmit,
onCancelForm,
}) {
const [submitPayload, setSubmitPayload] = useState({});
const history = useHistory();
const isNewMode = !expenseId;
const { formatMessage } = useIntl();
const {
setFiles,
saveMedia,
deletedFiles,
setDeletedFiles,
deleteMedia,
} = useMedia({
saveCallback: requestSubmitMedia,
deleteCallback: requestDeleteMedia,
});
const validationSchema = isNewMode
? CreateExpenseFormSchema
: EditExpenseFormSchema;
const handleDropFiles = useCallback((_files) => {
setFiles(_files.filter((file) => file.uploaded === false));
}, []);
const savedMediaIds = useRef([]);
const clearSavedMediaIds = () => {
savedMediaIds.current = [];
};
useEffect(() => {
if (expense && expense.id) {
changePageTitle(formatMessage({ id: 'edit_expense' }));
} else {
if (isNewMode) {
changePageTitle(formatMessage({ id: 'new_expense' }));
} else {
changePageTitle(formatMessage({ id: 'edit_expense' }));
}
}, [changePageTitle, expense, formatMessage]);
const saveInvokeSubmit = useCallback(
(payload) => {
onFormSubmit && onFormSubmit(payload);
},
[onFormSubmit],
);
const defaultCategory = useMemo(
() => ({
index: 0,
amount: 0,
expense_account_id: null,
description: '',
}),
[],
);
const defaultInitialValues = useMemo(
() => ({
payment_account_id: parseInt(preferredPaymentAccount),
beneficiary: '',
payment_date: moment(new Date()).format('YYYY-MM-DD'),
description: '',
reference_no: '',
currency_code: baseCurrency,
categories: [...repeatValue(defaultCategory, MIN_LINES_NUMBER)],
}),
[defaultCategory],
);
const orderingCategoriesIndex = (categories) => {
return categories.map((category, index) => ({
...category,
index: index + 1,
}));
};
}, [changePageTitle, isNewMode, formatMessage]);
const initialValues = useMemo(
() => ({
...(expense
? {
...pick(expense, Object.keys(defaultInitialValues)),
currency_code: baseCurrency,
payment_account_id: defaultTo(preferredPaymentAccount, ''),
categories: [
...expense.categories.map((category) => ({
...pick(category, Object.keys(defaultCategory)),
@@ -158,225 +115,87 @@ function ExpenseForm({
}
: {
...defaultInitialValues,
categories: orderingCategoriesIndex(
categories: orderingLinesIndexes(
defaultInitialValues.categories,
),
}),
}),
[expense, defaultInitialValues, defaultCategory],
[expense, baseCurrency, preferredPaymentAccount],
);
const initialAttachmentFiles = useMemo(() => {
return expense && expense.media
? expense.media.map((attach) => ({
preview: attach.attachment_file,
uploaded: true,
metadata: { ...attach },
}))
: [];
}, [expense]);
const handleSubmit = (values, { setSubmitting, setErrors, resetForm }) => {
setSubmitting(true);
const totalAmount = values.categories.reduce((total, item) => {
return total + item.amount;
}, 0);
// Transform API errors in toasts messages.
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',
}),
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: 1,
categories,
};
// Handle request success.
const handleSuccess = (response) => {
AppToaster.show({
message: formatMessage(
{ id: isNewMode ?
'the_expense_has_been_successfully_created' :
'the_expense_has_been_successfully_edited' },
{ number: values.payment_account_id },
),
intent: Intent.SUCCESS,
});
setSubmitting(false);
resetForm();
};
// Handle request error
const handleError = (error) => {
transformErrors(error, { setErrors });
setSubmitting(false);
};
if (isNewMode) {
requestSubmitExpense(form).then(handleSuccess).catch(handleError);
} else {
requestEditExpense(expense.id, form).then(handleSuccess).catch(handleError);
}
};
const {
values,
errors,
touched,
isSubmitting,
setFieldValue,
handleSubmit,
getFieldProps,
submitForm,
resetForm,
} = useFormik({
enableReinitialize: true,
validationSchema,
initialValues: {
...initialValues,
},
onSubmit: (values, { setSubmitting, setErrors, resetForm }) => {
setSubmitting(true);
const totalAmount = values.categories.reduce((total, item) => {
return total + item.amount;
}, 0);
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,
};
const saveExpense = (mdeiaIds) =>
new Promise((resolve, reject) => {
const requestForm = { ...form, media_ids: mdeiaIds };
if (expense && expense.id) {
requestEditExpense(expense.id, requestForm)
.then((response) => {
AppToaster.show({
message: formatMessage(
{ id: 'the_expense_has_been_successfully_edited' },
{ number: values.payment_account_id },
),
intent: Intent.SUCCESS,
});
setSubmitting(false);
saveInvokeSubmit({ action: 'update', ...submitPayload });
clearSavedMediaIds([]);
resetForm();
})
.catch((errors) => {
transformErrors(errors, { setErrors });
setSubmitting(false);
});
} else {
requestSubmitExpense(requestForm)
.then((response) => {
AppToaster.show({
message: formatMessage(
{ id: 'the_expense_has_been_successfully_created' },
{ number: values.payment_account_id },
),
intent: Intent.SUCCESS,
});
setSubmitting(false);
if (submitPayload.resetForm) {
resetForm();
}
saveInvokeSubmit({ action: 'new', ...submitPayload });
clearSavedMediaIds();
})
.catch((errors) => {
transformErrors(errors, { setErrors });
setSubmitting(false);
});
}
});
Promise.all([saveMedia(), deleteMedia()])
.then(([savedMediaResponses]) => {
const mediaIds = savedMediaResponses.map((res) => res.data.media.id);
savedMediaIds.current = mediaIds;
return savedMediaResponses;
})
.then(() => {
return saveExpense(savedMediaIds.current);
});
},
});
const handleSubmitClick = useCallback(
(event, payload) => {
setSubmitPayload({ ...payload });
},
[setSubmitPayload],
);
const handleCancelClick = useCallback(() => {
history.goBack();
}, [history]);
const handleDeleteFile = useCallback(
(_deletedFiles) => {
_deletedFiles.forEach((deletedFile) => {
if (deletedFile.uploaded && deletedFile.metadata.id) {
setDeletedFiles([...deletedFiles, deletedFile.metadata.id]);
}
});
},
[setDeletedFiles, deletedFiles],
);
// Handle click on add a new line/row.
const handleClickAddNewRow = () => {
setFieldValue(
'categories',
orderingCategoriesIndex([...values.categories, defaultCategory]),
);
};
const handleClearAllLines = () => {
setFieldValue(
'categories',
orderingCategoriesIndex([
...repeatValue(defaultCategory, MIN_LINES_NUMBER),
]),
);
};
return (
<div className={'expense-form'}>
<form onSubmit={handleSubmit}>
<ExpenseFormHeader
errors={errors}
touched={touched}
values={values}
setFieldValue={setFieldValue}
getFieldProps={getFieldProps}
/>
<ExpenseTable
categories={values.categories}
onClickAddNewRow={handleClickAddNewRow}
onClickClearAllLines={handleClearAllLines}
errors={errors}
setFieldValue={setFieldValue}
defaultRow={defaultCategory}
/>
<div class="expense-form-footer">
<FormGroup
label={<T id={'description'} />}
className={'form-group--description'}
>
<TextArea growVertically={true} {...getFieldProps('description')} />
</FormGroup>
<Dragzone
initialFiles={initialAttachmentFiles}
onDrop={handleDropFiles}
onDeleteFile={handleDeleteFile}
hint={'Attachments: Maxiumum size: 20MB'}
/>
</div>
<ExpenseFloatingFooter
isSubmitting={isSubmitting}
onSubmitClick={handleSubmitClick}
onCancelClick={handleCancelClick}
onSubmitForm={submitForm}
onResetForm={resetForm}
expense={expense}
/>
</form>
<div className={classNames(
CLASSES.PAGE_FORM,
CLASSES.PAGE_FORM_STRIP_STYLE,
CLASSES.PAGE_FORM_EXPENSE
)}>
<Formik
validationSchema={validationSchema}
initialValues={initialValues}
onSubmit={handleSubmit}
>
{({ isSubmitting, values }) => (
<Form>
<ExpenseFormHeader />
<ExpenseFormBody />
<ExpenseFormFooter />
<ExpenseFloatingFooter />
</Form>
)}
</Formik>
</div>
);
}
@@ -389,6 +208,6 @@ export default compose(
withExpenseDetail(),
withSettings(({ organizationSettings, expenseSettings }) => ({
baseCurrency: organizationSettings?.baseCurrency,
preferredPaymentAccount: expenseSettings?.preferredPaymentAccount,
preferredPaymentAccount: parseInt(expenseSettings?.preferredPaymentAccount, 10),
})),
)(ExpenseForm);

View File

@@ -1,6 +1,7 @@
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' })),
@@ -25,11 +26,11 @@ const Schema = Yup.object().shape({
categories: Yup.array().of(
Yup.object().shape({
index: Yup.number().min(1).max(DATATYPES_LENGTH.INT_10).nullable(),
amount: Yup.number().decimalScale(13).nullable(),
amount: Yup.number().nullable(),
expense_account_id: Yup.number()
.nullable()
.when(['amount'], {
is: (amount) => amount,
is: (amount) => !isBlank(amount),
then: Yup.number().required(),
}),
description: Yup.string().max(DATATYPES_LENGTH.TEXT).nullable(),

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

@@ -2,11 +2,19 @@ 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 classNames from 'classnames';
import { CLASSES } from 'common/classes';
import DataTable from 'components/DataTable';
import Icon from 'components/Icon';
import { Hint } from 'components';
import { compose, formattedAmount, transformUpdatedRows } from 'utils';
import {
compose,
formattedAmount,
transformUpdatedRows,
saveInvoke,
} from 'utils';
import {
AccountsListFieldCell,
MoneyFieldCell,
@@ -92,17 +100,17 @@ function ExpenseTable({
onClickRemoveRow,
onClickAddNewRow,
onClickClearAllLines,
defaultRow,
categories,
errors,
setFieldValue,
entries,
error,
onChange,
}) {
const [rows, setRows] = useState([]);
const { formatMessage } = useIntl();
useEffect(() => {
setRows([...categories.map((e) => ({ ...e, rowType: 'editor' }))]);
}, [categories]);
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]);
@@ -126,8 +134,7 @@ function ExpenseTable({
Cell: TotalExpenseCellRenderer(AccountsListFieldCell),
className: 'expense_account_id',
disableSortBy: true,
disableResizing: true,
width: 250,
width: 40,
filterAccountsByRootType: ['expense'],
},
{
@@ -135,8 +142,7 @@ function ExpenseTable({
accessor: 'amount',
Cell: TotalAmountCellRenderer(MoneyFieldCell, 'amount'),
disableSortBy: true,
disableResizing: true,
width: 180,
width: 40,
className: 'amount',
},
{
@@ -145,6 +151,7 @@ function ExpenseTable({
Cell: NoteCellRenderer(InputGroupCell),
disableSortBy: true,
className: 'description',
width: 100,
},
{
Header: '',
@@ -168,8 +175,8 @@ function ExpenseTable({
columnIdOrObj,
value,
);
setFieldValue(
'categories',
saveInvoke(
onChange,
newRows
.filter((row) => row.rowType === 'editor')
.map((row) => ({
@@ -177,7 +184,7 @@ function ExpenseTable({
})),
);
},
[rows, setFieldValue],
[rows, onChange],
);
// Handles click remove datatable row.
@@ -187,12 +194,11 @@ function ExpenseTable({
if (rows.length <= 1) {
return;
}
const removeIndex = parseInt(rowIndex, 10);
const newRows = rows.filter((row, index) => index !== removeIndex);
setFieldValue(
'categories',
saveInvoke(
onChange,
newRows
.filter((row) => row.rowType === 'editor')
.map((row, index) => ({
@@ -200,18 +206,18 @@ function ExpenseTable({
index: index + 1,
})),
);
onClickRemoveRow && onClickRemoveRow(removeIndex);
saveInvoke(onClickRemoveRow, removeIndex);
},
[rows, setFieldValue, onClickRemoveRow],
[rows, onChange, onClickRemoveRow],
);
// Invoke when click on add new line button.
const onClickNewRow = () => {
onClickAddNewRow && onClickAddNewRow();
saveInvoke(onClickAddNewRow);
};
// Invoke when click on clear all lines button.
const handleClickClearAllLines = () => {
onClickClearAllLines && onClickClearAllLines();
saveInvoke(onClickClearAllLines);
};
// Rows classnames callback.
const rowClassNames = useCallback(
@@ -222,7 +228,12 @@ function ExpenseTable({
);
return (
<div className={'dashboard__insider--expense-form__table'}>
<div
className={classNames(
CLASSES.DATATABLE_EDITOR,
CLASSES.DATATABLE_EDITOR_HAS_TOTAL_ROW,
)}
>
<DataTable
columns={columns}
data={tableRows}
@@ -230,12 +241,12 @@ function ExpenseTable({
sticky={true}
payload={{
accounts: accountsList,
errors: errors.categories || [],
errors: error,
updateData: handleUpdateData,
removeRow: handleRemoveRow,
}}
/>
<div className={'mt1'}>
<div className={classNames(CLASSES.DATATABLE_EDITOR_ACTIONS)}>
<Button
small={true}
className={'button--secondary button--new-line'}

View File

@@ -0,0 +1,21 @@
import { FastField } from 'formik';
import React from 'react';
import ExpenseFormEntries from './ExpenseFormEntries';
export default function ExpenseFormEntriesField({
}) {
return (
<FastField name={'categories'}>
{({ form, field: { value }, meta: { error, touched } }) => (
<ExpenseFormEntries
entries={value}
error={error}
onChange={(entries) => {
form.setFieldValue('categories', entries)
}}
/>
)}
</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

@@ -1,226 +1,22 @@
import React, { useMemo, useCallback, useState } from 'react';
import {
InputGroup,
FormGroup,
Intent,
Position,
MenuItem,
Classes,
} from '@blueprintjs/core';
import { DateInput } from '@blueprintjs/datetime';
import { FormattedMessage as T } from 'react-intl';
import { Row, Col } from 'react-grid-system';
import moment from 'moment';
import { momentFormatter, compose, tansformDateValue } from 'utils';
import React from 'react';
import classNames from 'classnames';
import {
CurrencySelectList,
ContactSelecetList,
ErrorMessage,
AccountsSelectList,
FieldRequiredHint,
Hint,
} from 'components';
import withCurrencies from 'containers/Currencies/withCurrencies';
import withAccounts from 'containers/Accounts/withAccounts';
import withCustomers from 'containers/Customers/withCustomers';
function ExpenseFormHeader({
// #ownProps
errors,
touched,
setFieldValue,
getFieldProps,
values,
import { CLASSES } from 'common/classes';
//withCurrencies
currenciesList,
// #withAccounts
accountsList,
accountsTypes,
// #withCustomers
customers,
}) {
const [selectedItems, setSelectedItems] = useState({});
const handleDateChange = useCallback(
(date) => {
const formatted = moment(date).format('YYYY-MM-DD');
setFieldValue('payment_date', formatted);
},
[setFieldValue],
);
// Handles change account.
const onChangeAccount = useCallback(
(account) => {
setFieldValue('payment_account_id', account.id);
},
[setFieldValue],
);
const onItemsSelect = useCallback(
(filedName) => {
return (filed) => {
setSelectedItems({
...selectedItems,
[filedName]: filed,
});
setFieldValue(filedName, filed.currency_code);
};
},
[setFieldValue, selectedItems],
);
// handle change customer
const onChangeCustomer = useCallback(
(filedName) => {
return (customer) => {
setFieldValue(filedName, customer.id);
};
},
[setFieldValue],
);
import ExpenseFormHeaderFields from './ExpenseFormHeaderFields';
import { PageFormBigNumber } from 'components';
// Expense form header.
export default function ExpenseFormHeader() {
return (
<div className={'dashboard__insider--expense-form__header'}>
<Row>
<Col width={300}>
<FormGroup
label={<T id={'assign_to_customer'} />}
className={classNames('form-group--select-list', Classes.FILL)}
labelInfo={<Hint />}
intent={errors.beneficiary && touched.beneficiary && Intent.DANGER}
helperText={
<ErrorMessage
name={'assign_to_customer'}
{...{ errors, touched }}
/>
}
>
<ContactSelecetList
contactsList={customers}
selectedContactId={values.customer_id}
defaultSelectText={<T id={'select_customer_account'} />}
onContactSelected={onChangeCustomer('customer_id')}
/>
</FormGroup>
</Col>
<div className={classNames(CLASSES.PAGE_FORM_HEADER)}>
<ExpenseFormHeaderFields />
<Col width={400}>
<FormGroup
label={<T id={'payment_account'} />}
className={classNames(
'form-group--payment_account',
'form-group--select-list',
Classes.FILL,
)}
labelInfo={<FieldRequiredHint />}
intent={
errors.payment_account_id &&
touched.payment_account_id &&
Intent.DANGER
}
helperText={
<ErrorMessage
name={'payment_account_id'}
{...{ errors, touched }}
/>
}
>
<AccountsSelectList
accounts={accountsList}
onAccountSelected={onChangeAccount}
defaultSelectText={<T id={'select_payment_account'} />}
selectedAccountId={values.payment_account_id}
filterByTypes={['current_asset']}
/>
</FormGroup>
</Col>
</Row>
<Row>
<Col width={300}>
<FormGroup
label={<T id={'payment_date'} />}
labelInfo={<Hint />}
className={classNames('form-group--select-list', Classes.FILL)}
intent={
errors.payment_date && touched.payment_date && Intent.DANGER
}
helperText={
<ErrorMessage name="payment_date" {...{ errors, touched }} />
}
>
<DateInput
{...momentFormatter('YYYY/MM/DD')}
value={tansformDateValue(values.payment_date)}
onChange={handleDateChange}
popoverProps={{ position: Position.BOTTOM, minimal: true }}
/>
</FormGroup>
</Col>
<Col width={200}>
<FormGroup
label={<T id={'currency'} />}
className={classNames(
'form-group--select-list',
'form-group--currency',
Classes.FILL,
)}
intent={
errors.currency_code && touched.currency_code && Intent.DANGER
}
helperText={
<ErrorMessage name="currency_code" {...{ errors, touched }} />
}
>
<CurrencySelectList
currenciesList={currenciesList}
selectedCurrencyCode={values.currency_code}
onCurrencySelected={onItemsSelect('currency_code')}
defaultSelectText={values.currency_code}
/>
</FormGroup>
</Col>
<Col width={200}>
<FormGroup
label={<T id={'ref_no'} />}
className={classNames('form-group--ref_no', Classes.FILL)}
intent={
errors.reference_no && touched.reference_no && Intent.DANGER
}
helperText={
<ErrorMessage name="reference_no" {...{ errors, touched }} />
}
>
<InputGroup
intent={
errors.reference_no && touched.reference_no && Intent.DANGER
}
minimal={true}
{...getFieldProps('reference_no')}
/>
</FormGroup>
</Col>
</Row>
<PageFormBigNumber
label={'Expense Amount'}
amount={0}
currencyCode={'LYD'}
/>
</div>
);
}
export default compose(
withAccounts(({ accountsList, accountsTypes }) => ({
accountsList,
accountsTypes,
})),
withCurrencies(({ currenciesList }) => ({
currenciesList,
})),
withCustomers(({ customers }) => ({
customers,
})),
)(ExpenseFormHeader);
)
}

View File

@@ -0,0 +1,164 @@
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 withCurrencies from 'containers/Currencies/withCurrencies';
import withAccounts from 'containers/Accounts/withAccounts';
import withCustomers from 'containers/Customers/withCustomers';
function ExpenseFormHeader({
//withCurrencies
currenciesList,
// #withAccounts
accountsList,
accountsTypes,
// #withCustomers
customers,
}) {
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={accountsList}
onAccountSelected={(account) => {
form.setFieldValue('payment_account_id', account.id);
}}
defaultSelectText={<T id={'select_payment_account'} />}
selectedAccountId={value}
filterByTypes={['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={currenciesList}
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>
);
}
export default compose(
withAccounts(({ accountsList, accountsTypes }) => ({
accountsList,
accountsTypes,
})),
withCurrencies(({ currenciesList }) => ({
currenciesList,
})),
withCustomers(({ customers }) => ({
customers,
})),
)(ExpenseFormHeader);

View File

@@ -1,4 +1,4 @@
import React, { useCallback } from 'react';
import React, { useCallback, useEffect } from 'react';
import { useParams, useHistory } from 'react-router-dom';
import { useQuery } from 'react-query';
@@ -9,6 +9,7 @@ import withAccountsActions from 'containers/Accounts/withAccountsActions';
import withExpensesActions from 'containers/Expenses/withExpensesActions';
import withCurrenciesActions from 'containers/Currencies/withCurrenciesActions';
import withCustomersActions from 'containers/Customers/withCustomersActions';
import withDashboardActions from 'containers/Dashboard/withDashboardActions';
import { compose } from 'utils';
@@ -25,10 +26,24 @@ function Expenses({
// #withCustomersActions
requestFetchCustomers,
// #withDashboardActions
setSidebarShrink,
resetSidebarPreviousExpand,
}) {
const history = useHistory();
const { id } = useParams();
useEffect(() => {
// Shrink the sidebar by foce.
setSidebarShrink();
return () => {
// Reset the sidebar to the previous status.
resetSidebarPreviousExpand();
};
}, [resetSidebarPreviousExpand, setSidebarShrink]);
const fetchAccounts = useQuery('accounts-list', (key) =>
requestFetchAccounts(),
);
@@ -83,4 +98,5 @@ export default compose(
withCurrenciesActions,
withExpensesActions,
withCustomersActions,
withDashboardActions,
)(Expenses);

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',
}),
}),
);
}
};