refactoring: WIP payment receive and made form.

This commit is contained in:
a.bouhuolia
2021-02-16 17:31:18 +02:00
parent a75177b9d1
commit d60429f5e0
17 changed files with 441 additions and 657 deletions

View File

@@ -15,7 +15,6 @@ import { useFormikContext } from 'formik';
import { usePaymentMadeFormContext } from './PaymentMadeFormProvider'; import { usePaymentMadeFormContext } from './PaymentMadeFormProvider';
import { CLASSES } from 'common/classes'; import { CLASSES } from 'common/classes';
import { Icon } from 'components'; import { Icon } from 'components';
/** /**

View File

@@ -9,7 +9,7 @@ import { CLASSES } from 'common/classes';
/** /**
* Payment made form footer. * Payment made form footer.
*/ */
export default function PaymentMadeFooter({ getFieldProps }) { export default function PaymentMadeFooter() {
return ( return (
<div className={classNames(CLASSES.PAGE_FORM_FOOTER)}> <div className={classNames(CLASSES.PAGE_FORM_FOOTER)}>
<Row> <Row>

View File

@@ -11,6 +11,7 @@ import { AppToaster } from 'components';
import PaymentMadeHeader from './PaymentMadeFormHeader'; import PaymentMadeHeader from './PaymentMadeFormHeader';
import PaymentMadeFloatingActions from './PaymentMadeFloatingActions'; import PaymentMadeFloatingActions from './PaymentMadeFloatingActions';
import PaymentMadeFooter from './PaymentMadeFooter'; import PaymentMadeFooter from './PaymentMadeFooter';
import PaymentMadeItemsTable from './PaymentMadeItemsTable';
import withSettings from 'containers/Settings/withSettings'; import withSettings from 'containers/Settings/withSettings';
import { import {
@@ -135,7 +136,7 @@ function PaymentMadeForm() {
<PaymentMadeHeader /> <PaymentMadeHeader />
<div className={classNames(CLASSES.PAGE_FORM_BODY)}> <div className={classNames(CLASSES.PAGE_FORM_BODY)}>
<PaymentMadeItemsTable />
</div> </div>
<PaymentMadeFooter /> <PaymentMadeFooter />
<PaymentMadeFloatingActions /> <PaymentMadeFloatingActions />

View File

@@ -34,7 +34,12 @@ import {
* Payment made form header fields. * Payment made form header fields.
*/ */
function PaymentMadeFormHeaderFields({ baseCurrency }) { function PaymentMadeFormHeaderFields({ baseCurrency }) {
const { vendors, accounts, isNewMode } = usePaymentMadeFormContext(); const {
vendors,
accounts,
isNewMode,
setPaymentVendorId,
} = usePaymentMadeFormContext();
const payableFullAmount = 0; const payableFullAmount = 0;
@@ -57,6 +62,7 @@ function PaymentMadeFormHeaderFields({ baseCurrency }) {
defaultSelectText={<T id={'select_vender_account'} />} defaultSelectText={<T id={'select_vender_account'} />}
onContactSelected={(contact) => { onContactSelected={(contact) => {
form.setFieldValue('vendor_id', contact.id); form.setFieldValue('vendor_id', contact.id);
setPaymentVendorId(contact.id);
}} }}
disabled={!isNewMode} disabled={!isNewMode}
popoverFill={true} popoverFill={true}

View File

@@ -6,7 +6,8 @@ import {
usePaymentMade, usePaymentMade,
useSettings, useSettings,
useCreatePaymentMade, useCreatePaymentMade,
useEditPaymentMade useEditPaymentMade,
useDueBills,
} from 'hooks/query'; } from 'hooks/query';
import { DashboardInsider } from 'components'; import { DashboardInsider } from 'components';
@@ -17,6 +18,9 @@ const PaymentMadeFormContext = createContext();
* Payment made form provider. * Payment made form provider.
*/ */
function PaymentMadeFormProvider({ paymentMadeId, ...props }) { function PaymentMadeFormProvider({ paymentMadeId, ...props }) {
const [submitPayload, setSubmitPayload] = React.useState({});
const [paymentVendorId, setPaymentVendorId] = React.useState(null);
// Handle fetch accounts data. // Handle fetch accounts data.
const { data: accounts, isFetching: isAccountsFetching } = useAccounts(); const { data: accounts, isFetching: isAccountsFetching } = useAccounts();
@@ -42,6 +46,13 @@ function PaymentMadeFormProvider({ paymentMadeId, ...props }) {
enabled: !!paymentMadeId, enabled: !!paymentMadeId,
}); });
// Retrieve the due bills of the given vendor.
const {
data: dueBills,
isLoading: isDueBillsLoading,
isFetching: isDueBillsFetching,
} = useDueBills(paymentVendorId, { enabled: !!paymentVendorId });
// Fetch payment made settings. // Fetch payment made settings.
useSettings(); useSettings();
@@ -49,6 +60,8 @@ function PaymentMadeFormProvider({ paymentMadeId, ...props }) {
const { mutateAsync: createPaymentMadeMutate } = useCreatePaymentMade(); const { mutateAsync: createPaymentMadeMutate } = useCreatePaymentMade();
const { mutateAsync: editPaymentMadeMutate } = useEditPaymentMade(); const { mutateAsync: editPaymentMadeMutate } = useEditPaymentMade();
const isNewMode = !paymentMadeId;
// Provider payload. // Provider payload.
const provider = { const provider = {
paymentMadeId, paymentMadeId,
@@ -58,16 +71,25 @@ function PaymentMadeFormProvider({ paymentMadeId, ...props }) {
paymentBills, paymentBills,
vendors, vendors,
items, items,
dueBills,
submitPayload,
paymentVendorId,
isNewMode,
isAccountsFetching, isAccountsFetching,
isItemsFetching, isItemsFetching,
isItemsLoading, isItemsLoading,
isVendorsFetching, isVendorsFetching,
isPaymentFetching, isPaymentFetching,
isPaymentLoading, isPaymentLoading,
isDueBillsLoading,
isDueBillsFetching,
createPaymentMadeMutate, createPaymentMadeMutate,
editPaymentMadeMutate, editPaymentMadeMutate,
setSubmitPayload,
setPaymentVendorId,
}; };
return ( return (

View File

@@ -1,153 +1,70 @@
import React, { useState, useEffect, useMemo, useCallback } from 'react'; import React, { useMemo, useCallback } from 'react';
import { useQuery } from 'react-query';
import { isEmpty } from 'lodash';
import { CloudLoadingIndicator } from 'components'; import { CloudLoadingIndicator } from 'components';
import PaymentMadeItemsTableEditor from './PaymentMadeItemsTableEditor'; import { Button } from '@blueprintjs/core';
import { FormattedMessage as T } from 'react-intl';
import classNames from 'classnames';
import withPaymentMadeActions from './withPaymentMadeActions'; import { CLASSES } from 'common/classes';
import withBillActions from '../Bill/withBillActions'; import { DataTableEditable } from 'components';
import withBills from '../Bill/withBills'; import { usePaymentMadeEntriesTableColumns } from './components';
import { compose } from 'utils'; import { usePaymentMadeFormContext } from './PaymentMadeFormProvider';
/** /**
* Payment made items table. * Payment made items table.
*/ */
function PaymentMadeItemsTable({ export default function PaymentMadeItemsTable() {
// #ownProps const {
paymentMadeId, paymentVendorId,
vendorId, dueBills,
fullAmount, isDueBillsFetching,
onUpdateData, isNewMode,
paymentEntries = [], // { bill_id: number, payment_amount: number, id?: number } } = usePaymentMadeFormContext();
onClickClearAllLines,
errors,
onFetchEntriesSuccess,
// #withBillActions const columns = usePaymentMadeEntriesTableColumns();
requestFetchDueBills,
// #withBills
vendorPayableBillsEntries,
}) {
const isNewMode = !paymentMadeId;
// Detarmines takes vendor payable bills entries in create mode // Detarmines takes vendor payable bills entries in create mode
// or payment made entries in edit mode. // or payment made entries in edit mode.
const computedTableEntries = useMemo( const computedTableEntries = useMemo(() => [], []);
() =>
!isEmpty(paymentEntries)
? paymentEntries
: (vendorPayableBillsEntries || []),
[vendorPayableBillsEntries, paymentEntries],
);
const [tableData, setTableData] = useState(computedTableEntries);
const [localEntries, setLocalEntries] = useState(computedTableEntries);
const [localAmount, setLocalAmount] = useState(fullAmount);
// Triggers `onUpdateData` event that passes changed entries. // Triggers `onUpdateData` event that passes changed entries.
const triggerUpdateData = useCallback( const triggerUpdateData = useCallback((entries) => {}, []);
(entries) => {
onUpdateData && onUpdateData(entries);
},
[onUpdateData],
);
const triggerOnFetchBillsSuccess = useCallback( const triggerOnFetchBillsSuccess = useCallback((bills) => {}, []);
(bills) => {
onFetchEntriesSuccess && onFetchEntriesSuccess(bills);
},
[onFetchEntriesSuccess],
);
useEffect(() => {
if (computedTableEntries !== localEntries) {
setTableData(computedTableEntries);
setLocalEntries(computedTableEntries);
}
}, [computedTableEntries, localEntries]);
// Handle mapping `fullAmount` prop to `localAmount` state.
useEffect(() => {
if (localAmount !== fullAmount) {
let _fullAmount = fullAmount;
const newTableData = tableData.map((data) => {
const amount = Math.min(data.due_amount, _fullAmount);
_fullAmount -= Math.max(amount, 0);
return {
...data,
payment_amount: amount,
};
});
setTableData(newTableData);
setLocalAmount(fullAmount);
triggerUpdateData(newTableData);
}
}, [
tableData,
setTableData,
setLocalAmount,
triggerUpdateData,
localAmount,
fullAmount,
]);
// Fetches vendor due bills.
const fetchVendorDueBills = useQuery(
['vendor-due-bills', vendorId],
(key, _vendorId) => requestFetchDueBills(_vendorId),
{ enabled: isNewMode && vendorId },
);
useEffect(() => {
const enabled = isNewMode && vendorId;
if (!fetchVendorDueBills.isFetching && enabled) {
triggerOnFetchBillsSuccess(computedTableEntries);
}
}, [
vendorId,
isNewMode,
fetchVendorDueBills.isFetching,
computedTableEntries,
triggerOnFetchBillsSuccess,
]);
// Handle update data. // Handle update data.
const handleUpdateData = useCallback( const handleUpdateData = useCallback((rows) => {}, []);
(rows) => {
setTableData(rows);
triggerUpdateData(rows);
},
[setTableData, triggerUpdateData],
);
// Detarmines the right no results message before selecting vendor and aftering // Detarmines the right no results message before selecting vendor and aftering
// selecting vendor id. // selecting vendor id.
const noResultsMessage = vendorId const noResultsMessage = paymentVendorId
? 'There is no payable bills for this vendor that can be applied for this payment' ? 'There is no payable bills for this vendor that can be applied for this payment'
: 'Please select a vendor to display all open bills for it.'; : 'Please select a vendor to display all open bills for it.';
return ( return (
<CloudLoadingIndicator isLoading={fetchVendorDueBills.isFetching}> <CloudLoadingIndicator isLoading={isDueBillsFetching}>
<PaymentMadeItemsTableEditor <DataTableEditable
noResultsMessage={noResultsMessage} progressBarLoading={isDueBillsFetching}
data={tableData} className={classNames(CLASSES.DATATABLE_EDITOR_ITEMS_ENTRIES)}
errors={errors} columns={columns}
onUpdateData={handleUpdateData} data={[]}
onClickClearAllLines={onClickClearAllLines} spinnerProps={false}
payload={{
errors: [],
updateData: handleUpdateData,
}}
noResults={noResultsMessage}
actions={
<Button
small={true}
className={'button--secondary button--clear-lines'}
// onClick={handleClickClearAllLines}
>
<T id={'clear_all_lines'} />
</Button>
}
totalRow={true}
/> />
</CloudLoadingIndicator> </CloudLoadingIndicator>
); );
} }
export default compose(
withPaymentMadeActions,
withBillActions,
withBills(({ paymentMadePayableBills, vendorPayableBillsEntries }) => ({
paymentMadePayableBills,
vendorPayableBillsEntries,
})),
)(PaymentMadeItemsTable);

View File

@@ -1,166 +0,0 @@
import React, { useState, useEffect, useMemo, useCallback } 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 { DataTableEditable, 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);
};
/**
* Payment made items editor table.
*/
export default function PaymentMadeItemsTableEditor({
//#ownProps
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: 'bill_date',
accessor: (r) => moment(r.bill_date).format('YYYY MMM DD'),
Cell: CellRenderer(EmptyDiv, 'bill_date'),
disableSortBy: true,
},
{
Header: formatMessage({ id: 'bill_number' }),
accessor: (row) => `#${row?.bill_number || ''}`,
Cell: CellRenderer(EmptyDiv, 'bill_number'),
disableSortBy: true,
className: 'bill_number',
},
{
Header: formatMessage({ id: 'bill_amount' }),
accessor: 'amount',
Cell: CellRenderer(DivFieldCell, 'amount'),
disableSortBy: true,
className: '',
},
{
Header: formatMessage({ id: 'amount_due' }),
accessor: 'due_amount',
Cell: TotalCellRederer(DivFieldCell, 'due_amount'),
disableSortBy: true,
className: '',
},
{
Header: formatMessage({ id: 'payment_amount' }),
accessor: 'payment_amount',
Cell: TotalCellRederer(MoneyFieldCell, 'payment_amount'),
disableSortBy: true,
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,
);
newRows.splice(-1, 1); // removes the total row.
setLocalData(newRows);
onUpdateData && onUpdateData(newRows);
},
[localData, setLocalData, onUpdateData],
);
return (
<DataTableEditable
className={classNames(CLASSES.DATATABLE_EDITOR_ITEMS_ENTRIES)}
columns={columns}
data={localData}
rowClassNames={rowClassNames}
spinnerProps={false}
payload={{
errors,
updateData: handleUpdateData,
}}
noResults={noResultsMessage}
actions={
<Button
small={true}
className={'button--secondary button--clear-lines'}
onClick={handleClickClearAllLines}
>
<T id={'clear_all_lines'} />
</Button>
}
totalRow={true}
/>
);
}

View File

@@ -0,0 +1,95 @@
import React from 'react';
import { useIntl } from "react-intl";
import moment from 'moment';
import { Money } from 'components';
import { safeSumBy, formattedAmount } from 'utils';
function BillNumberAccessor(row) {
return `#${row?.bill_number || ''}`
}
function IndexTableCell({ row: { index } }) {
return (<span>{index + 1}</span>);
}
function BillDateTableCell({ value }) {
return moment(value).format('YYYY MMM DD');
}
/**
* Balance footer cell.
*/
function AmountFooterCell({ rows }) {
const total = safeSumBy(rows, 'original.amount');
return <span>{ formattedAmount(total, 'USD') }</span>;
}
/**
* Due amount footer cell.
*/
function DueAmountFooterCell({ rows }) {
const totalDueAmount = safeSumBy(rows, 'original.due_amount');
return <span>{ formattedAmount(totalDueAmount, 'USD') }</span>;
}
/**
* Payment amount footer cell.
*/
function PaymentAmountFooterCell({ rows }) {
const totalPaymentAmount = safeSumBy(rows, 'original.payment_amount');
return <span>{ formattedAmount(totalPaymentAmount, 'USD') }</span>;
}
/**
* Payment made entries table columns
*/
export function usePaymentMadeEntriesTableColumns() {
const { formatMessage } = useIntl();
return React.useMemo(
() => [
{
Header: '#',
accessor: 'index',
Cell: IndexTableCell,
width: 40,
disableResizing: true,
disableSortBy: true,
},
{
Header: formatMessage({ id: 'Date' }),
id: 'bill_date',
accessor: 'bill_date',
Cell: BillDateTableCell,
disableSortBy: true,
},
{
Header: formatMessage({ id: 'bill_number' }),
accessor: BillNumberAccessor,
disableSortBy: true,
className: 'bill_number',
},
{
Header: formatMessage({ id: 'bill_amount' }),
accessor: 'amount',
Footer: AmountFooterCell,
disableSortBy: true,
className: '',
},
{
Header: formatMessage({ id: 'amount_due' }),
accessor: 'due_amount',
Footer: DueAmountFooterCell,
disableSortBy: true,
className: '',
},
{
Header: formatMessage({ id: 'payment_amount' }),
accessor: 'payment_amount',
Footer: PaymentAmountFooterCell,
disableSortBy: true,
className: '',
},
],
[formatMessage],
)
}

View File

@@ -9,7 +9,7 @@ import { useHistory } from 'react-router-dom';
import { CLASSES } from 'common/classes'; 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 PaymentReceiveFormFooter from './PaymentReceiveFormFooter'; import PaymentReceiveFormFooter from './PaymentReceiveFormFooter';
@@ -167,48 +167,16 @@ function PaymentReceiveForm({
> >
<Form> <Form>
<PaymentReceiveHeader /> <PaymentReceiveHeader />
<div className={classNames(CLASSES.PAGE_FORM_BODY)}>
<PaymentReceiveItemsTable />
</div>
<PaymentReceiveFormFooter /> <PaymentReceiveFormFooter />
<PaymentReceiveFloatingActions /> <PaymentReceiveFloatingActions />
</Form> </Form>
</Formik> </Formik>
{/* <form onSubmit={handleSubmit}> */}
{/* <PaymentReceiveHeader
errors={errors}
touched={touched}
setFieldValue={setFieldValue}
getFieldProps={getFieldProps}
values={values}
paymentReceiveId={paymentReceiveId}
customerId={values.customer_id}
onFullAmountChanged={handleFullAmountChange}
receivableFullAmount={receivableFullAmount}
amountReceived={fullAmountReceived}
onPaymentReceiveNumberChanged={handlePaymentReceiveNumberChanged}
/>
<div className={classNames(CLASSES.PAGE_FORM_BODY)}>
<PaymentReceiveItemsTable
paymentReceiveId={paymentReceiveId}
customerId={values.customer_id}
fullAmount={fullAmount}
onUpdateData={handleUpdataData}
paymentReceiveEntries={localPaymentEntries}
errors={errors?.entries}
onClickClearAllLines={handleClearAllLines}
onFetchEntriesSuccess={handleFetchEntriesSuccess}
/>
</div>
<PaymentReceiveFloatingActions
isSubmitting={isSubmitting}
paymentReceiveId={paymentReceiveId}
onClearClick={handleClearBtnClick}
onSubmitClick={handleSubmitClick}
onCancelClick={handleCancelClick}
onSubmitForm={submitForm}
/> */}
{/* </form> */} {/* </form> */}
</div> </div>
); );

View File

@@ -6,7 +6,8 @@ import {
useAccounts, useAccounts,
useCustomers, useCustomers,
useCreatePaymentReceive, useCreatePaymentReceive,
useEditPaymentReceive useEditPaymentReceive,
useDueInvoices,
} from 'hooks/query'; } from 'hooks/query';
// Payment receive form context. // Payment receive form context.
@@ -16,14 +17,16 @@ const PaymentReceiveFormContext = createContext();
* Payment receive form provider. * Payment receive form provider.
*/ */
function PaymentReceiveFormProvider({ paymentReceiveId, ...props }) { function PaymentReceiveFormProvider({ paymentReceiveId, ...props }) {
// Form state.
const [paymentCustomerId, setPaymentCustomerId] = React.useState(null);
const [submitPayload, setSubmitPayload] = React.useState({});
// Fetches payment recevie details. // Fetches payment recevie details.
const { const {
data: paymentReceive, data: paymentReceive,
isLoading: isPaymentLoading, isLoading: isPaymentLoading,
isFetching: isPaymentFetching, isFetching: isPaymentFetching,
} = usePaymentReceive(paymentReceiveId, { } = usePaymentReceive(paymentReceiveId, { enabled: !!paymentReceiveId });
enabled: !!paymentReceiveId,
});
// Handle fetch accounts data. // Handle fetch accounts data.
const { data: accounts, isFetching: isAccountsFetching } = useAccounts(); const { data: accounts, isFetching: isAccountsFetching } = useAccounts();
@@ -37,7 +40,16 @@ function PaymentReceiveFormProvider({ paymentReceiveId, ...props }) {
isFetching: isCustomersFetching, isFetching: isCustomersFetching,
} = useCustomers(); } = useCustomers();
const [submitPayload, setSubmitPayload] = React.useState({}); // Fetches customer receivable invoices.
const {
data: dueInvoices,
isLoading: isDueInvoicesLoading,
isFetching: isDueInvoicesFetching,
} = useDueInvoices(paymentCustomerId, {
enabled: !!paymentCustomerId,
});
// Detarmines whether the new mode.
const isNewMode = !paymentReceiveId; const isNewMode = !paymentReceiveId;
// Create and edit payment receive mutations. // Create and edit payment receive mutations.
@@ -49,27 +61,29 @@ function PaymentReceiveFormProvider({ paymentReceiveId, ...props }) {
paymentReceive, paymentReceive,
accounts, accounts,
customers, customers,
dueInvoices,
isPaymentLoading, isPaymentLoading,
isPaymentFetching, isPaymentFetching,
isAccountsFetching, isAccountsFetching,
isCustomersFetching, isCustomersFetching,
isDueInvoicesLoading,
isDueInvoicesFetching,
paymentCustomerId,
submitPayload, submitPayload,
setSubmitPayload,
isNewMode, isNewMode,
setSubmitPayload,
setPaymentCustomerId,
editPaymentReceiveMutate, editPaymentReceiveMutate,
createPaymentReceiveMutate createPaymentReceiveMutate,
}; };
return ( return (
<DashboardInsider <DashboardInsider
loading={ loading={isPaymentLoading || isAccountsFetching || isCustomersFetching}
isPaymentLoading ||
isAccountsFetching ||
isCustomersFetching
}
name={'payment-receive-form'} name={'payment-receive-form'}
> >
<PaymentReceiveFormContext.Provider value={provider} {...props} /> <PaymentReceiveFormContext.Provider value={provider} {...props} />

View File

@@ -37,7 +37,12 @@ import { compose } from 'utils';
*/ */
function PaymentReceiveHeaderFields({ baseCurrency }) { function PaymentReceiveHeaderFields({ baseCurrency }) {
// Payment receive form context. // Payment receive form context.
const { customers, accounts, isNewMode } = usePaymentReceiveFormContext(); const {
customers,
accounts,
isNewMode,
setPaymentCustomerId,
} = usePaymentReceiveFormContext();
// Formik form context. // Formik form context.
const { values } = useFormikContext(); const { values } = useFormikContext();
@@ -66,8 +71,9 @@ function PaymentReceiveHeaderFields({ baseCurrency }) {
contactsList={customers} contactsList={customers}
selectedContactId={value} selectedContactId={value}
defaultSelectText={<T id={'select_customer_account'} />} defaultSelectText={<T id={'select_customer_account'} />}
onContactSelected={(value) => { onContactSelected={(customer) => {
form.setFieldValue('customer_id', value); form.setFieldValue('customer_id', customer);
setPaymentCustomerId(customer.id);
}} }}
popoverFill={true} popoverFill={true}
disabled={!isNewMode} disabled={!isNewMode}

View File

@@ -1,137 +1,76 @@
import React, { useState, useMemo, useEffect, useCallback } from 'react'; import React, { useMemo, useCallback } from 'react';
import { Button } from '@blueprintjs/core';
import { FormattedMessage as T } from 'react-intl';
import { CloudLoadingIndicator } from 'components'; import { CloudLoadingIndicator } from 'components';
import { useQuery } from 'react-query'; import classNames from 'classnames';
import { omit } from 'lodash';
import { compose } from 'utils'; import { CLASSES } from 'common/classes';
import { usePaymentReceiveFormContext } from './PaymentReceiveFormProvider';
import { DataTableEditable } from 'components';
import { usePaymentReceiveEntriesColumns } from './components';
import withInvoices from '../Invoice/withInvoices'; /**
import PaymentReceiveItemsTableEditor from './PaymentReceiveItemsTableEditor'; * Payment receive items table.
import withInvoiceActions from 'containers/Sales/Invoice/withInvoiceActions'; */
export default function PaymentReceiveItemsTable() {
// Payment receive form context.
const {
isNewMode,
isDueInvoicesFetching,
paymentCustomerId,
dueInvoices,
} = usePaymentReceiveFormContext();
// Payment receive entries form context.
function PaymentReceiveItemsTable({ const columns = usePaymentReceiveEntriesColumns();
// #ownProps
paymentReceiveId,
customerId,
fullAmount,
onUpdateData,
paymentReceiveEntries = [],
errors,
onClickClearAllLines,
onFetchEntriesSuccess,
// #withInvoices
customerInvoiceEntries,
// #withPaymentReceiveActions
requestFetchDueInvoices
}) {
const isNewMode = !paymentReceiveId;
// Detarmines takes payment receive invoices entries from customer receivable // Detarmines takes payment receive invoices entries from customer receivable
// invoices or associated payment receive invoices. // invoices or associated payment receive invoices.
const computedTableData = useMemo(() => [ const computedTableData = useMemo(
...(!isNewMode ? (paymentReceiveEntries || []) : []), () => [
...(isNewMode ? (customerInvoiceEntries || []) : []), ...(!isNewMode ? [] || [] : []),
], [ ...(isNewMode ? dueInvoices || [] : []),
isNewMode, ],
paymentReceiveEntries, [isNewMode, dueInvoices],
customerInvoiceEntries,
]);
const [tableData, setTableData] = useState(computedTableData);
const [localEntries, setLocalEntries] = useState(computedTableData);
const [localAmount, setLocalAmount] = useState(fullAmount);
useEffect(() => {
if (computedTableData !== localEntries) {
setTableData(computedTableData);
setLocalEntries(computedTableData);
}
}, [computedTableData, localEntries]);
// Triggers `onUpdateData` prop event.
const triggerUpdateData = useCallback((entries) => {
onUpdateData && onUpdateData(entries);
}, [onUpdateData]);
useEffect(() => {
if (localAmount !== fullAmount) {
let _fullAmount = fullAmount;
const newTableData = tableData.map((data) => {
const amount = Math.min(data?.due_amount, _fullAmount);
_fullAmount -= Math.max(amount, 0);
return {
...data,
payment_amount: amount,
};
});
setTableData(newTableData);
setLocalAmount(fullAmount);
triggerUpdateData(newTableData);
}
}, [
fullAmount,
localAmount,
tableData,
triggerUpdateData,
]);
// Fetches customer receivable invoices.
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.';
const triggerOnFetchInvoicesSuccess = useCallback((bills) => { // No results message.
onFetchEntriesSuccess && onFetchEntriesSuccess(bills); const noResultsMessage = paymentCustomerId
}, [onFetchEntriesSuccess]) ? '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. // Handle update data.
const handleUpdateData = useCallback((rows) => { const handleUpdateData = useCallback((rows) => {}, []);
triggerUpdateData(rows);
setTableData(rows);
}, [triggerUpdateData]);
useEffect(() => { // Handle click clear all lines button.
const enabled = isNewMode && customerId; const handleClickClearAllLines = () => {
if (!fetchCustomerDueInvoices.isFetching && enabled) { };
triggerOnFetchInvoicesSuccess(computedTableData);
}
}, [
isNewMode,
customerId,
fetchCustomerDueInvoices.isFetching,
computedTableData,
triggerOnFetchInvoicesSuccess,
]);
return ( return (
<CloudLoadingIndicator isLoading={fetchCustomerDueInvoices.isFetching}> <CloudLoadingIndicator isLoading={isDueInvoicesFetching}>
<PaymentReceiveItemsTableEditor <DataTableEditable
noResultsMessage={noResultsMessage} progressBarLoading={isDueInvoicesFetching}
data={tableData} className={classNames(CLASSES.DATATABLE_EDITOR_ITEMS_ENTRIES)}
errors={errors} columns={columns}
onUpdateData={handleUpdateData} data={[]}
onClickClearAllLines={onClickClearAllLines} spinnerProps={false}
payload={{
errors: [],
updateData: handleUpdateData,
}}
noResults={noResultsMessage}
actions={
<Button
small={true}
className={'button--secondary button--clear-lines'}
onClick={handleClickClearAllLines}
>
<T id={'clear_all_lines'} />
</Button>
}
totalRow={true}
/> />
</CloudLoadingIndicator> </CloudLoadingIndicator>
); );
} }
export default compose(
withInvoices(({ customerInvoiceEntries }) => ({
customerInvoiceEntries,
})),
withInvoiceActions,
)(PaymentReceiveItemsTable);

View File

@@ -1,172 +0,0 @@
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 { DataTableEditable, 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_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) => {
const invNumber = row?.invoice_no || row?.id;
return `#INV-${invNumber || ''}`;
},
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: '',
},
],
[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 (
<DataTableEditable
className={classNames(CLASSES.DATATABLE_EDITOR_ITEMS_ENTRIES)}
columns={columns}
data={localData}
rowClassNames={rowClassNames}
spinnerProps={false}
payload={{
errors,
updateData: handleUpdateData,
}}
noResults={noResultsMessage}
actions={
<Button
small={true}
className={'button--secondary button--clear-lines'}
onClick={handleClickClearAllLines}
>
<T id={'clear_all_lines'} />
</Button>
}
totalRow={true}
/>
);
}

View File

@@ -0,0 +1,111 @@
import React from 'react';
import moment from 'moment';
import { useIntl } from 'react-intl';
import { safeSumBy, formattedAmount } from 'utils';
/**
* Invoice date cell.
*/
function InvoiceDateCell({ value }) {
return <span>{ moment(value).format('YYYY MMM DD') }</span>
}
/**
* Index table cell.
*/
function IndexCell({ row: { index } }) {
return (<span>{index + 1}</span>);
}
/**
* Invoice number table cell accessor.
*/
function InvNumberCellAccessor(row) {
const invNumber = row?.invoice_no || row?.id;
return `#INV-${invNumber || ''}`;
}
/**
* Balance footer cell.
*/
function BalanceFooterCell({ rows }) {
const total = safeSumBy(rows, 'original.balance');
return <span>{ formattedAmount(total, 'USD') }</span>;
}
/**
* Due amount footer cell.
*/
function DueAmountFooterCell({ rows }) {
const totalDueAmount = safeSumBy(rows, 'original.due_amount');
return <span>{ formattedAmount(totalDueAmount, 'USD') }</span>;
}
/**
* Payment amount footer cell.
*/
function PaymentAmountFooterCell({ rows }) {
const totalPaymentAmount = safeSumBy(rows, 'original.payment_amount');
return <span>{ formattedAmount(totalPaymentAmount, 'USD') }</span>;
}
/**
* Retrieve payment receive form entries columns.
*/
export const usePaymentReceiveEntriesColumns = () => {
const { formatMessage } = useIntl();
return React.useMemo(
() => [
{
Header: '#',
accessor: 'index',
Cell: IndexCell,
width: 40,
disableResizing: true,
disableSortBy: true,
},
{
Header: formatMessage({ id: 'Date' }),
id: 'invoice_date',
accessor: 'invoice_date',
Cell: InvoiceDateCell,
disableSortBy: true,
disableResizing: true,
width: 250,
},
{
Header: formatMessage({ id: 'invocie_number' }),
accessor: InvNumberCellAccessor,
Cell: 'invoice_no',
disableSortBy: true,
className: '',
},
{
Header: formatMessage({ id: 'invoice_amount' }),
accessor: 'balance',
Footer: BalanceFooterCell,
disableSortBy: true,
width: 100,
className: '',
},
{
Header: formatMessage({ id: 'amount_due' }),
accessor: 'due_amount',
Footer: DueAmountFooterCell,
disableSortBy: true,
width: 150,
className: '',
},
{
Header: formatMessage({ id: 'payment_amount' }),
accessor: 'payment_amount',
Footer: PaymentAmountFooterCell,
disableSortBy: true,
width: 150,
className: '',
},
],
[formatMessage],
)
}

View File

@@ -119,3 +119,26 @@ export function useOpenBill(props) {
}, },
); );
} }
/**
* Retrieve the due bills of the given vendor id.
* @param {number} vendorId -
*/
export function useDueBills(vendorId, props) {
const states = useQuery(
['BILLS_DUE', vendorId],
() =>
ApiService.get(`purchases/bills/due`, {
params: { vendor_id: vendorId },
}),
{
select: (res) => res.data.bills,
...props,
},
);
return {
...states,
data: defaultTo(states.data, []),
};
}

View File

@@ -77,8 +77,8 @@ export function useInvoices(query, props) {
total: 0, total: 0,
}, },
filterMeta: {}, filterMeta: {},
}) }),
} };
} }
/** /**
@@ -87,27 +87,25 @@ export function useInvoices(query, props) {
export function useDeliverInvoice(props) { export function useDeliverInvoice(props) {
const queryClient = useQueryClient(); const queryClient = useQueryClient();
return useMutation( return useMutation((id) => ApiService.post(`sales/invoices/${id}/deliver`), {
(id) => ApiService.post(`sales/invoices/${id}/deliver`), onSuccess: (res, id) => {
{ queryClient.invalidateQueries('SALE_INVOICES');
onSuccess: (res, id) => { queryClient.invalidateQueries(['SALE_INVOICE', id]);
queryClient.invalidateQueries('SALE_INVOICES');
queryClient.invalidateQueries(['SALE_INVOICE', id]);
},
...props,
}, },
); ...props,
});
} }
/** /**
* Retrieve the sale invoice details. * Retrieve the sale invoice details.
*/ */
export function useInvoice(id, props) { export function useInvoice(id, props) {
const states = useQuery(['SALE_INVOICE', id], () => const states = useQuery(
ApiService.get(`sales/invoices/${id}`), ['SALE_INVOICE', id],
{ () => ApiService.get(`sales/invoices/${id}`),
{
select: (res) => res.data.sale_invoice, select: (res) => res.data.sale_invoice,
...props ...props,
}, },
); );
@@ -116,3 +114,26 @@ export function useInvoice(id, props) {
data: defaultTo(states.data, {}), data: defaultTo(states.data, {}),
}; };
} }
/**
* Retrieve due invoices of the given customer id.
* @param {number} customerId - Customer id.
*/
export function useDueInvoices(customerId, props) {
const states = useQuery(
['SALE_INVOICE_DUE', customerId],
() =>
ApiService.get(`sales/invoices/payable`, {
params: { customer_id: customerId },
}),
{
select: (res) => res.data.sales_invoices,
...props,
},
);
return {
...states,
data: defaultTo(states.data, []),
};
}

View File

@@ -450,7 +450,7 @@ export default [
), ),
), ),
breadcrumb: 'New Payment Made', breadcrumb: 'New Payment Made',
pageTitle: formatMessage({ id: 'edit_payment_made' }), pageTitle: formatMessage({ id: 'new_payment_made' }),
sidebarShrink: true, sidebarShrink: true,
backLink: true, backLink: true,
}, },