From e6cd921b94336bf96819334fb1159751bb71411a Mon Sep 17 00:00:00 2001 From: Ahmed Bouhuolia Date: Mon, 2 Nov 2020 21:42:40 +0200 Subject: [PATCH] feat: payment receive of customers invoices. --- client/src/common/classes.js | 1 + .../containers/Purchases/Bill/withBills.js | 8 +- .../Purchases/PaymentMades/PaymentMadeForm.js | 4 - .../PaymentMades/PaymentMadeFormHeader.js | 10 +- .../PaymentMades/PaymentMadeItemsTable.js | 38 +- .../PaymentMadeItemsTableEditor.js | 10 +- .../Sales/Invoice/withInvoiceActions.js | 4 +- .../containers/Sales/Invoice/withInvoices.js | 13 +- .../PaymentReceiveFloatingActions.js | 52 +-- .../PaymentReceive/PaymentReceiveForm.js | 399 +++++++++--------- .../PaymentReceiveFormHeader.js | 147 ++++--- .../PaymentReceive/PaymentReceiveFormPage.js | 88 ++++ .../PaymentReceiveItemsTable.js | 336 +++++---------- .../PaymentReceiveItemsTableEditor.js | 171 ++++++++ client/src/store/Bills/bills.selectors.js | 14 + client/src/store/Invoice/invoices.actions.js | 48 ++- client/src/store/Invoice/invoices.reducer.js | 40 +- client/src/store/Invoice/invoices.selector.js | 52 ++- client/src/store/Invoice/invoices.types.js | 3 + .../PaymentReceive/paymentReceive.actions.js | 25 +- client/src/style/App.scss | 1 + client/src/style/pages/payment-receive.scss | 61 +++ .../api/controllers/Sales/PaymentReceives.ts | 6 +- server/src/services/Sales/PaymentsReceives.ts | 25 +- 24 files changed, 911 insertions(+), 645 deletions(-) create mode 100644 client/src/containers/Sales/PaymentReceive/PaymentReceiveFormPage.js create mode 100644 client/src/containers/Sales/PaymentReceive/PaymentReceiveItemsTableEditor.js create mode 100644 client/src/style/pages/payment-receive.scss diff --git a/client/src/common/classes.js b/client/src/common/classes.js index e3fd2ed82..f6112b1a8 100644 --- a/client/src/common/classes.js +++ b/client/src/common/classes.js @@ -16,6 +16,7 @@ const CLASSES = { PAGE_FORM_INVOICE: 'page-form--invoice', PAGE_FORM_RECEIPT: 'page-form--receipt', PAGE_FORM_PAYMENT_MADE: 'page-form--payment-made', + PAGE_FORM_PAYMENT_RECEIVE: 'page-form--payment-receive', CLOUD_SPINNER: 'cloud-spinner', IS_LOADING: 'is-loading', diff --git a/client/src/containers/Purchases/Bill/withBills.js b/client/src/containers/Purchases/Bill/withBills.js index 1182f2b1c..b3c4729f1 100644 --- a/client/src/containers/Purchases/Bill/withBills.js +++ b/client/src/containers/Purchases/Bill/withBills.js @@ -5,7 +5,8 @@ import { getBillPaginationMetaFactory, getBillTableQueryFactory, getVendorPayableBillsFactory, - getPayableBillsByPaymentMadeFactory + getPayableBillsByPaymentMadeFactory, + getPaymentMadeFormPayableBillsFactory } from 'store/Bills/bills.selectors'; export default (mapState) => { @@ -14,6 +15,7 @@ export default (mapState) => { const getBillTableQuery = getBillTableQueryFactory(); const getVendorPayableBills = getVendorPayableBillsFactory(); const getPayableBillsByPaymentMade = getPayableBillsByPaymentMadeFactory(); + const getPaymentMadeFormPayableBills = getPaymentMadeFormPayableBillsFactory(); const mapStateToProps = (state, props) => { const tableQuery = getBillTableQuery(state, props); @@ -28,8 +30,8 @@ export default (mapState) => { billsLoading: state.bills.loading, nextBillNumberChanged: state.bills.nextBillNumberChanged, - vendorPayableBills: getVendorPayableBills(state, props), - paymentMadePayableBills: getPayableBillsByPaymentMade(state, props), + // vendorPayableBills: getVendorPayableBills(state, props), + paymentMadePayableBills: getPaymentMadeFormPayableBills(state, props), }; return mapState ? mapState(mapped, state, props) : mapped; }; diff --git a/client/src/containers/Purchases/PaymentMades/PaymentMadeForm.js b/client/src/containers/Purchases/PaymentMades/PaymentMadeForm.js index 7796c0f5b..d23cfa486 100644 --- a/client/src/containers/Purchases/PaymentMades/PaymentMadeForm.js +++ b/client/src/containers/Purchases/PaymentMades/PaymentMadeForm.js @@ -203,12 +203,10 @@ function PaymentMadeForm({ }, [fullAmount, setAmountChangeAlert], ); - // Handle cancel button of amount change alert. const handleCancelAmountChangeAlert = () => { setAmountChangeAlert(false); }; - // Handle confirm button of amount change alert. const handleConfirmAmountChangeAlert = () => { setFullAmount(amountChangeAlert); @@ -286,7 +284,6 @@ function PaymentMadeForm({ values={values} onFullAmountChanged={handleFullAmountChange} /> - - } confirmButtonText={} diff --git a/client/src/containers/Purchases/PaymentMades/PaymentMadeFormHeader.js b/client/src/containers/Purchases/PaymentMades/PaymentMadeFormHeader.js index 7b48e2b18..7ceda4763 100644 --- a/client/src/containers/Purchases/PaymentMades/PaymentMadeFormHeader.js +++ b/client/src/containers/Purchases/PaymentMades/PaymentMadeFormHeader.js @@ -51,13 +51,13 @@ function PaymentMadeFormHeader({ accountsList, // #withBills - vendorPayableBills, + paymentMadePayableBills, }) { const isNewMode = !paymentMadeId; const payableFullAmount = useMemo( - () => sumBy(vendorPayableBills, 'due_amount'), - [vendorPayableBills], + () => sumBy(paymentMadePayableBills, 'due_amount'), + [paymentMadePayableBills], ); const handleDateChange = useCallback( @@ -276,7 +276,7 @@ export default compose( withAccounts(({ accountsList }) => ({ accountsList, })), - withBills(({ vendorPayableBills }) => ({ - vendorPayableBills, + withBills(({ paymentMadePayableBills }) => ({ + paymentMadePayableBills, })), )(PaymentMadeFormHeader); diff --git a/client/src/containers/Purchases/PaymentMades/PaymentMadeItemsTable.js b/client/src/containers/Purchases/PaymentMades/PaymentMadeItemsTable.js index 18a577fa8..3d323aa84 100644 --- a/client/src/containers/Purchases/PaymentMades/PaymentMadeItemsTable.js +++ b/client/src/containers/Purchases/PaymentMades/PaymentMadeItemsTable.js @@ -1,5 +1,6 @@ import React, { useState, useEffect, useMemo, useCallback } from 'react'; import { useQuery } from 'react-query'; +import { omit } from 'lodash'; import { CloudLoadingIndicator } from 'components' import PaymentMadeItemsTableEditor from './PaymentMadeItemsTableEditor'; @@ -26,7 +27,6 @@ function PaymentMadeItemsTable({ requestFetchDueBills, // #withBills - vendorPayableBills, paymentMadePayableBills, // #withPaymentMadeDetail @@ -35,20 +35,14 @@ function PaymentMadeItemsTable({ const [tableData, setTableData] = useState([]); 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 triggerUpdateData = useCallback((data) => { - onUpdateData && onUpdateData(data); + const triggerUpdateData = useCallback((entries) => { + const _data = entries.map((entry) => ({ + bill_id: entry?.bill?.id, + ...omit(entry, ['bill']), + })) + onUpdateData && onUpdateData(_data); }, [onUpdateData]); // Merges payment entries with payable bills. @@ -56,17 +50,16 @@ function PaymentMadeItemsTable({ const entriesTable = new Map( paymentEntries.map((e) => [e.bill_id, e]), ); - return payableBills.map((bill) => { + return paymentMadePayableBills.map((bill) => { const entry = entriesTable.get(bill.id); return { - ...bill, - bill_id: bill.id, - bill_payment_amount: bill.payment_amount, - payment_amount: entry ? entry.payment_amount : 0, - id: entry ? entry.id : null, + bill, + id: null, + payment_number: 0, + ...(entry || {}), } }); - }, [paymentEntries, payableBills]); + }, [paymentEntries, paymentMadePayableBills]); useEffect(() => { setTableData(computedTableData); @@ -127,11 +120,10 @@ function PaymentMadeItemsTable({ ); } -export default compose( +export default compose( withPaymentMadeActions, withBillActions, - withBills(({ vendorPayableBills, paymentMadePayableBills }) => ({ - vendorPayableBills, + withBills(({ paymentMadePayableBills }) => ({ paymentMadePayableBills, })), )(PaymentMadeItemsTable); diff --git a/client/src/containers/Purchases/PaymentMades/PaymentMadeItemsTableEditor.js b/client/src/containers/Purchases/PaymentMades/PaymentMadeItemsTableEditor.js index 7c0fd1215..38860402f 100644 --- a/client/src/containers/Purchases/PaymentMades/PaymentMadeItemsTableEditor.js +++ b/client/src/containers/Purchases/PaymentMades/PaymentMadeItemsTableEditor.js @@ -74,27 +74,27 @@ export default function PaymentMadeItemsTableEditor({ { Header: formatMessage({ id: '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'), disableSortBy: true, }, { Header: formatMessage({ id: 'bill_number' }), - accessor: (row) => `#${row.bill_number}`, + accessor: (row) => `#${row.bill?.bill_number}`, Cell: CellRenderer(EmptyDiv, 'bill_number'), disableSortBy: true, className: 'bill_number', }, { Header: formatMessage({ id: 'bill_amount' }), - accessor: 'amount', + accessor: r => r.bill?.amount, Cell: CellRenderer(DivFieldCell, 'amount'), disableSortBy: true, className: '', }, { Header: formatMessage({ id: 'amount_due' }), - accessor: 'due_amount', + accessor: r => r.bill?.due_amount, Cell: TotalCellRederer(DivFieldCell, 'due_amount'), disableSortBy: true, className: '', @@ -129,6 +129,8 @@ export default function PaymentMadeItemsTableEditor({ columnId, value, ); + newRows.splice(-1,1); // removes the total row. + setLocalData(newRows); onUpdateData && onUpdateData(newRows); }, diff --git a/client/src/containers/Sales/Invoice/withInvoiceActions.js b/client/src/containers/Sales/Invoice/withInvoiceActions.js index dea23cce0..15a5b8a9e 100644 --- a/client/src/containers/Sales/Invoice/withInvoiceActions.js +++ b/client/src/containers/Sales/Invoice/withInvoiceActions.js @@ -5,7 +5,7 @@ import { deleteInvoice, fetchInvoice, fetchInvoicesTable, - dueInvoices, + fetchDueInvoices, } from 'store/Invoice/invoices.actions'; import t from 'store/types'; @@ -16,7 +16,7 @@ const mapDipatchToProps = (dispatch) => ({ requestFetchInvoiceTable: (query = {}) => dispatch(fetchInvoicesTable({ query: { ...query } })), requestDeleteInvoice: (id) => dispatch(deleteInvoice({ id })), - requestFetchDueInvoices: (id) => dispatch(dueInvoices({ id })), + requestFetchDueInvoices: (customerId) => dispatch(fetchDueInvoices({ customerId })), changeInvoiceView: (id) => dispatch({ type: t.INVOICES_SET_CURRENT_VIEW, diff --git a/client/src/containers/Sales/Invoice/withInvoices.js b/client/src/containers/Sales/Invoice/withInvoices.js index 29eb45d58..49ec76389 100644 --- a/client/src/containers/Sales/Invoice/withInvoices.js +++ b/client/src/containers/Sales/Invoice/withInvoices.js @@ -4,8 +4,9 @@ import { getInvoiceCurrentPageFactory, getInvoicePaginationMetaFactory, getInvoiceTableQueryFactory, - getInvoiceTableQuery, - getdueInvoices, + getCustomerReceivableInvoicesFactory, + getPaymentReceivableInvoicesFactory, + getPaymentReceiveReceivableInvoicesFactory } from 'store/Invoice/invoices.selector'; export default (mapState) => { @@ -14,6 +15,11 @@ export default (mapState) => { const getInvoicesPaginationMeta = getInvoicePaginationMetaFactory(); const getInvoiceTableQuery = getInvoiceTableQueryFactory(); + // const getPaymentReceivableInvoices = getPaymentReceivableInvoicesFactory(); + // const getCustomerReceivableInvoices = getCustomerReceivableInvoicesFactory(); + + const getPaymentReceiveReceivableInvoices = getPaymentReceiveReceivableInvoicesFactory(); + const mapStateToProps = (state, props) => { const query = getInvoiceTableQuery(state, props); @@ -24,7 +30,8 @@ export default (mapState) => { invoicesTableQuery: query, invoicesPageination: getInvoicesPaginationMeta(state, props, query), invoicesLoading: state.salesInvoices.loading, - dueInvoices: getdueInvoices(state, props), + + paymentReceiveReceivableInvoices: getPaymentReceiveReceivableInvoices(state, props), }; return mapState ? mapState(mapped, state, props) : mapped; }; diff --git a/client/src/containers/Sales/PaymentReceive/PaymentReceiveFloatingActions.js b/client/src/containers/Sales/PaymentReceive/PaymentReceiveFloatingActions.js index be0ef4089..0dcf50020 100644 --- a/client/src/containers/Sales/PaymentReceive/PaymentReceiveFloatingActions.js +++ b/client/src/containers/Sales/PaymentReceive/PaymentReceiveFloatingActions.js @@ -1,29 +1,41 @@ import React from 'react'; import { Intent, Button } from '@blueprintjs/core'; import { FormattedMessage as T } from 'react-intl'; +import classNames from 'classnames'; -export default function PaymentReceiveFormFooter({ - formik: { isSubmitting, resetForm }, +import { CLASSES } from 'common/classes'; + +/** + * Payment receive floating actions bar. + */ +export default function PaymentReceiveFormFloatingActions({ + isSubmitting, onSubmitClick, onCancelClick, onClearClick, - paymentReceive, + paymentReceiveId, }) { + const handleSubmitClick = (event) => { + onSubmitClick && onSubmitClick(event); + }; + + const handleClearBtnClick = (event) => { + onClearClick && onClearClick(event); + }; + + const handleCloseBtnClick = (event) => { + onCancelClick && onCancelClick(event); + }; + return ( -
+
@@ -42,18 +52,12 @@ export default function PaymentReceiveFormFooter({ -
diff --git a/client/src/containers/Sales/PaymentReceive/PaymentReceiveForm.js b/client/src/containers/Sales/PaymentReceive/PaymentReceiveForm.js index 02eb07d85..366555a59 100644 --- a/client/src/containers/Sales/PaymentReceive/PaymentReceiveForm.js +++ b/client/src/containers/Sales/PaymentReceive/PaymentReceiveForm.js @@ -9,89 +9,41 @@ import React, { import * as Yup from 'yup'; import { useFormik } from 'formik'; 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 { 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 PaymentReceiveItemsTable from './PaymentReceiveItemsTable'; import PaymentReceiveFloatingActions from './PaymentReceiveFloatingActions'; -import withDashboardActions from 'containers/Dashboard/withDashboardActions'; import withMediaActions from 'containers/Media/withMediaActions'; import withPaymentReceivesActions from './withPaymentReceivesActions'; -import withInvoices from '../Invoice/withInvoices'; import withPaymentReceiveDetail from './withPaymentReceiveDetail'; -import withPaymentReceives from './withPaymentReceives'; import { AppToaster } from 'components'; -import Dragzone from 'components/Dragzone'; -import useMedia from 'hooks/useMedia'; -import { compose, repeatValue } from 'utils'; - -const MIN_LINES_NUMBER = 5; +import { compose } from 'utils'; function PaymentReceiveForm({ - //#withMedia - requestSubmitMedia, - requestDeleteMedia, + // #ownProps + paymentReceiveId, //#WithPaymentReceiveActions requestSubmitPaymentReceive, requestEditPaymentReceive, - //#withDashboard - changePageTitle, - changePageSubtitle, - - //#withPaymentReceiveDetail + // #withPaymentReceive 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 [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({ customer_id: Yup.string() .label(formatMessage({ id: 'customer_name_' })) @@ -102,9 +54,7 @@ function PaymentReceiveForm({ deposit_account_id: Yup.number() .required() .label(formatMessage({ id: 'deposit_account_' })), - // receive_amount: Yup.number() - // .required() - // .label(formatMessage({ id: 'receive_amount_' })), + full_amount: Yup.number().nullable(), payment_receive_no: Yup.number() .required() .label(formatMessage({ id: 'payment_receive_no_' })), @@ -112,11 +62,9 @@ function PaymentReceiveForm({ description: Yup.string().nullable(), entries: Yup.array().of( Yup.object().shape({ - payment_amount: Yup.number().nullable(), - invoice_no: Yup.number().nullable(), - balance: Yup.number().nullable(), + id: 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() .nullable() .when(['payment_amount'], { @@ -126,15 +74,7 @@ function PaymentReceiveForm({ }), ), }); - - const handleDropFiles = useCallback((_files) => { - setFiles(_files.filter((file) => file.uploaded === false)); - }, []); - - const savePaymentReceiveSubmit = useCallback((payload) => { - onFormSubmit && onFormSubmit(payload); - }); - + // Default payment receive. const defaultPaymentReceive = useMemo( () => ({ invoice_id: '', @@ -146,6 +86,12 @@ function PaymentReceiveForm({ }), [], ); + const defaultPaymentReceiveEntry = { + id: null, + payment_amount: null, + invoice_id: null, + }; + // Form initial values. const defaultInitialValues = useMemo( () => ({ customer_id: '', @@ -153,20 +99,13 @@ function PaymentReceiveForm({ payment_date: moment(new Date()).format('YYYY-MM-DD'), reference_no: '', payment_receive_no: '', - // receive_amount: '', description: '', - entries: [...repeatValue(defaultPaymentReceive, MIN_LINES_NUMBER)], + entries: [], }), - [defaultPaymentReceive], + [], ); - const orderingIndex = (_entries) => { - return _entries.map((item, index) => ({ - ...item, - index: index + 1, - })); - }; - + // Form initial values. const initialValues = useMemo( () => ({ ...(paymentReceive @@ -176,167 +115,205 @@ function PaymentReceiveForm({ ...paymentReceive.entries.map((paymentReceive) => ({ ...pick(paymentReceive, Object.keys(defaultPaymentReceive)), })), - ...repeatValue( - defaultPaymentReceive, - Math.max(MIN_LINES_NUMBER - paymentReceive.entries.length, 0), - ), ], } : { ...defaultInitialValues, - entries: orderingIndex(defaultInitialValues.entries), }), }), [paymentReceive, defaultInitialValues, defaultPaymentReceive], ); - const initialAttachmentFiles = useMemo(() => { - return paymentReceive && paymentReceive.media - ? paymentReceive.media.map((attach) => ({ - preview: attach.attachment_file, - uploaded: true, - metadata: { ...attach }, - })) - : []; - }, [paymentReceive]); + // Handle form submit. + const handleSubmitForm = ( + values, + { setSubmitting, resetForm, setFieldError }, + ) => { + setSubmitting(true); - 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, validationSchema, initialValues: { ...initialValues, }, - onSubmit: async (values, { setSubmitting, setErrors, resetForm }) => { - 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); - }); - } - }, + onSubmit: handleSubmitForm, }); - const handleDeleteFile = useCallback( - (_deletedFiles) => { - _deletedFiles.forEach((deletedFile) => { - if (deletedFile.upload && deletedFile.metadata.id) { - setDeletedFiles([...deletedFiles, deletedFile.metadata.id]); - } - }); + // Handle update data. + const handleUpdataData = useCallback( + (entries) => { + setFieldValue('entries', entries); }, - [setDeletedFiles, deletedFiles], + [setFieldValue], ); - const handleSubmitClick = useCallback( - (payload) => { - setPayload(payload); - formik.submitForm(); + const handleFullAmountChange = useCallback( + (value) => { + if (value !== fullAmount) { + setAmountChangeAlert(value); + } }, - [setPayload, formik], + [fullAmount, setAmountChangeAlert], ); - const handleCancelClick = useCallback( - (payload) => { - onCancelForm && onCancelForm(payload); - }, - [onCancelForm], - ); + // Handle clear all lines button click. + const handleClearAllLines = useCallback(() => { + setClearLinesAlert(true); + }, [setClearLinesAlert]); - const handleClearClick = () => { - formik.resetForm(); + // Handle cancel button click of clear lines alert + const handleCancelClearLines = useCallback(() => { + setClearLinesAlert(false); + }, [setClearLinesAlert]); + + // Handle cancel button of amount change alert. + const handleCancelAmountChangeAlert = () => { + setAmountChangeAlert(false); }; - - const handleClickAddNewRow = () => { - formik.setFieldValue( + // Handle confirm button of amount change alert. + const handleConfirmAmountChangeAlert = () => { + setFullAmount(amountChangeAlert); + setAmountChangeAlert(false); + }; + // Handle confirm clear all lines. + const handleConfirmClearLines = useCallback(() => { + setFieldValue( '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 ( -
-
- - + + - {/* */} - + + } + confirmButtonText={} + intent={Intent.WARNING} + isOpen={amountChangeAlert} + onCancel={handleCancelAmountChangeAlert} + onConfirm={handleConfirmAmountChangeAlert} + > +

Are you sure to discard full amount?

+
+ } + confirmButtonText={} + intent={Intent.WARNING} + isOpen={clearLinesAlert} + onCancel={handleCancelClearLines} + onConfirm={handleConfirmClearLines} + > +

Are you sure to discard full amount?

+
- + +
); } export default compose( withPaymentReceivesActions, - withDashboardActions, withMediaActions, - withPaymentReceives(({ paymentReceivesItems }) => ({ - paymentReceivesItems, + // withPaymentReceives(({ paymentReceivesItems }) => ({ + // paymentReceivesItems, + // })), + withPaymentReceiveDetail(({ paymentReceive }) => ({ + paymentReceive, })), - withPaymentReceiveDetail(), )(PaymentReceiveForm); diff --git a/client/src/containers/Sales/PaymentReceive/PaymentReceiveFormHeader.js b/client/src/containers/Sales/PaymentReceive/PaymentReceiveFormHeader.js index 3b0e3812b..0396e55da 100644 --- a/client/src/containers/Sales/PaymentReceive/PaymentReceiveFormHeader.js +++ b/client/src/containers/Sales/PaymentReceive/PaymentReceiveFormHeader.js @@ -5,35 +5,54 @@ import { Intent, Position, MenuItem, - Classes, } from '@blueprintjs/core'; import { DateInput } from '@blueprintjs/datetime'; import { FormattedMessage as T } from 'react-intl'; -import { useParams, useHistory } from 'react-router-dom'; -import { useQuery } from 'react-query'; - import moment from 'moment'; -import { momentFormatter, compose, tansformDateValue } from 'utils'; +import { sumBy } from 'lodash'; import classNames from 'classnames'; + +import { CLASSES } from 'common/classes' +import { momentFormatter, compose, tansformDateValue } from 'utils'; import { AccountsSelectList, ListSelect, ErrorMessage, FieldRequiredHint, + Hint, + Money, } from 'components'; +import withInvoices from 'containers/Sales/Invoice/withInvoices'; import withCustomers from 'containers/Customers/withCustomers'; import withAccounts from 'containers/Accounts/withAccounts'; function PaymentReceiveFormHeader({ - formik: { errors, touched, setFieldValue, getFieldProps, values }, + // #useFormik + errors, + touched, + setFieldValue, + getFieldProps, + values, + onFullAmountChanged, + paymentReceiveId, + customerId, //#withCustomers customers, + //#withAccouts accountsList, + + // #withInvoices + receivableInvoices, }) { + // Compute the total receivable amount. + const receivableFullAmount = useMemo( + () => sumBy(receivableInvoices, 'due_amount'), + [receivableInvoices], + ); const handleDateChange = useCallback( (date_filed) => (date) => { const formatted = moment(date).format('YYYY-MM-DD'); @@ -82,14 +101,28 @@ function PaymentReceiveFormHeader({ [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 ( -
-
- {/* Customer name */} +
+
+ {/* ------------- Customer name ------------- */} } inline={true} - className={classNames('form-group--select-list', Classes.FILL)} + className={classNames('form-group--select-list', CLASSES.FILL)} labelInfo={} intent={errors.customer_id && touched.customer_id && Intent.DANGER} helperText={ @@ -110,12 +143,12 @@ function PaymentReceiveFormHeader({ /> - {/* Payment date */} + {/* ------------- Payment date ------------- */} } inline={true} labelInfo={} - 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} helperText={ @@ -129,11 +162,39 @@ function PaymentReceiveFormHeader({ /> - {/* payment receive no */} + {/* ------------ Full amount ------------ */} + } + inline={true} + className={('form-group--full-amount', CLASSES.FILL)} + intent={ + errors.full_amount && touched.full_amount && Intent.DANGER + } + labelInfo={} + helperText={ + + } + > + + + + Receive full amount () + + + + {/* ------------ Payment receive no. ------------ */} } inline={true} - className={('form-group--payment_receive_no', Classes.FILL)} + className={('form-group--payment_receive_no', CLASSES.FILL)} labelInfo={} intent={ errors.payment_receive_no && @@ -155,13 +216,13 @@ function PaymentReceiveFormHeader({ /> - {/* deposit account */} + {/* ------------ Deposit account ------------ */} } className={classNames( 'form-group--deposit_account_id', 'form-group--select-list', - Classes.FILL, + CLASSES.FILL, )} inline={true} labelInfo={} @@ -185,45 +246,22 @@ function PaymentReceiveFormHeader({ selectedAccountId={values.deposit_account_id} /> -
- {/* Receive amount */} - - {/* } - inline={true} - labelInfo={} - className={classNames('form-group--', Classes.FILL)} - intent={ - errors.receive_amount && touched.receive_amount && Intent.DANGER - } - helperText={ - - } - > - - */} - - {/* reference_no */} - } - inline={true} - className={classNames('form-group--reference', Classes.FILL)} - intent={errors.reference_no && touched.reference_no && Intent.DANGER} - helperText={} - > - } + inline={true} + className={classNames('form-group--reference', CLASSES.FILL)} intent={errors.reference_no && touched.reference_no && Intent.DANGER} - minimal={true} - {...getFieldProps('reference_no')} - /> - + helperText={} + > + + +
); } @@ -235,4 +273,7 @@ export default compose( withAccounts(({ accountsList }) => ({ accountsList, })), + withInvoices(({ paymentReceiveReceivableInvoices }) => ({ + receivableInvoices: paymentReceiveReceivableInvoices, + })), )(PaymentReceiveFormHeader); diff --git a/client/src/containers/Sales/PaymentReceive/PaymentReceiveFormPage.js b/client/src/containers/Sales/PaymentReceive/PaymentReceiveFormPage.js new file mode 100644 index 000000000..f9e2166fd --- /dev/null +++ b/client/src/containers/Sales/PaymentReceive/PaymentReceiveFormPage.js @@ -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 ( + + {/* */} + + ) +} + +export default compose( + withDashboardActions, + withAccountsActions, + withSettingsActions, + withPaymentReceivesActions, + withCustomersActions, +)(PaymentReceiveFormPage); \ No newline at end of file diff --git a/client/src/containers/Sales/PaymentReceive/PaymentReceiveItemsTable.js b/client/src/containers/Sales/PaymentReceive/PaymentReceiveItemsTable.js index 0777fb519..a372ccd3d 100644 --- a/client/src/containers/Sales/PaymentReceive/PaymentReceiveItemsTable.js +++ b/client/src/containers/Sales/PaymentReceive/PaymentReceiveItemsTable.js @@ -1,258 +1,132 @@ import React, { useState, useMemo, useEffect, useCallback } from 'react'; import { Button, Intent, Position, Tooltip } from '@blueprintjs/core'; -import { FormattedMessage as T, useIntl } from 'react-intl'; -import { Icon, DataTable } from 'components'; -import moment from 'moment'; +import { FormattedMessage as T } from 'react-intl'; +import { Icon, CloudLoadingIndicator } from 'components'; 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 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 ( - } position={Position.LEFT}> - - - -
-
+ ); } export default compose( - withInvoices(({ dueInvoices }) => ({ - dueInvoices, + withInvoices(({ paymentReceiveReceivableInvoices }) => ({ + receivableInvoices: paymentReceiveReceivableInvoices, })), + withInvoiceActions, )(PaymentReceiveItemsTable); diff --git a/client/src/containers/Sales/PaymentReceive/PaymentReceiveItemsTableEditor.js b/client/src/containers/Sales/PaymentReceive/PaymentReceiveItemsTableEditor.js new file mode 100644 index 000000000..b67efd8c7 --- /dev/null +++ b/client/src/containers/Sales/PaymentReceive/PaymentReceiveItemsTableEditor.js @@ -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 + } + 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 } }) => {index + 1}, + 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 ( +
+ +
+ +
+
+ ); + +} \ No newline at end of file diff --git a/client/src/store/Bills/bills.selectors.js b/client/src/store/Bills/bills.selectors.js index c461be448..cd3fab932 100644 --- a/client/src/store/Bills/bills.selectors.js +++ b/client/src/store/Bills/bills.selectors.js @@ -87,4 +87,18 @@ export const getPayableBillsByPaymentMadeFactory = () => ? pickItemsFromIds(billsItems, payableBillsIds) || [] : []; }, + ); + +export const getPaymentMadeFormPayableBillsFactory = () => + createSelector( + billItemsSelector, + billsPayableVendorSelector, + billsPayableByPaymentMadeSelector, + (billsItems, vendorBillsIds, paymentMadeBillsIds) => { + const billsIds = [ + ...(vendorBillsIds || []), + ...(paymentMadeBillsIds || []) + ]; + return pickItemsFromIds(billsItems, billsIds); + }, ); \ No newline at end of file diff --git a/client/src/store/Invoice/invoices.actions.js b/client/src/store/Invoice/invoices.actions.js index 92787ad8c..ba9ad0b03 100644 --- a/client/src/store/Invoice/invoices.actions.js +++ b/client/src/store/Invoice/invoices.actions.js @@ -121,26 +121,32 @@ export const fetchInvoice = ({ id }) => { }); }); }; -export const dueInvoices = ({ id }) => { - return (dispatch) => - new Promise((resovle, reject) => { - ApiService.get(`sales/invoices/due_invoices`, { - params: { customer_id: id }, - }) - .then((response) => { - dispatch({ - type: t.DUE_INVOICES_SET, - payload: { - customer_id: id, - due_sales_invoices: response.data.due_sales_invoices, - }, - }); - resovle(response); - }) - .catch((error) => { - const { response } = error; - const { data } = response; - reject(data?.errors); + +export const fetchDueInvoices = ({ customerId }) => (dispatch) => new Promise((resovle, reject) => { + ApiService.get(`sales/invoices/payable`, { + params: { customer_id: customerId }, + }) + .then((response) => { + dispatch({ + type: t.INVOICES_ITEMS_SET, + payload: { + sales_invoices: response.data.sales_invoices, + }, + }); + if (customerId) { + dispatch({ + type: t.INVOICES_RECEIVABLE_BY_CUSTOMER_ID, + payload: { + customerId, + saleInvoices: response.data.sales_invoices, + }, }); + } + resovle(response); + }) + .catch((error) => { + const { response } = error; + const { data } = response; + reject(data?.errors); }); -}; + }); diff --git a/client/src/store/Invoice/invoices.reducer.js b/client/src/store/Invoice/invoices.reducer.js index 098f78db6..72d2b61c5 100644 --- a/client/src/store/Invoice/invoices.reducer.js +++ b/client/src/store/Invoice/invoices.reducer.js @@ -13,6 +13,10 @@ const initialState = { page: 1, }, dueInvoices: {}, + receivable: { + byCustomerId: [], + byPaymentReceiveId: [], + } }; 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); - const _invoices = {}; - due_sales_invoices.forEach((invoice) => { - _invoices[invoice.id] = { - ...invoice, - }; - }); - - state.items = { - ...state.dueInvoices, - ...state.items.dueInvoices, - ..._invoices, - }; + state.receivable.byPaymentReceiveId[paymentReceiveId] = saleInvoicesIds; }, - [t.RELOAD_INVOICES]: (state, action) => { - const { sales_invoices } = action.payload; - const _sales_invoices = {}; - sales_invoices.forEach((invoice) => { - _sales_invoices[invoice.id] = { - ...invoice, - }; - }); + [t.INVOICES_RECEIVABLE_BY_CUSTOMER_ID]: (state, action) => { + const { customerId, saleInvoices } = action.payload; + const saleInvoiceIds = saleInvoices.map((saleInvoice) => saleInvoice.id); - state.items = { - ...state.items, - ..._sales_invoices, - }; + state.receivable.byCustomerId[customerId] = saleInvoiceIds }, }); diff --git a/client/src/store/Invoice/invoices.selector.js b/client/src/store/Invoice/invoices.selector.js index 700d19acc..f8c7f5076 100644 --- a/client/src/store/Invoice/invoices.selector.js +++ b/client/src/store/Invoice/invoices.selector.js @@ -22,6 +22,10 @@ const invoicesPageSelector = (state, props, query) => { 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 = () => createSelector( paginationLocationQuery, @@ -55,17 +59,39 @@ export const getInvoicePaginationMetaFactory = () => return invoicePage?.paginationMeta || {}; }); -const dueInvoicesSelector = (state, props) => { - return state.salesInvoices.dueInvoices[props.customer_id] || []; -}; +// export const getCustomerReceivableInvoicesFactory = () => +// createSelector( +// invoicesItemsSelector, +// invoicesReceiableCustomerSelector, +// (invoicesItems, invoicesIds) => { +// return Array.isArray(invoicesIds) +// ? (pickItemsFromIds(invoicesItems, invoicesIds) || []) +// : []; +// }, +// ); -export const getdueInvoices = createSelector( - dueInvoicesSelector, - invoicesItemsSelector, - (customerIds, items) => { - - return typeof customerIds === 'object' - ? pickItemsFromIds(items, customerIds) || [] - : []; - }, -); +// export const getPaymentReceivableInvoicesFactory = () => +// createSelector( +// invoicesItemsSelector, +// paymentReceivableInvoicesSelector, +// (invoicesItems, invoicesIds) => { +// return Array.isArray(invoicesIds) +// ? (pickItemsFromIds(invoicesItems, invoicesIds) || []) +// : []; +// }, +// ); + + +export const getPaymentReceiveReceivableInvoicesFactory = () => + createSelector( + invoicesItemsSelector, + invoicesReceiableCustomerSelector, + paymentReceivableInvoicesSelector, + (invoicesItems, customerInvoicesIds, paymentInvoicesIds) => { + const invoicesIds = [ + ...(customerInvoicesIds || []), + ...(paymentInvoicesIds || []), + ]; + return pickItemsFromIds(invoicesItems, invoicesIds); + }, + ); \ No newline at end of file diff --git a/client/src/store/Invoice/invoices.types.js b/client/src/store/Invoice/invoices.types.js index 5e59bfa0b..cb6b242c0 100644 --- a/client/src/store/Invoice/invoices.types.js +++ b/client/src/store/Invoice/invoices.types.js @@ -11,4 +11,7 @@ export default { INVOICES_ITEMS_SET: 'INVOICES_ITEMS_SET', DUE_INVOICES_SET: 'DUE_INVOICES_SET', RELOAD_INVOICES: 'RELOAD_INVOICES', + + INVOICES_RECEIVABLE_BY_PAYMENT_ID: 'INVOICES_RECEIVABLE_BY_PAYMENT_ID', + INVOICES_RECEIVABLE_BY_CUSTOMER_ID: 'INVOICES_RECEIVABLE_BY_CUSTOMER_ID' }; diff --git a/client/src/store/PaymentReceive/paymentReceive.actions.js b/client/src/store/PaymentReceive/paymentReceive.actions.js index f0a56e4f6..8ef6d11ee 100644 --- a/client/src/store/PaymentReceive/paymentReceive.actions.js +++ b/client/src/store/PaymentReceive/paymentReceive.actions.js @@ -52,28 +52,29 @@ export const fetchPaymentReceive = ({ id }) => { new Promise((resovle, reject) => { ApiService.get(`sales/payment_receives/${id}`, {}) .then((response) => { - dispatch({ - type: t.RELOAD_INVOICES, - payload: { - sales_invoices: response.data.paymentReceive.entries.map( - (e) => e.invoice, - ), - }, - }); dispatch({ type: t.PAYMENT_RECEIVE_SET, payload: { id, 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); }) .catch((error) => { - // const { response } = error; - // const { data } = response; - // reject(data?.errors); reject(error); }); }); diff --git a/client/src/style/App.scss b/client/src/style/App.scss index a30b5bd74..bd82d0458 100644 --- a/client/src/style/App.scss +++ b/client/src/style/App.scss @@ -71,6 +71,7 @@ $pt-font-family: Noto Sans, -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, @import 'pages/invoice-form'; @import 'pages/receipt-form'; @import 'pages/payment-made'; +@import 'pages/payment-receive'; // Views @import 'views/filter-dropdown'; diff --git a/client/src/style/pages/payment-receive.scss b/client/src/style/pages/payment-receive.scss new file mode 100644 index 000000000..0d419d0cb --- /dev/null +++ b/client/src/style/pages/payment-receive.scss @@ -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{ + + } +} \ No newline at end of file diff --git a/server/src/api/controllers/Sales/PaymentReceives.ts b/server/src/api/controllers/Sales/PaymentReceives.ts index 997ce1ee7..37d66dc56 100644 --- a/server/src/api/controllers/Sales/PaymentReceives.ts +++ b/server/src/api/controllers/Sales/PaymentReceives.ts @@ -34,8 +34,9 @@ export default class PaymentReceivesController extends BaseController { this.handleServiceErrors, ); router.post( - '/', - this.newPaymentReceiveValidation, + '/', [ + ...this.newPaymentReceiveValidation, + ], this.validationResult, asyncMiddleware(this.newPaymentReceive.bind(this)), this.handleServiceErrors, @@ -87,6 +88,7 @@ export default class PaymentReceivesController extends BaseController { check('entries').isArray({ min: 1 }), + check('entries.*.id').optional({ nullable: true }).isNumeric().toInt(), check('entries.*.invoice_id').exists().isNumeric().toInt(), check('entries.*.payment_amount').exists().isNumeric().toInt(), ]; diff --git a/server/src/services/Sales/PaymentsReceives.ts b/server/src/services/Sales/PaymentsReceives.ts index ad9b61caa..f2c3112c3 100644 --- a/server/src/services/Sales/PaymentsReceives.ts +++ b/server/src/services/Sales/PaymentsReceives.ts @@ -1,4 +1,4 @@ -import { omit, sumBy, chain, difference } from 'lodash'; +import { omit, sumBy, difference } from 'lodash'; import moment from 'moment'; import { Service, Inject } from 'typedi'; import { @@ -26,7 +26,6 @@ import { formatDateFields, entriesAmountDiff } from 'utils'; import { ServiceError } from 'exceptions'; import CustomersService from 'services/Contacts/CustomersService'; import ItemsEntriesService from 'services/Items/ItemsEntriesService'; -import { SaleInvoice } from 'models'; const ERRORS = { PAYMENT_RECEIVE_NO_EXISTS: 'PAYMENT_RECEIVE_NO_EXISTS', @@ -327,16 +326,30 @@ export default class PaymentReceiveService { * @param {number} tenantId - Tenant id. * @param {Integer} paymentReceiveId - Payment receive id. */ - public async getPaymentReceive(tenantId: number, paymentReceiveId: number) { - const { PaymentReceive } = this.tenancy.models(tenantId); + public async getPaymentReceive( + tenantId: number, + paymentReceiveId: number + ): Promise<{ paymentReceive: IPaymentReceive[], receivableInvoices: ISaleInvoice }> { + const { PaymentReceive, SaleInvoice } = this.tenancy.models(tenantId); const paymentReceive = await PaymentReceive.query() .findById(paymentReceiveId) - .withGraphFetched('entries.invoice'); + .withGraphFetched('entries') + .withGraphFetched('customer') + .withGraphFetched('depositAccount'); if (!paymentReceive) { 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 }; } /**