feat: expand sidebar once open form editor page.

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

View File

@@ -0,0 +1,39 @@
import * as Yup from 'yup';
import { formatMessage } from 'services/intl';
const Schema = Yup.object().shape({
journal_number: Yup.string()
.required()
.min(1)
.max(255)
.label(formatMessage({ id: 'journal_number_' })),
journal_type: Yup.string()
.required()
.min(1)
.max(255)
.label(formatMessage({ id: 'journal_type' })),
date: Yup.date()
.required()
.label(formatMessage({ id: 'date' })),
currency_code: Yup.string(),
reference: Yup.string().min(1).max(255),
description: Yup.string().min(1).max(1024),
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(255).nullable(),
}),
),
});
export const CreateJournalSchema = Schema;
export const EditJournalSchema = Schema;

View File

@@ -0,0 +1,37 @@
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

@@ -1,58 +0,0 @@
import React from 'react';
import { Intent, Button } from '@blueprintjs/core';
import { FormattedMessage as T } from 'react-intl';
export default function MakeJournalEntriesFooter({
isSubmitting,
onSubmitClick,
onCancelClick,
manualJournalId,
}) {
return (
<div>
<div class="form__floating-footer">
<Button
disabled={isSubmitting}
intent={Intent.PRIMARY}
name={'save'}
onClick={() => {
onSubmitClick({ publish: true, redirect: true });
}}
>
{manualJournalId ? <T id={'edit'} /> : <T id={'save'} />}
</Button>
<Button
disabled={isSubmitting}
intent={Intent.PRIMARY}
className={'ml1'}
name={'save_and_new'}
onClick={() => {
onSubmitClick({ publish: true, redirect: false });
}}
>
<T id={'save_new'} />
</Button>
<Button
disabled={isSubmitting}
className={'button-secondary ml1'}
onClick={() => {
onSubmitClick({ publish: false, redirect: true });
}}
>
<T id={'save_as_draft'} />
</Button>
<Button
className={'button-secondary ml1'}
onClick={() => {
onCancelClick && onCancelClick();
}}
>
<T id={'cancel'} />
</Button>
</div>
</div>
);
}

View File

@@ -1,20 +1,21 @@
import React, {
useMemo,
useState,
useEffect,
useRef,
useCallback,
} from 'react';
import * as Yup from 'yup';
import { useFormik } from 'formik';
import React, { useMemo, 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, setWith } from 'lodash';
import { pick } from 'lodash';
import classNames from 'classnames';
import { CLASSES } from 'common/classes';
import {
CreateJournalSchema,
EditJournalSchema,
} from './MakeJournalEntries.schema';
import MakeJournalEntriesHeader from './MakeJournalEntriesHeader';
import MakeJournalEntriesFooter from './MakeJournalEntriesFooter';
import MakeJournalEntriesTable from './MakeJournalEntriesTable';
import MakeJournalFormFloatingActions from './MakeJournalFormFloatingActions';
import MakeJournalEntriesField from './MakeJournalEntriesField';
import MakeJournalNumberWatcher from './MakeJournalNumberWatcher';
import MakeJournalFormFooter from './MakeJournalFormFooter';
import withJournalsActions from 'containers/Accounting/withJournalsActions';
import withManualJournalDetail from 'containers/Accounting/withManualJournalDetail';
@@ -25,25 +26,31 @@ import withSettings from 'containers/Settings/withSettings';
import AppToaster from 'components/AppToaster';
import Dragzone from 'components/Dragzone';
import withMediaActions from 'containers/Media/withMediaActions';
import useMedia from 'hooks/useMedia';
import {
compose,
repeatValue,
orderingLinesIndexes,
defaultToTransform,
} from 'utils';
import { transformErrors } from './utils';
import withManualJournalsActions from './withManualJournalsActions';
import withManualJournals from './withManualJournals';
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',
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: '',
entries: [...repeatValue(defaultEntry, 4)],
};
/**
@@ -77,27 +84,9 @@ function MakeJournalEntriesForm({
onCancelForm,
}) {
const { formatMessage } = useIntl();
const {
setFiles,
saveMedia,
deletedFiles,
setDeletedFiles,
deleteMedia,
} = useMedia({
saveCallback: requestSubmitMedia,
deleteCallback: requestDeleteMedia,
});
const isNewMode = manualJournalId;
const handleDropFiles = useCallback((_files) => {
setFiles(_files.filter((file) => file.uploaded === false));
}, []);
const savedMediaIds = useRef([]);
const clearSavedMediaIds = () => {
savedMediaIds.current = [];
};
const journalNumber = journalNumberPrefix
const journalNumber = isNewMode
? `${journalNumberPrefix}-${journalNextNumber}`
: journalNextNumber;
@@ -122,73 +111,6 @@ function MakeJournalEntriesForm({
formatMessage,
]);
const validationSchema = Yup.object().shape({
journal_number: Yup.string()
.required()
.min(1)
.max(255)
.label(formatMessage({ id: 'journal_number_' })),
journal_type: Yup.string()
.required()
.min(1)
.max(255)
.label(formatMessage({ id: 'journal_type' })),
date: Yup.date()
.required()
.label(formatMessage({ id: 'date' })),
currency_code: Yup.string(),
reference: Yup.string().min(1).max(255),
description: Yup.string().min(1).max(1024),
entries: Yup.array().of(
Yup.object().shape({
credit: Yup.number().decimalScale(13).nullable(),
debit: Yup.number().decimalScale(13).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(255).nullable(),
}),
),
});
const saveInvokeSubmit = useCallback(
(payload) => {
onFormSubmit && onFormSubmit(payload);
},
[onFormSubmit],
);
const [payload, setPayload] = useState({});
const defaultEntry = useMemo(
() => ({
index: 0,
account_id: null,
credit: 0,
debit: 0,
contact_id: null,
note: '',
}),
[],
);
const defaultInitialValues = useMemo(
() => ({
journal_number: journalNumber,
journal_type: 'Journal',
date: moment(new Date()).format('YYYY-MM-DD'),
description: '',
reference: '',
currency_code: '',
entries: [...repeatValue(defaultEntry, 4)],
}),
[defaultEntry, journalNumber],
);
const initialValues = useMemo(
() => ({
...(manualJournal
@@ -198,300 +120,124 @@ function MakeJournalEntriesForm({
...pick(entry, Object.keys(defaultEntry)),
})),
}
: {
: {
...defaultInitialValues,
journal_number: journalNumber,
entries: orderingLinesIndexes(defaultInitialValues.entries),
}),
}),
[manualJournal, defaultInitialValues, defaultEntry],
[manualJournal, journalNumber],
);
const initialAttachmentFiles = useMemo(() => {
return manualJournal && manualJournal.media
? manualJournal.media.map((attach) => ({
preview: attach.attachment_file,
uploaded: true,
metadata: { ...attach },
}))
: [];
}, [manualJournal]);
// Transform API errors in toasts messages.
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.PAYABLE_ENTRIES_HAS_NO_VENDORS))) {
toastMessages.push(
formatMessage({
id: 'vendors_should_selected_with_payable_account_only',
}),
);
setEntriesErrors(error.indexes, 'contact_id', 'error');
}
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.CUSTOMERS_NOT_WITH_RECEVIABLE_ACC))) {
toastMessages.push(
formatMessage({
id: 'customers_should_selected_with_receivable_account_only',
}),
);
setEntriesErrors(error.indexes, 'account_id', 'error');
}
if ((error = getError(ERROR.VENDORS_NOT_WITH_PAYABLE_ACCOUNT))) {
toastMessages.push(
formatMessage({
id: 'vendors_should_selected_with_payable_account_only',
}),
);
setEntriesErrors(error.indexes, 'account_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,
});
}
};
const {
values,
errors,
setFieldError,
setFieldValue,
handleSubmit,
getFieldProps,
touched,
isSubmitting,
} = useFormik({
validationSchema,
initialValues,
onSubmit: (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, status: payload.publish, entries };
const saveJournal = (mediaIds) =>
new Promise((resolve, reject) => {
const requestForm = { ...form, media_ids: mediaIds };
if (manualJournal && manualJournal.id) {
requestEditManualJournal(manualJournal.id, requestForm)
.then((response) => {
AppToaster.show({
message: formatMessage(
{ id: 'the_journal_has_been_successfully_edited' },
{ number: values.journal_number },
),
intent: Intent.SUCCESS,
});
setSubmitting(false);
saveInvokeSubmit({ action: 'update', ...payload });
clearSavedMediaIds([]);
resetForm();
resolve(response);
})
.catch((errors) => {
transformErrors(errors, { setErrors });
setSubmitting(false);
});
} else {
requestMakeJournalEntries(requestForm)
.then((response) => {
AppToaster.show({
message: formatMessage(
{ id: 'the_journal_has_been_successfully_created' },
{ number: values.journal_number },
),
intent: Intent.SUCCESS,
});
setSubmitting(false);
saveInvokeSubmit({ action: 'new', ...payload });
clearSavedMediaIds();
resetForm();
resolve(response);
})
.catch((errors) => {
transformErrors(errors, { setErrors });
setSubmitting(false);
});
}
});
Promise.all([saveMedia(), deleteMedia()])
.then(([savedMediaResponses]) => {
const mediaIds = savedMediaResponses.map((res) => res.data.media.id);
savedMediaIds.current = mediaIds;
return savedMediaResponses;
})
.then(() => {
return saveJournal(savedMediaIds.current);
});
},
});
// Observes journal number settings changes.
useEffect(() => {
if (journalNumberChanged) {
setFieldValue('journal_number', journalNumber);
changePageSubtitle(
defaultToTransform(journalNumber, `No. ${journalNumber}`, ''),
);
setJournalNumberChanged(false);
}
}, [
journalNumber,
journalNumberChanged,
setJournalNumberChanged,
setFieldValue,
changePageSubtitle,
]);
const handleSubmitClick = useCallback(
(payload) => {
setPayload(payload);
// formik.resetForm();
handleSubmit();
},
[setPayload, handleSubmit],
);
const handleCancelClick = useCallback(
(payload) => {
onCancelForm && onCancelForm(payload);
},
[onCancelForm],
);
const handleDeleteFile = useCallback(
(_deletedFiles) => {
_deletedFiles.forEach((deletedFile) => {
if (deletedFile.uploaded && deletedFile.metadata.id) {
setDeletedFiles([...deletedFiles, deletedFile.metadata.id]);
}
});
},
[setDeletedFiles, deletedFiles],
);
// Handle click on add a new line/row.
const handleClickAddNewRow = useCallback(() => {
setFieldValue(
'entries',
orderingLinesIndexes([...values.entries, defaultEntry]),
);
}, [values.entries, defaultEntry, setFieldValue]);
// Handle click `Clear all lines` button.
const handleClickClearLines = useCallback(() => {
setFieldValue(
'entries',
orderingLinesIndexes([...repeatValue(defaultEntry, 4)]),
);
}, [defaultEntry, setFieldValue]);
// Handle journal number field change.
const handleJournalNumberChanged = useCallback(
(journalNumber) => {
changePageSubtitle(
defaultToTransform(journalNumber, `No. ${journalNumber}`, '')
defaultToTransform(journalNumber, `No. ${journalNumber}`, ''),
);
},
[changePageSubtitle],
);
return (
<div class="make-journal-entries">
<form onSubmit={handleSubmit}>
<MakeJournalEntriesHeader
manualJournal={manualJournalId}
errors={errors}
touched={touched}
values={values}
setFieldValue={setFieldValue}
getFieldProps={getFieldProps}
onJournalNumberChanged={handleJournalNumberChanged}
/>
<MakeJournalEntriesTable
values={values.entries}
errors={errors}
setFieldValue={setFieldValue}
defaultRow={defaultEntry}
onClickClearAllLines={handleClickClearLines}
onClickAddNewRow={handleClickAddNewRow}
/>
<MakeJournalEntriesFooter
isSubmitting={isSubmitting}
onSubmitClick={handleSubmitClick}
onCancelClick={handleCancelClick}
manualJournal={manualJournalId}
/>
</form>
<Dragzone
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, entries };
const handleError = (error) => {
transformErrors(error, { setErrors });
setSubmitting(false);
};
const handleSuccess = (errors) => {
AppToaster.show({
message: formatMessage(
{
id: isNewMode
? 'the_journal_has_been_successfully_created'
: 'the_journal_has_been_successfully_edited',
},
{ number: values.journal_number },
),
intent: Intent.SUCCESS,
});
setSubmitting(false);
resetForm();
};
if (isNewMode) {
requestEditManualJournal(manualJournal.id, form)
.then(handleSuccess)
.catch(handleError);
} else {
requestMakeJournalEntries(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}
>
{({ isSubmitting, values }) => (
<Form>
<MakeJournalEntriesHeader
manualJournal={manualJournalId}
onJournalNumberChanged={handleJournalNumberChanged}
/>
<MakeJournalNumberWatcher journalNumber={journalNumber} />
<MakeJournalEntriesField defaultRow={defaultEntry} />
<MakeJournalFormFooter />
<MakeJournalFormFloatingActions
isSubmitting={isSubmitting}
manualJournal={manualJournalId}
/>
</Form>
)}
</Formik>
{/* <Dragzone
initialFiles={initialAttachmentFiles}
onDrop={handleDropFiles}
onDeleteFile={handleDeleteFile}
hint={'Attachments: Maxiumum size: 20MB'}
/>
/> */}
</div>
);
}
@@ -503,11 +249,8 @@ export default compose(
withDashboardActions,
withMediaActions,
withSettings(({ manualJournalsSettings }) => ({
journalNextNumber: manualJournalsSettings.nextNumber,
journalNumberPrefix: manualJournalsSettings.numberPrefix,
journalNextNumber: parseInt(manualJournalsSettings?.nextNumber, 10),
journalNumberPrefix: manualJournalsSettings?.numberPrefix,
})),
withManualJournalsActions,
withManualJournals(({ journalNumberChanged }) => ({
journalNumberChanged,
})),
)(MakeJournalEntriesForm);

View File

@@ -1,238 +1,12 @@
import React, { useMemo, useState, useCallback } from 'react';
import {
InputGroup,
FormGroup,
Intent,
Position,
ControlGroup,
} from '@blueprintjs/core';
import { DateInput } from '@blueprintjs/datetime';
import { FormattedMessage as T } from 'react-intl';
import { Row, Col } from 'react-grid-system';
import moment from 'moment';
import React from 'react';
import classNames from 'classnames';
import { CLASSES } from 'common/classes';
import { momentFormatter, tansformDateValue, saveInvoke } from 'utils';
import {
ErrorMessage,
Hint,
FieldHint,
FieldRequiredHint,
Icon,
InputPrependButton,
CurrencySelectList,
} from 'components';
import withDialogActions from 'containers/Dialog/withDialogActions';
import withCurrencies from 'containers/Currencies/withCurrencies';
import { compose } from 'utils';
function MakeJournalEntriesHeader({
errors,
touched,
values,
setFieldValue,
getFieldProps,
// #ownProps
manualJournal,
onJournalNumberChanged,
// #withCurrencies
currenciesList,
// #withDialog
openDialog,
}) {
const [selectedItems, setSelectedItems] = useState({});
const handleDateChange = useCallback(
(date) => {
const formatted = moment(date).format('YYYY-MM-DD');
setFieldValue('date', formatted);
},
[setFieldValue],
);
const handleJournalNumberChange = useCallback(() => {
openDialog('journal-number-form', {});
}, [openDialog]);
// Handle journal number field blur event.
const handleJournalNumberChanged = (event) => {
saveInvoke(onJournalNumberChanged, event.currentTarget.value);
};
const onItemsSelect = useCallback(
(filedName) => {
return (filed) => {
setSelectedItems({
...selectedItems,
[filedName]: filed,
});
setFieldValue(filedName, filed.currency_code);
};
},
[setFieldValue, selectedItems],
);
import MakeJournalEntriesHeaderFields from "./MakeJournalEntriesHeaderFields";
export default function MakeJournalEntriesHeader() {
return (
<div class="make-journal-entries__header">
<Row>
<Col width={260}>
<FormGroup
label={<T id={'journal_number'} />}
labelInfo={
<>
<FieldRequiredHint />
<FieldHint />
</>
}
className={'form-group--journal-number'}
intent={
errors.journal_number && touched.journal_number && Intent.DANGER
}
helperText={
<ErrorMessage name="journal_number" {...{ errors, touched }} />
}
fill={true}
>
<ControlGroup fill={true}>
<InputGroup
intent={
errors.journal_number &&
touched.journal_number &&
Intent.DANGER
}
fill={true}
{...getFieldProps('journal_number')}
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>
</Col>
<Col width={220}>
<FormGroup
label={<T id={'date'} />}
labelInfo={<FieldRequiredHint />}
intent={errors.date && touched.date && Intent.DANGER}
helperText={<ErrorMessage name="date" {...{ errors, touched }} />}
minimal={true}
className={classNames(CLASSES.FILL)}
>
<DateInput
{...momentFormatter('YYYY/MM/DD')}
onChange={handleDateChange}
value={tansformDateValue(values.date)}
popoverProps={{
position: Position.BOTTOM,
minimal: true,
}}
/>
</FormGroup>
</Col>
<Col width={400}>
<FormGroup
label={<T id={'description'} />}
className={'form-group--description'}
intent={errors.name && touched.name && Intent.DANGER}
helperText={
<ErrorMessage name="description" {...{ errors, touched }} />
}
fill={true}
>
<InputGroup
intent={errors.name && touched.name && Intent.DANGER}
fill={true}
{...getFieldProps('description')}
/>
</FormGroup>
</Col>
</Row>
<Row>
<Col width={260}>
<FormGroup
label={<T id={'reference'} />}
labelInfo={
<Hint
content={<T id={'journal_reference_hint'} />}
position={Position.RIGHT}
/>
}
className={'form-group--reference'}
intent={errors.reference && touched.reference && Intent.DANGER}
helperText={
<ErrorMessage name="reference" {...{ errors, touched }} />
}
fill={true}
>
<InputGroup
intent={errors.reference && touched.reference && Intent.DANGER}
fill={true}
{...getFieldProps('reference')}
/>
</FormGroup>
</Col>
<Col width={220}>
<FormGroup
label={<T id={'journal_type'} />}
className={classNames(
'form-group--account-type',
'form-group--select-list',
CLASSES.FILL,
)}
>
<InputGroup
intent={
errors.journal_type && touched.journal_type && Intent.DANGER
}
fill={true}
{...getFieldProps('journal_type')}
/>
</FormGroup>
</Col>
<Col width={230}>
<FormGroup
label={<T id={'currency'} />}
className={classNames(
'form-group--select-list',
'form-group--currency',
CLASSES.FILL,
)}
>
<CurrencySelectList
currenciesList={currenciesList}
selectedCurrencyCode={values.currency_code}
onCurrencySelected={onItemsSelect('currency_code')}
defaultSelectText={values.currency_code}
/>
</FormGroup>
</Col>
</Row>
<div className={classNames(CLASSES.PAGE_FORM_HEADER)}>
<MakeJournalEntriesHeaderFields />
</div>
);
}
export default compose(
withDialogActions,
withCurrencies(({ currenciesList }) => ({
currenciesList,
})),
)(MakeJournalEntriesHeader);
)
}

View File

@@ -0,0 +1,188 @@
import React, { useCallback } 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 withDialogActions from 'containers/Dialog/withDialogActions';
import withCurrencies from 'containers/Currencies/withCurrencies';
import { compose, inputIntent, handleDateChange } from 'utils';
function MakeJournalEntriesHeader({
// #ownProps
manualJournal,
onJournalNumberChanged,
// #withCurrencies
currenciesList,
// #withDialog
openDialog,
}) {
const handleJournalNumberChange = useCallback(() => {
openDialog('journal-number-form', {});
}, [openDialog]);
// Handle journal number field blur event.
const handleJournalNumberChanged = (event) => {
saveInvoke(onJournalNumberChanged, event.currentTarget.value);
};
return (
<div className={classNames(CLASSES.PAGE_FORM_HEADER_FIELDS)}>
<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>
<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>
<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>
<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>
<FastField name={'currency'}>
{({ form, field: { value }, meta: { error, touched } }) => (
<FormGroup
label={<T id={'currency'} />}
className={classNames(
'form-group--currency',
CLASSES.FILL,
)}
inline={true}
>
<CurrencySelectList
currenciesList={currenciesList}
selectedCurrencyCode={value}
onCurrencySelected={(currencyItem) => {
form.setFieldValue('currency_code', currencyItem.currency_code);
}}
defaultSelectText={value}
/>
</FormGroup>
)}
</FastField>
</div>
);
}
export default compose(
withDialogActions,
withCurrencies(({ currenciesList }) => ({
currenciesList,
})),
)(MakeJournalEntriesHeader);

View File

@@ -1,4 +1,4 @@
import React, { useCallback } from 'react';
import React, { useCallback, useEffect } from 'react';
import { useParams, useHistory } from 'react-router-dom';
import { useQuery } from 'react-query';
@@ -10,6 +10,7 @@ import withAccountsActions from 'containers/Accounts/withAccountsActions';
import withManualJournalsActions from 'containers/Accounting/withManualJournalsActions';
import withCurrenciesActions from 'containers/Currencies/withCurrenciesActions';
import withSettingsActions from 'containers/Settings/withSettingsActions';
import withDashboardActions from 'containers/Dashboard/withDashboardActions';
import { compose } from 'utils';
@@ -28,10 +29,24 @@ function MakeJournalEntriesPage({
// #withSettingsActions
requestFetchOptions,
// #withDashboardActions
setSidebarShrink,
resetSidebarPreviousExpand
}) {
const history = useHistory();
const { id } = useParams();
useEffect(() => {
// Shrink the sidebar by foce.
setSidebarShrink();
return () => {
// Reset the sidebar to the previous status.
resetSidebarPreviousExpand();
};
}, [resetSidebarPreviousExpand, setSidebarShrink]);
const fetchAccounts = useQuery('accounts-list', (key) =>
requestFetchAccounts(),
);
@@ -88,4 +103,5 @@ export default compose(
withManualJournalsActions,
withCurrenciesActions,
withSettingsActions,
withDashboardActions,
)(MakeJournalEntriesPage);

View File

@@ -2,91 +2,32 @@ import React, { useState, useMemo, useEffect, useCallback } from 'react';
import { Button, Tooltip, Position, Intent } from '@blueprintjs/core';
import { FormattedMessage as T, useIntl } from 'react-intl';
import { omit } from 'lodash';
import classNames from 'classnames';
import { CLASSES } from 'common/classes';
import DataTable from 'components/DataTable';
import Icon from 'components/Icon';
import { Hint } from 'components';
import { compose, formattedAmount, transformUpdatedRows } from 'utils';
import {
compose,
formattedAmount,
transformUpdatedRows,
saveInvoke,
} from 'utils';
import {
AccountsListFieldCell,
MoneyFieldCell,
InputGroupCell,
ContactsListFieldCell,
} from 'components/DataTableCells';
import {
ContactHeaderCell,
ActionsCellRenderer,
TotalAccountCellRenderer,
TotalCreditDebitCellRenderer,
NoteCellRenderer,
} from './components';
import withAccounts from 'containers/Accounts/withAccounts';
import withCustomers from 'containers/Customers/withCustomers';
// Contact header cell.
function ContactHeaderCell() {
return (
<>
<T id={'contact'} />
<Hint
content={<T id={'contact_column_hint'} />}
position={Position.LEFT_BOTTOM}
/>
</>
);
}
// Actions cell renderer.
const ActionsCellRenderer = ({
row: { index },
column: { id },
cell: { value: initialValue },
data,
payload,
}) => {
if (data.length <= index + 2) {
return '';
}
const onClickRemoveRole = () => {
payload.removeRow(index);
};
return (
<Tooltip content={<T id={'remove_the_line'} />} position={Position.LEFT}>
<Button
icon={<Icon icon="times-circle" iconSize={14} />}
iconSize={14}
className="ml2"
minimal={true}
intent={Intent.DANGER}
onClick={onClickRemoveRole}
/>
</Tooltip>
);
};
// Total text cell renderer.
const TotalAccountCellRenderer = (chainedComponent) => (props) => {
if (props.data.length === props.row.index + 2) {
return <span>{'Total USD'}</span>;
}
return chainedComponent(props);
};
// Total credit/debit cell renderer.
const TotalCreditDebitCellRenderer = (chainedComponent, type) => (props) => {
if (props.data.length === props.row.index + 2) {
const total = props.data.reduce((total, entry) => {
const amount = parseInt(entry[type], 10);
const computed = amount ? total + amount : total;
return computed;
}, 0);
return <span>{formattedAmount(total, 'USD')}</span>;
}
return chainedComponent(props);
};
const NoteCellRenderer = (chainedComponent) => (props) => {
if (props.data.length === props.row.index + 2) {
return '';
}
return chainedComponent(props);
};
/**
* Make journal entries table component.
*/
@@ -101,22 +42,19 @@ function MakeJournalEntriesTable({
onClickRemoveRow,
onClickAddNewRow,
onClickClearAllLines,
defaultRow,
values,
errors, setFieldValue,
onChange,
entries,
error,
}) {
const [rows, setRows] = useState([]);
const { formatMessage } = useIntl();
useEffect(() => {
setRows([...values.map((e) => ({ ...e, rowType: 'editor' }))]);
}, [values, setRows]);
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' }, { rowType: 'final_space' }],
[rows],
);
const tableRows = useMemo(() => [...rows, { rowType: 'total' }], [rows]);
// Memorized data table columns.
const columns = useMemo(
@@ -138,8 +76,7 @@ function MakeJournalEntriesTable({
Cell: TotalAccountCellRenderer(AccountsListFieldCell),
className: 'account',
disableSortBy: true,
disableResizing: true,
width: 250,
width: 140,
},
{
Header: formatMessage({ id: 'credit_currency' }, { currency: 'USD' }),
@@ -147,8 +84,7 @@ function MakeJournalEntriesTable({
Cell: TotalCreditDebitCellRenderer(MoneyFieldCell, 'credit'),
className: 'credit',
disableSortBy: true,
disableResizing: true,
width: 150,
width: 100,
},
{
Header: formatMessage({ id: 'debit_currency' }, { currency: 'USD' }),
@@ -156,8 +92,7 @@ function MakeJournalEntriesTable({
Cell: TotalCreditDebitCellRenderer(MoneyFieldCell, 'debit'),
className: 'debit',
disableSortBy: true,
disableResizing: true,
width: 150,
width: 100,
},
{
Header: ContactHeaderCell,
@@ -165,9 +100,8 @@ function MakeJournalEntriesTable({
accessor: 'contact_id',
Cell: NoteCellRenderer(ContactsListFieldCell),
className: 'contact',
disableResizing: true,
disableSortBy: true,
width: 200,
width: 120,
},
{
Header: formatMessage({ id: 'note' }),
@@ -175,6 +109,7 @@ function MakeJournalEntriesTable({
Cell: NoteCellRenderer(InputGroupCell),
disableSortBy: true,
className: 'note',
width: 200,
},
{
Header: '',
@@ -190,9 +125,9 @@ function MakeJournalEntriesTable({
);
// Handles click new line.
const onClickNewRow = useCallback(() => {
onClickAddNewRow && onClickAddNewRow();
}, [defaultRow, rows, onClickAddNewRow]);
const onClickNewRow = () => {
saveInvoke(onClickAddNewRow);
};
// Handles update datatable data.
const handleUpdateData = useCallback(
@@ -203,8 +138,8 @@ function MakeJournalEntriesTable({
columnIdOrObj,
value,
);
setFieldValue(
'entries',
saveInvoke(
onChange,
newRows
.filter((row) => row.rowType === 'editor')
.map((row) => ({
@@ -212,26 +147,27 @@ function MakeJournalEntriesTable({
})),
);
},
[rows, setFieldValue],
[rows, onChange],
);
const handleRemoveRow = useCallback(
(rowIndex) => {
// Can't continue if there is just one row line or less.
if (rows.length <= 2) { return; }
if (rows.length <= 2) {
return;
}
const removeIndex = parseInt(rowIndex, 10);
const newRows = rows.filter((row, index) => index !== removeIndex);
setFieldValue(
'entries',
saveInvoke(
onChange,
newRows
.filter((row) => row.rowType === 'editor')
.map((row) => ({ ...omit(row, ['rowType']) })),
);
onClickRemoveRow && onClickRemoveRow(removeIndex);
saveInvoke(onClickRemoveRow, removeIndex);
},
[rows, setFieldValue, onClickRemoveRow],
[rows, onChange, onClickRemoveRow],
);
// Rows class names callback.
@@ -243,11 +179,16 @@ function MakeJournalEntriesTable({
);
const handleClickClearAllLines = () => {
onClickClearAllLines && onClickClearAllLines();
saveInvoke(onClickClearAllLines);
};
return (
<div class="make-journal-entries__table datatable-editor">
<div
className={classNames(
CLASSES.DATATABLE_EDITOR,
CLASSES.DATATABLE_EDITOR_HAS_TOTAL_ROW,
)}
>
<DataTable
columns={columns}
data={tableRows}
@@ -255,7 +196,7 @@ function MakeJournalEntriesTable({
sticky={true}
payload={{
accounts: accountsList,
errors: errors.entries || [],
errors: error,
updateData: handleUpdateData,
removeRow: handleRemoveRow,
contacts: [
@@ -266,8 +207,7 @@ function MakeJournalEntriesTable({
],
}}
/>
<div class="mt1">
<div className={classNames(CLASSES.DATATABLE_EDITOR_ACTIONS)}>
<Button
small={true}
className={'button--secondary button--new-line'}

View File

@@ -0,0 +1,59 @@
import React from 'react';
import { Intent, Button } from '@blueprintjs/core';
import classNames from 'classnames';
import { FormattedMessage as T } from 'react-intl';
import { saveInvoke } from 'utils';
import { CLASSES } from 'common/classes';
export default function MakeJournalFloatingAction({
isSubmitting,
onSubmitClick,
onCancelClick,
manualJournalId,
}) {
return (
<div className={classNames(CLASSES.PAGE_FORM_FLOATING_ACTIONS)}>
<Button
disabled={isSubmitting}
intent={Intent.PRIMARY}
name={'save'}
onClick={() => {
saveInvoke(onSubmitClick, { publish: true, redirect: true });
}}
>
{manualJournalId ? <T id={'edit'} /> : <T id={'save'} />}
</Button>
<Button
disabled={isSubmitting}
intent={Intent.PRIMARY}
className={'ml1'}
name={'save_and_new'}
onClick={() => {
saveInvoke(onSubmitClick, { publish: true, redirect: false });
}}
>
<T id={'save_new'} />
</Button>
<Button
disabled={isSubmitting}
className={'button-secondary ml1'}
onClick={() => {
saveInvoke(onSubmitClick, { publish: false, redirect: true });
}}
>
<T id={'save_as_draft'} />
</Button>
<Button
className={'button-secondary ml1'}
onClick={() => {
saveInvoke(onCancelClick);
}}
>
<T id={'cancel'} />
</Button>
</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,48 @@
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';
/**
*
*/
function MakeJournalNumberChangingWatcher({
journalNumber,
journalNumberChanged,
setJournalNumberChanged,
changePageSubtitle
}) {
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

@@ -1,13 +1,7 @@
import React from 'react';
import {
Intent,
Classes,
Tooltip,
Position,
Tag,
} from '@blueprintjs/core';
import { Intent, Classes, Tooltip, Position, Tag, Button } from '@blueprintjs/core';
import { FormattedMessage as T } from 'react-intl';
import { Choose, Money, If, Icon } from 'components';
import { Choose, Money, If, Icon, Hint } from 'components';
import withAccountDetails from 'containers/Accounts/withAccountDetail';
import { compose } from 'utils';
@@ -99,4 +93,77 @@ export function NoteAccessor(row) {
</Tooltip>
</If>
);
}
}
// Contact header cell.
export function ContactHeaderCell() {
return (
<>
<T id={'contact'} />
<Hint
content={<T id={'contact_column_hint'} />}
position={Position.LEFT_BOTTOM}
/>
</>
);
}
// 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>
);
};
// 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);
};

View File

@@ -0,0 +1,81 @@
import React from 'react';
import { Intent } from '@blueprintjs/core';
import { AppToaster } from 'components';
import { formatMessage } from 'services/intl';
import { setWith } from 'lodash';
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',
};
// 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.PAYABLE_ENTRIES_HAS_NO_VENDORS))) {
toastMessages.push(
formatMessage({
id: 'vendors_should_selected_with_payable_account_only',
}),
);
setEntriesErrors(error.indexes, 'contact_id', 'error');
}
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.CUSTOMERS_NOT_WITH_RECEVIABLE_ACC))) {
toastMessages.push(
formatMessage({
id: 'customers_should_selected_with_receivable_account_only',
}),
);
setEntriesErrors(error.indexes, 'account_id', 'error');
}
if ((error = getError(ERROR.VENDORS_NOT_WITH_PAYABLE_ACCOUNT))) {
toastMessages.push(
formatMessage({
id: 'vendors_should_selected_with_payable_account_only',
}),
);
setEntriesErrors(error.indexes, 'account_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,
});
}
};

View File

@@ -41,11 +41,22 @@ const mapActionsToProps = (dispatch) => ({
type: t.SIDEBAR_EXPEND_TOGGLE,
}),
changePreferencesPageTitle: (pageTitle) =>
dispatch({
type: 'CHANGE_PREFERENCES_PAGE_TITLE',
pageTitle,
}),
changePreferencesPageTitle: (pageTitle) => dispatch({
type: 'CHANGE_PREFERENCES_PAGE_TITLE',
pageTitle,
}),
setSidebarShrink: () => dispatch({
type: t.SIDEBAR_SHRINK,
}),
setSidebarExpand: () => dispatch({
type: t.SIDEBAR_SHRINK,
}),
resetSidebarPreviousExpand: () => dispatch({
type: t.RESET_SIDEBAR_PREVIOUS_EXPAND,
}),
recordSidebarPreviousExpand: () => dispatch({
type: t.RECORD_SIDEBAR_PREVIOUS_EXPAND,
}),
});
export default connect(null, mapActionsToProps);

View File

@@ -229,6 +229,7 @@ function ItemsEntriesTable({
className={classNames(
CLASSES.DATATABLE_EDITOR,
CLASSES.DATATABLE_EDITOR_ITEMS_ENTRIES,
CLASSES.DATATABLE_EDITOR_HAS_TOTAL_ROW,
)}
>
<DataTable
@@ -243,7 +244,7 @@ function ItemsEntriesTable({
removeRow: handleRemoveRow,
}}
/>
<div className={classNames(CLASSES.DATATABLE_EDITOR_ACTIONS, 'mt1')}>
<div className={classNames(CLASSES.DATATABLE_EDITOR_ACTIONS)}>
<Button
small={true}
className={'button--secondary button--new-line'}

View File

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

View File

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

View File

@@ -1,6 +1,7 @@
import * as Yup from 'yup';
import { formatMessage } from 'services/intl';
import { DATATYPES_LENGTH } from 'common/dataTypes';
import { isBlank } from 'utils';
const Schema = Yup.object().shape({
beneficiary: Yup.string().label(formatMessage({ id: 'beneficiary' })),
@@ -25,11 +26,11 @@ const Schema = Yup.object().shape({
categories: Yup.array().of(
Yup.object().shape({
index: Yup.number().min(1).max(DATATYPES_LENGTH.INT_10).nullable(),
amount: Yup.number().decimalScale(13).nullable(),
amount: Yup.number().nullable(),
expense_account_id: Yup.number()
.nullable()
.when(['amount'], {
is: (amount) => amount,
is: (amount) => !isBlank(amount),
then: Yup.number().required(),
}),
description: Yup.string().max(DATATYPES_LENGTH.TEXT).nullable(),

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -41,7 +41,7 @@ const ItemsCategoryList = ({
(category) => () => {
openDialog('item-category-form', { action: 'edit', id: category.id });
},
[],
[openDialog],
);
const handleDeleteCategory = useCallback(

View File

@@ -12,7 +12,7 @@ import { EditBillFormSchema, CreateBillFormSchema } from './BillForm.schema';
import BillFormHeader from './BillFormHeader';
import BillFloatingActions from './BillFloatingActions';
import BillFormFooter from './BillFormFooter';
import EditableItemsEntriesTable from 'containers/Entries/EditableItemsEntriesTable';
import withDashboardActions from 'containers/Dashboard/withDashboardActions';
import withMediaActions from 'containers/Media/withMediaActions';
@@ -23,6 +23,7 @@ import { AppToaster } from 'components';
import { ERROR } from 'common/errors';
import { compose, repeatValue, defaultToTransform, orderingLinesIndexes } from 'utils';
import BillFormBody from './BillFormBody';
const MIN_LINES_NUMBER = 5;
@@ -31,7 +32,7 @@ const defaultBill = {
item_id: '',
rate: '',
discount: '',
quantity: '',
quantity: 1,
description: '',
};
@@ -198,7 +199,11 @@ function BillForm({
}, [history]);
return (
<div className={classNames(CLASSES.PAGE_FORM, CLASSES.PAGE_FORM_BILL)}>
<div className={classNames(
CLASSES.PAGE_FORM,
CLASSES.PAGE_FORM_STRIP_STYLE,
CLASSES.PAGE_FORM_BILL,
)}>
<Formik
validationSchema={isNewMode ? CreateBillFormSchema : EditBillFormSchema}
initialValues={initialValues}
@@ -207,10 +212,7 @@ function BillForm({
{({ isSubmitting, values }) => (
<Form>
<BillFormHeader onBillNumberChanged={handleBillNumberChanged} />
<EditableItemsEntriesTable
defaultEntry={defaultBill}
filterPurchasableItems={true}
/>
<BillFormBody defaultBill={defaultBill} />
<BillFormFooter
oninitialFiles={[]}
// onDropFiles={handleDeleteFile}

View File

@@ -1,6 +1,7 @@
import * as Yup from 'yup';
import { formatMessage } from 'services/intl';
import { DATATYPES_LENGTH } from 'common/dataTypes';
import { isBlank } from 'utils';
const BillFormSchema = Yup.object().shape({
vendor_id: Yup.number()
@@ -34,7 +35,7 @@ const BillFormSchema = Yup.object().shape({
item_id: Yup.number()
.nullable()
.when(['quantity', 'rate'], {
is: (quantity, rate) => quantity || rate,
is: (quantity, rate) => !isBlank(quantity) && !isBlank(rate),
then: Yup.number().required(),
}),
total: Yup.number().nullable(),

View File

@@ -0,0 +1,15 @@
import React from 'react';
import classNames from 'classnames';
import { CLASSES } from 'common/classes';
import EditableItemsEntriesTable from 'containers/Entries/EditableItemsEntriesTable';
export default function BillFormBody({ defaultBill }) {
return (
<div className={classNames(CLASSES.PAGE_FORM_BODY)}>
<EditableItemsEntriesTable
defaultEntry={defaultBill}
filterPurchasableItems={true}
/>
</div>
);
}

View File

@@ -1,164 +1,20 @@
import React from 'react';
import { FormGroup, InputGroup, Position } from '@blueprintjs/core';
import { DateInput } from '@blueprintjs/datetime';
import { FormattedMessage as T } from 'react-intl';
import { FastField, ErrorMessage } from 'formik';
import classNames from 'classnames';
import { CLASSES } from 'common/classes';
import { ContactSelecetList, FieldRequiredHint, Row, Col } from 'components';
import withVendors from 'containers/Vendors/withVendors';
import withAccounts from 'containers/Accounts/withAccounts';
import withDialogActions from 'containers/Dialog/withDialogActions';
import {
momentFormatter,
compose,
tansformDateValue,
handleDateChange,
inputIntent,
saveInvoke,
} from 'utils';
import BillFormHeaderFields from './BillFormHeaderFields';
import { PageFormBigNumber } from 'components';
/**
* Fill form header.
*/
function BillFormHeader({
onBillNumberChanged,
//#withVendors
vendorItems,
}) {
const handleBillNumberBlur = (event) => {
saveInvoke(onBillNumberChanged, event.currentTarget.value);
};
export default function BillFormHeader({ onBillNumberChanged }) {
return (
<div className={classNames(CLASSES.PAGE_FORM_HEADER)}>
<div className={'page-form__primary-section'}>
{/* ------- Vendor name ------ */}
<FastField name={'vendor_id'}>
{({ form, field: { value }, meta: { error, touched } }) => (
<FormGroup
label={<T id={'vendor_name'} />}
inline={true}
className={classNames(CLASSES.FILL, 'form-group--vendor')}
labelInfo={<FieldRequiredHint />}
intent={inputIntent({ error, touched })}
helperText={<ErrorMessage name={'vendor_id'} />}
>
<ContactSelecetList
contactsList={vendorItems}
selectedContactId={value}
defaultSelectText={<T id={'select_vender_account'} />}
onContactSelected={(contact) => {
form.setFieldValue('vendor_id', contact.id);
}}
popoverFill={true}
/>
</FormGroup>
)}
</FastField>
<BillFormHeaderFields onBillNumberChanged={onBillNumberChanged} />
<Row className={'row--bill-date'}>
<Col md={7}>
{/* ------- Bill date ------- */}
<FastField name={'bill_date'}>
{({ form, field: { value }, meta: { error, touched } }) => (
<FormGroup
label={<T id={'bill_date'} />}
inline={true}
labelInfo={<FieldRequiredHint />}
className={classNames(CLASSES.FILL)}
intent={inputIntent({ error, touched })}
helperText={<ErrorMessage name="bill_date" />}
>
<DateInput
{...momentFormatter('YYYY/MM/DD')}
value={tansformDateValue(value)}
onChange={handleDateChange((formattedDate) => {
form.setFieldValue('bill_date', formattedDate);
})}
popoverProps={{ position: Position.BOTTOM, minimal: true }}
/>
</FormGroup>
)}
</FastField>
</Col>
<Col md={5}>
{/* ------- Due date ------- */}
<FastField name={'due_date'}>
{({ form, field: { value }, meta: { error, touched } }) => (
<FormGroup
label={<T id={'due_date'} />}
inline={true}
className={classNames(
'form-group--due-date',
'form-group--select-list',
CLASSES.FILL,
)}
intent={inputIntent({ error, touched })}
helperText={<ErrorMessage name="due_date" />}
>
<DateInput
{...momentFormatter('YYYY/MM/DD')}
value={tansformDateValue(value)}
onChange={handleDateChange((formattedDate) => {
form.setFieldValue('due_date', formattedDate);
})}
popoverProps={{ position: Position.BOTTOM, minimal: true }}
/>
</FormGroup>
)}
</FastField>
</Col>
</Row>
{/* ------- Bill number ------- */}
<FastField name={'bill_number'}>
{({ field, meta: { error, touched } }) => (
<FormGroup
label={<T id={'bill_number'} />}
inline={true}
className={('form-group--bill_number', CLASSES.FILL)}
intent={inputIntent({ error, touched })}
helperText={<ErrorMessage name="bill_number" />}
>
<InputGroup
minimal={true}
{...field}
onBlur={handleBillNumberBlur}
/>
</FormGroup>
)}
</FastField>
{/* ------- Reference ------- */}
<FastField name={'reference_no'}>
{({ field, meta: { error, touched } }) => (
<FormGroup
label={<T id={'reference'} />}
inline={true}
className={classNames('form-group--reference', CLASSES.FILL)}
intent={inputIntent({ error, touched })}
helperText={<ErrorMessage name="reference" />}
>
<InputGroup minimal={true} {...field} />
</FormGroup>
)}
</FastField>
</div>
<PageFormBigNumber label={'Due Amount'} amount={0} currencyCode={'LYD'} />
</div>
);
}
export default compose(
withVendors(({ vendorItems }) => ({
vendorItems,
})),
withAccounts(({ accountsList }) => ({
accountsList,
})),
withDialogActions,
)(BillFormHeader);

View File

@@ -0,0 +1,161 @@
import React from 'react';
import { FormGroup, InputGroup, Position } from '@blueprintjs/core';
import { DateInput } from '@blueprintjs/datetime';
import { FormattedMessage as T } from 'react-intl';
import { FastField, ErrorMessage } from 'formik';
import classNames from 'classnames';
import { CLASSES } from 'common/classes';
import { ContactSelecetList, FieldRequiredHint, Icon } from 'components';
import withVendors from 'containers/Vendors/withVendors';
import withAccounts from 'containers/Accounts/withAccounts';
import withDialogActions from 'containers/Dialog/withDialogActions';
import {
momentFormatter,
compose,
tansformDateValue,
handleDateChange,
inputIntent,
saveInvoke,
} from 'utils';
/**
* Fill form header.
*/
function BillFormHeader({
onBillNumberChanged,
//#withVendors
vendorItems,
}) {
const handleBillNumberBlur = (event) => {
saveInvoke(onBillNumberChanged, event.currentTarget.value);
};
return (
<div className={classNames(CLASSES.PAGE_FORM_HEADER_FIELDS)}>
{/* ------- Vendor name ------ */}
<FastField name={'vendor_id'}>
{({ form, field: { value }, meta: { error, touched } }) => (
<FormGroup
label={<T id={'vendor_name'} />}
inline={true}
className={classNames(CLASSES.FILL, 'form-group--vendor')}
labelInfo={<FieldRequiredHint />}
intent={inputIntent({ error, touched })}
helperText={<ErrorMessage name={'vendor_id'} />}
>
<ContactSelecetList
contactsList={vendorItems}
selectedContactId={value}
defaultSelectText={<T id={'select_vender_account'} />}
onContactSelected={(contact) => {
form.setFieldValue('vendor_id', contact.id);
}}
popoverFill={true}
/>
</FormGroup>
)}
</FastField>
{/* ------- Bill date ------- */}
<FastField name={'bill_date'}>
{({ form, field: { value }, meta: { error, touched } }) => (
<FormGroup
label={<T id={'bill_date'} />}
inline={true}
labelInfo={<FieldRequiredHint />}
className={classNames(CLASSES.FILL)}
intent={inputIntent({ error, touched })}
helperText={<ErrorMessage name="bill_date" />}
>
<DateInput
{...momentFormatter('YYYY/MM/DD')}
value={tansformDateValue(value)}
onChange={handleDateChange((formattedDate) => {
form.setFieldValue('bill_date', formattedDate);
})}
popoverProps={{ position: Position.BOTTOM, minimal: true }}
inputProps={{ leftIcon: <Icon icon={'date-range'} /> }}
/>
</FormGroup>
)}
</FastField>
{/* ------- Due date ------- */}
<FastField name={'due_date'}>
{({ form, field: { value }, meta: { error, touched } }) => (
<FormGroup
label={<T id={'due_date'} />}
inline={true}
className={classNames(
'form-group--due-date',
'form-group--select-list',
CLASSES.FILL,
)}
intent={inputIntent({ error, touched })}
helperText={<ErrorMessage name="due_date" />}
>
<DateInput
{...momentFormatter('YYYY/MM/DD')}
value={tansformDateValue(value)}
onChange={handleDateChange((formattedDate) => {
form.setFieldValue('due_date', formattedDate);
})}
popoverProps={{ position: Position.BOTTOM, minimal: true }}
inputProps={{
leftIcon: <Icon icon={'date-range'} />,
}}
/>
</FormGroup>
)}
</FastField>
{/* ------- Bill number ------- */}
<FastField name={'bill_number'}>
{({ field, meta: { error, touched } }) => (
<FormGroup
label={<T id={'bill_number'} />}
inline={true}
className={('form-group--bill_number', CLASSES.FILL)}
intent={inputIntent({ error, touched })}
helperText={<ErrorMessage name="bill_number" />}
>
<InputGroup
minimal={true}
{...field}
onBlur={handleBillNumberBlur}
/>
</FormGroup>
)}
</FastField>
{/* ------- Reference ------- */}
<FastField name={'reference_no'}>
{({ field, meta: { error, touched } }) => (
<FormGroup
label={<T id={'reference'} />}
inline={true}
className={classNames('form-group--reference', CLASSES.FILL)}
intent={inputIntent({ error, touched })}
helperText={<ErrorMessage name="reference" />}
>
<InputGroup minimal={true} {...field} />
</FormGroup>
)}
</FastField>
</div>
);
}
export default compose(
withVendors(({ vendorItems }) => ({
vendorItems,
})),
withAccounts(({ accountsList }) => ({
accountsList,
})),
withDialogActions,
)(BillFormHeader);

View File

@@ -1,4 +1,4 @@
import React, { useCallback } from 'react';
import React, { useCallback, useEffect } from 'react';
import { useParams, useHistory } from 'react-router-dom';
import { useQuery } from 'react-query';
@@ -10,6 +10,7 @@ import withAccountsActions from 'containers/Accounts/withAccountsActions';
import withItemsActions from 'containers/Items/withItemsActions';
import withBillActions from './withBillActions';
import withSettingsActions from 'containers/Settings/withSettingsActions';
import withDashboardActions from 'containers/Dashboard/withDashboardActions';
import { compose } from 'utils';
@@ -28,10 +29,24 @@ function Bills({
// #withSettingsActions
requestFetchOptions,
// #withDashboardActions
setSidebarShrink,
resetSidebarPreviousExpand,
}) {
const history = useHistory();
const { id } = useParams();
useEffect(() => {
// Shrink the sidebar by foce.
setSidebarShrink();
return () => {
// Reset the sidebar to the previous status.
resetSidebarPreviousExpand();
};
}, [resetSidebarPreviousExpand, setSidebarShrink]);
// Handle fetch accounts
const fetchAccounts = useQuery('accounts-list', (key) =>
requestFetchAccounts(),
@@ -89,4 +104,5 @@ export default compose(
withItemsActions,
withAccountsActions,
withSettingsActions,
withDashboardActions
)(Bills);

View File

@@ -34,10 +34,22 @@ function PaymentMade({
// #withDashboardActions
changePageTitle,
setSidebarShrink,
resetSidebarPreviousExpand,
}) {
const { id: paymentMadeId } = useParams();
const { formatMessage } = useIntl();
useEffect(() => {
// Shrink the sidebar by foce.
setSidebarShrink();
return () => {
// Reset the sidebar to the previous status.
resetSidebarPreviousExpand();
};
}, [resetSidebarPreviousExpand, setSidebarShrink]);
// Handle page title change in new and edit mode.
useEffect(() => {
if (paymentMadeId) {

View File

@@ -31,7 +31,7 @@ const ERRORS = {
PAYMENT_NUMBER_NOT_UNIQUE: 'PAYMENT.NUMBER.NOT.UNIQUE',
};
// Default payment made entry values.
// Default payment made entry values.x
const defaultPaymentMadeEntry = {
bill_id: '',
payment_amount: '',
@@ -96,7 +96,7 @@ function PaymentMadeForm({
const validationSchema = isNewMode
? CreatePaymentMadeFormSchema
: EditPaymentMadeFormSchema;
// Form initial values.
const initialValues = useMemo(
() => ({
@@ -302,7 +302,11 @@ function PaymentMadeForm({
return (
<div
className={classNames(CLASSES.PAGE_FORM, CLASSES.PAGE_FORM_PAYMENT_MADE)}
className={classNames(
CLASSES.PAGE_FORM,
CLASSES.PAGE_FORM_STRIP_STYLE,
CLASSES.PAGE_FORM_PAYMENT_MADE,
)}
>
<form onSubmit={handleSubmit}>
<PaymentMadeHeader
@@ -318,17 +322,20 @@ function PaymentMadeForm({
onPaymentNumberChanged={handlePaymentNoChanged}
amountPaid={fullAmountPaid}
/>
<PaymentMadeItemsTable
fullAmount={fullAmount}
paymentEntries={localPaymentEntries}
vendorId={values.vendor_id}
paymentMadeId={paymentMadeId}
onUpdateData={handleUpdataData}
onClickClearAllLines={handleClearAllLines}
errors={errors?.entries}
onFetchEntriesSuccess={handleFetchEntriesSuccess}
vendorPayableBillsEntrie={[]}
/>
<div className={classNames(CLASSES.PAGE_FORM_BODY)}>
<PaymentMadeItemsTable
fullAmount={fullAmount}
paymentEntries={localPaymentEntries}
vendorId={values.vendor_id}
paymentMadeId={paymentMadeId}
onUpdateData={handleUpdataData}
onClickClearAllLines={handleClearAllLines}
errors={errors?.entries}
onFetchEntriesSuccess={handleFetchEntriesSuccess}
vendorPayableBillsEntrie={[]}
/>
</div>
<Alert
cancelButtonText={<T id={'cancel'} />}
confirmButtonText={<T id={'ok'} />}

View File

@@ -1,13 +1,12 @@
import React, { useMemo, useCallback } from 'react';
import React, { useCallback } from 'react';
import {
FormGroup,
InputGroup,
Intent,
Position,
MenuItem,
Classes,
ControlGroup,
} from '@blueprintjs/core';
import { sumBy } from 'lodash';
import { DateInput } from '@blueprintjs/datetime';
import { FormattedMessage as T } from 'react-intl';
import moment from 'moment';
@@ -19,12 +18,15 @@ import {
ContactSelecetList,
ErrorMessage,
FieldRequiredHint,
InputPrependText,
Money,
Hint,
Icon,
} from 'components';
import withVender from 'containers/Vendors/withVendors';
import withAccounts from 'containers/Accounts/withAccounts';
import withSettings from 'containers/Settings/withSettings';
/**
* Payment made header form.
@@ -42,6 +44,9 @@ function PaymentMadeFormHeader({
getFieldProps,
values,
//#withSettings
baseCurrency,
onFullAmountChanged,
//#withVender
@@ -83,7 +88,7 @@ function PaymentMadeFormHeader({
};
const handlePaymentNumberBlur = (event) => {
onPaymentNumberChanged && onPaymentNumberChanged(event.currentTarget.value)
onPaymentNumberChanged && onPaymentNumberChanged(event.currentTarget.value);
};
return (
@@ -104,7 +109,7 @@ function PaymentMadeFormHeader({
<ContactSelecetList
contactsList={vendorItems}
selectedContactId={values.vendor_id}
defaultSelectText={ <T id={'select_vender_account'} /> }
defaultSelectText={<T id={'select_vender_account'} />}
onContactSelected={onChangeSelect('vendor_id')}
disabled={!isNewMode}
popoverFill={true}
@@ -117,7 +122,9 @@ function PaymentMadeFormHeader({
inline={true}
labelInfo={<FieldRequiredHint />}
className={classNames('form-group--select-list', Classes.FILL)}
intent={errors.payment_date && touched.payment_date && Intent.DANGER}
intent={
errors.payment_date && touched.payment_date && Intent.DANGER
}
helperText={
<ErrorMessage name="payment_date" {...{ errors, touched }} />
}
@@ -127,6 +134,9 @@ function PaymentMadeFormHeader({
value={tansformDateValue(values.payment_date)}
onChange={handleDateChange('payment_date')}
popoverProps={{ position: Position.BOTTOM, minimal: true }}
inputProps={{
leftIcon: <Icon icon={'date-range'} />,
}}
/>
</FormGroup>
@@ -135,26 +145,32 @@ function PaymentMadeFormHeader({
label={<T id={'full_amount'} />}
inline={true}
className={('form-group--full-amount', Classes.FILL)}
intent={
errors.full_amount && touched.full_amount && Intent.DANGER
}
intent={errors.full_amount && touched.full_amount && Intent.DANGER}
labelInfo={<Hint />}
helperText={
<ErrorMessage name="full_amount" {...{ errors, touched }} />
}
>
<InputGroup
intent={
errors.full_amount && touched.full_amount && Intent.DANGER
}
minimal={true}
value={values.full_amount}
{...getFieldProps('full_amount')}
onBlur={handleFullAmountBlur}
/>
<ControlGroup>
<InputPrependText text={baseCurrency} />
<InputGroup
intent={
errors.full_amount && touched.full_amount && Intent.DANGER
}
minimal={true}
value={values.full_amount}
{...getFieldProps('full_amount')}
onBlur={handleFullAmountBlur}
/>
</ControlGroup>
<a onClick={handleReceiveFullAmountClick} href="#" className={'receive-full-amount'}>
Receive full amount (<Money amount={payableFullAmount} currency={'USD'} />)
<a
onClick={handleReceiveFullAmountClick}
href="#"
className={'receive-full-amount'}
>
Receive full amount (
<Money amount={payableFullAmount} currency={'USD'} />)
</a>
</FormGroup>
@@ -200,7 +216,7 @@ function PaymentMadeFormHeader({
name={'payment_account_id'}
{...{ errors, touched }}
/>
}
}
>
<AccountsSelectList
accounts={accountsList}
@@ -218,7 +234,9 @@ function PaymentMadeFormHeader({
inline={true}
className={classNames('form-group--reference', Classes.FILL)}
intent={errors.reference && touched.reference && Intent.DANGER}
helperText={<ErrorMessage name="reference" {...{ errors, touched }} />}
helperText={
<ErrorMessage name="reference" {...{ errors, touched }} />
}
>
<InputGroup
intent={errors.reference && touched.reference && Intent.DANGER}
@@ -249,4 +267,7 @@ export default compose(
withAccounts(({ accountsList }) => ({
accountsList,
})),
withSettings(({ organizationSettings }) => ({
baseCurrency: organizationSettings?.baseCurrency,
})),
)(PaymentMadeFormHeader);

View File

@@ -14,7 +14,7 @@ import {
} from './EstimateForm.schema';
import EstimateFormHeader from './EstimateFormHeader';
import EditableItemsEntriesTable from 'containers/Entries/EditableItemsEntriesTable';
import EstimateFormBody from './EstimateFormBody';
import EstimateFloatingActions from './EstimateFloatingActions';
import EstimateFormFooter from './EstimateFormFooter';
import EstimateNumberWatcher from './EstimateNumberWatcher';
@@ -43,9 +43,9 @@ const MIN_LINES_NUMBER = 4;
const defaultEstimate = {
index: 0,
item_id: '',
rate: '',
discount: 0,
quantity: '',
rate: 0,
discount: '',
quantity: 1,
description: '',
};
@@ -242,7 +242,11 @@ const EstimateForm = ({
);
return (
<div className={classNames(CLASSES.PAGE_FORM, CLASSES.PAGE_FORM_ESTIMATE)}>
<div className={classNames(
CLASSES.PAGE_FORM,
CLASSES.PAGE_FORM_STRIP_STYLE,
CLASSES.PAGE_FORM_ESTIMATE,
)}>
<Formik
validationSchema={
isNewMode ? CreateEstimateFormSchema : EditEstimateFormSchema
@@ -256,7 +260,7 @@ const EstimateForm = ({
onEstimateNumberChanged={handleEstimateNumberChange}
/>
<EstimateNumberWatcher estimateNumber={estimateNumber} />
<EditableItemsEntriesTable filterSellableItems={true} />
<EstimateFormBody />
<EstimateFormFooter />
<EstimateFloatingActions
isSubmitting={isSubmitting}

View File

@@ -1,6 +1,7 @@
import * as Yup from 'yup';
import { formatMessage } from 'services/intl';
import { DATATYPES_LENGTH } from 'common/dataTypes';
import { isBlank } from 'utils';
const Schema = Yup.object().shape({
customer_id: Yup.number()
@@ -40,7 +41,7 @@ const Schema = Yup.object().shape({
item_id: Yup.number()
.nullable()
.when(['quantity', 'rate'], {
is: (quantity, rate) => quantity || rate,
is: (quantity, rate) => !isBlank(quantity) && !isBlank(rate),
then: Yup.number().required(),
}),
discount: Yup.number().nullable().min(0).max(DATATYPES_LENGTH.INT_10),

View File

@@ -0,0 +1,15 @@
import React from 'react';
import classNames from 'classnames';
import { CLASSES } from 'common/classes';
import EditableItemsEntriesTable from 'containers/Entries/EditableItemsEntriesTable';
export default function EstimateFormBody() {
return (
<div className={classNames(CLASSES.PAGE_FORM_BODY)}>
<EditableItemsEntriesTable filterSellableItems={true} />
</div>
)
}

View File

@@ -1,183 +1,26 @@
import React, { useCallback } from 'react';
import {
FormGroup,
InputGroup,
Position,
ControlGroup,
} from '@blueprintjs/core';
import { DateInput } from '@blueprintjs/datetime';
import { FormattedMessage as T } from 'react-intl';
import { FastField, ErrorMessage } from 'formik';
import { momentFormatter, compose, tansformDateValue, saveInvoke } from 'utils';
import React from 'react';
import { compose } from 'utils';
import classNames from 'classnames';
import { CLASSES } from 'common/classes';
import {
ContactSelecetList,
FieldRequiredHint,
Icon,
InputPrependButton,
Row,
Col,
} from 'components';
import { PageFormBigNumber } from 'components';
import withCustomers from 'containers/Customers/withCustomers';
import withDialogActions from 'containers/Dialog/withDialogActions';
import { inputIntent, handleDateChange } from 'utils';
import { formatMessage } from 'services/intl';
import EstimateFormHeaderFields from './EstimateFormHeaderFields';
function EstimateFormHeader({
//#withCustomers
customers,
// #withDialogActions
openDialog,
// #ownProps
onEstimateNumberChanged,
}) {
const handleEstimateNumberChange = useCallback(() => {
openDialog('estimate-number-form', {});
}, [openDialog]);
const handleEstimateNumberChanged = (event) => {
saveInvoke(onEstimateNumberChanged, event.currentTarget.value);
};
return (
<div className={classNames(CLASSES.PAGE_FORM_HEADER)}>
<div className={'page-form__primary-section'}>
{/* ----------- Customer name ----------- */}
<FastField name={'customer_id'}>
{({ form, field: { value }, meta: { error, touched } }) => (
<FormGroup
label={<T id={'customer_name'} />}
inline={true}
className={classNames(
CLASSES.FILL,
'form-group--customer',
)}
labelInfo={<FieldRequiredHint />}
intent={inputIntent({ error, touched })}
helperText={<ErrorMessage name={'customer_id'} />}
>
<ContactSelecetList
contactsList={customers}
selectedContactId={value}
defaultSelectText={<T id={'select_customer_account'} />}
onContactSelected={(customer) => {
form.setFieldValue('customer_id', customer.id);
}}
popoverFill={true}
/>
</FormGroup>
)}
</FastField>
<EstimateFormHeaderFields
onEstimateNumberChanged={onEstimateNumberChanged}
/>
<Row>
<Col md={8} className={'col--estimate-date'}>
{/* ----------- Estimate date ----------- */}
<FastField name={'estimate_date'}>
{({ form, field: { value }, meta: { error, touched } }) => (
<FormGroup
label={<T id={'estimate_date'} />}
inline={true}
labelInfo={<FieldRequiredHint />}
className={classNames(
CLASSES.FILL,
'form-group--estimate-date',
)}
intent={inputIntent({ error, touched })}
helperText={<ErrorMessage name="estimate_date" />}
>
<DateInput
{...momentFormatter('YYYY/MM/DD')}
value={tansformDateValue(value)}
onChange={handleDateChange((formattedDate) => {
form.setFieldValue('estimate_date', formatMessage);
})}
popoverProps={{ position: Position.BOTTOM, minimal: true }}
/>
</FormGroup>
)}
</FastField>
</Col>
<Col md={4} className={'col--expiration-date'}>
{/* ----------- Expiration date ----------- */}
<FastField name={'expiration_date'}>
{({ form, field: { value }, meta: { error, touched } }) => (
<FormGroup
label={<T id={'expiration_date'} />}
inline={true}
className={classNames(
CLASSES.FORM_GROUP_LIST_SELECT,
CLASSES.FILL,
'form-group--expiration-date',
)}
intent={inputIntent({ error, touched })}
helperText={<ErrorMessage name="expiration_date" />}
>
<DateInput
{...momentFormatter('YYYY/MM/DD')}
value={tansformDateValue(value)}
onChange={handleDateChange((formattedDate) => {
form.setFieldValue('expiration_date', formattedDate);
})}
popoverProps={{ position: Position.BOTTOM, minimal: true }}
/>
</FormGroup>
)}
</FastField>
</Col>
</Row>
{/* ----------- Estimate number ----------- */}
<FastField name={'estimate_number'}>
{({ form, field, meta: { error, touched } }) => (
<FormGroup
label={<T id={'estimate'} />}
inline={true}
className={('form-group--estimate-number', CLASSES.FILL)}
labelInfo={<FieldRequiredHint />}
intent={inputIntent({ error, touched })}
helperText={<ErrorMessage name="estimate_number" />}
>
<ControlGroup fill={true}>
<InputGroup
minimal={true}
{...field}
onBlur={handleEstimateNumberChanged}
/>
<InputPrependButton
buttonProps={{
onClick: handleEstimateNumberChange,
icon: <Icon icon={'settings-18'} />,
}}
tooltip={true}
tooltipProps={{
content: 'Setting your auto-generated estimate number',
position: Position.BOTTOM_LEFT,
}}
/>
</ControlGroup>
</FormGroup>
)}
</FastField>
{/* ----------- Reference ----------- */}
<FastField name={'reference'}>
{({ form, field, meta: { error, touched } }) => (
<FormGroup
label={<T id={'reference'} />}
inline={true}
className={classNames('form-group--reference', CLASSES.FILL)}
intent={inputIntent({ error, touched })}
helperText={<ErrorMessage name="reference" />}
>
<InputGroup minimal={true} {...field} />
</FormGroup>
)}
</FastField>
</div>
<PageFormBigNumber label={'Amount'} amount={0} currencyCode={'LYD'} />
</div>
);
}

View File

@@ -0,0 +1,182 @@
import React, { useCallback } from 'react';
import {
FormGroup,
InputGroup,
Position,
ControlGroup,
} from '@blueprintjs/core';
import { DateInput } from '@blueprintjs/datetime';
import { FormattedMessage as T } from 'react-intl';
import { FastField, ErrorMessage } from 'formik';
import { momentFormatter, compose, tansformDateValue, saveInvoke } from 'utils';
import classNames from 'classnames';
import { CLASSES } from 'common/classes';
import {
ContactSelecetList,
FieldRequiredHint,
Icon,
InputPrependButton,
Row,
Col,
} from 'components';
import withCustomers from 'containers/Customers/withCustomers';
import withDialogActions from 'containers/Dialog/withDialogActions';
import { inputIntent, handleDateChange } from 'utils';
import { formatMessage } from 'services/intl';
function EstimateFormHeader({
//#withCustomers
customers,
// #withDialogActions
openDialog,
// #ownProps
onEstimateNumberChanged,
}) {
const handleEstimateNumberChange = useCallback(() => {
openDialog('estimate-number-form', {});
}, [openDialog]);
const handleEstimateNumberChanged = (event) => {
saveInvoke(onEstimateNumberChanged, event.currentTarget.value);
};
return (
<div className={classNames(CLASSES.PAGE_FORM_HEADER_FIELDS)}>
{/* ----------- Customer name ----------- */}
<FastField name={'customer_id'}>
{({ form, field: { value }, meta: { error, touched } }) => (
<FormGroup
label={<T id={'customer_name'} />}
inline={true}
className={classNames(CLASSES.FILL, 'form-group--customer')}
labelInfo={<FieldRequiredHint />}
intent={inputIntent({ error, touched })}
helperText={<ErrorMessage name={'customer_id'} />}
>
<ContactSelecetList
contactsList={customers}
selectedContactId={value}
defaultSelectText={<T id={'select_customer_account'} />}
onContactSelected={(customer) => {
form.setFieldValue('customer_id', customer.id);
}}
popoverFill={true}
/>
</FormGroup>
)}
</FastField>
{/* ----------- Estimate date ----------- */}
<FastField name={'estimate_date'}>
{({ form, field: { value }, meta: { error, touched } }) => (
<FormGroup
label={<T id={'estimate_date'} />}
inline={true}
labelInfo={<FieldRequiredHint />}
className={classNames(CLASSES.FILL, 'form-group--estimate-date')}
intent={inputIntent({ error, touched })}
helperText={<ErrorMessage name="estimate_date" />}
>
<DateInput
{...momentFormatter('YYYY/MM/DD')}
value={tansformDateValue(value)}
onChange={handleDateChange((formattedDate) => {
form.setFieldValue('estimate_date', formatMessage);
})}
popoverProps={{ position: Position.BOTTOM, minimal: true }}
inputProps={{
leftIcon: <Icon icon={'date-range'} />,
}}
/>
</FormGroup>
)}
</FastField>
{/* ----------- Expiration date ----------- */}
<FastField name={'expiration_date'}>
{({ form, field: { value }, meta: { error, touched } }) => (
<FormGroup
label={<T id={'expiration_date'} />}
inline={true}
className={classNames(
CLASSES.FORM_GROUP_LIST_SELECT,
CLASSES.FILL,
'form-group--expiration-date',
)}
intent={inputIntent({ error, touched })}
helperText={<ErrorMessage name="expiration_date" />}
>
<DateInput
{...momentFormatter('YYYY/MM/DD')}
value={tansformDateValue(value)}
onChange={handleDateChange((formattedDate) => {
form.setFieldValue('expiration_date', formattedDate);
})}
popoverProps={{ position: Position.BOTTOM, minimal: true }}
inputProps={{
leftIcon: <Icon icon={'date-range'} />,
}}
/>
</FormGroup>
)}
</FastField>
{/* ----------- Estimate number ----------- */}
<FastField name={'estimate_number'}>
{({ form, field, meta: { error, touched } }) => (
<FormGroup
label={<T id={'estimate'} />}
inline={true}
className={('form-group--estimate-number', CLASSES.FILL)}
labelInfo={<FieldRequiredHint />}
intent={inputIntent({ error, touched })}
helperText={<ErrorMessage name="estimate_number" />}
>
<ControlGroup fill={true}>
<InputGroup
minimal={true}
{...field}
onBlur={handleEstimateNumberChanged}
/>
<InputPrependButton
buttonProps={{
onClick: handleEstimateNumberChange,
icon: <Icon icon={'settings-18'} />,
}}
tooltip={true}
tooltipProps={{
content: 'Setting your auto-generated estimate number',
position: Position.BOTTOM_LEFT,
}}
/>
</ControlGroup>
</FormGroup>
)}
</FastField>
{/* ----------- Reference ----------- */}
<FastField name={'reference'}>
{({ form, field, meta: { error, touched } }) => (
<FormGroup
label={<T id={'reference'} />}
inline={true}
className={classNames('form-group--reference', CLASSES.FILL)}
intent={inputIntent({ error, touched })}
helperText={<ErrorMessage name="reference" />}
>
<InputGroup minimal={true} {...field} />
</FormGroup>
)}
</FastField>
</div>
);
}
export default compose(
withCustomers(({ customers }) => ({
customers,
})),
withDialogActions,
)(EstimateFormHeader);

View File

@@ -1,4 +1,4 @@
import React, { useCallback } from 'react';
import React, { useCallback, useEffect } from 'react';
import { useParams, useHistory } from 'react-router-dom';
import { useQuery } from 'react-query';
@@ -9,6 +9,7 @@ import withCustomersActions from 'containers/Customers/withCustomersActions';
import withItemsActions from 'containers/Items/withItemsActions';
import withEstimateActions from './withEstimateActions';
import withSettingsActions from 'containers/Settings/withSettingsActions';
import withDashboardActions from 'containers/Dashboard/withDashboardActions';
import { compose } from 'utils';
@@ -24,10 +25,24 @@ function Estimates({
// #withSettingsActions
requestFetchOptions,
// #withDashboardActions
setSidebarShrink,
resetSidebarPreviousExpand
}) {
const history = useHistory();
const { id } = useParams();
useEffect(() => {
// Shrink the sidebar by foce.
setSidebarShrink();
return () => {
// Reset the sidebar to the previous status.
resetSidebarPreviousExpand();
};
}, [resetSidebarPreviousExpand, setSidebarShrink]);
const fetchEstimate = useQuery(
['estimate', id],
(key, _id) => requsetFetchEstimate(_id),
@@ -78,4 +93,5 @@ export default compose(
withCustomersActions,
withItemsActions,
withSettingsActions,
withDashboardActions,
)(Estimates);

View File

@@ -44,8 +44,8 @@ const defaultInvoice = {
index: 0,
item_id: '',
rate: '',
discount: 0,
quantity: '',
discount: '',
quantity: 1,
description: '',
};
@@ -232,7 +232,11 @@ function InvoiceForm({
);
return (
<div className={classNames(CLASSES.PAGE_FORM, CLASSES.PAGE_FORM_INVOICE)}>
<div className={classNames(
CLASSES.PAGE_FORM,
CLASSES.PAGE_FORM_STRIP_STYLE,
CLASSES.PAGE_FORM_INVOICE
)}>
<Formik
validationSchema={
isNewMode ? CreateInvoiceFormSchema : EditInvoiceFormSchema
@@ -246,10 +250,13 @@ function InvoiceForm({
onInvoiceNumberChanged={handleInvoiceNumberChanged}
/>
<InvoiceNumberChangeWatcher invoiceNumber={invoiceNumber} />
<EditableItemsEntriesTable
defaultEntry={defaultInvoice}
filterSellableItems={true}
/>
<div className={classNames(CLASSES.PAGE_FORM_BODY)}>
<EditableItemsEntriesTable
defaultEntry={defaultInvoice}
filterSellableItems={true}
/>
</div>
<InvoiceFormFooter />
<InvoiceFloatingActions
isSubmitting={isSubmitting}
@@ -269,7 +276,6 @@ export default compose(
withDashboardActions,
withMediaActions,
withInvoiceDetail(),
withSettings(({ invoiceSettings }) => ({
invoiceNextNumber: invoiceSettings?.nextNumber,
invoiceNumberPrefix: invoiceSettings?.numberPrefix,

View File

@@ -1,6 +1,7 @@
import * as Yup from 'yup';
import { formatMessage } from 'services/intl';
import { DATATYPES_LENGTH } from 'common/dataTypes';
import { isBlank } from 'utils';
const Schema = Yup.object().shape({
customer_id: Yup.string()
@@ -39,7 +40,7 @@ const Schema = Yup.object().shape({
item_id: Yup.number()
.nullable()
.when(['quantity', 'rate'], {
is: (quantity, rate) => quantity || rate,
is: (quantity, rate) => !isBlank(quantity) && !isBlank(rate),
then: Yup.number().required(),
}),
discount: Yup.number().nullable().min(0).max(DATATYPES_LENGTH.INT_10),

View File

@@ -1,188 +1,28 @@
import React, { useCallback } from 'react';
import {
FormGroup,
InputGroup,
Position,
ControlGroup,
} from '@blueprintjs/core';
import { DateInput } from '@blueprintjs/datetime';
import { FastField, ErrorMessage } from 'formik';
import { FormattedMessage as T } from 'react-intl';
import { momentFormatter, compose, tansformDateValue, saveInvoke } from 'utils';
import React from 'react';
import classNames from 'classnames';
import { CLASSES } from 'common/classes';
import {
ContactSelecetList,
FieldRequiredHint,
Icon,
InputPrependButton,
Row,
Col,
} from 'components';
import withCustomers from 'containers/Customers/withCustomers';
import withDialogActions from 'containers/Dialog/withDialogActions';
import { inputIntent, handleDateChange } from 'utils';
function InvoiceFormHeader({
//#withCustomers
customers,
//#withDialogActions
openDialog,
import InvoiceFormHeaderFields from './InvoiceFormHeaderFields';
import { PageFormBigNumber } from 'components';
/**
* Invoice form header section.
*/
export default function InvoiceFormHeader({
// #ownProps
onInvoiceNumberChanged,
}) {
const handleInvoiceNumberChange = useCallback(() => {
openDialog('invoice-number-form', {});
}, [openDialog]);
const handleInvoiceNumberChanged = (event) => {
saveInvoke(onInvoiceNumberChanged, event.currentTarget.value);
};
return (
<div className={classNames(CLASSES.PAGE_FORM_HEADER)}>
<div className={classNames(CLASSES.PAGE_FORM_HEADER_PRIMARY)}>
{/* ----------- Customer name ----------- */}
<FastField name={'customer_id'}>
{({ form, field: { value }, meta: { error, touched } }) => (
<FormGroup
label={<T id={'customer_name'} />}
inline={true}
className={classNames(
'form-group--customer-name',
CLASSES.FILL,
)}
labelInfo={<FieldRequiredHint />}
intent={inputIntent({ error, touched })}
helperText={<ErrorMessage name={'customer_id'} />}
>
<ContactSelecetList
contactsList={customers}
selectedContactId={value}
defaultSelectText={<T id={'select_customer_account'} />}
onContactSelected={(customer) => {
form.setFieldValue('customer_id', customer.id);
}}
popoverFill={true}
/>
</FormGroup>
)}
</FastField>
<InvoiceFormHeaderFields
onInvoiceNumberChanged={onInvoiceNumberChanged}
/>
<Row>
<Col md={7} className={'col--invoice-date'}>
{/* ----------- Invoice date ----------- */}
<FastField name={'invoice_date'}>
{({ form, field: { value }, meta: { error, touched } }) => (
<FormGroup
label={<T id={'invoice_date'} />}
inline={true}
labelInfo={<FieldRequiredHint />}
className={classNames(
'form-group--invoice-date',
CLASSES.FILL,
)}
intent={inputIntent({ error, touched })}
helperText={<ErrorMessage name="invoice_date" />}
>
<DateInput
{...momentFormatter('YYYY/MM/DD')}
value={tansformDateValue(value)}
onChange={handleDateChange((formattedDate) => {
form.setFieldValue('invoice_date', formattedDate);
})}
popoverProps={{ position: Position.BOTTOM, minimal: true }}
/>
</FormGroup>
)}
</FastField>
</Col>
<Col md={5} className={'col--due-date'}>
{/* ----------- Due date ----------- */}
<FastField name={'due_date'}>
{({ form, field: { value }, meta: { error, touched } }) => (
<FormGroup
label={<T id={'due_date'} />}
inline={true}
className={classNames(
'form-group--due-date',
CLASSES.FILL,
)}
intent={inputIntent({ error, touched })}
helperText={<ErrorMessage name="due_date" />}
>
<DateInput
{...momentFormatter('YYYY/MM/DD')}
value={tansformDateValue(value)}
onChange={handleDateChange((formattedDate) => {
form.setFieldValue('due_date', formattedDate);
})}
popoverProps={{ position: Position.BOTTOM, minimal: true }}
/>
</FormGroup>
)}
</FastField>
</Col>
</Row>
{/* ----------- Invoice number ----------- */}
<FastField name={'invoice_no'}>
{({ form, field, meta: { error, touched } }) => (
<FormGroup
label={<T id={'invoice_no'} />}
inline={true}
className={classNames('form-group--invoice-no', CLASSES.FILL)}
intent={inputIntent({ error, touched })}
helperText={<ErrorMessage name="invoice_no" />}
>
<ControlGroup fill={true}>
<InputGroup
minimal={true}
{...field}
onBlur={handleInvoiceNumberChanged}
/>
<InputPrependButton
buttonProps={{
onClick: handleInvoiceNumberChange,
icon: <Icon icon={'settings-18'} />,
}}
tooltip={true}
tooltipProps={{
content: 'Setting your auto-generated invoice number',
position: Position.BOTTOM_LEFT,
}}
/>
</ControlGroup>
</FormGroup>
)}
</FastField>
{/* ----------- Reference ----------- */}
<FastField name={'reference'}>
{({ field, meta: { error, touched } }) => (
<FormGroup
label={<T id={'reference'} />}
inline={true}
className={classNames('form-group--reference', CLASSES.FILL)}
intent={inputIntent({ error, touched })}
helperText={<ErrorMessage name="reference" />}
>
<InputGroup minimal={true} {...field} />
</FormGroup>
)}
</FastField>
</div>
<PageFormBigNumber
label={'Due Amount'}
amount={0}
currencyCode={'LYD'}
/>
</div>
);
}
export default compose(
withCustomers(({ customers }) => ({
customers,
})),
withDialogActions,
)(InvoiceFormHeader);
}

View File

@@ -0,0 +1,176 @@
import React, { useCallback } from 'react';
import {
FormGroup,
InputGroup,
Position,
ControlGroup,
} from '@blueprintjs/core';
import { DateInput } from '@blueprintjs/datetime';
import { FastField, ErrorMessage } from 'formik';
import { FormattedMessage as T } from 'react-intl';
import { momentFormatter, compose, tansformDateValue, saveInvoke } from 'utils';
import classNames from 'classnames';
import { CLASSES } from 'common/classes';
import {
ContactSelecetList,
FieldRequiredHint,
Icon,
InputPrependButton,
} from 'components';
import withCustomers from 'containers/Customers/withCustomers';
import withDialogActions from 'containers/Dialog/withDialogActions';
import { inputIntent, handleDateChange } from 'utils';
function InvoiceFormHeaderFields({
// #withCustomers
customers,
// #withDialogActions
openDialog,
// #ownProps
onInvoiceNumberChanged,
}) {
const handleInvoiceNumberChange = useCallback(() => {
openDialog('invoice-number-form', {});
}, [openDialog]);
const handleInvoiceNumberChanged = (event) => {
saveInvoke(onInvoiceNumberChanged, event.currentTarget.value);
};
return (
<div className={classNames(CLASSES.PAGE_FORM_HEADER_FIELDS)}>
{/* ----------- Customer name ----------- */}
<FastField name={'customer_id'}>
{({ form, field: { value }, meta: { error, touched } }) => (
<FormGroup
label={<T id={'customer_name'} />}
inline={true}
className={classNames('form-group--customer-name', CLASSES.FILL)}
labelInfo={<FieldRequiredHint />}
intent={inputIntent({ error, touched })}
helperText={<ErrorMessage name={'customer_id'} />}
>
<ContactSelecetList
contactsList={customers}
selectedContactId={value}
defaultSelectText={<T id={'select_customer_account'} />}
onContactSelected={(customer) => {
form.setFieldValue('customer_id', customer.id);
}}
popoverFill={true}
/>
</FormGroup>
)}
</FastField>
{/* ----------- Invoice date ----------- */}
<FastField name={'invoice_date'}>
{({ form, field: { value }, meta: { error, touched } }) => (
<FormGroup
label={<T id={'invoice_date'} />}
inline={true}
labelInfo={<FieldRequiredHint />}
className={classNames('form-group--invoice-date', CLASSES.FILL)}
intent={inputIntent({ error, touched })}
helperText={<ErrorMessage name="invoice_date" />}
>
<DateInput
{...momentFormatter('YYYY/MM/DD')}
value={tansformDateValue(value)}
onChange={handleDateChange((formattedDate) => {
form.setFieldValue('invoice_date', formattedDate);
})}
popoverProps={{ position: Position.BOTTOM_LEFT, minimal: true }}
inputProps={{
leftIcon: <Icon icon={'date-range'} />,
}}
/>
</FormGroup>
)}
</FastField>
{/* ----------- Due date ----------- */}
<FastField name={'due_date'}>
{({ form, field: { value }, meta: { error, touched } }) => (
<FormGroup
label={<T id={'due_date'} />}
inline={true}
className={classNames('form-group--due-date', CLASSES.FILL)}
intent={inputIntent({ error, touched })}
helperText={<ErrorMessage name="due_date" />}
>
<DateInput
{...momentFormatter('YYYY/MM/DD')}
value={tansformDateValue(value)}
onChange={handleDateChange((formattedDate) => {
form.setFieldValue('due_date', formattedDate);
})}
popoverProps={{ position: Position.BOTTOM_LEFT, minimal: true }}
inputProps={{
leftIcon: <Icon icon={'date-range'} />,
}}
/>
</FormGroup>
)}
</FastField>
{/* ----------- Invoice number ----------- */}
<FastField name={'invoice_no'}>
{({ form, field, meta: { error, touched } }) => (
<FormGroup
label={<T id={'invoice_no'} />}
inline={true}
className={classNames('form-group--invoice-no', CLASSES.FILL)}
intent={inputIntent({ error, touched })}
helperText={<ErrorMessage name="invoice_no" />}
>
<ControlGroup fill={true}>
<InputGroup
minimal={true}
{...field}
onBlur={handleInvoiceNumberChanged}
/>
<InputPrependButton
buttonProps={{
onClick: handleInvoiceNumberChange,
icon: <Icon icon={'settings-18'} />,
}}
tooltip={true}
tooltipProps={{
content: 'Setting your auto-generated invoice number',
position: Position.BOTTOM_LEFT,
}}
/>
</ControlGroup>
</FormGroup>
)}
</FastField>
{/* ----------- Reference ----------- */}
<FastField name={'reference'}>
{({ field, meta: { error, touched } }) => (
<FormGroup
label={<T id={'reference'} />}
inline={true}
className={classNames('form-group--reference', CLASSES.FILL)}
intent={inputIntent({ error, touched })}
helperText={<ErrorMessage name="reference" />}
>
<InputGroup minimal={true} {...field} />
</FormGroup>
)}
</FastField>
</div>
);
}
export default compose(
withCustomers(({ customers }) => ({
customers,
})),
withDialogActions,
)(InvoiceFormHeaderFields);

View File

@@ -1,4 +1,4 @@
import React, { useCallback } from 'react';
import React, { useCallback, useEffect } from 'react';
import { useParams, useHistory } from 'react-router-dom';
import { useQuery } from 'react-query';
@@ -9,6 +9,7 @@ import withCustomersActions from 'containers/Customers/withCustomersActions';
import withItemsActions from 'containers/Items/withItemsActions';
import withInvoiceActions from './withInvoiceActions';
import withSettingsActions from 'containers/Settings/withSettingsActions';
import withDashboardActions from 'containers/Dashboard/withDashboardActions';
import { compose } from 'utils';
@@ -24,15 +25,29 @@ function Invoices({
// #withSettingsActions
requestFetchOptions,
// #withDashboardActions
setSidebarShrink,
resetSidebarPreviousExpand,
}) {
const history = useHistory();
const { id } = useParams();
useEffect(() => {
// Shrink the sidebar by foce.
setSidebarShrink();
return () => {
// Reset the sidebar to the previous status.
resetSidebarPreviousExpand();
};
}, [resetSidebarPreviousExpand, setSidebarShrink]);
const fetchInvoice = useQuery(
['invoice', id],
(key, _id) => requsetFetchInvoice(_id),
{ enabled: !!id },
);
);
const fetchSettings = useQuery(['settings'], () => requestFetchOptions({}));
@@ -64,8 +79,8 @@ function Invoices({
name={'invoice-form'}
>
<InvoiceForm
onFormSubmit={handleFormSubmit}
invoiceId={id}
onFormSubmit={handleFormSubmit}
onCancelForm={handleCancel}
/>
</DashboardInsider>
@@ -77,4 +92,5 @@ export default compose(
withCustomersActions,
withItemsActions,
withSettingsActions,
withDashboardActions,
)(Invoices);

View File

@@ -371,6 +371,7 @@ function PaymentReceiveForm({
<div
className={classNames(
CLASSES.PAGE_FORM,
CLASSES.PAGE_FORM_STRIP_STYLE,
CLASSES.PAGE_FORM_PAYMENT_RECEIVE,
)}
>
@@ -388,16 +389,18 @@ function PaymentReceiveForm({
amountReceived={fullAmountReceived}
onPaymentReceiveNumberChanged={handlePaymentReceiveNumberChanged}
/>
<PaymentReceiveItemsTable
paymentReceiveId={paymentReceiveId}
customerId={values.customer_id}
fullAmount={fullAmount}
onUpdateData={handleUpdataData}
paymentReceiveEntries={localPaymentEntries}
errors={errors?.entries}
onClickClearAllLines={handleClearAllLines}
onFetchEntriesSuccess={handleFetchEntriesSuccess}
/>
<div className={classNames(CLASSES.PAGE_FORM_BODY)}>
<PaymentReceiveItemsTable
paymentReceiveId={paymentReceiveId}
customerId={values.customer_id}
fullAmount={fullAmount}
onUpdateData={handleUpdataData}
paymentReceiveEntries={localPaymentEntries}
errors={errors?.entries}
onClickClearAllLines={handleClearAllLines}
onFetchEntriesSuccess={handleFetchEntriesSuccess}
/>
</div>
<Alert
cancelButtonText={<T id={'cancel'} />}
confirmButtonText={<T id={'ok'} />}

View File

@@ -142,6 +142,9 @@ function PaymentReceiveFormHeader({
value={tansformDateValue(values.payment_date)}
onChange={handleDateChange('payment_date')}
popoverProps={{ position: Position.BOTTOM, minimal: true }}
inputProps={{
leftIcon: (<Icon icon={'date-range'} />),
}}
/>
</FormGroup>

View File

@@ -32,10 +32,24 @@ function PaymentReceiveFormPage({
requestFetchPaymentReceive,
// #withCustomersActions
requestFetchCustomers
requestFetchCustomers,
// #withDashboardActions
setSidebarShrink,
resetSidebarPreviousExpand,
}) {
const { id: paymentReceiveId } = useParams();
useEffect(() => {
// Shrink the sidebar by foce.
setSidebarShrink();
return () => {
// Reset the sidebar to the previous status.
resetSidebarPreviousExpand();
};
}, [resetSidebarPreviousExpand, setSidebarShrink]);
// Fetches payment recevie details.
const fetchPaymentReceive = useQuery(
['payment-receive', paymentReceiveId],

View File

@@ -16,7 +16,7 @@ import {
} from './ReceiptForm.schema';
import ReceiptFromHeader from './ReceiptFormHeader';
import EditableItemsEntriesTable from 'containers/Entries/EditableItemsEntriesTable';
import ReceiptFormBody from './ReceiptFormBody';
import ReceiptFormFloatingActions from './ReceiptFormFloatingActions';
import ReceiptFormFooter from './ReceiptFormFooter';
import ReceiptNumberWatcher from './ReceiptNumberWatcher';
@@ -245,7 +245,11 @@ function ReceiptForm({
);
return (
<div className={classNames(CLASSES.PAGE_FORM_RECEIPT, CLASSES.PAGE_FORM)}>
<div className={classNames(
CLASSES.PAGE_FORM,
CLASSES.PAGE_FORM_STRIP_STYLE,
CLASSES.PAGE_FORM_RECEIPT,
)}>
<Formik
validationSchema={
isNewMode ? CreateReceiptFormSchema : EditReceiptFormSchema
@@ -259,7 +263,7 @@ function ReceiptForm({
onReceiptNumberChanged={handleReceiptNumberChanged}
/>
<ReceiptNumberWatcher receiptNumber={receiptNumber} />
<EditableItemsEntriesTable filterSellableItems={true} />
<ReceiptFormBody />
<ReceiptFormFooter />
<ReceiptFormFloatingActions
isSubmitting={isSubmitting}

View File

@@ -1,6 +1,7 @@
import * as Yup from 'yup';
import { formatMessage } from 'services/intl';
import { DATATYPES_LENGTH } from 'common/dataTypes';
import { isBlank } from 'utils';
const Schema = Yup.object().shape({
customer_id: Yup.string()
@@ -40,7 +41,7 @@ const Schema = Yup.object().shape({
item_id: Yup.number()
.nullable()
.when(['quantity', 'rate'], {
is: (quantity, rate) => quantity || rate,
is: (quantity, rate) => !isBlank(quantity) && !isBlank(rate),
then: Yup.number().required(),
}),
discount: Yup.number().nullable().min(0).max(DATATYPES_LENGTH.INT_10),

View File

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

View File

@@ -1,195 +1,28 @@
import React, { useCallback } from 'react';
import {
FormGroup,
InputGroup,
Position,
ControlGroup,
} from '@blueprintjs/core';
import { DateInput } from '@blueprintjs/datetime';
import { FormattedMessage as T } from 'react-intl';
import React from 'react';
import classNames from 'classnames';
import { FastField, ErrorMessage } from 'formik';
import { Money } from 'components';
import { CLASSES } from 'common/classes';
import {
AccountsSelectList,
ContactSelecetList,
FieldRequiredHint,
Icon,
InputPrependButton,
} from 'components';
import withCustomers from 'containers/Customers/withCustomers';
import withAccounts from 'containers/Accounts/withAccounts';
import withDialogActions from 'containers/Dialog/withDialogActions';
import {
momentFormatter,
compose,
tansformDateValue,
saveInvoke,
handleDateChange,
inputIntent,
} from 'utils';
function ReceiptFormHeader({
//#withCustomers
customers,
//#withAccouts
accountsList,
//#withDialogActions
openDialog,
import ReceiptFormHeaderFields from './ReceiptFormHeaderFields';
export default function ReceiptFormHeader({
// #ownProps
onReceiptNumberChanged,
}) {
const handleReceiptNumberChange = useCallback(() => {
openDialog('receipt-number-form', {});
}, [openDialog]);
const handleReceiptNumberChanged = (event) => {
saveInvoke(onReceiptNumberChanged, event.currentTarget.value);
};
return (
<div className={classNames(CLASSES.PAGE_FORM_HEADER)}>
<div className={classNames(CLASSES.PAGE_FORM_HEADER_PRIMARY)}>
{/* ----------- Customer name ----------- */}
<FastField name={'customer_id'}>
{({ form, field: { value }, meta: { error, touched } }) => (
<FormGroup
label={<T id={'customer_name'} />}
inline={true}
className={classNames(CLASSES.FILL, 'form-group--customer')}
labelInfo={<FieldRequiredHint />}
intent={inputIntent({ error, touched })}
helperText={<ErrorMessage name={'customer_id'} />}
>
<ContactSelecetList
contactsList={customers}
selectedContactId={value}
defaultSelectText={<T id={'select_customer_account'} />}
onContactSelected={(contact) => {
form.setFieldValue('customer_id', contact.id);
}}
popoverFill={true}
/>
</FormGroup>
)}
</FastField>
<ReceiptFormHeaderFields
onReceiptNumberChanged={onReceiptNumberChanged}
/>
{/* ----------- Deposit account ----------- */}
<FastField name={'deposit_account_id'}>
{({ form, field: { value }, meta: { error, touched } }) => (
<FormGroup
label={<T id={'deposit_account'} />}
className={classNames(
'form-group--deposit-account',
CLASSES.FILL,
)}
inline={true}
labelInfo={<FieldRequiredHint />}
intent={inputIntent({ error, touched })}
helperText={<ErrorMessage name={'deposit_account_id'} />}
>
<AccountsSelectList
accounts={accountsList}
onAccountSelected={(account) => {
form.setFieldValue('deposit_account_id', account.id);
}}
defaultSelectText={<T id={'select_deposit_account'} />}
selectedAccountId={value}
filterByTypes={['current_asset']}
popoverFill={true}
/>
</FormGroup>
)}
</FastField>
{/* ----------- Receipt date ----------- */}
<FastField name={'receipt_date'}>
{({ form, field: { value }, meta: { error, touched } }) => (
<FormGroup
label={<T id={'receipt_date'} />}
inline={true}
className={classNames(CLASSES.FILL)}
intent={inputIntent({ error, touched })}
helperText={<ErrorMessage name="receipt_date" />}
>
<DateInput
{...momentFormatter('YYYY/MM/DD')}
value={tansformDateValue(value)}
onChange={handleDateChange((formattedDate) => {
form.setFieldValue('receipt_date', formattedDate);
})}
popoverProps={{ position: Position.BOTTOM, minimal: true }}
/>
</FormGroup>
)}
</FastField>
{/* ----------- Receipt number ----------- */}
<FastField name={'receipt_number'}>
{({ field, meta: { error, touched } }) => (
<FormGroup
label={<T id={'receipt'} />}
inline={true}
className={('form-group--receipt_number', CLASSES.FILL)}
labelInfo={<FieldRequiredHint />}
intent={inputIntent({ error, touched })}
helperText={<ErrorMessage name="receipt_number" />}
>
<ControlGroup fill={true}>
<InputGroup
minimal={true}
{...field}
onBlur={handleReceiptNumberChanged}
/>
<InputPrependButton
buttonProps={{
onClick: handleReceiptNumberChange,
icon: <Icon icon={'settings-18'} />,
}}
tooltip={true}
tooltipProps={{
content: 'Setting your auto-generated receipt number',
position: Position.BOTTOM_LEFT,
}}
/>
</ControlGroup>
</FormGroup>
)}
</FastField>
{/* ----------- Reference ----------- */}
<FastField name={'reference'}>
{({ field, meta: { error, touched } }) => (
<FormGroup
label={<T id={'reference'} />}
inline={true}
className={classNames('form-group--reference', CLASSES.FILL)}
intent={inputIntent({ error, touched })}
helperText={<ErrorMessage name="reference" />}
>
<InputGroup minimal={true} {...field} />
</FormGroup>
)}
</FastField>
<div className={classNames(CLASSES.PAGE_FORM_HEADER_BIG_NUMBERS)}>
<div class="big-amount">
<span class="big-amount__label">Due Amount</span>
<h1 class="big-amount__number">
<Money amount={0} currency={'LYD'} />
</h1>
</div>
</div>
</div>
);
}
export default compose(
withCustomers(({ customers }) => ({
customers,
})),
withAccounts(({ accountsList }) => ({
accountsList,
})),
withDialogActions,
)(ReceiptFormHeader);

View File

@@ -0,0 +1,195 @@
import React, { useCallback } from 'react';
import {
FormGroup,
InputGroup,
Position,
ControlGroup,
} from '@blueprintjs/core';
import { DateInput } from '@blueprintjs/datetime';
import { FormattedMessage as T } from 'react-intl';
import classNames from 'classnames';
import { FastField, ErrorMessage } from 'formik';
import { CLASSES } from 'common/classes';
import {
AccountsSelectList,
ContactSelecetList,
FieldRequiredHint,
Icon,
InputPrependButton,
} from 'components';
import withCustomers from 'containers/Customers/withCustomers';
import withAccounts from 'containers/Accounts/withAccounts';
import withDialogActions from 'containers/Dialog/withDialogActions';
import {
momentFormatter,
compose,
tansformDateValue,
saveInvoke,
handleDateChange,
inputIntent,
} from 'utils';
function ReceiptFormHeader({
//#withCustomers
customers,
//#withAccouts
accountsList,
//#withDialogActions
openDialog,
// #ownProps
onReceiptNumberChanged,
}) {
const handleReceiptNumberChange = useCallback(() => {
openDialog('receipt-number-form', {});
}, [openDialog]);
const handleReceiptNumberChanged = (event) => {
saveInvoke(onReceiptNumberChanged, event.currentTarget.value);
};
return (
<div className={classNames(CLASSES.PAGE_FORM_HEADER_FIELDS)}>
{/* ----------- Customer name ----------- */}
<FastField name={'customer_id'}>
{({ form, field: { value }, meta: { error, touched } }) => (
<FormGroup
label={<T id={'customer_name'} />}
inline={true}
className={classNames(CLASSES.FILL, 'form-group--customer')}
labelInfo={<FieldRequiredHint />}
intent={inputIntent({ error, touched })}
helperText={<ErrorMessage name={'customer_id'} />}
>
<ContactSelecetList
contactsList={customers}
selectedContactId={value}
defaultSelectText={<T id={'select_customer_account'} />}
onContactSelected={(contact) => {
form.setFieldValue('customer_id', contact.id);
}}
popoverFill={true}
/>
</FormGroup>
)}
</FastField>
{/* ----------- Deposit account ----------- */}
<FastField name={'deposit_account_id'}>
{({ form, field: { value }, meta: { error, touched } }) => (
<FormGroup
label={<T id={'deposit_account'} />}
className={classNames('form-group--deposit-account', CLASSES.FILL)}
inline={true}
labelInfo={<FieldRequiredHint />}
intent={inputIntent({ error, touched })}
helperText={<ErrorMessage name={'deposit_account_id'} />}
>
<AccountsSelectList
accounts={accountsList}
onAccountSelected={(account) => {
form.setFieldValue('deposit_account_id', account.id);
}}
defaultSelectText={<T id={'select_deposit_account'} />}
selectedAccountId={value}
filterByTypes={['current_asset']}
popoverFill={true}
/>
</FormGroup>
)}
</FastField>
{/* ----------- Receipt date ----------- */}
<FastField name={'receipt_date'}>
{({ form, field: { value }, meta: { error, touched } }) => (
<FormGroup
label={<T id={'receipt_date'} />}
inline={true}
className={classNames(CLASSES.FILL)}
intent={inputIntent({ error, touched })}
helperText={<ErrorMessage name="receipt_date" />}
>
<DateInput
{...momentFormatter('YYYY/MM/DD')}
value={tansformDateValue(value)}
onChange={handleDateChange((formattedDate) => {
form.setFieldValue('receipt_date', formattedDate);
})}
popoverProps={{ position: Position.BOTTOM, minimal: true }}
inputProps={{
leftIcon: <Icon icon={'date-range'} />,
}}
/>
</FormGroup>
)}
</FastField>
{/* ----------- Receipt number ----------- */}
<FastField name={'receipt_number'}>
{({ field, meta: { error, touched } }) => (
<FormGroup
label={<T id={'receipt'} />}
inline={true}
className={('form-group--receipt_number', CLASSES.FILL)}
labelInfo={<FieldRequiredHint />}
intent={inputIntent({ error, touched })}
helperText={<ErrorMessage name="receipt_number" />}
>
<ControlGroup fill={true}>
<InputGroup
minimal={true}
{...field}
onBlur={handleReceiptNumberChanged}
/>
<InputPrependButton
buttonProps={{
onClick: handleReceiptNumberChange,
icon: <Icon icon={'settings-18'} />,
}}
tooltip={true}
tooltipProps={{
content: 'Setting your auto-generated receipt number',
position: Position.BOTTOM_LEFT,
}}
inputProps={{
leftIcon: <Icon icon={'date-range'} />,
}}
/>
</ControlGroup>
</FormGroup>
)}
</FastField>
{/* ----------- Reference ----------- */}
<FastField name={'reference'}>
{({ field, meta: { error, touched } }) => (
<FormGroup
label={<T id={'reference'} />}
inline={true}
className={classNames('form-group--reference', CLASSES.FILL)}
intent={inputIntent({ error, touched })}
helperText={<ErrorMessage name="reference" />}
>
<InputGroup minimal={true} {...field} />
</FormGroup>
)}
</FastField>
</div>
);
}
export default compose(
withCustomers(({ customers }) => ({
customers,
})),
withAccounts(({ accountsList }) => ({
accountsList,
})),
withDialogActions,
)(ReceiptFormHeader);

View File

@@ -1,4 +1,4 @@
import React, { useCallback } from 'react';
import React, { useCallback, useEffect } from 'react';
import { useParams, useHistory } from 'react-router-dom';
import { useQuery } from 'react-query';
@@ -10,6 +10,7 @@ import withAccountsActions from 'containers/Accounts/withAccountsActions';
import withItemsActions from 'containers/Items/withItemsActions';
import withReceiptActions from './withReceiptActions';
import withSettingsActions from 'containers/Settings/withSettingsActions';
import withDashboardActions from 'containers/Dashboard/withDashboardActions';
import { compose } from 'utils';
@@ -28,10 +29,24 @@ function Receipts({
// #withSettingsActions
requestFetchOptions,
// #withDashboardActions
setSidebarShrink,
resetSidebarPreviousExpand,
}) {
const history = useHistory();
const { id } = useParams();
useEffect(() => {
// Shrink the sidebar by foce.
setSidebarShrink();
return () => {
// Reset the sidebar to the previous status.
resetSidebarPreviousExpand();
};
}, [resetSidebarPreviousExpand, setSidebarShrink]);
const fetchReceipt = useQuery(
['receipt', id],
(key, _id) => requestFetchReceipt(_id),
@@ -86,4 +101,5 @@ export default compose(
withItemsActions,
withAccountsActions,
withSettingsActions,
withDashboardActions
)(Receipts);