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,41 @@
import * as Yup from 'yup';
import { formatMessage } from 'services/intl';
import { DATATYPES_LENGTH } from 'common/dataTypes';
const Schema = Yup.object().shape({
journal_number: Yup.string()
.required()
.min(1)
.max(DATATYPES_LENGTH.STRING)
.label(formatMessage({ id: 'journal_number_' })),
journal_type: Yup.string()
.required()
.min(1)
.max(DATATYPES_LENGTH.STRING)
.label(formatMessage({ id: 'journal_type' })),
date: Yup.date()
.required()
.label(formatMessage({ id: 'date' })),
currency_code: Yup.string().max(3),
publish: Yup.boolean(),
reference: Yup.string().nullable().min(1).max(DATATYPES_LENGTH.STRING),
description: Yup.string().min(1).max(DATATYPES_LENGTH.STRING).nullable(),
entries: Yup.array().of(
Yup.object().shape({
credit: Yup.number().nullable(),
debit: Yup.number().nullable(),
account_id: Yup.number()
.nullable()
.when(['credit', 'debit'], {
is: (credit, debit) => credit || debit,
then: Yup.number().required(),
}),
contact_id: Yup.number().nullable(),
contact_type: Yup.string().nullable(),
note: Yup.string().max(DATATYPES_LENGTH.TEXT).nullable(),
}),
),
});
export const CreateJournalSchema = Schema;
export const EditJournalSchema = Schema;

View File

@@ -0,0 +1,36 @@
import React from 'react';
import { FastField } from 'formik';
import classNames from 'classnames';
import { CLASSES } from 'common/classes';
import MakeJournalEntriesTable from './MakeJournalEntriesTable';
import { orderingLinesIndexes, repeatValue } from 'utils';
export default function MakeJournalEntriesField({
defaultRow,
linesNumber = 4,
}) {
return (
<div className={classNames(CLASSES.PAGE_FORM_BODY)}>
<FastField name={'entries'}>
{({ form, field: { value }, meta: { error, touched } }) => (
<MakeJournalEntriesTable
onChange={(entries) => {
form.setFieldValue('entries', entries);
}}
entries={value}
error={error}
onClickAddNewRow={() => {
form.setFieldValue('entries', [...value, defaultRow]);
}}
onClickClearAllLines={() => {
form.setFieldValue(
'entries',
orderingLinesIndexes([...repeatValue(defaultRow, linesNumber)])
);
}}
/>
)}
</FastField>
</div>
);
}

View File

@@ -0,0 +1,202 @@
import React from 'react';
import {
Intent,
Button,
ButtonGroup,
Popover,
PopoverInteractionKind,
Position,
Menu,
MenuItem,
} from '@blueprintjs/core';
import { FormattedMessage as T } from 'react-intl';
import { CLASSES } from 'common/classes';
import classNames from 'classnames';
import { saveInvoke } from 'utils';
import { If, Icon } from 'components';
/**
* Make Journal floating actions bar.
*/
export default function MakeJournalEntriesFooter({
isSubmitting,
onSubmitClick,
onCancelClick,
manualJournalId,
onSubmitForm,
onResetForm,
manualJournalPublished,
}) {
const handleSubmitPublishBtnClick = (event) => {
saveInvoke(onSubmitClick, event, {
redirect: true,
publish: true,
});
};
const handleSubmitPublishAndNewBtnClick = (event) => {
onSubmitForm();
saveInvoke(onSubmitClick, event, {
redirect: false,
publish: true,
resetForm: true,
});
};
const handleSubmitPublishContinueEditingBtnClick = (event) => {
onSubmitForm();
saveInvoke(onSubmitClick, event, {
redirect: false,
publish: true,
});
};
const handleSubmitDraftBtnClick = (event) => {
saveInvoke(onSubmitClick, event, {
redirect: true,
publish: false,
});
};
const handleSubmitDraftAndNewBtnClick = (event) => {
onSubmitForm();
saveInvoke(onSubmitClick, event, {
redirect: false,
publish: false,
resetForm: true,
});
};
const handleSubmitDraftContinueEditingBtnClick = (event) => {
onSubmitForm();
saveInvoke(onSubmitClick, event, {
redirect: false,
publish: false,
});
};
const handleCancelBtnClick = (event) => {
saveInvoke(onCancelClick, event);
};
const handleClearBtnClick = (event) => {
// saveInvoke(onClearClick, event);
onResetForm();
};
return (
<div className={classNames(CLASSES.PAGE_FORM_FLOATING_ACTIONS)}>
{/* ----------- Save And Publish ----------- */}
<If condition={!manualJournalId || !manualJournalPublished}>
<ButtonGroup>
<Button
disabled={isSubmitting}
intent={Intent.PRIMARY}
type="submit"
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'}
type="submit"
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={manualJournalId && manualJournalPublished}>
<ButtonGroup>
<Button
disabled={isSubmitting}
intent={Intent.PRIMARY}
type="submit"
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={manualJournalId ? <T id={'reset'} /> : <T id={'clear'} />}
/>
{/* ----------- Cancel ----------- */}
<Button
className={'ml1'}
onClick={handleCancelBtnClick}
text={<T id={'cancel'} />}
/>
</div>
);
}

View File

@@ -0,0 +1,245 @@
import React, { useMemo, useState, useEffect, useCallback } from 'react';
import { Formik, Form } from 'formik';
import moment from 'moment';
import { Intent } from '@blueprintjs/core';
import { useIntl } from 'react-intl';
import { pick, defaultTo } from 'lodash';
import classNames from 'classnames';
import { useHistory } from 'react-router-dom';
import { CLASSES } from 'common/classes';
import {
CreateJournalSchema,
EditJournalSchema,
} from './MakeJournalEntries.schema';
import MakeJournalEntriesHeader from './MakeJournalEntriesHeader';
import MakeJournalFormFloatingActions from './MakeJournalFormFloatingActions';
import MakeJournalEntriesField from './MakeJournalEntriesField';
import MakeJournalNumberWatcher from './MakeJournalNumberWatcher';
import MakeJournalFormFooter from './MakeJournalFormFooter';
import withDashboardActions from 'containers/Dashboard/withDashboardActions';
import withSettings from 'containers/Settings/withSettings';
import AppToaster from 'components/AppToaster';
import withMediaActions from 'containers/Media/withMediaActions';
import {
compose,
repeatValue,
orderingLinesIndexes,
defaultToTransform,
transactionNumber,
} from 'utils';
import { transformErrors } from './utils';
import { useMakeJournalFormContext } from './MakeJournalProvider';
const defaultEntry = {
index: 0,
account_id: '',
credit: '',
debit: '',
contact_id: '',
note: '',
};
const defaultInitialValues = {
journal_number: '',
journal_type: 'Journal',
date: moment(new Date()).format('YYYY-MM-DD'),
description: '',
reference: '',
currency_code: '',
publish: '',
entries: [...repeatValue(defaultEntry, 4)],
};
/**
* Journal entries form.
*/
function MakeJournalEntriesForm({
// #withDashboard
changePageTitle,
changePageSubtitle,
// #withSettings
journalNextNumber,
journalNumberPrefix,
baseCurrency,
}) {
const {
createJournalMutate,
editJournalMutate,
isNewMode,
manualJournal,
submitPayload,
} = useMakeJournalFormContext();
const { formatMessage } = useIntl();
const history = useHistory();
// New journal number.
const journalNumber = transactionNumber(
journalNumberPrefix,
journalNextNumber,
);
// Changes the page title based on the form in new and edit mode.
useEffect(() => {
const transactionNumber = manualJournal
? manualJournal.journal_number
: journalNumber;
if (isNewMode) {
changePageTitle(formatMessage({ id: 'new_journal' }));
} else {
changePageTitle(formatMessage({ id: 'edit_journal' }));
}
changePageSubtitle(
defaultToTransform(transactionNumber, `No. ${transactionNumber}`, ''),
);
}, [
changePageTitle,
changePageSubtitle,
journalNumber,
manualJournal,
formatMessage,
isNewMode,
]);
const initialValues = useMemo(
() => ({
...(manualJournal
? {
...pick(manualJournal, Object.keys(defaultInitialValues)),
entries: manualJournal.entries.map((entry) => ({
...pick(entry, Object.keys(defaultEntry)),
})),
}
: {
...defaultInitialValues,
journal_number: defaultTo(journalNumber, ''),
currency_code: defaultTo(baseCurrency, ''),
entries: orderingLinesIndexes(defaultInitialValues.entries),
}),
}),
[manualJournal, baseCurrency, journalNumber],
);
// Handle journal number field change.
const handleJournalNumberChanged = useCallback(
(journalNumber) => {
changePageSubtitle(
defaultToTransform(journalNumber, `No. ${journalNumber}`, ''),
);
},
[changePageSubtitle],
);
// Handle the form submiting.
const handleSubmit = (values, { setErrors, setSubmitting, resetForm }) => {
setSubmitting(true);
const entries = values.entries.filter(
(entry) => entry.debit || entry.credit,
);
const getTotal = (type = 'credit') => {
return entries.reduce((total, item) => {
return item[type] ? item[type] + total : total;
}, 0);
};
const totalCredit = getTotal('credit');
const totalDebit = getTotal('debit');
// Validate the total credit should be eqials total debit.
if (totalCredit !== totalDebit) {
AppToaster.show({
message: formatMessage({
id: 'should_total_of_credit_and_debit_be_equal',
}),
intent: Intent.DANGER,
});
setSubmitting(false);
return;
} else if (totalCredit === 0 || totalDebit === 0) {
AppToaster.show({
message: formatMessage({
id: 'amount_cannot_be_zero_or_empty',
}),
intent: Intent.DANGER,
});
setSubmitting(false);
return;
}
const form = { ...values, publish: submitPayload.publish, entries };
// Handle the request error.
const handleError = (error) => {
transformErrors(error, { setErrors });
setSubmitting(false);
};
// Handle the request success.
const handleSuccess = (errors) => {
AppToaster.show({
message: formatMessage(
{
id: isNewMode
? 'the_journal_has_been_created_successfully'
: 'the_journal_has_been_edited_successfully',
},
{ number: values.journal_number },
),
intent: Intent.SUCCESS,
});
setSubmitting(false);
if (submitPayload.redirect) {
history.push('/manual-journals');
}
if (submitPayload.resetForm) {
resetForm();
}
};
if (isNewMode) {
createJournalMutate(form).then(handleSuccess).catch(handleError);
} else {
editJournalMutate(manualJournal.id, form)
.then(handleSuccess)
.catch(handleError);
}
};
return (
<div
className={classNames(
CLASSES.PAGE_FORM,
CLASSES.PAGE_FORM_STRIP_STYLE,
CLASSES.PAGE_FORM_MAKE_JOURNAL,
)}
>
<Formik
initialValues={initialValues}
validationSchema={isNewMode ? CreateJournalSchema : EditJournalSchema}
onSubmit={handleSubmit}
>
<Form>
<MakeJournalEntriesHeader
onJournalNumberChanged={handleJournalNumberChanged}
/>
<MakeJournalNumberWatcher journalNumber={journalNumber} />
<MakeJournalEntriesField defaultRow={defaultEntry} />
<MakeJournalFormFooter />
<MakeJournalFormFloatingActions />
</Form>
</Formik>
</div>
);
}
export default compose(
withDashboardActions,
withMediaActions,
withSettings(({ manualJournalsSettings, organizationSettings }) => ({
journalNextNumber: parseInt(manualJournalsSettings?.nextNumber, 10),
journalNumberPrefix: manualJournalsSettings?.numberPrefix,
baseCurrency: organizationSettings?.baseCurrency,
})),
)(MakeJournalEntriesForm);

View File

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

View File

@@ -0,0 +1,186 @@
import React from 'react';
import {
InputGroup,
FormGroup,
Position,
ControlGroup,
} from '@blueprintjs/core';
import { FastField, ErrorMessage } from 'formik';
import { DateInput } from '@blueprintjs/datetime';
import { FormattedMessage as T } from 'react-intl';
import classNames from 'classnames';
import { CLASSES } from 'common/classes';
import { momentFormatter, tansformDateValue, saveInvoke } from 'utils';
import {
Hint,
FieldHint,
FieldRequiredHint,
Icon,
InputPrependButton,
CurrencySelectList,
} from 'components';
import { useMakeJournalFormContext } from './MakeJournalProvider';
import withDialogActions from 'containers/Dialog/withDialogActions';
import { compose, inputIntent, handleDateChange } from 'utils';
/**
* Make journal entries header.
*/
function MakeJournalEntriesHeader({
// #ownProps
onJournalNumberChanged,
// #withDialog
openDialog,
}) {
const { currencies } = useMakeJournalFormContext();
// Handle journal number change.
const handleJournalNumberChange = () => {
openDialog('journal-number-form', {});
};
// Handle journal number field blur event.
const handleJournalNumberChanged = (event) => {
saveInvoke(onJournalNumberChanged, event.currentTarget.value);
};
return (
<div className={classNames(CLASSES.PAGE_FORM_HEADER_FIELDS)}>
{/*------------ Posting date -----------*/}
<FastField name={'date'}>
{({ form, field: { value }, meta: { error, touched } }) => (
<FormGroup
label={<T id={'posting_date'} />}
labelInfo={<FieldRequiredHint />}
intent={inputIntent({ error, touched })}
helperText={<ErrorMessage name="date" />}
minimal={true}
inline={true}
className={classNames(CLASSES.FILL)}
>
<DateInput
{...momentFormatter('YYYY/MM/DD')}
onChange={handleDateChange((formattedDate) => {
form.setFieldValue('date', formattedDate);
})}
value={tansformDateValue(value)}
popoverProps={{
position: Position.BOTTOM,
minimal: true,
}}
inputProps={{
leftIcon: <Icon icon={'date-range'} />,
}}
/>
</FormGroup>
)}
</FastField>
{/*------------ Journal number -----------*/}
<FastField name={'journal_number'}>
{({ form, field, meta: { error, touched } }) => (
<FormGroup
label={<T id={'journal_no'} />}
labelInfo={
<>
<FieldRequiredHint />
<FieldHint />
</>
}
className={'form-group--journal-number'}
intent={inputIntent({ error, touched })}
helperText={<ErrorMessage name="journal_number" />}
fill={true}
inline={true}
>
<ControlGroup fill={true}>
<InputGroup
fill={true}
{...field}
onBlur={handleJournalNumberChanged}
/>
<InputPrependButton
buttonProps={{
onClick: handleJournalNumberChange,
icon: <Icon icon={'settings-18'} />,
}}
tooltip={true}
tooltipProps={{
content: 'Setting your auto-generated journal number',
position: Position.BOTTOM_LEFT,
}}
/>
</ControlGroup>
</FormGroup>
)}
</FastField>
{/*------------ Reference -----------*/}
<FastField name={'reference'}>
{({ form, field, meta: { error, touched } }) => (
<FormGroup
label={<T id={'reference'} />}
labelInfo={
<Hint
content={<T id={'journal_reference_hint'} />}
position={Position.RIGHT}
/>
}
className={'form-group--reference'}
intent={inputIntent({ error, touched })}
helperText={<ErrorMessage name="reference" />}
fill={true}
inline={true}
>
<InputGroup fill={true} {...field} />
</FormGroup>
)}
</FastField>
{/*------------ Journal type -----------*/}
<FastField name={'journal_type'}>
{({ form, field, meta: { error, touched } }) => (
<FormGroup
label={<T id={'journal_type'} />}
className={classNames('form-group--account-type', CLASSES.FILL)}
inline={true}
>
<InputGroup
intent={inputIntent({ error, touched })}
fill={true}
{...field}
/>
</FormGroup>
)}
</FastField>
{/*------------ Currency -----------*/}
<FastField name={'currency_code'}>
{({ form, field: { value }, meta: { error, touched } }) => (
<FormGroup
label={<T id={'currency'} />}
className={classNames('form-group--currency', CLASSES.FILL)}
inline={true}
>
<CurrencySelectList
currenciesList={currencies}
selectedCurrencyCode={value}
onCurrencySelected={(currencyItem) => {
form.setFieldValue('currency_code', currencyItem.currency_code);
}}
defaultSelectText={value}
/>
</FormGroup>
)}
</FastField>
</div>
);
}
export default compose(
withDialogActions,
)(MakeJournalEntriesHeader);

View File

@@ -0,0 +1,62 @@
import React, { useCallback, useEffect } from 'react';
import { useParams, useHistory } from 'react-router-dom';
import MakeJournalEntriesForm from './MakeJournalEntriesForm';
import { MakeJournalProvider } from './MakeJournalProvider';
import withDashboardActions from 'containers/Dashboard/withDashboardActions';
import { compose } from 'utils';
import 'style/pages/ManualJournal/MakeJournal.scss';
/**
* Make journal entries page.
*/
function MakeJournalEntriesPage({
// #withDashboardActions
setSidebarShrink,
resetSidebarPreviousExpand,
setDashboardBackLink,
}) {
const history = useHistory();
const { id: journalId } = useParams();
useEffect(() => {
// Shrink the sidebar by foce.
setSidebarShrink();
// Show the back link on dashboard topbar.
setDashboardBackLink('/manual-journals');
return () => {
// Reset the sidebar to the previous status.
resetSidebarPreviousExpand();
// Hide the back link on dashboard topbar.
setDashboardBackLink(false);
};
}, [resetSidebarPreviousExpand, setDashboardBackLink, setSidebarShrink]);
const handleFormSubmit = useCallback(
(payload) => {
payload.redirect && history.push('/manual-journals');
},
[history],
);
const handleCancel = useCallback(() => {
history.goBack();
}, [history]);
return (
<MakeJournalProvider journalId={journalId}>
<MakeJournalEntriesForm
onFormSubmit={handleFormSubmit}
onCancelForm={handleCancel}
/>
</MakeJournalProvider>
);
}
export default compose(
withDashboardActions,
)(MakeJournalEntriesPage);

View File

@@ -0,0 +1,205 @@
import React, { useState, useMemo, useEffect, useCallback } from 'react';
import { Button } from '@blueprintjs/core';
import { FormattedMessage as T, useIntl } from 'react-intl';
import { omit } from 'lodash';
import { saveInvoke } from 'utils';
import {
AccountsListFieldCell,
MoneyFieldCell,
InputGroupCell,
ContactsListFieldCell,
} from 'components/DataTableCells';
import {
ContactHeaderCell,
ActionsCellRenderer,
TotalAccountCellRenderer,
TotalCreditDebitCellRenderer,
NoteCellRenderer,
} from './components';
import { DataTableEditable } from 'components';
import { updateDataReducer } from './utils';
import { useMakeJournalFormContext } from './MakeJournalProvider';
/**
* Make journal entries table component.
*/
export default function MakeJournalEntriesTable({
// #ownPorps
onClickRemoveRow,
onClickAddNewRow,
onClickClearAllLines,
onChange,
entries,
error,
}) {
const [rows, setRows] = useState([]);
const { formatMessage } = useIntl();
const { accounts, customers } = useMakeJournalFormContext();
useEffect(() => {
setRows([...entries.map((e) => ({ ...e, rowType: 'editor' }))]);
}, [entries, setRows]);
// Final table rows editor rows and total and final blank row.
const tableRows = useMemo(() => [...rows, { rowType: 'total' }], [rows]);
// Memorized data table columns.
const columns = useMemo(
() => [
{
Header: '#',
accessor: 'index',
Cell: ({ row: { index } }) => <span>{index + 1}</span>,
className: 'index',
width: 40,
disableResizing: true,
disableSortBy: true,
sticky: 'left',
},
{
Header: formatMessage({ id: 'account' }),
id: 'account_id',
accessor: 'account_id',
Cell: TotalAccountCellRenderer(AccountsListFieldCell),
className: 'account',
disableSortBy: true,
width: 140,
},
{
Header: formatMessage({ id: 'credit_currency' }, { currency: 'USD' }),
accessor: 'credit',
Cell: TotalCreditDebitCellRenderer(MoneyFieldCell, 'credit'),
className: 'credit',
disableSortBy: true,
width: 100,
},
{
Header: formatMessage({ id: 'debit_currency' }, { currency: 'USD' }),
accessor: 'debit',
Cell: TotalCreditDebitCellRenderer(MoneyFieldCell, 'debit'),
className: 'debit',
disableSortBy: true,
width: 100,
},
{
Header: ContactHeaderCell,
id: 'contact_id',
accessor: 'contact_id',
Cell: NoteCellRenderer(ContactsListFieldCell),
className: 'contact',
disableSortBy: true,
width: 120,
},
{
Header: formatMessage({ id: 'note' }),
accessor: 'note',
Cell: NoteCellRenderer(InputGroupCell),
disableSortBy: true,
className: 'note',
width: 200,
},
{
Header: '',
accessor: 'action',
Cell: ActionsCellRenderer,
className: 'actions',
disableSortBy: true,
disableResizing: true,
width: 45,
},
],
[formatMessage],
);
// Handles click new line.
const onClickNewRow = () => {
saveInvoke(onClickAddNewRow);
};
// Handles update datatable data.
const handleUpdateData = (rowIndex, columnId, value) => {
const newRows = updateDataReducer(rows, rowIndex, columnId, value);
saveInvoke(
onChange,
newRows
.filter((row) => row.rowType === 'editor')
.map((row) => ({
...omit(row, ['rowType']),
})),
);
};
// Handle remove datatable row.
const handleRemoveRow = (rowIndex) => {
// Can't continue if there is just one row line or less.
if (rows.length <= 2) {
return;
}
const removeIndex = parseInt(rowIndex, 10);
const newRows = rows.filter((row, index) => index !== removeIndex);
saveInvoke(
onChange,
newRows
.filter((row) => row.rowType === 'editor')
.map((row) => ({ ...omit(row, ['rowType']) })),
);
saveInvoke(onClickRemoveRow, removeIndex);
};
// Rows class names callback.
const rowClassNames = useCallback(
(row) => ({
'row--total': rows.length === row.index + 2,
}),
[rows],
);
const handleClickClearAllLines = () => {
saveInvoke(onClickClearAllLines);
};
return (
<DataTableEditable
columns={columns}
data={tableRows}
rowClassNames={rowClassNames}
sticky={true}
totalRow={true}
payload={{
accounts,
errors: error,
updateData: handleUpdateData,
removeRow: handleRemoveRow,
contacts: [
...customers.map((customer) => ({
...customer,
contact_type: 'customer',
})),
],
autoFocus: ['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>
</>
}
/>
);
}

View File

@@ -0,0 +1,190 @@
import React from 'react';
import {
Intent,
Button,
ButtonGroup,
Popover,
PopoverInteractionKind,
Position,
Menu,
MenuItem,
} from '@blueprintjs/core';
import { useFormikContext } from 'formik';
import classNames from 'classnames';
import { FormattedMessage as T } from 'react-intl';
import { CLASSES } from 'common/classes';
import { Icon, If } from 'components';
import { useHistory } from 'react-router-dom';
import { useMakeJournalFormContext } from './MakeJournalProvider';
/**
* Make Journal floating actions bar.
*/
export default function MakeJournalFloatingAction() {
const history = useHistory();
// Formik context.
const { submitForm, resetForm, isSubmitting } = useFormikContext();
// Make journal form context.
const { setSubmitPayload, manualJournal } = useMakeJournalFormContext();
// Handle submit & publish button click.
const handleSubmitPublishBtnClick = (event) => {
submitForm();
setSubmitPayload({ redirect: true, publish: true });
};
// Handle submit, publish & new button click.
const handleSubmitPublishAndNewBtnClick = (event) => {
submitForm();
setSubmitPayload({ redirect: false, publish: true, resetForm: true });
};
// Handle submit, publish & edit button click.
const handleSubmitPublishContinueEditingBtnClick = (event) => {
submitForm();
setSubmitPayload({ redirect: false, publish: true });
};
// Handle submit as draft button click.
const handleSubmitDraftBtnClick = (event) => {
setSubmitPayload({ redirect: true, publish: false });
};
// Handle submit as draft & new button click.
const handleSubmitDraftAndNewBtnClick = (event) => {
submitForm();
setSubmitPayload({ redirect: false, publish: false, resetForm: true });
};
// Handle submit as draft & continue editing button click.
const handleSubmitDraftContinueEditingBtnClick = (event) => {
submitForm();
setSubmitPayload({ redirect: false, publish: false });
};
// Handle cancel button click.
const handleCancelBtnClick = (event) => {
history.goBack();
};
// Handle clear button click.
const handleClearBtnClick = (event) => {
resetForm();
};
return (
<div className={classNames(CLASSES.PAGE_FORM_FLOATING_ACTIONS)}>
{/* ----------- Save And Publish ----------- */}
<If condition={!manualJournal || !manualJournal?.is_published}>
<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
intent={Intent.PRIMARY}
rightIcon={<Icon icon="arrow-drop-up-16" iconSize={20} />}
disabled={isSubmitting}
/>
</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
rightIcon={<Icon icon="arrow-drop-up-16" iconSize={20} />}
disabled={isSubmitting}
/>
</Popover>
</ButtonGroup>
</If>
{/* ----------- Save and New ----------- */}
<If condition={manualJournal && manualJournal?.is_published}>
<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
intent={Intent.PRIMARY}
rightIcon={<Icon icon="arrow-drop-up-16" iconSize={20} />}
disabled={isSubmitting}
/>
</Popover>
</ButtonGroup>
</If>
{/* ----------- Clear & Reset----------- */}
<Button
className={'ml1'}
disabled={isSubmitting}
onClick={handleClearBtnClick}
text={manualJournal ? <T id={'reset'} /> : <T id={'clear'} />}
/>
{/* ----------- Cancel ----------- */}
<Button
className={'ml1'}
onClick={handleCancelBtnClick}
text={<T id={'cancel'} />}
/>
</div>
);
}

View File

@@ -0,0 +1,42 @@
import React from 'react';
import { FastField } from 'formik';
import classNames from 'classnames';
import { CLASSES } from 'common/classes';
import { FormGroup, TextArea } from '@blueprintjs/core';
import { FormattedMessage as T } from 'react-intl';
import { ErrorMessage, Row, Col } from 'components';
import Dragzone from 'components/Dragzone';
import { inputIntent } from 'utils';
export default function MakeJournalFormFooter() {
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 })}
helperText={<ErrorMessage name="description" />}
fill={true}
>
<TextArea fill={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,54 @@
import { useEffect } from 'react';
import { compose } from 'redux';
import { useFormikContext } from 'formik';
import withManualJournalsActions from './withManualJournalsActions';
import withDashboardActions from 'containers/Dashboard/withDashboardActions';
import withManualJournals from './withManualJournals';
import { defaultToTransform } from 'utils';
/**
* Journal number chaلing watcher.
*/
function MakeJournalNumberChangingWatcher({
// #withDashboardActions
changePageSubtitle,
// #withManualJournals
journalNumberChanged,
// #withManualJournalsActions
setJournalNumberChanged,
// #ownProps
journalNumber,
}) {
const { setFieldValue } = useFormikContext();
// Observes journal number settings changes.
useEffect(() => {
if (journalNumberChanged) {
setFieldValue('journal_number', journalNumber);
changePageSubtitle(
defaultToTransform(journalNumber, `No. ${journalNumber}`, ''),
);
setJournalNumberChanged(false);
}
}, [
journalNumber,
journalNumberChanged,
setJournalNumberChanged,
setFieldValue,
changePageSubtitle,
]);
return null;
}
export default compose(
withManualJournals(({ journalNumberChanged }) => ({
journalNumberChanged,
})),
withManualJournalsActions,
withDashboardActions,
)(MakeJournalNumberChangingWatcher);

View File

@@ -0,0 +1,87 @@
import React, { createContext, useState } from 'react';
import DashboardInsider from 'components/Dashboard/DashboardInsider';
import {
useAccounts,
useCustomers,
useCurrencies,
useJournal,
useCreateJournal,
useEditJournal,
useSettings
} from 'hooks/query';
const MakeJournalFormContext = createContext();
/**
* Make journal form provider.
*/
function MakeJournalProvider({ journalId, ...props }) {
// Load the accounts list.
const { data: accounts, isFetching: isAccountsLoading } = useAccounts();
// Load the customers list.
const {
data: { customers },
isFetching: isCustomersLoading,
} = useCustomers();
// Load the currencies list.
const { data: currencies, isFetching: isCurrenciesLoading } = useCurrencies();
// Load the details of the given manual journal.
const { data: manualJournal, isFetching: isJournalLoading } = useJournal(
journalId,
{
enabled: !!journalId,
},
);
// Create and edit journal mutations.
const { mutateAsync: createJournalMutate } = useCreateJournal();
const { mutateAsync: editJournalMutate } = useEditJournal();
// Loading the journal settings.
const { isFetching: isSettingsLoading } = useSettings();
const [submitPayload, setSubmitPayload] = useState({});
const provider = {
accounts,
customers,
currencies,
manualJournal,
createJournalMutate,
editJournalMutate,
isAccountsLoading,
isCustomersLoading,
isCurrenciesLoading,
isJournalLoading,
isSettingsLoading,
isNewMode: !journalId,
submitPayload,
setSubmitPayload
};
return (
<DashboardInsider
loading={
isJournalLoading ||
isAccountsLoading ||
isCurrenciesLoading ||
isCustomersLoading ||
isSettingsLoading
}
name={'make-journal-page'}
>
<MakeJournalFormContext.Provider value={provider} {...props} />
</DashboardInsider>
);
}
const useMakeJournalFormContext = () =>
React.useContext(MakeJournalFormContext);
export { MakeJournalProvider, useMakeJournalFormContext };

View File

@@ -0,0 +1,91 @@
import React from 'react';
import { Position } from '@blueprintjs/core';
import { FormattedMessage as T } from 'react-intl';
import { Money, Hint } from 'components';
/**
* Contact header cell.
*/
export function ContactHeaderCell() {
return (
<>
<T id={'contact'} />
<Hint
content={<T id={'contact_column_hint'} />}
position={Position.LEFT_BOTTOM}
/>
</>
);
}
/**
* Total text cell renderer.
*/
export const TotalAccountCellRenderer = (chainedComponent) => (props) => {
if (props.data.length === props.row.index + 1) {
return <span>{'Total USD'}</span>;
}
return chainedComponent(props);
};
/**
* Total credit/debit cell renderer.
*/
export const TotalCreditDebitCellRenderer = (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>
<Money amount={total} currency={'USD'} />
</span>
);
}
return chainedComponent(props);
};
export const NoteCellRenderer = (chainedComponent) => (props) => {
if (props.data.length === props.row.index + 1) {
return '';
}
return chainedComponent(props);
};
/**
* Actions cell renderer.
*/
export 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>
);
};

View File

@@ -0,0 +1,117 @@
import React from 'react';
import { Intent } from '@blueprintjs/core';
import { sumBy, setWith, toSafeInteger, get } from 'lodash';
import { transformUpdatedRows } from 'utils';
import { AppToaster } from 'components';
import { formatMessage } from 'services/intl';
const ERROR = {
JOURNAL_NUMBER_ALREADY_EXISTS: 'JOURNAL.NUMBER.ALREADY.EXISTS',
CUSTOMERS_NOT_WITH_RECEVIABLE_ACC: 'CUSTOMERS.NOT.WITH.RECEIVABLE.ACCOUNT',
VENDORS_NOT_WITH_PAYABLE_ACCOUNT: 'VENDORS.NOT.WITH.PAYABLE.ACCOUNT',
PAYABLE_ENTRIES_HAS_NO_VENDORS: 'PAYABLE.ENTRIES.HAS.NO.VENDORS',
RECEIVABLE_ENTRIES_HAS_NO_CUSTOMERS: 'RECEIVABLE.ENTRIES.HAS.NO.CUSTOMERS',
CREDIT_DEBIT_SUMATION_SHOULD_NOT_EQUAL_ZERO:
'CREDIT.DEBIT.SUMATION.SHOULD.NOT.EQUAL.ZERO',
ENTRIES_SHOULD_ASSIGN_WITH_CONTACT: 'ENTRIES_SHOULD_ASSIGN_WITH_CONTACT',
};
/**
* Entries adjustment.
*/
function adjustmentEntries(entries) {
const credit = sumBy(entries, (e) => toSafeInteger(e.credit));
const debit = sumBy(entries, (e) => toSafeInteger(e.debit));
return {
debit: Math.max(credit - debit, 0),
credit: Math.max(debit - credit, 0),
};
}
export const updateDataReducer = (rows, rowIndex, columnId, value) => {
let newRows = transformUpdatedRows(rows, rowIndex, columnId, value);
const oldCredit = get(rows, `[${rowIndex}].credit`);
const oldDebit = get(rows, `[${rowIndex}].debit`);
if (columnId === 'account_id' && !oldCredit && !oldDebit) {
const adjustment = adjustmentEntries(rows);
if (adjustment.credit) {
newRows = transformUpdatedRows(
newRows,
rowIndex,
'credit',
adjustment.credit,
);
}
if (adjustment.debit) {
newRows = transformUpdatedRows(
newRows,
rowIndex,
'debit',
adjustment.debit,
);
}
}
return newRows;
};
// Transform API errors in toasts messages.
export const transformErrors = (resErrors, { setErrors, errors }) => {
const getError = (errorType) => resErrors.find((e) => e.type === errorType);
const toastMessages = [];
let error;
let newErrors = { ...errors, entries: [] };
const setEntriesErrors = (indexes, prop, message) =>
indexes.forEach((i) => {
const index = Math.max(i - 1, 0);
newErrors = setWith(newErrors, `entries.[${index}].${prop}`, message);
});
if ((error = getError(ERROR.RECEIVABLE_ENTRIES_HAS_NO_CUSTOMERS))) {
toastMessages.push(
formatMessage({
id: 'should_select_customers_with_entries_have_receivable_account',
}),
);
setEntriesErrors(error.indexes, 'contact_id', 'error');
}
if ((error = getError(ERROR.ENTRIES_SHOULD_ASSIGN_WITH_CONTACT))) {
if (error.meta.contact_type === 'customer') {
toastMessages.push(
formatMessage({
id: 'receivable_accounts_should_assign_with_customers',
}),
);
}
if (error.meta.contact_type === 'vendor') {
toastMessages.push(
formatMessage({ id: 'payable_accounts_should_assign_with_vendors' }),
);
}
setEntriesErrors(error.meta.indexes, 'contact_id', 'error');
}
if ((error = getError(ERROR.JOURNAL_NUMBER_ALREADY_EXISTS))) {
newErrors = setWith(
newErrors,
'journal_number',
formatMessage({
id: 'journal_number_is_already_used',
}),
);
}
setErrors({ ...newErrors });
if (toastMessages.length > 0) {
AppToaster.show({
message: toastMessages.map((message) => {
return <div>- {message}</div>;
}),
intent: Intent.DANGER,
});
}
};