mirror of
https://github.com/bigcapitalhq/bigcapital.git
synced 2026-02-16 12:50:38 +00:00
refactoring: invoice form.
refactoring: receipt form. refactoring: bill form. refactoring: estimate form.
This commit is contained in:
@@ -21,9 +21,6 @@ function EstimateApproveAlert({
|
||||
isOpen,
|
||||
payload: { estimateId },
|
||||
|
||||
// #withEstimateActions
|
||||
requestApproveEstimate,
|
||||
|
||||
// #withAlertActions
|
||||
closeAlert,
|
||||
}) {
|
||||
@@ -59,7 +56,6 @@ function EstimateApproveAlert({
|
||||
<Alert
|
||||
cancelButtonText={<T id={'cancel'} />}
|
||||
confirmButtonText={<T id={'approve'} />}
|
||||
icon="trash"
|
||||
intent={Intent.WARNING}
|
||||
isOpen={isOpen}
|
||||
loading={isLoading}
|
||||
|
||||
@@ -31,12 +31,12 @@ function EstimateDeleteAlert({
|
||||
const { mutateAsync: deleteEstimateMutate, isLoading } = useDeleteEstimate();
|
||||
|
||||
// handle cancel delete alert.
|
||||
const handleCancelEstimateDelete = () => {
|
||||
const handleAlertCancel = () => {
|
||||
closeAlert(name);
|
||||
};
|
||||
|
||||
// handle confirm delete estimate
|
||||
const handleConfirmEstimateDelete = useCallback(() => {
|
||||
const handleAlertConfirm = () => {
|
||||
deleteEstimateMutate(estimateId)
|
||||
.then(() => {
|
||||
AppToaster.show({
|
||||
@@ -50,7 +50,7 @@ function EstimateDeleteAlert({
|
||||
.finally(() => {
|
||||
closeAlert(name);
|
||||
});
|
||||
}, [deleteEstimateMutate, name, closeAlert, formatMessage, estimateId]);
|
||||
};
|
||||
|
||||
return (
|
||||
<Alert
|
||||
@@ -60,8 +60,8 @@ function EstimateDeleteAlert({
|
||||
intent={Intent.DANGER}
|
||||
isOpen={isOpen}
|
||||
loading={isLoading}
|
||||
onCancel={handleCancelEstimateDelete}
|
||||
onConfirm={handleConfirmEstimateDelete}
|
||||
onCancel={handleAlertCancel}
|
||||
onConfirm={handleAlertConfirm}
|
||||
>
|
||||
<p>
|
||||
<FormattedHTMLMessage
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import React, { useCallback, useState } from 'react';
|
||||
import React from 'react';
|
||||
import { FormattedMessage as T, useIntl } from 'react-intl';
|
||||
import { Intent, Alert } from '@blueprintjs/core';
|
||||
import { queryCache } from 'react-query';
|
||||
|
||||
import { useDeliverEstimate } from 'hooks/query';
|
||||
import { AppToaster } from 'components';
|
||||
@@ -28,12 +27,12 @@ function EstimateDeliveredAlert({
|
||||
const { mutateAsync: deliverEstimateMutate, isLoading } = useDeliverEstimate();
|
||||
|
||||
// Handle cancel delivered estimate alert.
|
||||
const handleCancelDeliveredEstimate = () => {
|
||||
const handleAlertCancel = () => {
|
||||
closeAlert(name);
|
||||
};
|
||||
|
||||
// Handle confirm estimate delivered.
|
||||
const handleConfirmEstimateDelivered = useCallback(() => {
|
||||
const handleAlertConfirm = () => {
|
||||
deliverEstimateMutate(estimateId)
|
||||
.then(() => {
|
||||
AppToaster.show({
|
||||
@@ -41,14 +40,13 @@ function EstimateDeliveredAlert({
|
||||
id: 'the_estimate_has_been_delivered_successfully',
|
||||
}),
|
||||
intent: Intent.SUCCESS,
|
||||
});
|
||||
queryCache.invalidateQueries('estimates-table');
|
||||
})
|
||||
})
|
||||
.catch((error) => {})
|
||||
.finally(() => {
|
||||
closeAlert(name);
|
||||
});
|
||||
}, [estimateId, deliverEstimateMutate, formatMessage]);
|
||||
};
|
||||
|
||||
return (
|
||||
<Alert
|
||||
@@ -56,8 +54,8 @@ function EstimateDeliveredAlert({
|
||||
confirmButtonText={<T id={'deliver'} />}
|
||||
intent={Intent.WARNING}
|
||||
isOpen={isOpen}
|
||||
onCancel={handleCancelDeliveredEstimate}
|
||||
onConfirm={handleConfirmEstimateDelivered}
|
||||
onCancel={handleAlertCancel}
|
||||
onConfirm={handleAlertConfirm}
|
||||
loading={isLoading}
|
||||
>
|
||||
<p>
|
||||
|
||||
@@ -83,7 +83,6 @@ export default function BillFloatingActions() {
|
||||
disabled={isSubmitting}
|
||||
loading={isSubmitting}
|
||||
intent={Intent.PRIMARY}
|
||||
type="submit"
|
||||
onClick={handleSubmitOpenBtnClick}
|
||||
text={<T id={'save_open'} />}
|
||||
/>
|
||||
@@ -116,7 +115,6 @@ export default function BillFloatingActions() {
|
||||
<Button
|
||||
disabled={isSubmitting}
|
||||
className={'ml1'}
|
||||
type="submit"
|
||||
onClick={handleSubmitDraftBtnClick}
|
||||
text={<T id={'save_as_draft'} />}
|
||||
/>
|
||||
@@ -150,7 +148,6 @@ export default function BillFloatingActions() {
|
||||
<Button
|
||||
disabled={isSubmitting}
|
||||
intent={Intent.PRIMARY}
|
||||
type="submit"
|
||||
onClick={handleSubmitOpenBtnClick}
|
||||
text={<T id={'save'} />}
|
||||
/>
|
||||
|
||||
@@ -1,85 +1,51 @@
|
||||
import React, { useMemo } from 'react';
|
||||
import { Formik, Form } from 'formik';
|
||||
import moment from 'moment';
|
||||
import { Intent } from '@blueprintjs/core';
|
||||
import classNames from 'classnames';
|
||||
import { useIntl } from 'react-intl';
|
||||
import { useHistory } from 'react-router-dom';
|
||||
import { pick, sumBy, isEmpty, omit } from 'lodash';
|
||||
import { sumBy, isEmpty, omit } from 'lodash';
|
||||
import { CLASSES } from 'common/classes';
|
||||
|
||||
import { EditBillFormSchema, CreateBillFormSchema } from './BillForm.schema';
|
||||
import BillFormHeader from './BillFormHeader';
|
||||
import BillFloatingActions from './BillFloatingActions';
|
||||
import BillFormFooter from './BillFormFooter';
|
||||
import BillItemsEntriesEditor from './BillItemsEntriesEditor';
|
||||
|
||||
import { AppToaster } from 'components';
|
||||
|
||||
import { ERROR } from 'common/errors';
|
||||
import { repeatValue, orderingLinesIndexes } from 'utils';
|
||||
import BillFormBody from './BillFormBody';
|
||||
import { useBillFormContext } from './BillFormProvider';
|
||||
|
||||
const MIN_LINES_NUMBER = 5;
|
||||
|
||||
const defaultBill = {
|
||||
index: 0,
|
||||
item_id: '',
|
||||
rate: '',
|
||||
discount: 0,
|
||||
quantity: 1,
|
||||
description: '',
|
||||
};
|
||||
|
||||
const defaultInitialValues = {
|
||||
vendor_id: '',
|
||||
bill_number: '',
|
||||
bill_date: moment(new Date()).format('YYYY-MM-DD'),
|
||||
due_date: moment(new Date()).format('YYYY-MM-DD'),
|
||||
reference_no: '',
|
||||
note: '',
|
||||
open: '',
|
||||
entries: [...repeatValue(defaultBill, MIN_LINES_NUMBER)],
|
||||
};
|
||||
import { orderingLinesIndexes, safeSumBy } from 'utils';
|
||||
import { defaultBill, transformToEditForm } from './utils';
|
||||
|
||||
/**
|
||||
* Bill form.
|
||||
*/
|
||||
export default function BillForm({
|
||||
|
||||
}) {
|
||||
export default function BillForm() {
|
||||
const { formatMessage } = useIntl();
|
||||
const history = useHistory();
|
||||
|
||||
// Bill form context.
|
||||
const {
|
||||
bill,
|
||||
billId,
|
||||
isNewMode,
|
||||
submitPayload,
|
||||
createBillMutate,
|
||||
editBillMutate,
|
||||
} = useBillFormContext();
|
||||
|
||||
const isNewMode = !billId;
|
||||
|
||||
// Initial values in create and edit mode.
|
||||
const initialValues = useMemo(
|
||||
() => ({
|
||||
...(!isEmpty(bill)
|
||||
? {
|
||||
...pick(bill, Object.keys(defaultInitialValues)),
|
||||
entries: [
|
||||
...bill.entries.map((bill) => ({
|
||||
...pick(bill, Object.keys(defaultBill)),
|
||||
})),
|
||||
...repeatValue(
|
||||
defaultBill,
|
||||
Math.max(MIN_LINES_NUMBER - bill.entries.length, 0),
|
||||
),
|
||||
],
|
||||
...transformToEditForm(bill),
|
||||
}
|
||||
: {
|
||||
...defaultInitialValues,
|
||||
entries: orderingLinesIndexes(defaultInitialValues.entries),
|
||||
...defaultBill,
|
||||
entries: orderingLinesIndexes(defaultBill.entries),
|
||||
}),
|
||||
}),
|
||||
[bill],
|
||||
@@ -102,7 +68,7 @@ export default function BillForm({
|
||||
const entries = values.entries.filter(
|
||||
(item) => item.item_id && item.quantity,
|
||||
);
|
||||
const totalQuantity = sumBy(entries, (entry) => parseInt(entry.quantity));
|
||||
const totalQuantity = safeSumBy(entries, (entry) => entry.quantity);
|
||||
|
||||
if (totalQuantity === 0) {
|
||||
AppToaster.show({
|
||||
@@ -146,10 +112,10 @@ export default function BillForm({
|
||||
handleErrors(errors, { setErrors });
|
||||
setSubmitting(false);
|
||||
};
|
||||
if (bill && bill.id) {
|
||||
editBillMutate(bill.id, form).then(onSuccess).catch(onError);
|
||||
} else {
|
||||
if (isNewMode) {
|
||||
createBillMutate(form).then(onSuccess).catch(onError);
|
||||
} else {
|
||||
editBillMutate([bill.id, form]).then(onSuccess).catch(onError);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -168,7 +134,7 @@ export default function BillForm({
|
||||
>
|
||||
<Form>
|
||||
<BillFormHeader />
|
||||
<BillFormBody defaultBill={defaultBill} />
|
||||
<BillItemsEntriesEditor />
|
||||
<BillFormFooter />
|
||||
<BillFloatingActions />
|
||||
</Form>
|
||||
|
||||
@@ -1,14 +0,0 @@
|
||||
import React from 'react';
|
||||
import classNames from 'classnames';
|
||||
import { CLASSES } from 'common/classes';
|
||||
import { useBillFormContext } from './BillFormProvider';
|
||||
|
||||
export default function BillFormBody({ defaultBill }) {
|
||||
const { items } = useBillFormContext();
|
||||
|
||||
return (
|
||||
<div className={classNames(CLASSES.PAGE_FORM_BODY)}>
|
||||
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -16,22 +16,15 @@ import {
|
||||
tansformDateValue,
|
||||
handleDateChange,
|
||||
inputIntent,
|
||||
saveInvoke,
|
||||
} from 'utils';
|
||||
|
||||
/**
|
||||
* Fill form header.
|
||||
*/
|
||||
function BillFormHeader({
|
||||
onBillNumberChanged,
|
||||
}) {
|
||||
function BillFormHeader() {
|
||||
// Bill form context.
|
||||
const { vendors } = useBillFormContext();
|
||||
|
||||
const handleBillNumberBlur = (event) => {
|
||||
saveInvoke(onBillNumberChanged, event.currentTarget.value);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={classNames(CLASSES.PAGE_FORM_HEADER_FIELDS)}>
|
||||
{/* ------- Vendor name ------ */}
|
||||
@@ -121,11 +114,7 @@ function BillFormHeader({
|
||||
intent={inputIntent({ error, touched })}
|
||||
helperText={<ErrorMessage name="bill_number" />}
|
||||
>
|
||||
<InputGroup
|
||||
minimal={true}
|
||||
{...field}
|
||||
onBlur={handleBillNumberBlur}
|
||||
/>
|
||||
<InputGroup minimal={true} {...field} />
|
||||
</FormGroup>
|
||||
)}
|
||||
</FastField>
|
||||
@@ -148,6 +137,4 @@ function BillFormHeader({
|
||||
);
|
||||
}
|
||||
|
||||
export default compose(
|
||||
withDialogActions,
|
||||
)(BillFormHeader);
|
||||
export default compose(withDialogActions)(BillFormHeader);
|
||||
|
||||
@@ -46,12 +46,15 @@ function BillFormProvider({ billId, ...props }) {
|
||||
const { mutateAsync: createBillMutate } = useCreateBill();
|
||||
const { mutateAsync: editBillMutate } = useEditBill();
|
||||
|
||||
const isNewMode = !billId;
|
||||
|
||||
const provider = {
|
||||
accounts,
|
||||
vendors,
|
||||
items,
|
||||
bill,
|
||||
submitPayload,
|
||||
isNewMode,
|
||||
|
||||
isSettingLoading,
|
||||
isBillLoading,
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
import React from 'react';
|
||||
import classNames from 'classnames';
|
||||
import { FastField } from 'formik';
|
||||
import { CLASSES } from 'common/classes';
|
||||
import { useBillFormContext } from './BillFormProvider';
|
||||
import ItemsEntriesTable from 'containers/Entries/ItemsEntriesTable';
|
||||
|
||||
export default function BillFormBody({ defaultBill }) {
|
||||
const { items } = useBillFormContext();
|
||||
|
||||
return (
|
||||
<div className={classNames(CLASSES.PAGE_FORM_BODY)}>
|
||||
<FastField name={'entries'}>
|
||||
{({ form, field: { value }, meta: { error, touched } }) => (
|
||||
<ItemsEntriesTable
|
||||
entries={value}
|
||||
onUpdateData={(entries) => {
|
||||
form.setFieldValue('entries', entries);
|
||||
}}
|
||||
items={items}
|
||||
errors={error}
|
||||
linesNumber={4}
|
||||
/>
|
||||
)}
|
||||
</FastField>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
39
client/src/containers/Purchases/Bills/BillForm/utils.js
Normal file
39
client/src/containers/Purchases/Bills/BillForm/utils.js
Normal file
@@ -0,0 +1,39 @@
|
||||
import moment from 'moment';
|
||||
import { transformToForm, repeatValue } from 'utils';
|
||||
|
||||
export const MIN_LINES_NUMBER = 4;
|
||||
|
||||
export const defaultBillEntry = {
|
||||
index: 0,
|
||||
item_id: '',
|
||||
rate: '',
|
||||
discount: 0,
|
||||
quantity: 1,
|
||||
description: '',
|
||||
};
|
||||
|
||||
export const defaultBill = {
|
||||
vendor_id: '',
|
||||
bill_number: '',
|
||||
bill_date: moment(new Date()).format('YYYY-MM-DD'),
|
||||
due_date: moment(new Date()).format('YYYY-MM-DD'),
|
||||
reference_no: '',
|
||||
note: '',
|
||||
open: '',
|
||||
entries: [...repeatValue(defaultBillEntry, MIN_LINES_NUMBER)],
|
||||
};
|
||||
|
||||
export const transformToEditForm = (bill) => {
|
||||
return {
|
||||
...transformToForm(bill, defaultBill),
|
||||
entries: [
|
||||
...bill.entries.map((bill) => ({
|
||||
...transformToForm(bill, defaultBill.entries[0]),
|
||||
})),
|
||||
...repeatValue(
|
||||
defaultBill,
|
||||
Math.max(MIN_LINES_NUMBER - bill.entries.length, 0),
|
||||
),
|
||||
],
|
||||
};
|
||||
}
|
||||
@@ -1,276 +0,0 @@
|
||||
import React, { useState, useMemo, useEffect, useCallback } from 'react';
|
||||
import { omit } from 'lodash';
|
||||
import { Button, Intent, Position, Tooltip } from '@blueprintjs/core';
|
||||
import { FormattedMessage as T, useIntl } from 'react-intl';
|
||||
import classNames from 'classnames';
|
||||
import { CLASSES } from 'common/classes';
|
||||
import { Hint, Icon } from 'components';
|
||||
import DataTable from 'components/DataTable';
|
||||
import {
|
||||
InputGroupCell,
|
||||
MoneyFieldCell,
|
||||
ItemsListCell,
|
||||
PercentFieldCell,
|
||||
DivFieldCell,
|
||||
} from 'components/DataTableCells';
|
||||
|
||||
import withItems from 'containers/Items/withItems';
|
||||
import { compose, formattedAmount } from 'utils';
|
||||
|
||||
const ActionsCellRenderer = ({
|
||||
row: { index },
|
||||
column: { id },
|
||||
cell: { value },
|
||||
data,
|
||||
payload,
|
||||
}) => {
|
||||
if (data.length <= index + 1) {
|
||||
return '';
|
||||
}
|
||||
const onRemoveRole = () => {
|
||||
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="m12"
|
||||
intent={Intent.DANGER}
|
||||
onClick={onRemoveRole}
|
||||
/>
|
||||
</Tooltip>
|
||||
);
|
||||
};
|
||||
|
||||
const TotalEstimateCellRederer = (content, 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>{formattedAmount(total, 'USD')}</span>;
|
||||
}
|
||||
return content(props);
|
||||
};
|
||||
|
||||
const calculateDiscount = (discount, quantity, rate) =>
|
||||
quantity * rate - (quantity * rate * discount) / 100;
|
||||
|
||||
const CellRenderer = (content, type) => (props) => {
|
||||
if (props.data.length === props.row.index + 1) {
|
||||
return '';
|
||||
}
|
||||
return content(props);
|
||||
};
|
||||
|
||||
const ItemHeaderCell = () => {
|
||||
return (
|
||||
<>
|
||||
<T id={'product_and_service'} />
|
||||
<Hint />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
function EntriesItemsTable({
|
||||
//#withitems
|
||||
itemsCurrentPage,
|
||||
|
||||
//#ownProps
|
||||
onClickRemoveRow,
|
||||
onClickAddNewRow,
|
||||
onClickClearAllLines,
|
||||
entries,
|
||||
|
||||
errors,
|
||||
setFieldValue,
|
||||
values,
|
||||
}) {
|
||||
const [rows, setRows] = useState([]);
|
||||
const { formatMessage } = useIntl();
|
||||
|
||||
useEffect(() => {
|
||||
setRows([...entries.map((e) => ({ ...e }))]);
|
||||
}, [entries]);
|
||||
|
||||
const columns = useMemo(
|
||||
() => [
|
||||
{
|
||||
Header: '#',
|
||||
accessor: 'index',
|
||||
Cell: ({ row: { index } }) => <span>{index + 1}</span>,
|
||||
width: 40,
|
||||
disableResizing: true,
|
||||
disableSortBy: true,
|
||||
className: 'index',
|
||||
},
|
||||
{
|
||||
Header: ItemHeaderCell,
|
||||
id: 'item_id',
|
||||
accessor: 'item_id',
|
||||
Cell: ItemsListCell,
|
||||
// ItemsListCell
|
||||
disableSortBy: true,
|
||||
width: 180,
|
||||
},
|
||||
{
|
||||
Header: formatMessage({ id: 'description' }),
|
||||
accessor: 'description',
|
||||
Cell: InputGroupCell,
|
||||
disableSortBy: true,
|
||||
className: 'description',
|
||||
width: 100,
|
||||
},
|
||||
|
||||
{
|
||||
Header: formatMessage({ id: 'quantity' }),
|
||||
accessor: 'quantity',
|
||||
Cell: CellRenderer(InputGroupCell, 'quantity'),
|
||||
disableSortBy: true,
|
||||
width: 80,
|
||||
className: 'quantity',
|
||||
},
|
||||
{
|
||||
Header: formatMessage({ id: 'rate' }),
|
||||
accessor: 'rate',
|
||||
Cell: TotalEstimateCellRederer(MoneyFieldCell, 'rate'),
|
||||
disableSortBy: true,
|
||||
width: 80,
|
||||
className: 'rate',
|
||||
},
|
||||
{
|
||||
Header: formatMessage({ id: 'discount' }),
|
||||
accessor: 'discount',
|
||||
Cell: CellRenderer(PercentFieldCell, InputGroupCell),
|
||||
disableSortBy: true,
|
||||
width: 80,
|
||||
className: 'discount',
|
||||
},
|
||||
{
|
||||
Header: formatMessage({ id: 'total' }),
|
||||
accessor: (row) =>
|
||||
calculateDiscount(row.discount, row.quantity, row.rate),
|
||||
Cell: TotalEstimateCellRederer(DivFieldCell, 'total'),
|
||||
disableSortBy: true,
|
||||
width: 120,
|
||||
className: 'total',
|
||||
},
|
||||
{
|
||||
Header: '',
|
||||
accessor: 'action',
|
||||
Cell: ActionsCellRenderer,
|
||||
className: 'actions',
|
||||
disableSortBy: true,
|
||||
disableResizing: true,
|
||||
width: 45,
|
||||
},
|
||||
],
|
||||
[formatMessage],
|
||||
);
|
||||
|
||||
const handleUpdateData = useCallback(
|
||||
(rowIndex, columnId, value) => {
|
||||
const newRow = rows.map((row, index) => {
|
||||
if (index === rowIndex) {
|
||||
const newRow = { ...rows[rowIndex], [columnId]: value };
|
||||
return {
|
||||
...newRow,
|
||||
total: calculateDiscount(
|
||||
newRow.discount,
|
||||
newRow.quantity,
|
||||
newRow.rate,
|
||||
),
|
||||
};
|
||||
}
|
||||
return row;
|
||||
});
|
||||
setFieldValue(
|
||||
'entries',
|
||||
newRow.map((row) => ({
|
||||
...omit(row, ['total']),
|
||||
})),
|
||||
);
|
||||
},
|
||||
[rows, setFieldValue],
|
||||
);
|
||||
|
||||
const handleRemoveRow = useCallback(
|
||||
(rowIndex) => {
|
||||
if (rows.length <= 1) {
|
||||
return;
|
||||
}
|
||||
|
||||
const removeIndex = parseInt(rowIndex, 10);
|
||||
const newRows = rows.filter((row, index) => index !== removeIndex);
|
||||
setFieldValue(
|
||||
'entries',
|
||||
newRows.map((row, index) => ({
|
||||
...omit(row),
|
||||
index: index + 1,
|
||||
})),
|
||||
);
|
||||
onClickRemoveRow && onClickRemoveRow(removeIndex);
|
||||
},
|
||||
[rows, setFieldValue, onClickRemoveRow],
|
||||
);
|
||||
|
||||
const onClickNewRow = () => {
|
||||
onClickAddNewRow && onClickAddNewRow();
|
||||
};
|
||||
|
||||
const handleClickClearAllLines = () => {
|
||||
onClickClearAllLines && onClickClearAllLines();
|
||||
};
|
||||
|
||||
const rowClassNames = useCallback(
|
||||
(row) => ({
|
||||
'row--total': rows.length === row.index + 1,
|
||||
}),
|
||||
[rows],
|
||||
);
|
||||
|
||||
return (
|
||||
<div className={classNames(
|
||||
CLASSES.DATATABLE_EDITOR,
|
||||
CLASSES.DATATABLE_EDITOR_ITEMS_ENTRIES,
|
||||
)}>
|
||||
<DataTable
|
||||
columns={columns}
|
||||
data={rows}
|
||||
rowClassNames={rowClassNames}
|
||||
payload={{
|
||||
items: itemsCurrentPage,
|
||||
errors: errors.entries || [],
|
||||
updateData: handleUpdateData,
|
||||
removeRow: handleRemoveRow,
|
||||
}}
|
||||
/>
|
||||
<div className={classNames(CLASSES.DATATABLE_EDITOR_ACTIONS, 'mt1')}>
|
||||
<Button
|
||||
small={true}
|
||||
className={'button--secondary button--new-line'}
|
||||
onClick={onClickNewRow}
|
||||
>
|
||||
<T id={'new_lines'} />
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
small={true}
|
||||
className={'button--secondary button--clear-lines ml1'}
|
||||
onClick={handleClickClearAllLines}
|
||||
>
|
||||
<T id={'clear_all_lines'} />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default compose(
|
||||
withItems(({ itemsCurrentPage }) => ({
|
||||
itemsCurrentPage,
|
||||
})),
|
||||
)(EntriesItemsTable);
|
||||
@@ -27,7 +27,8 @@ export default function EstimateFloatingActions() {
|
||||
|
||||
// Handle submit & deliver button click.
|
||||
const handleSubmitDeliverBtnClick = (event) => {
|
||||
setSubmitPayload({ redirect: true, deliver: true, });
|
||||
setSubmitPayload({ redirect: true, deliver: true });
|
||||
submitForm();
|
||||
};
|
||||
|
||||
// Handle submit, deliver & new button click.
|
||||
@@ -77,7 +78,6 @@ export default function EstimateFloatingActions() {
|
||||
disabled={isSubmitting}
|
||||
loading={isSubmitting}
|
||||
intent={Intent.PRIMARY}
|
||||
type="submit"
|
||||
onClick={handleSubmitDeliverBtnClick}
|
||||
text={<T id={'save_and_deliver'} />}
|
||||
/>
|
||||
@@ -105,12 +105,12 @@ export default function EstimateFloatingActions() {
|
||||
/>
|
||||
</Popover>
|
||||
</ButtonGroup>
|
||||
|
||||
{/* ----------- Save As Draft ----------- */}
|
||||
<ButtonGroup>
|
||||
<Button
|
||||
disabled={isSubmitting}
|
||||
className={'ml1'}
|
||||
type="submit"
|
||||
onClick={handleSubmitDraftBtnClick}
|
||||
text={<T id={'save_as_draft'} />}
|
||||
/>
|
||||
@@ -138,13 +138,13 @@ export default function EstimateFloatingActions() {
|
||||
</Popover>
|
||||
</ButtonGroup>
|
||||
</If>
|
||||
|
||||
{/* ----------- Save and New ----------- */}
|
||||
<If condition={estimate && estimate?.is_delivered}>
|
||||
<ButtonGroup>
|
||||
<Button
|
||||
disabled={isSubmitting}
|
||||
intent={Intent.PRIMARY}
|
||||
type="submit"
|
||||
onClick={handleSubmitDeliverBtnClick}
|
||||
text={<T id={'save'} />}
|
||||
/>
|
||||
@@ -177,6 +177,7 @@ export default function EstimateFloatingActions() {
|
||||
onClick={handleClearBtnClick}
|
||||
text={estimate ? <T id={'reset'} /> : <T id={'clear'} />}
|
||||
/>
|
||||
|
||||
{/* ----------- Cancel ----------- */}
|
||||
<Button
|
||||
className={'ml1'}
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
import React, { useMemo } from 'react';
|
||||
import { Formik, Form } from 'formik';
|
||||
import moment from 'moment';
|
||||
import { Intent } from '@blueprintjs/core';
|
||||
import { FormattedMessage as T, useIntl } from 'react-intl';
|
||||
import { pick, sumBy } from 'lodash';
|
||||
import { omit, sumBy, isEmpty } from 'lodash';
|
||||
import classNames from 'classnames';
|
||||
import { useHistory } from 'react-router-dom';
|
||||
|
||||
@@ -14,57 +13,31 @@ import {
|
||||
} from './EstimateForm.schema';
|
||||
|
||||
import EstimateFormHeader from './EstimateFormHeader';
|
||||
import EstimateFormBody from './EstimateFormBody';
|
||||
import EstimateItemsEntriesField from './EstimateItemsEntriesField';
|
||||
import EstimateFloatingActions from './EstimateFloatingActions';
|
||||
import EstimateFormFooter from './EstimateFormFooter';
|
||||
|
||||
import withDashboardActions from 'containers/Dashboard/withDashboardActions';
|
||||
import withMediaActions from 'containers/Media/withMediaActions';
|
||||
import withSettings from 'containers/Settings/withSettings';
|
||||
|
||||
import { AppToaster } from 'components';
|
||||
import { ERROR } from 'common/errors';
|
||||
import {
|
||||
compose,
|
||||
repeatValue,
|
||||
orderingLinesIndexes,
|
||||
} from 'utils';
|
||||
import { useEstimateFormContext } from './EstimateFormProvider';
|
||||
|
||||
const MIN_LINES_NUMBER = 4;
|
||||
|
||||
const defaultEstimate = {
|
||||
index: 0,
|
||||
item_id: '',
|
||||
rate: '',
|
||||
discount: 0,
|
||||
quantity: 1,
|
||||
description: '',
|
||||
};
|
||||
|
||||
const defaultInitialValues = {
|
||||
customer_id: '',
|
||||
estimate_date: moment(new Date()).format('YYYY-MM-DD'),
|
||||
expiration_date: moment(new Date()).format('YYYY-MM-DD'),
|
||||
estimate_number: '',
|
||||
delivered: '',
|
||||
reference: '',
|
||||
note: '',
|
||||
terms_conditions: '',
|
||||
entries: [...repeatValue(defaultEstimate, MIN_LINES_NUMBER)],
|
||||
};
|
||||
import { transformToEditForm, defaultEstimate } from './utils';
|
||||
|
||||
/**
|
||||
* Estimate form.
|
||||
*/
|
||||
const EstimateForm = ({
|
||||
function EstimateForm({
|
||||
// #withSettings
|
||||
estimateNextNumber,
|
||||
estimateNumberPrefix,
|
||||
}) => {
|
||||
}) {
|
||||
const { formatMessage } = useIntl();
|
||||
const history = useHistory();
|
||||
|
||||
const {
|
||||
estimate,
|
||||
isNewMode,
|
||||
@@ -80,23 +53,14 @@ const EstimateForm = ({
|
||||
// Initial values in create and edit mode.
|
||||
const initialValues = useMemo(
|
||||
() => ({
|
||||
...(estimate
|
||||
...(!isEmpty(estimate)
|
||||
? {
|
||||
...pick(estimate, Object.keys(defaultInitialValues)),
|
||||
entries: [
|
||||
...estimate.entries.map((estimate) => ({
|
||||
...pick(estimate, Object.keys(defaultEstimate)),
|
||||
})),
|
||||
...repeatValue(
|
||||
defaultEstimate,
|
||||
Math.max(MIN_LINES_NUMBER - estimate.entries.length, 0),
|
||||
),
|
||||
],
|
||||
...transformToEditForm(estimate)
|
||||
}
|
||||
: {
|
||||
...defaultInitialValues,
|
||||
...defaultEstimate,
|
||||
estimate_number: estimateNumber,
|
||||
entries: orderingLinesIndexes(defaultInitialValues.entries),
|
||||
entries: orderingLinesIndexes(defaultEstimate.entries),
|
||||
}),
|
||||
}),
|
||||
[estimate, estimateNumber],
|
||||
@@ -138,10 +102,7 @@ const EstimateForm = ({
|
||||
const form = {
|
||||
...values,
|
||||
delivered: submitPayload.deliver,
|
||||
// Exclude all entries properties that out of request schema.
|
||||
entries: entries.map((entry) => ({
|
||||
...pick(entry, Object.keys(defaultEstimate)),
|
||||
})),
|
||||
entries: entries.map((entry) => ({ ...omit(entry, ['total']) })),
|
||||
};
|
||||
const onSuccess = (response) => {
|
||||
AppToaster.show({
|
||||
@@ -172,7 +133,7 @@ const EstimateForm = ({
|
||||
setSubmitting(false);
|
||||
};
|
||||
|
||||
if (estimate && estimate.id) {
|
||||
if (!isNewMode) {
|
||||
editEstimateMutate([estimate.id, form]).then(onSuccess).catch(onError);
|
||||
} else {
|
||||
createEstimateMutate(form).then(onSuccess).catch(onError);
|
||||
@@ -196,18 +157,19 @@ const EstimateForm = ({
|
||||
>
|
||||
<Form>
|
||||
<EstimateFormHeader />
|
||||
<EstimateFormBody defaultEstimate={defaultEstimate} />
|
||||
|
||||
<div className={classNames(CLASSES.PAGE_FORM_BODY)}>
|
||||
<EstimateItemsEntriesField />
|
||||
</div>
|
||||
<EstimateFormFooter />
|
||||
<EstimateFloatingActions />
|
||||
</Form>
|
||||
</Formik>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
export default compose(
|
||||
withDashboardActions,
|
||||
withMediaActions,
|
||||
withSettings(({ estimatesSettings }) => ({
|
||||
estimateNextNumber: estimatesSettings?.nextNumber,
|
||||
estimateNumberPrefix: estimatesSettings?.numberPrefix,
|
||||
|
||||
@@ -1,15 +0,0 @@
|
||||
import React from 'react';
|
||||
import classNames from 'classnames';
|
||||
import { CLASSES } from 'common/classes';
|
||||
// import EditableItemsEntriesTable from 'containers/Entries/EditableItemsEntriesTable';
|
||||
|
||||
export default function EstimateFormBody({ defaultEstimate }) {
|
||||
return (
|
||||
<div className={classNames(CLASSES.PAGE_FORM_BODY)}>
|
||||
{/* <EditableItemsEntriesTable
|
||||
defaultEntry={defaultEstimate}
|
||||
filterSellableItems={true}
|
||||
/> */}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -18,7 +18,6 @@ import {
|
||||
InputPrependButton,
|
||||
} from 'components';
|
||||
|
||||
import withCustomers from 'containers/Customers/withCustomers';
|
||||
import withDialogActions from 'containers/Dialog/withDialogActions';
|
||||
|
||||
import { inputIntent, handleDateChange } from 'utils';
|
||||
@@ -169,8 +168,5 @@ function EstimateFormHeader({
|
||||
}
|
||||
|
||||
export default compose(
|
||||
withCustomers(({ customers }) => ({
|
||||
customers,
|
||||
})),
|
||||
withDialogActions,
|
||||
)(EstimateFormHeader);
|
||||
|
||||
@@ -1,49 +1,20 @@
|
||||
import React, { useEffect } from 'react';
|
||||
import { useParams, useHistory } from 'react-router-dom';
|
||||
import React from 'react';
|
||||
import { useParams } from 'react-router-dom';
|
||||
|
||||
import 'style/pages/SaleEstimate/PageForm.scss';
|
||||
|
||||
import EstimateForm from './EstimateForm';
|
||||
|
||||
import { EstimateFormProvider } from './EstimateFormProvider';
|
||||
import withDashboardActions from 'containers/Dashboard/withDashboardActions';
|
||||
import { compose } from 'utils';
|
||||
|
||||
/**
|
||||
* Estimate form page.
|
||||
*/
|
||||
function EstimateFormPage({
|
||||
// #withDashboardActions
|
||||
setSidebarShrink,
|
||||
resetSidebarPreviousExpand,
|
||||
setDashboardBackLink,
|
||||
}) {
|
||||
|
||||
export default function EstimateFormPage() {
|
||||
const { id } = useParams();
|
||||
|
||||
useEffect(() => {
|
||||
// Shrink the sidebar by foce.
|
||||
setSidebarShrink();
|
||||
// Show the back link on dashboard topbar.
|
||||
setDashboardBackLink(true);
|
||||
|
||||
return () => {
|
||||
// Reset the sidebar to the previous status.
|
||||
resetSidebarPreviousExpand();
|
||||
// Hide the back link on dashboard topbar.
|
||||
setDashboardBackLink(false);
|
||||
};
|
||||
}, [resetSidebarPreviousExpand, setSidebarShrink, setDashboardBackLink]);
|
||||
|
||||
|
||||
return (
|
||||
<EstimateFormProvider estimateId={id}>
|
||||
<EstimateForm />
|
||||
</EstimateFormProvider>
|
||||
);
|
||||
}
|
||||
|
||||
export default compose(
|
||||
|
||||
withDashboardActions,
|
||||
)(EstimateFormPage);
|
||||
}
|
||||
@@ -7,7 +7,7 @@ import {
|
||||
useSettings,
|
||||
useCreateEstimate,
|
||||
useEditEstimate
|
||||
} from 'query/hooks';
|
||||
} from 'hooks/query';
|
||||
|
||||
const EstimateFormContext = createContext();
|
||||
|
||||
@@ -16,7 +16,7 @@ const EstimateFormContext = createContext();
|
||||
*/
|
||||
function EstimateFormProvider({ estimateId, ...props }) {
|
||||
const { data: estimate, isFetching: isEstimateFetching } = useEstimate(
|
||||
estimateId,
|
||||
estimateId, { enabled: !!estimateId }
|
||||
);
|
||||
|
||||
// Handle fetch Items data table or list
|
||||
@@ -32,16 +32,16 @@ function EstimateFormProvider({ estimateId, ...props }) {
|
||||
} = useCustomers();
|
||||
|
||||
// Handle fetch settings.
|
||||
const {
|
||||
data: { settings },
|
||||
} = useSettings();
|
||||
useSettings();
|
||||
|
||||
// Form submit payload.
|
||||
const [submitPayload, setSubmitPayload] = React.useState({});
|
||||
|
||||
const isNewMode = !estimateId;
|
||||
|
||||
|
||||
// Create and edit estimate form.
|
||||
const { mutateAsync: createEstimateMutate } = useCreateEstimate();
|
||||
const { mutateAsync: editEstimateMutate } = useEditEstimate();
|
||||
|
||||
const isNewMode = !estimateId;
|
||||
|
||||
// Provider payload.
|
||||
const provider = {
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
import React from 'react';
|
||||
import { FastField } from 'formik';
|
||||
import ItemsEntriesTable from 'containers/Entries/ItemsEntriesTable';
|
||||
import { useEstimateFormContext } from './EstimateFormProvider';
|
||||
|
||||
/**
|
||||
* Estimate form items entries editor.
|
||||
*/
|
||||
export default function EstimateFormItemsEntriesField() {
|
||||
const { items } = useEstimateFormContext();
|
||||
|
||||
return (
|
||||
<FastField name={'entries'}>
|
||||
{({ form, field: { value }, meta: { error, touched } }) => (
|
||||
<ItemsEntriesTable
|
||||
entries={value}
|
||||
onUpdateData={(entries) => {
|
||||
form.setFieldValue('entries', entries);
|
||||
}}
|
||||
items={items}
|
||||
errors={error}
|
||||
linesNumber={4}
|
||||
/>
|
||||
)}
|
||||
</FastField>
|
||||
);
|
||||
}
|
||||
@@ -1,46 +0,0 @@
|
||||
import { useEffect } from 'react';
|
||||
import { useFormikContext } from 'formik';
|
||||
|
||||
import withEstimates from './withEstimates';
|
||||
import withDashboardActions from 'containers/Dashboard/withDashboardActions';
|
||||
import withEstimateActions from './withEstimateActions';
|
||||
import { compose } from 'utils';
|
||||
|
||||
function EstimateNumberWatcher({
|
||||
estimateNumberChanged,
|
||||
|
||||
// #WithEstimateActions
|
||||
setEstimateNumberChanged,
|
||||
|
||||
// #withDashboardActions
|
||||
changePageSubtitle,
|
||||
|
||||
// #ownProps
|
||||
estimateNumber,
|
||||
}) {
|
||||
const { setFieldValue } = useFormikContext();
|
||||
|
||||
useEffect(() => {
|
||||
if (estimateNumberChanged) {
|
||||
setFieldValue('estimate_number', estimateNumber);
|
||||
changePageSubtitle(`No. ${estimateNumber}`);
|
||||
setEstimateNumberChanged(false);
|
||||
}
|
||||
}, [
|
||||
estimateNumber,
|
||||
estimateNumberChanged,
|
||||
setEstimateNumberChanged,
|
||||
setFieldValue,
|
||||
changePageSubtitle,
|
||||
]);
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
export default compose(
|
||||
withEstimates(({ estimateNumberChanged }) => ({
|
||||
estimateNumberChanged,
|
||||
})),
|
||||
withEstimateActions,
|
||||
withDashboardActions
|
||||
)(EstimateNumberWatcher)
|
||||
38
client/src/containers/Sales/Estimates/EstimateForm/utils.js
Normal file
38
client/src/containers/Sales/Estimates/EstimateForm/utils.js
Normal file
@@ -0,0 +1,38 @@
|
||||
import moment from 'moment';
|
||||
import { repeatValue, transformToForm } from 'utils';
|
||||
|
||||
export const MIN_LINES_NUMBER = 4;
|
||||
|
||||
export const defaultEstimateEntry = {
|
||||
index: 0,
|
||||
item_id: '',
|
||||
rate: '',
|
||||
discount: 0,
|
||||
quantity: 1,
|
||||
description: '',
|
||||
};
|
||||
|
||||
export const defaultEstimate = {
|
||||
customer_id: '',
|
||||
estimate_date: moment(new Date()).format('YYYY-MM-DD'),
|
||||
expiration_date: moment(new Date()).format('YYYY-MM-DD'),
|
||||
estimate_number: '',
|
||||
delivered: '',
|
||||
reference: '',
|
||||
note: '',
|
||||
terms_conditions: '',
|
||||
entries: [...repeatValue(defaultEstimateEntry, MIN_LINES_NUMBER)],
|
||||
};
|
||||
|
||||
export const transformToEditForm = (estimate) => ({
|
||||
...transformToForm(estimate, defaultEstimate),
|
||||
entries: [
|
||||
...estimate.entries.map((estimate) => ({
|
||||
...transformToForm(estimate, defaultEstimateEntry),
|
||||
})),
|
||||
...repeatValue(
|
||||
defaultEstimate,
|
||||
Math.max(MIN_LINES_NUMBER - estimate.entries.length, 0),
|
||||
),
|
||||
],
|
||||
});
|
||||
@@ -67,6 +67,7 @@ export function ActionsMenu({
|
||||
/>
|
||||
<If condition={!original.is_delivered}>
|
||||
<MenuItem
|
||||
icon={<Icon icon={'check'} iconSize={18} />}
|
||||
text={formatMessage({ id: 'mark_as_delivered' })}
|
||||
onClick={safeCallback(onDeliver, original)}
|
||||
/>
|
||||
@@ -80,16 +81,19 @@ export function ActionsMenu({
|
||||
</Choose.When>
|
||||
<Choose.When condition={original.is_delivered && original.is_rejected}>
|
||||
<MenuItem
|
||||
icon={<Icon icon={'check'} iconSize={18} />}
|
||||
text={formatMessage({ id: 'mark_as_approved' })}
|
||||
onClick={safeCallback(onApprove, original)}
|
||||
/>
|
||||
</Choose.When>
|
||||
<Choose.When condition={original.is_delivered}>
|
||||
<MenuItem
|
||||
icon={<Icon icon={'check'} iconSize={18} />}
|
||||
text={formatMessage({ id: 'mark_as_approved' })}
|
||||
onClick={safeCallback(onApprove, original)}
|
||||
/>
|
||||
<MenuItem
|
||||
icon={<Icon icon={'close-black'} />}
|
||||
text={formatMessage({ id: 'mark_as_rejected' })}
|
||||
onClick={safeCallback(onReject, original)}
|
||||
/>
|
||||
|
||||
@@ -22,17 +22,9 @@ import withSettings from 'containers/Settings/withSettings';
|
||||
|
||||
import { AppToaster } from 'components';
|
||||
import { ERROR } from 'common/errors';
|
||||
import {
|
||||
compose,
|
||||
orderingLinesIndexes,
|
||||
transactionNumber,
|
||||
} from 'utils';
|
||||
import { compose, orderingLinesIndexes, transactionNumber } from 'utils';
|
||||
import { useInvoiceFormContext } from './InvoiceFormProvider';
|
||||
import { transformToEditForm } from './utils';
|
||||
import {
|
||||
MIN_LINES_NUMBER,
|
||||
defaultInitialValues
|
||||
} from './constants';
|
||||
import { transformToEditForm, defaultInvoice } from './utils';
|
||||
|
||||
/**
|
||||
* Invoice form.
|
||||
@@ -64,11 +56,11 @@ function InvoiceForm({
|
||||
const initialValues = useMemo(
|
||||
() => ({
|
||||
...(!isEmpty(invoice)
|
||||
? transformToEditForm(invoice, defaultInitialValues, MIN_LINES_NUMBER)
|
||||
? transformToEditForm(invoice)
|
||||
: {
|
||||
...defaultInitialValues,
|
||||
...defaultInvoice,
|
||||
invoice_no: invoiceNumber,
|
||||
entries: orderingLinesIndexes(defaultInitialValues.entries),
|
||||
entries: orderingLinesIndexes(defaultInvoice.entries),
|
||||
}),
|
||||
}),
|
||||
[invoice, invoiceNumber],
|
||||
|
||||
@@ -1,26 +0,0 @@
|
||||
import { moment } from 'moment';
|
||||
import { repeatValue } from 'utils';
|
||||
|
||||
export const MIN_LINES_NUMBER = 4;
|
||||
|
||||
export const defaultInvoice = {
|
||||
index: 0,
|
||||
item_id: '',
|
||||
rate: '',
|
||||
discount: 0,
|
||||
quantity: 1,
|
||||
description: '',
|
||||
total: 0,
|
||||
};
|
||||
|
||||
export const defaultInitialValues = {
|
||||
customer_id: '',
|
||||
invoice_date: moment(new Date()).format('YYYY-MM-DD'),
|
||||
due_date: moment(new Date()).format('YYYY-MM-DD'),
|
||||
delivered: '',
|
||||
invoice_no: '',
|
||||
reference_no: '',
|
||||
invoice_message: '',
|
||||
terms_conditions: '',
|
||||
entries: [...repeatValue(defaultInvoice, MIN_LINES_NUMBER)],
|
||||
};
|
||||
@@ -1,16 +1,43 @@
|
||||
import moment from 'moment';
|
||||
import { transformToForm, repeatValue } from 'utils';
|
||||
|
||||
export const MIN_LINES_NUMBER = 4;
|
||||
|
||||
export function transformToEditForm(invoice, defaultInvoice, linesNumber) {
|
||||
export const defaultInvoiceEntry = {
|
||||
index: 0,
|
||||
item_id: '',
|
||||
rate: '',
|
||||
discount: 0,
|
||||
quantity: 1,
|
||||
description: '',
|
||||
total: 0,
|
||||
};
|
||||
|
||||
export const defaultInvoice = {
|
||||
customer_id: '',
|
||||
invoice_date: moment(new Date()).format('YYYY-MM-DD'),
|
||||
due_date: moment().format('YYYY-MM-DD'),
|
||||
delivered: '',
|
||||
invoice_no: '',
|
||||
reference_no: '',
|
||||
invoice_message: '',
|
||||
terms_conditions: '',
|
||||
entries: [...repeatValue(defaultInvoiceEntry, MIN_LINES_NUMBER)],
|
||||
};
|
||||
|
||||
/**
|
||||
* Transform invoice to initial values in edit mode.
|
||||
*/
|
||||
export function transformToEditForm(invoice) {
|
||||
return {
|
||||
...transformToForm(invoice, defaultInvoice),
|
||||
entries: [
|
||||
...invoice.entries.map((invoice) => ({
|
||||
...transformToForm(invoice, defaultInvoice.entries[0]),
|
||||
...transformToForm(invoice, defaultInvoiceEntry),
|
||||
})),
|
||||
...repeatValue(
|
||||
defaultInvoice,
|
||||
Math.max(linesNumber - invoice.entries.length, 0),
|
||||
defaultInvoiceEntry,
|
||||
Math.max(MIN_LINES_NUMBER - invoice.entries.length, 0),
|
||||
),
|
||||
],
|
||||
};
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
import React, { useMemo } from 'react';
|
||||
import { Formik, Form } from 'formik';
|
||||
import moment from 'moment';
|
||||
import { Intent } from '@blueprintjs/core';
|
||||
import { useIntl } from 'react-intl';
|
||||
import { pick, sumBy, isEmpty } from 'lodash';
|
||||
import { omit, sumBy, isEmpty } from 'lodash';
|
||||
import classNames from 'classnames';
|
||||
import { useHistory } from 'react-router-dom';
|
||||
|
||||
@@ -19,7 +18,7 @@ import 'style/pages/SaleReceipt/PageForm.scss';
|
||||
import { useReceiptFormContext } from './ReceiptFormProvider';
|
||||
|
||||
import ReceiptFromHeader from './ReceiptFormHeader';
|
||||
import ReceiptFormBody from './ReceiptFormBody';
|
||||
import ReceiptItemsEntriesEditor from './ReceiptItemsEntriesEditor';
|
||||
import ReceiptFormFloatingActions from './ReceiptFormFloatingActions';
|
||||
import ReceiptFormFooter from './ReceiptFormFooter';
|
||||
|
||||
@@ -29,33 +28,11 @@ import withSettings from 'containers/Settings/withSettings';
|
||||
import { AppToaster } from 'components';
|
||||
import {
|
||||
compose,
|
||||
repeatValue,
|
||||
orderingLinesIndexes,
|
||||
transactionNumber,
|
||||
} from 'utils';
|
||||
import { transformToEditForm, defaultReceipt } from './utils'
|
||||
|
||||
const MIN_LINES_NUMBER = 4;
|
||||
|
||||
const defaultReceipt = {
|
||||
index: 0,
|
||||
item_id: '',
|
||||
rate: '',
|
||||
discount: 0,
|
||||
quantity: '',
|
||||
description: '',
|
||||
};
|
||||
|
||||
const defaultInitialValues = {
|
||||
customer_id: '',
|
||||
deposit_account_id: '',
|
||||
receipt_number: '',
|
||||
receipt_date: moment(new Date()).format('YYYY-MM-DD'),
|
||||
reference_no: '',
|
||||
receipt_message: '',
|
||||
statement: '',
|
||||
closed: '',
|
||||
entries: [...repeatValue(defaultReceipt, MIN_LINES_NUMBER)],
|
||||
};
|
||||
|
||||
/**
|
||||
* Receipt form.
|
||||
@@ -75,11 +52,10 @@ function ReceiptForm({
|
||||
receipt,
|
||||
editReceiptMutate,
|
||||
createReceiptMutate,
|
||||
submitPayload
|
||||
submitPayload,
|
||||
isNewMode
|
||||
} = useReceiptFormContext();
|
||||
|
||||
const isNewMode = !receiptId;
|
||||
|
||||
// The next receipt number.
|
||||
const receiptNumber = transactionNumber(
|
||||
receiptNumberPrefix,
|
||||
@@ -89,23 +65,12 @@ function ReceiptForm({
|
||||
const initialValues = useMemo(
|
||||
() => ({
|
||||
...(!isEmpty(receipt)
|
||||
? {
|
||||
...pick(receipt, Object.keys(defaultInitialValues)),
|
||||
entries: [
|
||||
...receipt.entries.map((receipt) => ({
|
||||
...pick(receipt, Object.keys(defaultReceipt)),
|
||||
})),
|
||||
...repeatValue(
|
||||
defaultReceipt,
|
||||
Math.max(MIN_LINES_NUMBER - receipt.entries.length, 0),
|
||||
),
|
||||
],
|
||||
}
|
||||
? transformToEditForm(receipt)
|
||||
: {
|
||||
...defaultInitialValues,
|
||||
...defaultReceipt,
|
||||
receipt_number: receiptNumber,
|
||||
deposit_account_id: parseInt(preferredDepositAccount),
|
||||
entries: orderingLinesIndexes(defaultInitialValues.entries),
|
||||
entries: orderingLinesIndexes(defaultReceipt.entries),
|
||||
}),
|
||||
}),
|
||||
[receipt, preferredDepositAccount, receiptNumber],
|
||||
@@ -145,10 +110,7 @@ function ReceiptForm({
|
||||
const form = {
|
||||
...values,
|
||||
closed: submitPayload.status,
|
||||
entries: entries.map((entry) => ({
|
||||
// Exclude all properties that out of request entries schema.
|
||||
...pick(entry, Object.keys(defaultReceipt)),
|
||||
})),
|
||||
entries: entries.map((entry) => ({ ...omit(entry, ['total']), })),
|
||||
};
|
||||
// Handle the request success.
|
||||
const onSuccess = (response) => {
|
||||
@@ -179,8 +141,8 @@ function ReceiptForm({
|
||||
setSubmitting(false);
|
||||
};
|
||||
|
||||
if (receipt && receipt.id) {
|
||||
editReceiptMutate(receipt.id, form).then(onSuccess).catch(onError);
|
||||
if (!isNewMode) {
|
||||
editReceiptMutate([receipt.id, form]).then(onSuccess).catch(onError);
|
||||
} else {
|
||||
createReceiptMutate(form).then(onSuccess).catch(onError);
|
||||
}
|
||||
@@ -203,7 +165,7 @@ function ReceiptForm({
|
||||
>
|
||||
<Form>
|
||||
<ReceiptFromHeader />
|
||||
<ReceiptFormBody defaultReceipt={defaultReceipt} />
|
||||
<ReceiptItemsEntriesEditor />
|
||||
<ReceiptFormFooter />
|
||||
<ReceiptFormFloatingActions />
|
||||
</Form>
|
||||
|
||||
@@ -1,20 +0,0 @@
|
||||
import React from 'react';
|
||||
import classNames from 'classnames';
|
||||
import { CLASSES } from 'common/classes';
|
||||
|
||||
// import EditableItemsEntriesTable from 'containers/Entries/EditableItemsEntriesTable';
|
||||
import { useReceiptFormContext } from './ReceiptFormProvider';
|
||||
|
||||
export default function ExpenseFormBody({ defaultReceipt }) {
|
||||
const { items } = useReceiptFormContext();
|
||||
|
||||
return (
|
||||
<div className={classNames(CLASSES.PAGE_FORM_BODY)}>
|
||||
{/* <EditableItemsEntriesTable
|
||||
items={items}
|
||||
defaultEntry={defaultReceipt}
|
||||
filterSellableItems={true}
|
||||
/> */}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -46,6 +46,8 @@ function ReceiptFormProvider({ receiptId, ...props }) {
|
||||
|
||||
const [submitPayload, setSubmitPayload] = useState({});
|
||||
|
||||
const isNewMode = !receiptId;
|
||||
|
||||
const provider = {
|
||||
receiptId,
|
||||
receipt,
|
||||
@@ -54,6 +56,7 @@ function ReceiptFormProvider({ receiptId, ...props }) {
|
||||
items,
|
||||
submitPayload,
|
||||
|
||||
isNewMode,
|
||||
isReceiptLoading,
|
||||
isAccountsLoading,
|
||||
isCustomersLoading,
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
import React from 'react';
|
||||
import classNames from 'classnames';
|
||||
import { FastField } from 'formik';
|
||||
import ItemsEntriesTable from 'containers/Entries/ItemsEntriesTable';
|
||||
import { CLASSES } from 'common/classes';
|
||||
import { useReceiptFormContext } from './ReceiptFormProvider';
|
||||
|
||||
export default function ReceiptItemsEntriesEditor({ defaultReceipt }) {
|
||||
const { items } = useReceiptFormContext();
|
||||
|
||||
return (
|
||||
<div className={classNames(CLASSES.PAGE_FORM_BODY)}>
|
||||
<FastField name={'entries'}>
|
||||
{({ form, field: { value }, meta: { error, touched } }) => (
|
||||
<ItemsEntriesTable
|
||||
entries={value}
|
||||
onUpdateData={(entries) => {
|
||||
form.setFieldValue('entries', entries);
|
||||
}}
|
||||
items={items}
|
||||
errors={error}
|
||||
linesNumber={4}
|
||||
/>
|
||||
)}
|
||||
</FastField>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,45 +0,0 @@
|
||||
import { useEffect } from 'react';
|
||||
import { useFormikContext } from 'formik';
|
||||
|
||||
import withDashboardActions from "containers/Dashboard/withDashboardActions";
|
||||
import withReceipts from './withReceipts';
|
||||
import withReceiptActions from './withReceiptActions';
|
||||
|
||||
import { compose } from 'utils';
|
||||
|
||||
function ReceiptNumberWatcher({
|
||||
receiptNumber,
|
||||
|
||||
// #withDashboardActions
|
||||
changePageSubtitle,
|
||||
|
||||
// #withReceipts
|
||||
receiptNumberChanged,
|
||||
|
||||
//#withReceiptActions
|
||||
setReceiptNumberChanged,
|
||||
}) {
|
||||
const { setFieldValue } = useFormikContext();
|
||||
|
||||
useEffect(() => {
|
||||
if (receiptNumberChanged) {
|
||||
setFieldValue('receipt_number', receiptNumber);
|
||||
changePageSubtitle(`No. ${receiptNumber}`);
|
||||
setReceiptNumberChanged(false);
|
||||
}
|
||||
}, [
|
||||
receiptNumber,
|
||||
receiptNumberChanged,
|
||||
setReceiptNumberChanged,
|
||||
setFieldValue,
|
||||
changePageSubtitle,
|
||||
]);
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
export default compose(
|
||||
withReceipts(({ receiptNumberChanged }) => ({ receiptNumberChanged })),
|
||||
withReceiptActions,
|
||||
withDashboardActions
|
||||
)(ReceiptNumberWatcher);
|
||||
41
client/src/containers/Sales/Receipts/ReceiptForm/utils.js
Normal file
41
client/src/containers/Sales/Receipts/ReceiptForm/utils.js
Normal file
@@ -0,0 +1,41 @@
|
||||
import moment from 'moment';
|
||||
import { repeatValue, transformToForm } from 'utils';
|
||||
|
||||
export const MIN_LINES_NUMBER = 4;
|
||||
|
||||
export const defaultReceiptEntry = {
|
||||
index: 0,
|
||||
item_id: '',
|
||||
rate: '',
|
||||
discount: 0,
|
||||
quantity: '',
|
||||
description: '',
|
||||
};
|
||||
|
||||
export const defaultReceipt = {
|
||||
customer_id: '',
|
||||
deposit_account_id: '',
|
||||
receipt_number: '',
|
||||
receipt_date: moment(new Date()).format('YYYY-MM-DD'),
|
||||
reference_no: '',
|
||||
receipt_message: '',
|
||||
statement: '',
|
||||
closed: '',
|
||||
entries: [...repeatValue(defaultReceiptEntry, MIN_LINES_NUMBER)],
|
||||
};
|
||||
|
||||
/**
|
||||
* Transform to form in edit mode.
|
||||
*/
|
||||
export const transformToEditForm = (receipt) => ({
|
||||
...transformToForm(receipt, defaultReceipt),
|
||||
entries: [
|
||||
...receipt.entries.map((entry) => ({
|
||||
...transformToForm(entry, defaultReceiptEntry),
|
||||
})),
|
||||
...repeatValue(
|
||||
defaultReceiptEntry,
|
||||
Math.max(MIN_LINES_NUMBER - receipt.entries.length, 0),
|
||||
),
|
||||
],
|
||||
});
|
||||
Reference in New Issue
Block a user