feat: payment receive of customers invoices.

This commit is contained in:
Ahmed Bouhuolia
2020-11-02 21:42:40 +02:00
parent 731b8fd119
commit e6cd921b94
24 changed files with 911 additions and 645 deletions

View File

@@ -16,6 +16,7 @@ const CLASSES = {
PAGE_FORM_INVOICE: 'page-form--invoice', PAGE_FORM_INVOICE: 'page-form--invoice',
PAGE_FORM_RECEIPT: 'page-form--receipt', PAGE_FORM_RECEIPT: 'page-form--receipt',
PAGE_FORM_PAYMENT_MADE: 'page-form--payment-made', PAGE_FORM_PAYMENT_MADE: 'page-form--payment-made',
PAGE_FORM_PAYMENT_RECEIVE: 'page-form--payment-receive',
CLOUD_SPINNER: 'cloud-spinner', CLOUD_SPINNER: 'cloud-spinner',
IS_LOADING: 'is-loading', IS_LOADING: 'is-loading',

View File

@@ -5,7 +5,8 @@ import {
getBillPaginationMetaFactory, getBillPaginationMetaFactory,
getBillTableQueryFactory, getBillTableQueryFactory,
getVendorPayableBillsFactory, getVendorPayableBillsFactory,
getPayableBillsByPaymentMadeFactory getPayableBillsByPaymentMadeFactory,
getPaymentMadeFormPayableBillsFactory
} from 'store/Bills/bills.selectors'; } from 'store/Bills/bills.selectors';
export default (mapState) => { export default (mapState) => {
@@ -14,6 +15,7 @@ export default (mapState) => {
const getBillTableQuery = getBillTableQueryFactory(); const getBillTableQuery = getBillTableQueryFactory();
const getVendorPayableBills = getVendorPayableBillsFactory(); const getVendorPayableBills = getVendorPayableBillsFactory();
const getPayableBillsByPaymentMade = getPayableBillsByPaymentMadeFactory(); const getPayableBillsByPaymentMade = getPayableBillsByPaymentMadeFactory();
const getPaymentMadeFormPayableBills = getPaymentMadeFormPayableBillsFactory();
const mapStateToProps = (state, props) => { const mapStateToProps = (state, props) => {
const tableQuery = getBillTableQuery(state, props); const tableQuery = getBillTableQuery(state, props);
@@ -28,8 +30,8 @@ export default (mapState) => {
billsLoading: state.bills.loading, billsLoading: state.bills.loading,
nextBillNumberChanged: state.bills.nextBillNumberChanged, nextBillNumberChanged: state.bills.nextBillNumberChanged,
vendorPayableBills: getVendorPayableBills(state, props), // vendorPayableBills: getVendorPayableBills(state, props),
paymentMadePayableBills: getPayableBillsByPaymentMade(state, props), paymentMadePayableBills: getPaymentMadeFormPayableBills(state, props),
}; };
return mapState ? mapState(mapped, state, props) : mapped; return mapState ? mapState(mapped, state, props) : mapped;
}; };

View File

@@ -203,12 +203,10 @@ function PaymentMadeForm({
}, },
[fullAmount, setAmountChangeAlert], [fullAmount, setAmountChangeAlert],
); );
// Handle cancel button of amount change alert. // Handle cancel button of amount change alert.
const handleCancelAmountChangeAlert = () => { const handleCancelAmountChangeAlert = () => {
setAmountChangeAlert(false); setAmountChangeAlert(false);
}; };
// Handle confirm button of amount change alert. // Handle confirm button of amount change alert.
const handleConfirmAmountChangeAlert = () => { const handleConfirmAmountChangeAlert = () => {
setFullAmount(amountChangeAlert); setFullAmount(amountChangeAlert);
@@ -286,7 +284,6 @@ function PaymentMadeForm({
values={values} values={values}
onFullAmountChanged={handleFullAmountChange} onFullAmountChanged={handleFullAmountChange}
/> />
<PaymentMadeItemsTable <PaymentMadeItemsTable
fullAmount={fullAmount} fullAmount={fullAmount}
paymentEntries={values.entries} paymentEntries={values.entries}
@@ -296,7 +293,6 @@ function PaymentMadeForm({
onClickClearAllLines={handleClearAllLines} onClickClearAllLines={handleClearAllLines}
errors={errors?.entries} errors={errors?.entries}
/> />
<Alert <Alert
cancelButtonText={<T id={'cancel'} />} cancelButtonText={<T id={'cancel'} />}
confirmButtonText={<T id={'ok'} />} confirmButtonText={<T id={'ok'} />}

View File

@@ -51,13 +51,13 @@ function PaymentMadeFormHeader({
accountsList, accountsList,
// #withBills // #withBills
vendorPayableBills, paymentMadePayableBills,
}) { }) {
const isNewMode = !paymentMadeId; const isNewMode = !paymentMadeId;
const payableFullAmount = useMemo( const payableFullAmount = useMemo(
() => sumBy(vendorPayableBills, 'due_amount'), () => sumBy(paymentMadePayableBills, 'due_amount'),
[vendorPayableBills], [paymentMadePayableBills],
); );
const handleDateChange = useCallback( const handleDateChange = useCallback(
@@ -276,7 +276,7 @@ export default compose(
withAccounts(({ accountsList }) => ({ withAccounts(({ accountsList }) => ({
accountsList, accountsList,
})), })),
withBills(({ vendorPayableBills }) => ({ withBills(({ paymentMadePayableBills }) => ({
vendorPayableBills, paymentMadePayableBills,
})), })),
)(PaymentMadeFormHeader); )(PaymentMadeFormHeader);

View File

@@ -1,5 +1,6 @@
import React, { useState, useEffect, useMemo, useCallback } from 'react'; import React, { useState, useEffect, useMemo, useCallback } from 'react';
import { useQuery } from 'react-query'; import { useQuery } from 'react-query';
import { omit } from 'lodash';
import { CloudLoadingIndicator } from 'components' import { CloudLoadingIndicator } from 'components'
import PaymentMadeItemsTableEditor from './PaymentMadeItemsTableEditor'; import PaymentMadeItemsTableEditor from './PaymentMadeItemsTableEditor';
@@ -26,7 +27,6 @@ function PaymentMadeItemsTable({
requestFetchDueBills, requestFetchDueBills,
// #withBills // #withBills
vendorPayableBills,
paymentMadePayableBills, paymentMadePayableBills,
// #withPaymentMadeDetail // #withPaymentMadeDetail
@@ -35,20 +35,14 @@ function PaymentMadeItemsTable({
const [tableData, setTableData] = useState([]); const [tableData, setTableData] = useState([]);
const [localAmount, setLocalAmount] = useState(fullAmount); const [localAmount, setLocalAmount] = useState(fullAmount);
// Payable bills based on selected vendor or specific payment made.
const payableBills = useMemo(
() =>
paymentMadeId
? paymentMadePayableBills
: vendorId
? vendorPayableBills
: [],
[paymentMadeId, paymentMadePayableBills, vendorId, vendorPayableBills],
);
const isNewMode = !paymentMadeId; const isNewMode = !paymentMadeId;
const triggerUpdateData = useCallback((data) => { const triggerUpdateData = useCallback((entries) => {
onUpdateData && onUpdateData(data); const _data = entries.map((entry) => ({
bill_id: entry?.bill?.id,
...omit(entry, ['bill']),
}))
onUpdateData && onUpdateData(_data);
}, [onUpdateData]); }, [onUpdateData]);
// Merges payment entries with payable bills. // Merges payment entries with payable bills.
@@ -56,17 +50,16 @@ function PaymentMadeItemsTable({
const entriesTable = new Map( const entriesTable = new Map(
paymentEntries.map((e) => [e.bill_id, e]), paymentEntries.map((e) => [e.bill_id, e]),
); );
return payableBills.map((bill) => { return paymentMadePayableBills.map((bill) => {
const entry = entriesTable.get(bill.id); const entry = entriesTable.get(bill.id);
return { return {
...bill, bill,
bill_id: bill.id, id: null,
bill_payment_amount: bill.payment_amount, payment_number: 0,
payment_amount: entry ? entry.payment_amount : 0, ...(entry || {}),
id: entry ? entry.id : null,
} }
}); });
}, [paymentEntries, payableBills]); }, [paymentEntries, paymentMadePayableBills]);
useEffect(() => { useEffect(() => {
setTableData(computedTableData); setTableData(computedTableData);
@@ -127,11 +120,10 @@ function PaymentMadeItemsTable({
); );
} }
export default compose( export default compose(
withPaymentMadeActions, withPaymentMadeActions,
withBillActions, withBillActions,
withBills(({ vendorPayableBills, paymentMadePayableBills }) => ({ withBills(({ paymentMadePayableBills }) => ({
vendorPayableBills,
paymentMadePayableBills, paymentMadePayableBills,
})), })),
)(PaymentMadeItemsTable); )(PaymentMadeItemsTable);

View File

@@ -74,27 +74,27 @@ export default function PaymentMadeItemsTableEditor({
{ {
Header: formatMessage({ id: 'Date' }), Header: formatMessage({ id: 'Date' }),
id: 'bill_date', id: 'bill_date',
accessor: (r) => moment(r.bill_date).format('YYYY MMM DD'), accessor: (r) => moment(r.bill?.bill_date).format('YYYY MMM DD'),
Cell: CellRenderer(EmptyDiv, 'bill_date'), Cell: CellRenderer(EmptyDiv, 'bill_date'),
disableSortBy: true, disableSortBy: true,
}, },
{ {
Header: formatMessage({ id: 'bill_number' }), Header: formatMessage({ id: 'bill_number' }),
accessor: (row) => `#${row.bill_number}`, accessor: (row) => `#${row.bill?.bill_number}`,
Cell: CellRenderer(EmptyDiv, 'bill_number'), Cell: CellRenderer(EmptyDiv, 'bill_number'),
disableSortBy: true, disableSortBy: true,
className: 'bill_number', className: 'bill_number',
}, },
{ {
Header: formatMessage({ id: 'bill_amount' }), Header: formatMessage({ id: 'bill_amount' }),
accessor: 'amount', accessor: r => r.bill?.amount,
Cell: CellRenderer(DivFieldCell, 'amount'), Cell: CellRenderer(DivFieldCell, 'amount'),
disableSortBy: true, disableSortBy: true,
className: '', className: '',
}, },
{ {
Header: formatMessage({ id: 'amount_due' }), Header: formatMessage({ id: 'amount_due' }),
accessor: 'due_amount', accessor: r => r.bill?.due_amount,
Cell: TotalCellRederer(DivFieldCell, 'due_amount'), Cell: TotalCellRederer(DivFieldCell, 'due_amount'),
disableSortBy: true, disableSortBy: true,
className: '', className: '',
@@ -129,6 +129,8 @@ export default function PaymentMadeItemsTableEditor({
columnId, columnId,
value, value,
); );
newRows.splice(-1,1); // removes the total row.
setLocalData(newRows); setLocalData(newRows);
onUpdateData && onUpdateData(newRows); onUpdateData && onUpdateData(newRows);
}, },

View File

@@ -5,7 +5,7 @@ import {
deleteInvoice, deleteInvoice,
fetchInvoice, fetchInvoice,
fetchInvoicesTable, fetchInvoicesTable,
dueInvoices, fetchDueInvoices,
} from 'store/Invoice/invoices.actions'; } from 'store/Invoice/invoices.actions';
import t from 'store/types'; import t from 'store/types';
@@ -16,7 +16,7 @@ const mapDipatchToProps = (dispatch) => ({
requestFetchInvoiceTable: (query = {}) => requestFetchInvoiceTable: (query = {}) =>
dispatch(fetchInvoicesTable({ query: { ...query } })), dispatch(fetchInvoicesTable({ query: { ...query } })),
requestDeleteInvoice: (id) => dispatch(deleteInvoice({ id })), requestDeleteInvoice: (id) => dispatch(deleteInvoice({ id })),
requestFetchDueInvoices: (id) => dispatch(dueInvoices({ id })), requestFetchDueInvoices: (customerId) => dispatch(fetchDueInvoices({ customerId })),
changeInvoiceView: (id) => changeInvoiceView: (id) =>
dispatch({ dispatch({
type: t.INVOICES_SET_CURRENT_VIEW, type: t.INVOICES_SET_CURRENT_VIEW,

View File

@@ -4,8 +4,9 @@ import {
getInvoiceCurrentPageFactory, getInvoiceCurrentPageFactory,
getInvoicePaginationMetaFactory, getInvoicePaginationMetaFactory,
getInvoiceTableQueryFactory, getInvoiceTableQueryFactory,
getInvoiceTableQuery, getCustomerReceivableInvoicesFactory,
getdueInvoices, getPaymentReceivableInvoicesFactory,
getPaymentReceiveReceivableInvoicesFactory
} from 'store/Invoice/invoices.selector'; } from 'store/Invoice/invoices.selector';
export default (mapState) => { export default (mapState) => {
@@ -14,6 +15,11 @@ export default (mapState) => {
const getInvoicesPaginationMeta = getInvoicePaginationMetaFactory(); const getInvoicesPaginationMeta = getInvoicePaginationMetaFactory();
const getInvoiceTableQuery = getInvoiceTableQueryFactory(); const getInvoiceTableQuery = getInvoiceTableQueryFactory();
// const getPaymentReceivableInvoices = getPaymentReceivableInvoicesFactory();
// const getCustomerReceivableInvoices = getCustomerReceivableInvoicesFactory();
const getPaymentReceiveReceivableInvoices = getPaymentReceiveReceivableInvoicesFactory();
const mapStateToProps = (state, props) => { const mapStateToProps = (state, props) => {
const query = getInvoiceTableQuery(state, props); const query = getInvoiceTableQuery(state, props);
@@ -24,7 +30,8 @@ export default (mapState) => {
invoicesTableQuery: query, invoicesTableQuery: query,
invoicesPageination: getInvoicesPaginationMeta(state, props, query), invoicesPageination: getInvoicesPaginationMeta(state, props, query),
invoicesLoading: state.salesInvoices.loading, invoicesLoading: state.salesInvoices.loading,
dueInvoices: getdueInvoices(state, props),
paymentReceiveReceivableInvoices: getPaymentReceiveReceivableInvoices(state, props),
}; };
return mapState ? mapState(mapped, state, props) : mapped; return mapState ? mapState(mapped, state, props) : mapped;
}; };

View File

@@ -1,29 +1,41 @@
import React from 'react'; import React from 'react';
import { Intent, Button } from '@blueprintjs/core'; import { Intent, Button } from '@blueprintjs/core';
import { FormattedMessage as T } from 'react-intl'; import { FormattedMessage as T } from 'react-intl';
import classNames from 'classnames';
export default function PaymentReceiveFormFooter({ import { CLASSES } from 'common/classes';
formik: { isSubmitting, resetForm },
/**
* Payment receive floating actions bar.
*/
export default function PaymentReceiveFormFloatingActions({
isSubmitting,
onSubmitClick, onSubmitClick,
onCancelClick, onCancelClick,
onClearClick, onClearClick,
paymentReceive, paymentReceiveId,
}) { }) {
const handleSubmitClick = (event) => {
onSubmitClick && onSubmitClick(event);
};
const handleClearBtnClick = (event) => {
onClearClick && onClearClick(event);
};
const handleCloseBtnClick = (event) => {
onCancelClick && onCancelClick(event);
};
return ( return (
<div className={'estimate-form__floating-footer'}> <div className={classNames(CLASSES.PAGE_FORM_FLOATING_ACTIONS)}>
<Button <Button
disabled={isSubmitting} disabled={isSubmitting}
intent={Intent.PRIMARY} intent={Intent.PRIMARY}
type="submit" type="submit"
onClick={() => { onClick={handleSubmitClick}
onSubmitClick({ redirect: true }); >
}} {paymentReceiveId ? <T id={'edit'} /> : <T id={'save_send'} />}
>
{paymentReceive && paymentReceive.id ? (
<T id={'edit'} />
) : (
<T id={'save_send'} />
)}
</Button> </Button>
<Button <Button
@@ -32,9 +44,7 @@ export default function PaymentReceiveFormFooter({
className={'ml1'} className={'ml1'}
name={'save'} name={'save'}
type="submit" type="submit"
onClick={() => { onClick={handleSubmitClick}
onSubmitClick({ redirect: false });
}}
> >
<T id={'save'} /> <T id={'save'} />
</Button> </Button>
@@ -42,18 +52,12 @@ export default function PaymentReceiveFormFooter({
<Button <Button
className={'ml1'} className={'ml1'}
disabled={isSubmitting} disabled={isSubmitting}
onClick={() => onClearClick && onClearClick()} onClick={handleClearBtnClick}
> >
<T id={'clear'} /> <T id={'clear'} />
</Button> </Button>
<Button <Button className={'ml1'} type="submit" onClick={handleCloseBtnClick}>
className={'ml1'}
type="submit"
onClick={() => {
onCancelClick && onCancelClick();
}}
>
<T id={'close'} /> <T id={'close'} />
</Button> </Button>
</div> </div>

View File

@@ -9,89 +9,41 @@ import React, {
import * as Yup from 'yup'; import * as Yup from 'yup';
import { useFormik } from 'formik'; import { useFormik } from 'formik';
import moment from 'moment'; import moment from 'moment';
import { Intent, FormGroup, TextArea } from '@blueprintjs/core';
import { useParams, useHistory } from 'react-router-dom';
import { FormattedMessage as T, useIntl } from 'react-intl'; import { FormattedMessage as T, useIntl } from 'react-intl';
import { pick, values } from 'lodash'; import { pick, sumBy } from 'lodash';
import { Intent, Alert } from '@blueprintjs/core';
import classNames from 'classnames';
import { CLASSES } from 'common/classes';
import PaymentReceiveHeader from './PaymentReceiveFormHeader'; import PaymentReceiveHeader from './PaymentReceiveFormHeader';
import PaymentReceiveItemsTable from './PaymentReceiveItemsTable'; import PaymentReceiveItemsTable from './PaymentReceiveItemsTable';
import PaymentReceiveFloatingActions from './PaymentReceiveFloatingActions'; import PaymentReceiveFloatingActions from './PaymentReceiveFloatingActions';
import withDashboardActions from 'containers/Dashboard/withDashboardActions';
import withMediaActions from 'containers/Media/withMediaActions'; import withMediaActions from 'containers/Media/withMediaActions';
import withPaymentReceivesActions from './withPaymentReceivesActions'; import withPaymentReceivesActions from './withPaymentReceivesActions';
import withInvoices from '../Invoice/withInvoices';
import withPaymentReceiveDetail from './withPaymentReceiveDetail'; import withPaymentReceiveDetail from './withPaymentReceiveDetail';
import withPaymentReceives from './withPaymentReceives';
import { AppToaster } from 'components'; import { AppToaster } from 'components';
import Dragzone from 'components/Dragzone';
import useMedia from 'hooks/useMedia';
import { compose, repeatValue } from 'utils'; import { compose } from 'utils';
const MIN_LINES_NUMBER = 5;
function PaymentReceiveForm({ function PaymentReceiveForm({
//#withMedia // #ownProps
requestSubmitMedia, paymentReceiveId,
requestDeleteMedia,
//#WithPaymentReceiveActions //#WithPaymentReceiveActions
requestSubmitPaymentReceive, requestSubmitPaymentReceive,
requestEditPaymentReceive, requestEditPaymentReceive,
//#withDashboard // #withPaymentReceive
changePageTitle,
changePageSubtitle,
//#withPaymentReceiveDetail
paymentReceive, paymentReceive,
paymentReceiveInvoices,
paymentReceivesItems,
//#OWn Props
// payment_receive,
onFormSubmit,
onCancelForm,
dueInvoiceLength,
onCustomerChange,
}) { }) {
const [amountChangeAlert, setAmountChangeAlert] = useState(false);
const [clearLinesAlert, setClearLinesAlert] = useState(false);
const [fullAmount, setFullAmount] = useState(null);
const { formatMessage } = useIntl(); const { formatMessage } = useIntl();
const [payload, setPayload] = useState({});
const { id } = useParams();
const {
setFiles,
saveMedia,
deletedFiles,
setDeletedFiles,
deleteMedia,
} = useMedia({
saveCallback: requestSubmitMedia,
deleteCallback: requestDeleteMedia,
});
const savedMediaIds = useRef([]);
const clearSavedMediaIds = () => {
savedMediaIds.current = [];
};
useEffect(() => {
if (paymentReceive && paymentReceive.id) {
return;
} else {
onCustomerChange && onCustomerChange(formik.values.customer_id);
}
});
useEffect(() => {
if (paymentReceive && paymentReceive.id) {
changePageTitle(formatMessage({ id: 'edit_payment_receive' }));
} else {
changePageTitle(formatMessage({ id: 'payment_receive' }));
}
}, [changePageTitle, paymentReceive, formatMessage]);
// Form validation schema.
const validationSchema = Yup.object().shape({ const validationSchema = Yup.object().shape({
customer_id: Yup.string() customer_id: Yup.string()
.label(formatMessage({ id: 'customer_name_' })) .label(formatMessage({ id: 'customer_name_' }))
@@ -102,9 +54,7 @@ function PaymentReceiveForm({
deposit_account_id: Yup.number() deposit_account_id: Yup.number()
.required() .required()
.label(formatMessage({ id: 'deposit_account_' })), .label(formatMessage({ id: 'deposit_account_' })),
// receive_amount: Yup.number() full_amount: Yup.number().nullable(),
// .required()
// .label(formatMessage({ id: 'receive_amount_' })),
payment_receive_no: Yup.number() payment_receive_no: Yup.number()
.required() .required()
.label(formatMessage({ id: 'payment_receive_no_' })), .label(formatMessage({ id: 'payment_receive_no_' })),
@@ -112,11 +62,9 @@ function PaymentReceiveForm({
description: Yup.string().nullable(), description: Yup.string().nullable(),
entries: Yup.array().of( entries: Yup.array().of(
Yup.object().shape({ Yup.object().shape({
payment_amount: Yup.number().nullable(), id: Yup.number().nullable(),
invoice_no: Yup.number().nullable(),
balance: Yup.number().nullable(),
due_amount: Yup.number().nullable(), due_amount: Yup.number().nullable(),
invoice_date: Yup.date(), payment_amount: Yup.number().nullable().max(Yup.ref('due_amount')),
invoice_id: Yup.number() invoice_id: Yup.number()
.nullable() .nullable()
.when(['payment_amount'], { .when(['payment_amount'], {
@@ -126,15 +74,7 @@ function PaymentReceiveForm({
}), }),
), ),
}); });
// Default payment receive.
const handleDropFiles = useCallback((_files) => {
setFiles(_files.filter((file) => file.uploaded === false));
}, []);
const savePaymentReceiveSubmit = useCallback((payload) => {
onFormSubmit && onFormSubmit(payload);
});
const defaultPaymentReceive = useMemo( const defaultPaymentReceive = useMemo(
() => ({ () => ({
invoice_id: '', invoice_id: '',
@@ -146,6 +86,12 @@ function PaymentReceiveForm({
}), }),
[], [],
); );
const defaultPaymentReceiveEntry = {
id: null,
payment_amount: null,
invoice_id: null,
};
// Form initial values.
const defaultInitialValues = useMemo( const defaultInitialValues = useMemo(
() => ({ () => ({
customer_id: '', customer_id: '',
@@ -153,20 +99,13 @@ function PaymentReceiveForm({
payment_date: moment(new Date()).format('YYYY-MM-DD'), payment_date: moment(new Date()).format('YYYY-MM-DD'),
reference_no: '', reference_no: '',
payment_receive_no: '', payment_receive_no: '',
// receive_amount: '',
description: '', description: '',
entries: [...repeatValue(defaultPaymentReceive, MIN_LINES_NUMBER)], entries: [],
}), }),
[defaultPaymentReceive], [],
); );
const orderingIndex = (_entries) => { // Form initial values.
return _entries.map((item, index) => ({
...item,
index: index + 1,
}));
};
const initialValues = useMemo( const initialValues = useMemo(
() => ({ () => ({
...(paymentReceive ...(paymentReceive
@@ -176,167 +115,205 @@ function PaymentReceiveForm({
...paymentReceive.entries.map((paymentReceive) => ({ ...paymentReceive.entries.map((paymentReceive) => ({
...pick(paymentReceive, Object.keys(defaultPaymentReceive)), ...pick(paymentReceive, Object.keys(defaultPaymentReceive)),
})), })),
...repeatValue(
defaultPaymentReceive,
Math.max(MIN_LINES_NUMBER - paymentReceive.entries.length, 0),
),
], ],
} }
: { : {
...defaultInitialValues, ...defaultInitialValues,
entries: orderingIndex(defaultInitialValues.entries),
}), }),
}), }),
[paymentReceive, defaultInitialValues, defaultPaymentReceive], [paymentReceive, defaultInitialValues, defaultPaymentReceive],
); );
const initialAttachmentFiles = useMemo(() => { // Handle form submit.
return paymentReceive && paymentReceive.media const handleSubmitForm = (
? paymentReceive.media.map((attach) => ({ values,
preview: attach.attachment_file, { setSubmitting, resetForm, setFieldError },
uploaded: true, ) => {
metadata: { ...attach }, setSubmitting(true);
}))
: [];
}, [paymentReceive]);
const formik = useFormik({ // Filters entries that have no `invoice_id` and `payment_amount`.
const entries = values.entries
.filter((entry) => entry.invoice_id && entry.payment_amount)
.map((entry) => ({
...pick(entry, Object.keys(defaultPaymentReceiveEntry)),
}));
// Calculates the total payment amount of entries.
const totalPaymentAmount = sumBy(entries, 'payment_amount');
if (totalPaymentAmount <= 0) {
AppToaster.show({
message: formatMessage({
id: 'you_cannot_make_payment_with_zero_total_amount',
intent: Intent.WARNING,
}),
});
return;
}
const form = { ...values, entries };
// Handle request response success.
const onSaved = (response) => {
AppToaster.show({
message: formatMessage({
id: paymentReceiveId
? 'the_payment_has_been_received_successfully_edited'
: 'the_payment_has_been_received_successfully_created',
}),
intent: Intent.SUCCESS,
});
setSubmitting(false);
resetForm();
};
// Handle request response errors.
const onError = (errors) => {
const getError = (errorType) => errors.find((e) => e.type === errorType);
if (getError('PAYMENT_RECEIVE_NO_EXISTS')) {
setFieldError(
'payment_receive_no',
formatMessage({ id: 'payment_number_is_not_unique' }),
);
}
setSubmitting(false);
};
if (paymentReceiveId) {
requestEditPaymentReceive(paymentReceiveId, form)
.then(onSaved)
.catch(onError);
} else {
requestSubmitPaymentReceive(form).then(onSaved).catch(onError);
}
};
const {
errors,
values,
setFieldValue,
getFieldProps,
setValues,
handleSubmit,
isSubmitting,
touched,
} = useFormik({
enableReinitialize: true, enableReinitialize: true,
validationSchema, validationSchema,
initialValues: { initialValues: {
...initialValues, ...initialValues,
}, },
onSubmit: async (values, { setSubmitting, setErrors, resetForm }) => { onSubmit: handleSubmitForm,
setSubmitting(true);
const entries = formik.values.entries.filter((item) => {
if (item.invoice_id !== undefined) {
return { ...item };
}
});
const form = {
...values,
entries,
};
const requestForm = { ...form };
if (paymentReceive && paymentReceive.id) {
requestEditPaymentReceive(paymentReceive.id, requestForm)
.then((response) => {
AppToaster.show({
message: formatMessage({
id: 'the_payment_receive_has_been_successfully_edited',
}),
intent: Intent.SUCCESS,
});
setSubmitting(false);
savePaymentReceiveSubmit({ action: 'update', ...payload });
resetForm();
})
.catch((error) => {
setSubmitting(false);
});
} else {
requestSubmitPaymentReceive(requestForm)
.then((response) => {
AppToaster.show({
message: formatMessage({
id: 'the_payment_receive_has_been_successfully_created',
}),
intent: Intent.SUCCESS,
});
setSubmitting(false);
resetForm();
savePaymentReceiveSubmit({ action: 'new', ...payload });
})
.catch((errors) => {
setSubmitting(false);
});
}
},
}); });
const handleDeleteFile = useCallback( // Handle update data.
(_deletedFiles) => { const handleUpdataData = useCallback(
_deletedFiles.forEach((deletedFile) => { (entries) => {
if (deletedFile.upload && deletedFile.metadata.id) { setFieldValue('entries', entries);
setDeletedFiles([...deletedFiles, deletedFile.metadata.id]);
}
});
}, },
[setDeletedFiles, deletedFiles], [setFieldValue],
); );
const handleSubmitClick = useCallback( const handleFullAmountChange = useCallback(
(payload) => { (value) => {
setPayload(payload); if (value !== fullAmount) {
formik.submitForm(); setAmountChangeAlert(value);
}
}, },
[setPayload, formik], [fullAmount, setAmountChangeAlert],
); );
const handleCancelClick = useCallback( // Handle clear all lines button click.
(payload) => { const handleClearAllLines = useCallback(() => {
onCancelForm && onCancelForm(payload); setClearLinesAlert(true);
}, }, [setClearLinesAlert]);
[onCancelForm],
);
const handleClearClick = () => { // Handle cancel button click of clear lines alert
formik.resetForm(); const handleCancelClearLines = useCallback(() => {
setClearLinesAlert(false);
}, [setClearLinesAlert]);
// Handle cancel button of amount change alert.
const handleCancelAmountChangeAlert = () => {
setAmountChangeAlert(false);
}; };
// Handle confirm button of amount change alert.
const handleClickAddNewRow = () => { const handleConfirmAmountChangeAlert = () => {
formik.setFieldValue( setFullAmount(amountChangeAlert);
setAmountChangeAlert(false);
};
// Handle confirm clear all lines.
const handleConfirmClearLines = useCallback(() => {
setFieldValue(
'entries', 'entries',
orderingIndex([...formik.values.entries, defaultPaymentReceive]), values.entries.map((entry) => ({
...entry,
payment_amount: 0,
})),
); );
}; setClearLinesAlert(false);
}, [setFieldValue, setClearLinesAlert, values.entries]);
const handleClearAllLines = () => {
formik.setFieldValue(
'entries',
orderingIndex([...repeatValue(defaultPaymentReceive, MIN_LINES_NUMBER)]),
);
};
return ( return (
<div className={'payment_receive_form'}> <div className={classNames(
<form onSubmit={formik.handleSubmit}> CLASSES.PAGE_FORM, CLASSES.PAGE_FORM_PAYMENT_RECEIVE
<PaymentReceiveHeader formik={formik} /> )}>
<PaymentReceiveItemsTable <form onSubmit={handleSubmit}>
entries={formik.values.entries} <PaymentReceiveHeader
customer_id={formik.values.customer_id} errors={errors}
onClickAddNewRow={handleClickAddNewRow} touched={touched}
onClickClearAllLines={handleClearAllLines} setFieldValue={setFieldValue}
formik={formik} getFieldProps={getFieldProps}
invoices={paymentReceiveInvoices} values={values}
paymentReceiveId={paymentReceiveId}
customerId={values.customer_id}
onFullAmountChanged={handleFullAmountChange}
/> />
{/* <Dragzone <PaymentReceiveItemsTable
initialFiles={initialAttachmentFiles} paymentReceiveId={paymentReceiveId}
onDrop={handleDropFiles} customerId={values.customer_id}
onDeleteFile={handleDeleteFile} fullAmount={fullAmount}
hint={'Attachments: Maxiumum size: 20MB'} onUpdateData={handleUpdataData}
/> */} paymentEntries={values.entries}
</form> errors={errors?.entries}
onClickClearAllLines={handleClearAllLines}
/>
<Alert
cancelButtonText={<T id={'cancel'} />}
confirmButtonText={<T id={'ok'} />}
intent={Intent.WARNING}
isOpen={amountChangeAlert}
onCancel={handleCancelAmountChangeAlert}
onConfirm={handleConfirmAmountChangeAlert}
>
<p>Are you sure to discard full amount?</p>
</Alert>
<Alert
cancelButtonText={<T id={'cancel'} />}
confirmButtonText={<T id={'ok'} />}
intent={Intent.WARNING}
isOpen={clearLinesAlert}
onCancel={handleCancelClearLines}
onConfirm={handleConfirmClearLines}
>
<p>Are you sure to discard full amount?</p>
</Alert>
<PaymentReceiveFloatingActions <PaymentReceiveFloatingActions
formik={formik} isSubmitting={isSubmitting}
onSubmitClick={handleSubmitClick} paymentReceiveId={paymentReceiveId}
paymentReceive={paymentReceive} />
onCancel={handleCancelClick} </form>
onClearClick={handleClearClick}
/>
</div> </div>
); );
} }
export default compose( export default compose(
withPaymentReceivesActions, withPaymentReceivesActions,
withDashboardActions,
withMediaActions, withMediaActions,
withPaymentReceives(({ paymentReceivesItems }) => ({ // withPaymentReceives(({ paymentReceivesItems }) => ({
paymentReceivesItems, // paymentReceivesItems,
// })),
withPaymentReceiveDetail(({ paymentReceive }) => ({
paymentReceive,
})), })),
withPaymentReceiveDetail(),
)(PaymentReceiveForm); )(PaymentReceiveForm);

View File

@@ -5,35 +5,54 @@ import {
Intent, Intent,
Position, Position,
MenuItem, MenuItem,
Classes,
} from '@blueprintjs/core'; } from '@blueprintjs/core';
import { DateInput } from '@blueprintjs/datetime'; import { DateInput } from '@blueprintjs/datetime';
import { FormattedMessage as T } from 'react-intl'; import { FormattedMessage as T } from 'react-intl';
import { useParams, useHistory } from 'react-router-dom';
import { useQuery } from 'react-query';
import moment from 'moment'; import moment from 'moment';
import { momentFormatter, compose, tansformDateValue } from 'utils'; import { sumBy } from 'lodash';
import classNames from 'classnames'; import classNames from 'classnames';
import { CLASSES } from 'common/classes'
import { momentFormatter, compose, tansformDateValue } from 'utils';
import { import {
AccountsSelectList, AccountsSelectList,
ListSelect, ListSelect,
ErrorMessage, ErrorMessage,
FieldRequiredHint, FieldRequiredHint,
Hint,
Money,
} from 'components'; } from 'components';
import withInvoices from 'containers/Sales/Invoice/withInvoices';
import withCustomers from 'containers/Customers/withCustomers'; import withCustomers from 'containers/Customers/withCustomers';
import withAccounts from 'containers/Accounts/withAccounts'; import withAccounts from 'containers/Accounts/withAccounts';
function PaymentReceiveFormHeader({ function PaymentReceiveFormHeader({
formik: { errors, touched, setFieldValue, getFieldProps, values }, // #useFormik
errors,
touched,
setFieldValue,
getFieldProps,
values,
onFullAmountChanged,
paymentReceiveId,
customerId,
//#withCustomers //#withCustomers
customers, customers,
//#withAccouts //#withAccouts
accountsList, accountsList,
// #withInvoices
receivableInvoices,
}) { }) {
// Compute the total receivable amount.
const receivableFullAmount = useMemo(
() => sumBy(receivableInvoices, 'due_amount'),
[receivableInvoices],
);
const handleDateChange = useCallback( const handleDateChange = useCallback(
(date_filed) => (date) => { (date_filed) => (date) => {
const formatted = moment(date).format('YYYY-MM-DD'); const formatted = moment(date).format('YYYY-MM-DD');
@@ -82,14 +101,28 @@ function PaymentReceiveFormHeader({
[accountsList], [accountsList],
); );
const triggerFullAmountChanged = (value) => {
onFullAmountChanged && onFullAmountChanged(value);
};
// Handle full amount changed event.
const handleFullAmountBlur = (event) => {
triggerFullAmountChanged(event.currentTarget.value);
};
// Handle link click of receive full amount.
const handleReceiveFullAmountClick = () => {
setFieldValue('full_amount', receivableFullAmount);
triggerFullAmountChanged(receivableFullAmount);
};
return ( return (
<div> <div className={classNames(CLASSES.PAGE_FORM_HEADER)}>
<div> <div className={classNames(CLASSES.PAGE_FORM_HEADER_PRIMARY)}>
{/* Customer name */} {/* ------------- Customer name ------------- */}
<FormGroup <FormGroup
label={<T id={'customer_name'} />} label={<T id={'customer_name'} />}
inline={true} inline={true}
className={classNames('form-group--select-list', Classes.FILL)} className={classNames('form-group--select-list', CLASSES.FILL)}
labelInfo={<FieldRequiredHint />} labelInfo={<FieldRequiredHint />}
intent={errors.customer_id && touched.customer_id && Intent.DANGER} intent={errors.customer_id && touched.customer_id && Intent.DANGER}
helperText={ helperText={
@@ -110,12 +143,12 @@ function PaymentReceiveFormHeader({
/> />
</FormGroup> </FormGroup>
{/* Payment date */} {/* ------------- Payment date ------------- */}
<FormGroup <FormGroup
label={<T id={'payment_date'} />} label={<T id={'payment_date'} />}
inline={true} inline={true}
labelInfo={<FieldRequiredHint />} labelInfo={<FieldRequiredHint />}
className={classNames('form-group--select-list', Classes.FILL)} 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={ helperText={
<ErrorMessage name="payment_date" {...{ errors, touched }} /> <ErrorMessage name="payment_date" {...{ errors, touched }} />
@@ -129,11 +162,39 @@ function PaymentReceiveFormHeader({
/> />
</FormGroup> </FormGroup>
{/* payment receive no */} {/* ------------ Full amount ------------ */}
<FormGroup
label={<T id={'full_amount'} />}
inline={true}
className={('form-group--full-amount', CLASSES.FILL)}
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}
/>
<a onClick={handleReceiveFullAmountClick} href="#" className={'receive-full-amount'}>
Receive full amount (<Money amount={receivableFullAmount} currency={'USD'} />)
</a>
</FormGroup>
{/* ------------ Payment receive no. ------------ */}
<FormGroup <FormGroup
label={<T id={'payment_receive_no'} />} label={<T id={'payment_receive_no'} />}
inline={true} inline={true}
className={('form-group--payment_receive_no', Classes.FILL)} className={('form-group--payment_receive_no', CLASSES.FILL)}
labelInfo={<FieldRequiredHint />} labelInfo={<FieldRequiredHint />}
intent={ intent={
errors.payment_receive_no && errors.payment_receive_no &&
@@ -155,13 +216,13 @@ function PaymentReceiveFormHeader({
/> />
</FormGroup> </FormGroup>
{/* deposit account */} {/* ------------ Deposit account ------------ */}
<FormGroup <FormGroup
label={<T id={'deposit_to'} />} label={<T id={'deposit_to'} />}
className={classNames( className={classNames(
'form-group--deposit_account_id', 'form-group--deposit_account_id',
'form-group--select-list', 'form-group--select-list',
Classes.FILL, CLASSES.FILL,
)} )}
inline={true} inline={true}
labelInfo={<FieldRequiredHint />} labelInfo={<FieldRequiredHint />}
@@ -185,45 +246,22 @@ function PaymentReceiveFormHeader({
selectedAccountId={values.deposit_account_id} selectedAccountId={values.deposit_account_id}
/> />
</FormGroup> </FormGroup>
</div>
{/* Receive amount */} {/* ------------ Reference No. ------------ */}
<FormGroup
{/* <FormGroup label={<T id={'reference'} />}
label={<T id={'receive_amount'} />} inline={true}
inline={true} className={classNames('form-group--reference', CLASSES.FILL)}
labelInfo={<FieldRequiredHint />}
className={classNames('form-group--', Classes.FILL)}
intent={
errors.receive_amount && touched.receive_amount && Intent.DANGER
}
helperText={
<ErrorMessage name="receive_amount" {...{ errors, touched }} />
}
>
<InputGroup
intent={
errors.receive_amount && touched.receive_amount && Intent.DANGER
}
minimal={true}
{...getFieldProps('receive_amount')}
/>
</FormGroup> */}
{/* reference_no */}
<FormGroup
label={<T id={'reference'} />}
inline={true}
className={classNames('form-group--reference', Classes.FILL)}
intent={errors.reference_no && touched.reference_no && Intent.DANGER}
helperText={<ErrorMessage name="reference" {...{ errors, touched }} />}
>
<InputGroup
intent={errors.reference_no && touched.reference_no && Intent.DANGER} intent={errors.reference_no && touched.reference_no && Intent.DANGER}
minimal={true} helperText={<ErrorMessage name="reference" {...{ errors, touched }} />}
{...getFieldProps('reference_no')} >
/> <InputGroup
</FormGroup> intent={errors.reference_no && touched.reference_no && Intent.DANGER}
minimal={true}
{...getFieldProps('reference_no')}
/>
</FormGroup>
</div>
</div> </div>
); );
} }
@@ -235,4 +273,7 @@ export default compose(
withAccounts(({ accountsList }) => ({ withAccounts(({ accountsList }) => ({
accountsList, accountsList,
})), })),
withInvoices(({ paymentReceiveReceivableInvoices }) => ({
receivableInvoices: paymentReceiveReceivableInvoices,
})),
)(PaymentReceiveFormHeader); )(PaymentReceiveFormHeader);

View File

@@ -0,0 +1,88 @@
import React, { useEffect } from 'react';
import { useParams } from 'react-router-dom';
import { useIntl } from 'react-intl';
import { useQuery } from 'react-query';
import { DashboardInsider } from 'components'
// import PaymentReceiveForm from './PaymentReceiveForm';
import withDashboardActions from "containers/Dashboard/withDashboardActions";
import withAccountsActions from 'containers/Accounts/withAccountsActions';
import withSettingsActions from 'containers/Settings/withSettingsActions';
import withPaymentReceivesActions from './withPaymentReceivesActions';
import { compose } from 'utils';
/**
* Payment receive form page.
*/
function PaymentReceiveFormPage({
// #withDashboardAction
changePageTitle,
// #withAccountsActions
requestFetchAccounts,
// #withSettingsActions
requestFetchOptions,
// #withPaymentReceivesActions
requestFetchPaymentReceive
// #withCustomersActions
requestFetchCustomers
}) {
const { id: paymentReceiveId } = useParams();
const { formatMessage } = useIntl();
useEffect(() => {
if (paymentReceiveId) {
changePageTitle(formatMessage({ id: 'edit_payment_receive' }));
} else {
changePageTitle(formatMessage({ id: 'payment_receive' }));
}
}, [changePageTitle, paymentReceiveId, formatMessage]);
// Fetches payment recevie details.
const fetchPaymentReceive = useQuery(
['payment-receive', paymentReceiveId],
(key, _id) => requestFetchPaymentReceive(_id),
{ enabled: paymentReceiveId },
)
// Handle fetch accounts data.
const fetchAccounts = useQuery('accounts-list', (key) =>
requestFetchAccounts(),
);
// Fetch payment made settings.
const fetchSettings = useQuery(['settings'], () => requestFetchOptions({}));
// Fetches customers list.
const fetchCustomers = useQuery(
['customers-list'], () => requestFetchCustomers(),
);
return (
<DashboardInsider
loading={
fetchPaymentReceive.isFetching ||
fetchAccounts.isFetching ||
fetchSettings.isFetching ||
fetchCustomers.isFetching
}>
{/* <PaymentReceiveForm
paymentReceiveId={paymentReceiveId}
/> */}
</DashboardInsider>
)
}
export default compose(
withDashboardActions,
withAccountsActions,
withSettingsActions,
withPaymentReceivesActions,
withCustomersActions,
)(PaymentReceiveFormPage);

View File

@@ -1,258 +1,132 @@
import React, { useState, useMemo, useEffect, useCallback } from 'react'; import React, { useState, useMemo, useEffect, useCallback } from 'react';
import { Button, Intent, Position, Tooltip } from '@blueprintjs/core'; import { Button, Intent, Position, Tooltip } from '@blueprintjs/core';
import { FormattedMessage as T, useIntl } from 'react-intl'; import { FormattedMessage as T } from 'react-intl';
import { Icon, DataTable } from 'components'; import { Icon, CloudLoadingIndicator } from 'components';
import moment from 'moment';
import { useQuery } from 'react-query'; import { useQuery } from 'react-query';
import { useParams, useHistory } from 'react-router-dom'; import { omit } from 'lodash';
import { compose, formattedAmount } from 'utils';
import { compose, formattedAmount, transformUpdatedRows } from 'utils';
import {
InputGroupCell,
MoneyFieldCell,
ItemsListCell,
DivFieldCell,
EmptyDiv,
} from 'components/DataTableCells';
import withInvoices from '../Invoice/withInvoices'; import withInvoices from '../Invoice/withInvoices';
import withInvoiceActions from '../Invoice/withInvoiceActions'; import PaymentReceiveItemsTableEditor from './PaymentReceiveItemsTableEditor';
import withInvoiceActions from 'containers/Sales/Invoice/withInvoiceActions';
import DashboardInsider from 'components/Dashboard/DashboardInsider';
import { useUpdateEffect } from 'hooks';
import { omit, pick } from 'lodash';
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 CellRenderer = (content, type) => (props) => {
if (props.data.length === props.row.index + 1) {
return '';
}
return content(props);
};
const TotalCellRederer = (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);
};
function PaymentReceiveItemsTable({ function PaymentReceiveItemsTable({
//#ownProps // #ownProps
onClickRemoveRow, paymentReceiveId,
onClickAddNewRow, customerId,
fullAmount,
onUpdateData,
paymentEntries = [],// { invoice_id: number, payment_amount: number, id?: number }
errors,
onClickClearAllLines, onClickClearAllLines,
entries,
formik: { errors, setFieldValue, values },
dueInvoices, // #withInvoices
customer_id, paymentReceiveReceivableInvoices,
invoices,
// #withPaymentReceive
receivableInvoices,
// #withPaymentReceiveActions
requestFetchDueInvoices
}) { }) {
const [rows, setRows] = useState([]); const isNewMode = !paymentReceiveId;
const { formatMessage } = useIntl();
const [tableData, setTableData] = useState([]);
const [localAmount, setLocalAmount] = useState(fullAmount);
const computedTableData = useMemo(() => {
const entriesTable = new Map(
paymentEntries.map((e) => [e.invoice_id, e]),
);
return receivableInvoices.map((invoice) => {
const entry = entriesTable.get(invoice.id);
return {
invoice,
id: null,
payment_amount: 0,
...(entry || {}),
};
});
}, [
receivableInvoices,
paymentEntries,
]);
useEffect(() => { useEffect(() => {
setRows([...dueInvoices.map((e) => ({ ...e })), ...invoices, {}]); setTableData(computedTableData);
}, [invoices]); }, [computedTableData]);
// useEffect(() => { // Triggers `onUpdateData` prop event.
// setRows([...dueInvoices.map((e) => ({ ...e })), {}]); const triggerUpdateData = useCallback((entries) => {
const _data = entries.map((entry) => ({
invoice_id: entry?.invoice?.id,
...omit(entry, ['invoice']),
due_amount: entry?.invoice?.due_amount,
}))
onUpdateData && onUpdateData(_data);
}, [onUpdateData]);
// setEntrie([ useEffect(() => {
// ...dueInvoices.map((e) => { if (localAmount !== fullAmount) {
// return { id: e.id, payment_amount: e.payment_amount }; let _fullAmount = fullAmount;
// }),
// ]); const newTableData = tableData.map((data) => {
// }, [dueInvoices]); const amount = Math.min(data?.invoice?.due_amount, _fullAmount);
_fullAmount -= Math.max(amount, 0);
const columns = useMemo( return {
() => [ ...data,
{ payment_amount: amount,
Header: '#', };
accessor: 'index',
Cell: ({ row: { index } }) => <span>{index + 1}</span>,
width: 40,
disableResizing: true,
disableSortBy: true,
},
{
Header: formatMessage({ id: 'Date' }),
id: 'invoice_date',
accessor: (r) => moment(r.invoice_date).format('YYYY MMM DD'),
Cell: CellRenderer(EmptyDiv, 'invoice_date'),
disableSortBy: true,
disableResizing: true,
width: 250,
},
{
Header: formatMessage({ id: 'invocie_number' }),
accessor: (row) => `#${row.invoice_no}`,
Cell: CellRenderer(EmptyDiv, 'invoice_no'),
disableSortBy: true,
className: '',
},
{
Header: formatMessage({ id: 'invoice_amount' }),
accessor: 'balance',
Cell: CellRenderer(DivFieldCell, 'balance'),
disableSortBy: true,
width: 100,
className: '',
},
{
Header: formatMessage({ id: 'amount_due' }),
accessor: 'due_amount',
Cell: TotalCellRederer(DivFieldCell, 'due_amount'),
disableSortBy: true,
width: 150,
className: '',
},
{
Header: formatMessage({ id: 'payment_amount' }),
accessor: 'payment_amount',
Cell: TotalCellRederer(MoneyFieldCell, 'payment_amount'),
disableSortBy: true,
width: 150,
className: '',
},
{
Header: '',
accessor: 'action',
Cell: ActionsCellRenderer,
className: 'actions',
disableSortBy: true,
disableResizing: true,
width: 45,
},
],
[formatMessage],
);
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) => {
return { 'row--total': rows.length === row.index + 1 };
});
const handleUpdateData = useCallback(
(rowIndex, columnId, value) => {
const newRows = rows.map((row, index) => {
if (index === rowIndex) {
return {
...rows[rowIndex],
[columnId]: value,
};
}
return row;
}); });
setRows(newRows); setTableData(newTableData);
setFieldValue( setLocalAmount(fullAmount);
'entries', triggerUpdateData(newTableData);
newRows.map((row) => ({ }
...pick(row, ['payment_amount']), }, [
invoice_id: row.id, fullAmount,
})), localAmount,
); tableData,
}, triggerUpdateData,
[rows, setFieldValue, setRows], ]);
const fetchCustomerDueInvoices = useQuery(
['customer-due-invoices', customerId],
(key, _customerId) => requestFetchDueInvoices(_customerId),
{ enabled: isNewMode && customerId },
); );
// No results message.
const noResultsMessage = (customerId) ?
'There is no receivable invoices for this customer that can be applied for this payment' :
'Please select a customer to display all open invoices for it.';
// Handle update data.
const handleUpdateData = useCallback((rows) => {
triggerUpdateData(rows);
}, []);
console.log(tableData, 'XX');
return ( return (
<div className={'estimate-form__table'}> <CloudLoadingIndicator isLoading={fetchCustomerDueInvoices.isFetching}>
<DataTable <PaymentReceiveItemsTableEditor
columns={columns} noResultsMessage={noResultsMessage}
data={rows} data={tableData}
rowClassNames={rowClassNames} errors={errors}
spinnerProps={false} onUpdateData={handleUpdateData}
payload={{ onClickClearAllLines={onClickClearAllLines}
errors: errors.entries || [],
updateData: handleUpdateData,
removeRow: handleRemoveRow,
}}
/> />
<div className={'mt1'}> </CloudLoadingIndicator>
<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( export default compose(
withInvoices(({ dueInvoices }) => ({ withInvoices(({ paymentReceiveReceivableInvoices }) => ({
dueInvoices, receivableInvoices: paymentReceiveReceivableInvoices,
})), })),
withInvoiceActions,
)(PaymentReceiveItemsTable); )(PaymentReceiveItemsTable);

View File

@@ -0,0 +1,171 @@
import React, { useState, useEffect, useCallback, useMemo } from 'react';
import { Button } from '@blueprintjs/core';
import { FormattedMessage as T, useIntl } from 'react-intl';
import moment from 'moment';
import { sumBy } from 'lodash';
import classNames from 'classnames';
import { CLASSES } from 'common/classes';
import { DataTable, Money } from 'components';
import { transformUpdatedRows } from 'utils';
import {
MoneyFieldCell,
DivFieldCell,
EmptyDiv,
} from 'components/DataTableCells';
/**
* Cell renderer guard.
*/
const CellRenderer = (content, type) => (props) => {
if (props.data.length === props.row.index + 1) {
return '';
}
return content(props);
};
const TotalCellRederer = (content, type) => (props) => {
if (props.data.length === props.row.index + 1) {
return <Money amount={props.cell.row.original[type]} currency={'USD'} />
}
return content(props);
};
export default function PaymentReceiveItemsTableEditor ({
onClickClearAllLines,
onUpdateData,
data,
errors,
noResultsMessage,
}) {
const transformedData = useMemo(() => {
const rows = [ ...data ];
const totalRow = {
due_amount: sumBy(data, 'due_amount'),
payment_amount: sumBy(data, 'payment_amount'),
};
if (rows.length > 0) { rows.push(totalRow) }
return rows;
}, [data]);
const [localData, setLocalData] = useState(transformedData);
const { formatMessage } = useIntl();
useEffect(() => {
if (localData !== transformedData) {
setLocalData(transformedData);
}
}, [setLocalData, localData, transformedData]);
const columns = useMemo(
() => [
{
Header: '#',
accessor: 'index',
Cell: ({ row: { index } }) => <span>{index + 1}</span>,
width: 40,
disableResizing: true,
disableSortBy: true,
},
{
Header: formatMessage({ id: 'Date' }),
id: 'invoice.invoice_date',
accessor: (r) => moment(r.invoice_date).format('YYYY MMM DD'),
Cell: CellRenderer(EmptyDiv, 'invoice_date'),
disableSortBy: true,
disableResizing: true,
width: 250,
},
{
Header: formatMessage({ id: 'invocie_number' }),
accessor: (row) => `#${row?.invoice?.invoice_no}`,
Cell: CellRenderer(EmptyDiv, 'invoice_no'),
disableSortBy: true,
className: '',
},
{
Header: formatMessage({ id: 'invoice_amount' }),
accessor: 'invoice.balance',
Cell: CellRenderer(DivFieldCell, 'balance'),
disableSortBy: true,
width: 100,
className: '',
},
{
Header: formatMessage({ id: 'amount_due' }),
accessor: 'invoice.due_amount',
Cell: TotalCellRederer(DivFieldCell, 'due_amount'),
disableSortBy: true,
width: 150,
className: '',
},
{
Header: formatMessage({ id: 'payment_amount' }),
accessor: 'payment_amount',
Cell: TotalCellRederer(MoneyFieldCell, 'payment_amount'),
disableSortBy: true,
width: 150,
className: '',
},
],
[formatMessage],
);
// Handle click clear all lines button.
const handleClickClearAllLines = () => {
onClickClearAllLines && onClickClearAllLines();
};
const rowClassNames = useCallback(
(row) => ({ 'row--total': localData.length === row.index + 1 }),
[localData],
);
// Handle update data.
const handleUpdateData = useCallback(
(rowIndex, columnId, value) => {
const newRows = transformUpdatedRows(
localData,
rowIndex,
columnId,
value,
);
if (newRows.length > 0) {
newRows.splice(-1, 1);
}
setLocalData(newRows);
onUpdateData && onUpdateData(newRows);
},
[localData, setLocalData, onUpdateData],
);
return (
<div className={classNames(
CLASSES.DATATABLE_EDITOR,
CLASSES.DATATABLE_EDITOR_ITEMS_ENTRIES,
)}>
<DataTable
columns={columns}
data={localData}
rowClassNames={rowClassNames}
spinnerProps={false}
payload={{
errors,
updateData: handleUpdateData,
}}
noResults={noResultsMessage}
/>
<div className={classNames(CLASSES.DATATABLE_EDITOR_ACTIONS, 'mt1')}>
<Button
small={true}
className={'button--secondary button--clear-lines'}
onClick={handleClickClearAllLines}
>
<T id={'clear_all_lines'} />
</Button>
</div>
</div>
);
}

View File

@@ -87,4 +87,18 @@ export const getPayableBillsByPaymentMadeFactory = () =>
? pickItemsFromIds(billsItems, payableBillsIds) || [] ? pickItemsFromIds(billsItems, payableBillsIds) || []
: []; : [];
}, },
);
export const getPaymentMadeFormPayableBillsFactory = () =>
createSelector(
billItemsSelector,
billsPayableVendorSelector,
billsPayableByPaymentMadeSelector,
(billsItems, vendorBillsIds, paymentMadeBillsIds) => {
const billsIds = [
...(vendorBillsIds || []),
...(paymentMadeBillsIds || [])
];
return pickItemsFromIds(billsItems, billsIds);
},
); );

View File

@@ -121,26 +121,32 @@ export const fetchInvoice = ({ id }) => {
}); });
}); });
}; };
export const dueInvoices = ({ id }) => {
return (dispatch) => export const fetchDueInvoices = ({ customerId }) => (dispatch) => new Promise((resovle, reject) => {
new Promise((resovle, reject) => { ApiService.get(`sales/invoices/payable`, {
ApiService.get(`sales/invoices/due_invoices`, { params: { customer_id: customerId },
params: { customer_id: id }, })
}) .then((response) => {
.then((response) => { dispatch({
dispatch({ type: t.INVOICES_ITEMS_SET,
type: t.DUE_INVOICES_SET, payload: {
payload: { sales_invoices: response.data.sales_invoices,
customer_id: id, },
due_sales_invoices: response.data.due_sales_invoices, });
}, if (customerId) {
}); dispatch({
resovle(response); type: t.INVOICES_RECEIVABLE_BY_CUSTOMER_ID,
}) payload: {
.catch((error) => { customerId,
const { response } = error; saleInvoices: response.data.sales_invoices,
const { data } = response; },
reject(data?.errors);
}); });
}
resovle(response);
})
.catch((error) => {
const { response } = error;
const { data } = response;
reject(data?.errors);
}); });
}; });

View File

@@ -13,6 +13,10 @@ const initialState = {
page: 1, page: 1,
}, },
dueInvoices: {}, dueInvoices: {},
receivable: {
byCustomerId: [],
byPaymentReceiveId: [],
}
}; };
const defaultInvoice = { const defaultInvoice = {
@@ -97,39 +101,19 @@ const reducer = createReducer(initialState, {
}, },
}; };
}, },
[t.DUE_INVOICES_SET]: (state, action) => {
const { customer_id, due_sales_invoices } = action.payload;
const _dueInvoices = []; [t.INVOICES_RECEIVABLE_BY_PAYMENT_ID]: (state, action) => {
const { paymentReceiveId, saleInvoices } = action.payload;
const saleInvoicesIds = saleInvoices.map((saleInvoice) => saleInvoice.id);
state.dueInvoices[customer_id] = due_sales_invoices.map((due) => due.id); state.receivable.byPaymentReceiveId[paymentReceiveId] = saleInvoicesIds;
const _invoices = {};
due_sales_invoices.forEach((invoice) => {
_invoices[invoice.id] = {
...invoice,
};
});
state.items = {
...state.dueInvoices,
...state.items.dueInvoices,
..._invoices,
};
}, },
[t.RELOAD_INVOICES]: (state, action) => {
const { sales_invoices } = action.payload;
const _sales_invoices = {}; [t.INVOICES_RECEIVABLE_BY_CUSTOMER_ID]: (state, action) => {
sales_invoices.forEach((invoice) => { const { customerId, saleInvoices } = action.payload;
_sales_invoices[invoice.id] = { const saleInvoiceIds = saleInvoices.map((saleInvoice) => saleInvoice.id);
...invoice,
};
});
state.items = { state.receivable.byCustomerId[customerId] = saleInvoiceIds
...state.items,
..._sales_invoices,
};
}, },
}); });

View File

@@ -22,6 +22,10 @@ const invoicesPageSelector = (state, props, query) => {
const invoicesItemsSelector = (state) => state.salesInvoices.items; const invoicesItemsSelector = (state) => state.salesInvoices.items;
const invoicesReceiableCustomerSelector = (state, props) => state.salesInvoices.receivable.byCustomerId[props.customerId];
const paymentReceivableInvoicesSelector = (state, props) => state.salesInvoices.receivable.byPaymentReceiveId[props.paymentReceiveId];
export const getInvoiceTableQueryFactory = () => export const getInvoiceTableQueryFactory = () =>
createSelector( createSelector(
paginationLocationQuery, paginationLocationQuery,
@@ -55,17 +59,39 @@ export const getInvoicePaginationMetaFactory = () =>
return invoicePage?.paginationMeta || {}; return invoicePage?.paginationMeta || {};
}); });
const dueInvoicesSelector = (state, props) => { // export const getCustomerReceivableInvoicesFactory = () =>
return state.salesInvoices.dueInvoices[props.customer_id] || []; // createSelector(
}; // invoicesItemsSelector,
// invoicesReceiableCustomerSelector,
// (invoicesItems, invoicesIds) => {
// return Array.isArray(invoicesIds)
// ? (pickItemsFromIds(invoicesItems, invoicesIds) || [])
// : [];
// },
// );
export const getdueInvoices = createSelector( // export const getPaymentReceivableInvoicesFactory = () =>
dueInvoicesSelector, // createSelector(
invoicesItemsSelector, // invoicesItemsSelector,
(customerIds, items) => { // paymentReceivableInvoicesSelector,
// (invoicesItems, invoicesIds) => {
return typeof customerIds === 'object' // return Array.isArray(invoicesIds)
? pickItemsFromIds(items, customerIds) || [] // ? (pickItemsFromIds(invoicesItems, invoicesIds) || [])
: []; // : [];
}, // },
); // );
export const getPaymentReceiveReceivableInvoicesFactory = () =>
createSelector(
invoicesItemsSelector,
invoicesReceiableCustomerSelector,
paymentReceivableInvoicesSelector,
(invoicesItems, customerInvoicesIds, paymentInvoicesIds) => {
const invoicesIds = [
...(customerInvoicesIds || []),
...(paymentInvoicesIds || []),
];
return pickItemsFromIds(invoicesItems, invoicesIds);
},
);

View File

@@ -11,4 +11,7 @@ export default {
INVOICES_ITEMS_SET: 'INVOICES_ITEMS_SET', INVOICES_ITEMS_SET: 'INVOICES_ITEMS_SET',
DUE_INVOICES_SET: 'DUE_INVOICES_SET', DUE_INVOICES_SET: 'DUE_INVOICES_SET',
RELOAD_INVOICES: 'RELOAD_INVOICES', RELOAD_INVOICES: 'RELOAD_INVOICES',
INVOICES_RECEIVABLE_BY_PAYMENT_ID: 'INVOICES_RECEIVABLE_BY_PAYMENT_ID',
INVOICES_RECEIVABLE_BY_CUSTOMER_ID: 'INVOICES_RECEIVABLE_BY_CUSTOMER_ID'
}; };

View File

@@ -52,28 +52,29 @@ export const fetchPaymentReceive = ({ id }) => {
new Promise((resovle, reject) => { new Promise((resovle, reject) => {
ApiService.get(`sales/payment_receives/${id}`, {}) ApiService.get(`sales/payment_receives/${id}`, {})
.then((response) => { .then((response) => {
dispatch({
type: t.RELOAD_INVOICES,
payload: {
sales_invoices: response.data.paymentReceive.entries.map(
(e) => e.invoice,
),
},
});
dispatch({ dispatch({
type: t.PAYMENT_RECEIVE_SET, type: t.PAYMENT_RECEIVE_SET,
payload: { payload: {
id, id,
paymentReceive: response.data.paymentReceive, paymentReceive: response.data.paymentReceive,
},
});
dispatch({
type: t.INVOICES_ITEMS_SET,
payload: {
sales_invoices: response.data.sale_invoice.receivable_invoices,
},
});
dispatch({
type: t.INVOICES_RECEIVABLE_BY_PAYMENT_ID,
payload: {
paymentReceiveid: response.data.id,
saleInvoices: response.data.sale_invoice.receivable_invoices,
}, },
}); });
resovle(response); resovle(response);
}) })
.catch((error) => { .catch((error) => {
// const { response } = error;
// const { data } = response;
// reject(data?.errors);
reject(error); reject(error);
}); });
}); });

View File

@@ -71,6 +71,7 @@ $pt-font-family: Noto Sans, -apple-system, BlinkMacSystemFont, Segoe UI, Roboto,
@import 'pages/invoice-form'; @import 'pages/invoice-form';
@import 'pages/receipt-form'; @import 'pages/receipt-form';
@import 'pages/payment-made'; @import 'pages/payment-made';
@import 'pages/payment-receive';
// Views // Views
@import 'views/filter-dropdown'; @import 'views/filter-dropdown';

View File

@@ -0,0 +1,61 @@
.page-form--payment-receive {
$self: '.page-form';
#{$self}__header{
.bp3-label{
min-width: 160px;
}
.bp3-form-content{
width: 100%;
}
.bp3-form-group{
margin-bottom: 18px;
&.bp3-inline{
max-width: 470px;
}
a.receive-full-amount{
font-size: 12px;
margin-top: 6px;
display: inline-block;
}
}
}
#{$self}__primary-section{
padding-bottom: 2px;
}
.datatable-editor{
.table .tbody{
.tr.no-results{
.td{
border-bottom: 1px solid #e2e2e2;
font-size: 15px;
padding: 26px 0;
color: #5a5a77;
}
}
}
.table{
.tr{
.td:first-of-type,
.th:first-of-type{
span, div{
margin-left: auto;
margin-right: auto;
}
}
}
}
}
#{$self}__footer{
}
}

View File

@@ -34,8 +34,9 @@ export default class PaymentReceivesController extends BaseController {
this.handleServiceErrors, this.handleServiceErrors,
); );
router.post( router.post(
'/', '/', [
this.newPaymentReceiveValidation, ...this.newPaymentReceiveValidation,
],
this.validationResult, this.validationResult,
asyncMiddleware(this.newPaymentReceive.bind(this)), asyncMiddleware(this.newPaymentReceive.bind(this)),
this.handleServiceErrors, this.handleServiceErrors,
@@ -87,6 +88,7 @@ export default class PaymentReceivesController extends BaseController {
check('entries').isArray({ min: 1 }), check('entries').isArray({ min: 1 }),
check('entries.*.id').optional({ nullable: true }).isNumeric().toInt(),
check('entries.*.invoice_id').exists().isNumeric().toInt(), check('entries.*.invoice_id').exists().isNumeric().toInt(),
check('entries.*.payment_amount').exists().isNumeric().toInt(), check('entries.*.payment_amount').exists().isNumeric().toInt(),
]; ];

View File

@@ -1,4 +1,4 @@
import { omit, sumBy, chain, difference } from 'lodash'; import { omit, sumBy, difference } from 'lodash';
import moment from 'moment'; import moment from 'moment';
import { Service, Inject } from 'typedi'; import { Service, Inject } from 'typedi';
import { import {
@@ -26,7 +26,6 @@ import { formatDateFields, entriesAmountDiff } from 'utils';
import { ServiceError } from 'exceptions'; import { ServiceError } from 'exceptions';
import CustomersService from 'services/Contacts/CustomersService'; import CustomersService from 'services/Contacts/CustomersService';
import ItemsEntriesService from 'services/Items/ItemsEntriesService'; import ItemsEntriesService from 'services/Items/ItemsEntriesService';
import { SaleInvoice } from 'models';
const ERRORS = { const ERRORS = {
PAYMENT_RECEIVE_NO_EXISTS: 'PAYMENT_RECEIVE_NO_EXISTS', PAYMENT_RECEIVE_NO_EXISTS: 'PAYMENT_RECEIVE_NO_EXISTS',
@@ -327,16 +326,30 @@ export default class PaymentReceiveService {
* @param {number} tenantId - Tenant id. * @param {number} tenantId - Tenant id.
* @param {Integer} paymentReceiveId - Payment receive id. * @param {Integer} paymentReceiveId - Payment receive id.
*/ */
public async getPaymentReceive(tenantId: number, paymentReceiveId: number) { public async getPaymentReceive(
const { PaymentReceive } = this.tenancy.models(tenantId); tenantId: number,
paymentReceiveId: number
): Promise<{ paymentReceive: IPaymentReceive[], receivableInvoices: ISaleInvoice }> {
const { PaymentReceive, SaleInvoice } = this.tenancy.models(tenantId);
const paymentReceive = await PaymentReceive.query() const paymentReceive = await PaymentReceive.query()
.findById(paymentReceiveId) .findById(paymentReceiveId)
.withGraphFetched('entries.invoice'); .withGraphFetched('entries')
.withGraphFetched('customer')
.withGraphFetched('depositAccount');
if (!paymentReceive) { if (!paymentReceive) {
throw new ServiceError(ERRORS.PAYMENT_RECEIVE_NOT_EXISTS); throw new ServiceError(ERRORS.PAYMENT_RECEIVE_NOT_EXISTS);
} }
return paymentReceive; // Receivable open invoices.
const receivableInvoices = await SaleInvoice.query().onBuild((builder) => {
const invoicesIds = paymentReceive.entries.map((entry) => entry.invoiceId);
builder.where('customer_id', paymentReceive.customerId);
builder.orWhereIn('id', invoicesIds);
builder.orderByRaw(`FIELD(id, ${invoicesIds.join(', ')}) DESC`);
builder.orderBy('invoice_date', 'ASC');
});
return { paymentReceive, receivableInvoices };
} }
/** /**