mirror of
https://github.com/bigcapitalhq/bigcapital.git
synced 2026-02-17 13:20:31 +00:00
refactoring: expenses landing list.
refactoring: customers landing list. refactoring: vendors landing list. refactoring: manual journals landing list.
This commit is contained in:
@@ -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;
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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);
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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);
|
||||
@@ -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);
|
||||
@@ -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>
|
||||
</>
|
||||
}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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);
|
||||
@@ -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 };
|
||||
91
client/src/containers/Accounting/MakeJournal/components.js
Normal file
91
client/src/containers/Accounting/MakeJournal/components.js
Normal 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>
|
||||
);
|
||||
};
|
||||
|
||||
117
client/src/containers/Accounting/MakeJournal/utils.js
Normal file
117
client/src/containers/Accounting/MakeJournal/utils.js
Normal 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,
|
||||
});
|
||||
}
|
||||
};
|
||||
Reference in New Issue
Block a user