chrone: sperate client and server to different repos.

This commit is contained in:
a.bouhuolia
2021-09-21 17:13:53 +02:00
parent e011b2a82b
commit 18df5530c7
10015 changed files with 17686 additions and 97524 deletions

View File

@@ -0,0 +1,193 @@
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 'components';
import { useHistory } from 'react-router-dom';
import { CLASSES } from 'common/classes';
import classNames from 'classnames';
import { Icon, If } from 'components';
import { useExpenseFormContext } from './ExpenseFormPageProvider';
/**
* Expense form floating actions.
*/
export default function ExpenseFloatingFooter() {
const history = useHistory();
// Formik context.
const { isSubmitting, submitForm, resetForm } = useFormikContext();
// Expense form context.
const { setSubmitPayload, isNewMode } = useExpenseFormContext();
// Handle submit & publish button click.
const handleSubmitPublishBtnClick = (event) => {
setSubmitPayload({ redirect: true, publish: true});
submitForm();
};
// Handle submit, publish & new button click.
const handleSubmitPublishAndNewBtnClick = (event) => {
setSubmitPayload({ redirect: false, publish: true, resetForm: true });
submitForm();
};
// Handle submit, publish & continue editing button click.
const handleSubmitPublishContinueEditingBtnClick = (event) => {
setSubmitPayload({ redirect: false, publish: true });
submitForm();
};
// Handle submit as draft button click.
const handleSubmitDraftBtnClick = (event) => {
setSubmitPayload({ redirect: true, publish: false });
submitForm();
};
// Handle submit as draft & new button click.
const handleSubmitDraftAndNewBtnClick = (event) => {
setSubmitPayload({ redirect: false, publish: false, resetForm: true });
submitForm();
};
// Handles submit as draft & continue editing button click.
const handleSubmitDraftContinueEditingBtnClick = (event) => {
setSubmitPayload({ redirect: false, publish: false });
submitForm();
};
// Handle cancel button click.
const handleCancelBtnClick = (event) => {
history.goBack();
};
// Handles clear form button click.
const handleClearBtnClick = (event) => {
resetForm();
};
return (
<div className={classNames(CLASSES.PAGE_FORM_FLOATING_ACTIONS)}>
{/* ----------- Save And Publish ----------- */}
<If condition={isNewMode}>
<ButtonGroup>
<Button
disabled={isSubmitting}
loading={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={!isNewMode}>
<ButtonGroup>
<Button
disabled={isSubmitting}
loading={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={!isNewMode ? <T id={'reset'} /> : <T id={'clear'} />}
/>
{/* ----------- Cancel ----------- */}
<Button
className={'ml1'}
onClick={handleCancelBtnClick}
text={<T id={'cancel'} />}
/>
</div>
);
}

View File

@@ -0,0 +1,167 @@
import React, { useMemo } from 'react';
import { Intent } from '@blueprintjs/core';
import intl from 'react-intl-universal';
import { defaultTo, sumBy, isEmpty } from 'lodash';
import { Formik, Form } from 'formik';
import classNames from 'classnames';
import { useHistory } from 'react-router-dom';
import { CLASSES } from 'common/classes';
import * as R from 'ramda';
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 withCurrentOrganization from 'containers/Organization/withCurrentOrganization';
import AppToaster from 'components/AppToaster';
import {
CreateExpenseFormSchema,
EditExpenseFormSchema,
} from './ExpenseForm.schema';
import { transformErrors, defaultExpense, transformToEditForm } from './utils';
import { compose, orderingLinesIndexes } from 'utils';
/**
* Expense form.
*/
function ExpenseForm({
// #withSettings
preferredPaymentAccount,
// #withCurrentOrganization
organization: { base_currency },
}) {
// Expense form context.
const {
editExpenseMutate,
createExpenseMutate,
expense,
expenseId,
submitPayload,
} = useExpenseFormContext();
const isNewMode = !expenseId;
// History context.
const history = useHistory();
// Form initial values.
const initialValues = useMemo(
() => ({
...(!isEmpty(expense)
? {
...transformToEditForm(expense, defaultExpense),
}
: {
...defaultExpense,
currency_code: base_currency,
payment_account_id: defaultTo(preferredPaymentAccount, ''),
}),
}),
[expense, base_currency, preferredPaymentAccount],
);
// Handle form submit.
const handleSubmit = (values, { setSubmitting, setErrors, resetForm }) => {
setSubmitting(true);
const totalAmount = sumBy(values.categories, 'amount');
if (totalAmount <= 0) {
AppToaster.show({
message: intl.get('amount_cannot_be_zero_or_empty'),
intent: Intent.DANGER,
});
return;
}
// Filter expense entries that has no amount or expense account.
const categories = values.categories.filter(
(category) => category.amount && category.expense_account_id,
);
const form = {
...values,
publish: submitPayload.publish,
categories: R.compose(orderingLinesIndexes)(categories),
};
// Handle request success.
const handleSuccess = (response) => {
AppToaster.show({
message: intl.get(
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 the request error.
const handleError = ({
response: {
data: { errors },
},
}) => {
transformErrors(errors, { setErrors });
setSubmitting(false);
};
if (isNewMode) {
createExpenseMutate(form).then(handleSuccess).catch(handleError);
} else {
editExpenseMutate([expense.id, form])
.then(handleSuccess)
.catch(handleError);
}
};
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}
>
<Form>
<ExpenseFormHeader />
<ExpenseFormBody />
<ExpenseFormFooter />
<ExpenseFloatingFooter />
</Form>
</Formik>
</div>
);
}
export default compose(
withDashboardActions,
withMediaActions,
withSettings(({ expenseSettings }) => ({
preferredPaymentAccount: parseInt(
expenseSettings?.preferredPaymentAccount,
10,
),
})),
withCurrentOrganization(),
)(ExpenseForm);

View File

@@ -0,0 +1,41 @@
import * as Yup from 'yup';
import intl from 'react-intl-universal';
import { DATATYPES_LENGTH } from 'common/dataTypes';
import { isBlank } from 'utils';
const Schema = Yup.object().shape({
beneficiary: Yup.string().label(intl.get('beneficiary')),
payment_account_id: Yup.number()
.required()
.label(intl.get('payment_account_')),
payment_date: Yup.date().required().label(intl.get('payment_date_')),
reference_no: Yup.string().min(1).max(DATATYPES_LENGTH.STRING).nullable(),
currency_code: Yup.string()
.nullable()
.max(3)
.label(intl.get('currency_code')),
description: Yup.string()
.trim()
.min(1)
.max(DATATYPES_LENGTH.TEXT)
.nullable()
.label(intl.get('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(),
}),
landed_cost: Yup.boolean(),
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,38 @@
import { FastField } from 'formik';
import React from 'react';
import ExpenseFormEntriesTable from './ExpenseFormEntriesTable';
import { useExpenseFormContext } from './ExpenseFormPageProvider';
import { defaultExpenseEntry, accountsFieldShouldUpdate } from './utils';
/**
* Expense form entries field.
*/
export default function ExpenseFormEntriesField({ linesNumber = 4 }) {
// Expense form context.
const { accounts } = useExpenseFormContext();
return (
<FastField
name={'categories'}
accounts={accounts}
shouldUpdate={accountsFieldShouldUpdate}
>
{({
form: { values, setFieldValue },
field: { value },
meta: { error, touched },
}) => (
<ExpenseFormEntriesTable
entries={value}
error={error}
onChange={(entries) => {
setFieldValue('categories', entries);
}}
defaultEntry={defaultExpenseEntry}
linesNumber={linesNumber}
currencyCode={values.currency_code}
/>
)}
</FastField>
);
}

View File

@@ -0,0 +1,81 @@
import React, { useCallback } from 'react';
import { DataTableEditable } from 'components';
import { useExpenseFormContext } from './ExpenseFormPageProvider';
import { useExpenseFormTableColumns } from './components';
import {
saveInvoke,
compose,
updateTableCell,
updateMinEntriesLines,
updateAutoAddNewLine,
updateRemoveLineByIndex,
orderingLinesIndexes,
} from 'utils';
/**
* Expenses form entries.
*/
export default function ExpenseFormEntriesTable({
// #ownPorps
entries,
defaultEntry,
error,
onChange,
currencyCode,
landedCost = true,
}) {
// Expense form context.
const { accounts } = useExpenseFormContext();
// Memorized data table columns.
const columns = useExpenseFormTableColumns({ landedCost });
// Handles update datatable data.
const handleUpdateData = useCallback(
(rowIndex, columnId, value) => {
const newRows = compose(
// Update auto-adding new line.
updateAutoAddNewLine(defaultEntry, ['expense_account_id']),
// Update the row value of the given row index and column id.
updateTableCell(rowIndex, columnId, value),
)(entries);
saveInvoke(onChange, newRows);
},
[entries, defaultEntry, onChange],
);
// Handles click remove datatable row.
const handleRemoveRow = useCallback(
(rowIndex) => {
const newRows = compose(
// Ensure minimum lines count.
updateMinEntriesLines(4, defaultEntry),
// Remove the line by the given index.
updateRemoveLineByIndex(rowIndex),
)(entries);
saveInvoke(onChange, newRows);
},
[entries, defaultEntry, onChange],
);
return (
<DataTableEditable
name={'expense-form'}
columns={columns}
data={entries}
sticky={true}
payload={{
accounts: accounts,
errors: error,
updateData: handleUpdateData,
removeRow: handleRemoveRow,
autoFocus: ['expense_account_id', 0],
currencyCode
}}
footer={true}
/>
);
}

View File

@@ -0,0 +1,41 @@
import React from 'react';
import { FastField } from 'formik';
import { FormGroup, TextArea } from '@blueprintjs/core';
import { FormattedMessage as T } from 'components';
import classNames from 'classnames';
import { inputIntent } from 'utils';
import { Row, Dragzone, Col, Postbox } from 'components';
import { CLASSES } from 'common/classes';
export default function ExpenseFormFooter() {
return (
<div className={classNames(CLASSES.PAGE_FORM_FOOTER)}>
<Postbox title={<T id={'expense_details'} />} defaultOpen={false}>
<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={<T id={'attachments_maximum'} />}
/>
</Col>
</Row>
</Postbox>
</div>
);
}

View File

@@ -0,0 +1,31 @@
import React, { useMemo } from 'react';
import classNames from 'classnames';
import { sumBy } from 'lodash';
import { useFormikContext } from 'formik';
import { FormattedMessage as T } from 'components';
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={<T id={'expense_amount'}/>}
amount={totalExpenseAmount}
currencyCode={values?.currency_code}
/>
</div>
);
}

View File

@@ -0,0 +1,153 @@
import React from 'react';
import { InputGroup, FormGroup, Position, Classes } from '@blueprintjs/core';
import { DateInput } from '@blueprintjs/datetime';
import { FastField, ErrorMessage } from 'formik';
import { FormattedMessage as T } from 'components';
import classNames from 'classnames';
import { CLASSES } from 'common/classes';
import {
momentFormatter,
tansformDateValue,
inputIntent,
handleDateChange,
} from 'utils';
import { customersFieldShouldUpdate, accountsFieldShouldUpdate } from './utils';
import {
CurrencySelectList,
ContactSelecetList,
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'}
accounts={accounts}
shouldUpdate={accountsFieldShouldUpdate}
>
{({ 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}
disabled={true}
/>
</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'}
customers={customers}
shouldUpdate={customersFieldShouldUpdate}
>
{({ 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,22 @@
import React from 'react';
import { useParams } from 'react-router-dom';
import 'style/pages/Expense/PageForm.scss';
import ExpenseForm from './ExpenseForm';
import { ExpenseFormPageProvider } from './ExpenseFormPageProvider';
/**
* Expense page form.
*/
export default function ExpenseFormPage() {
const { id } = useParams();
const expenseId = parseInt(id, 10);
return (
<ExpenseFormPageProvider expenseId={expenseId}>
<ExpenseForm />
</ExpenseFormPageProvider>
);
}

View File

@@ -0,0 +1,85 @@
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, isLoading: isCurrenciesLoading } = useCurrencies();
// Fetches customers list.
const {
data: { customers },
isLoading: isCustomersLoading,
} = useCustomers();
// Fetch the expense details.
const { data: expense, isLoading: isExpenseLoading } = useExpense(
expenseId,
{
enabled: !!expenseId,
},
);
// Fetch accounts list.
const { data: accounts, isLoading: isAccountsLoading } = useAccounts();
// Create and edit expense mutate.
const { mutateAsync: createExpenseMutate } = useCreateExpense();
const { mutateAsync: editExpenseMutate } = useEditExpense();
// Submit form payload.
const [submitPayload, setSubmitPayload] = React.useState({});
//
const isNewMode = !expenseId;
// Provider payload.
const provider = {
isNewMode,
expenseId,
submitPayload,
currencies,
customers,
expense,
accounts,
isCurrenciesLoading,
isExpenseLoading,
isCustomersLoading,
isAccountsLoading,
createExpenseMutate,
editExpenseMutate,
setSubmitPayload,
};
return (
<DashboardInsider
loading={
isCurrenciesLoading ||
isExpenseLoading ||
isCustomersLoading ||
isAccountsLoading
}
name={'expense-form'}
>
<ExpenseFormPageContext.Provider value={provider} {...props} />
</DashboardInsider>
);
}
const useExpenseFormContext = () => React.useContext(ExpenseFormPageContext);
export { ExpenseFormPageProvider, useExpenseFormContext };

View File

@@ -0,0 +1,159 @@
import React from 'react';
import { Button, Tooltip, Intent, Position, Checkbox } from '@blueprintjs/core';
import { FormattedMessage as T } from 'components';
import { Icon, Hint } from 'components';
import intl from 'react-intl-universal';
import {
InputGroupCell,
MoneyFieldCell,
AccountsListFieldCell,
CheckBoxFieldCell,
} from 'components/DataTableCells';
import { formattedAmount, safeSumBy } from 'utils';
/**
* Expense category header cell.
*/
const ExpenseCategoryHeaderCell = () => {
return (
<>
<T id={'expense_category'} />
<Hint />
</>
);
};
/**
* Actions cell renderer.
*/
const ActionsCellRenderer = ({
row: { index },
column: { id },
cell: { value: initialValue },
data,
payload,
}) => {
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>
);
};
/**
* Landed cost header cell.
*/
const LandedCostHeaderCell = () => {
return (
<>
<T id={'Landed'} />
<Hint
content={
'This options allows you to be able to add additional cost eg. freight then allocate cost to the items in your bills.'
}
/>
</>
);
};
/**
* Amount footer cell.
*/
function AmountFooterCell({ payload: { currencyCode }, rows }) {
const total = safeSumBy(rows, 'original.amount');
return <span>{formattedAmount(total, currencyCode)}</span>;
}
/**
* Expense amount header cell.
*/
export function ExpenseAmountHeaderCell({ payload: { currencyCode } }) {
return intl.get('amount_currency', { currency: currencyCode });
}
/**
* Expense account footer cell.
*/
function ExpenseAccountFooterCell() {
return <T id={'total'} />;
}
/**
* Retrieve expense form table entries columns.
*/
export function useExpenseFormTableColumns({ landedCost }) {
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: AccountsListFieldCell,
Footer: ExpenseAccountFooterCell,
className: 'expense_account_id',
disableSortBy: true,
width: 40,
filterAccountsByRootTypes: ['expense'],
},
{
Header: ExpenseAmountHeaderCell,
accessor: 'amount',
Cell: MoneyFieldCell,
Footer: AmountFooterCell,
disableSortBy: true,
width: 40,
className: 'amount',
},
{
Header: intl.get('description'),
accessor: 'description',
Cell: InputGroupCell,
disableSortBy: true,
className: 'description',
width: 100,
},
...(landedCost
? [
{
Header: LandedCostHeaderCell,
accessor: 'landed_cost',
Cell: CheckBoxFieldCell,
disableSortBy: true,
disableResizing: true,
width: 100,
className: 'landed-cost',
},
]
: []),
{
Header: '',
accessor: 'action',
Cell: ActionsCellRenderer,
className: 'actions',
disableSortBy: true,
disableResizing: true,
width: 45,
},
],
[],
);
}

View File

@@ -0,0 +1,95 @@
import { AppToaster } from 'components';
import moment from 'moment';
import intl from 'react-intl-universal';
import * as R from 'ramda';
import {
defaultFastFieldShouldUpdate,
transformToForm,
repeatValue,
ensureEntriesHasEmptyLine
} from 'utils';
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: intl.get('the_expense_is_already_published'),
}),
);
}
};
export const MIN_LINES_NUMBER = 4;
export const defaultExpenseEntry = {
amount: '',
expense_account_id: '',
description: '',
landed_cost: 0,
};
export const defaultExpense = {
payment_account_id: '',
beneficiary: '',
payment_date: moment(new Date()).format('YYYY-MM-DD'),
description: '',
reference_no: '',
currency_code: '',
publish: '',
categories: [...repeatValue(defaultExpenseEntry, MIN_LINES_NUMBER)],
};
/**
* Transformes the expense to form initial values in edit mode.
*/
export const transformToEditForm = (
expense,
defaultExpense,
linesNumber = 4,
) => {
const expenseEntry = defaultExpense.categories[0];
const initialEntries = [
...expense.categories.map((category) => ({
...transformToForm(category, expenseEntry),
})),
...repeatValue(
expenseEntry,
Math.max(linesNumber - expense.categories.length, 0),
),
];
const categories = R.compose(
ensureEntriesHasEmptyLine(MIN_LINES_NUMBER, expenseEntry),
)(initialEntries);
return {
...transformToForm(expense, defaultExpense),
categories,
};
};
/**
* Detarmine cusotmers fast-field should update.
*/
export const customersFieldShouldUpdate = (newProps, oldProps) => {
return (
newProps.customers !== oldProps.customers ||
defaultFastFieldShouldUpdate(newProps, oldProps)
);
};
/**
* Detarmine accounts fast-field should update.
*/
export const accountsFieldShouldUpdate = (newProps, oldProps) => {
return (
newProps.accounts !== oldProps.accounts ||
defaultFastFieldShouldUpdate(newProps, oldProps)
);
};