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 { CLASSES } from 'common/classes';
import { Icon } from 'components';
/**

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,153 +1,70 @@
import React, { useState, useEffect, useMemo, useCallback } from 'react';
import { useQuery } from 'react-query';
import { isEmpty } from 'lodash';
import React, { useMemo, useCallback } from 'react';
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 withBillActions from '../Bill/withBillActions';
import withBills from '../Bill/withBills';
import { CLASSES } from 'common/classes';
import { DataTableEditable } from 'components';
import { usePaymentMadeEntriesTableColumns } from './components';
import { compose } from 'utils';
import { usePaymentMadeFormContext } from './PaymentMadeFormProvider';
/**
* Payment made items table.
*/
function PaymentMadeItemsTable({
// #ownProps
paymentMadeId,
vendorId,
fullAmount,
onUpdateData,
paymentEntries = [], // { bill_id: number, payment_amount: number, id?: number }
onClickClearAllLines,
errors,
onFetchEntriesSuccess,
export default function PaymentMadeItemsTable() {
const {
paymentVendorId,
dueBills,
isDueBillsFetching,
isNewMode,
} = usePaymentMadeFormContext();
// #withBillActions
requestFetchDueBills,
// #withBills
vendorPayableBillsEntries,
}) {
const isNewMode = !paymentMadeId;
const columns = usePaymentMadeEntriesTableColumns();
// Detarmines takes vendor payable bills entries in create mode
// or payment made entries in edit mode.
const computedTableEntries = useMemo(
() =>
!isEmpty(paymentEntries)
? paymentEntries
: (vendorPayableBillsEntries || []),
[vendorPayableBillsEntries, paymentEntries],
);
const [tableData, setTableData] = useState(computedTableEntries);
const [localEntries, setLocalEntries] = useState(computedTableEntries);
const [localAmount, setLocalAmount] = useState(fullAmount);
const computedTableEntries = useMemo(() => [], []);
// Triggers `onUpdateData` event that passes changed entries.
const triggerUpdateData = useCallback(
(entries) => {
onUpdateData && onUpdateData(entries);
},
[onUpdateData],
);
const triggerUpdateData = useCallback((entries) => {}, []);
const triggerOnFetchBillsSuccess = useCallback(
(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,
]);
const triggerOnFetchBillsSuccess = useCallback((bills) => {}, []);
// Handle update data.
const handleUpdateData = useCallback(
(rows) => {
setTableData(rows);
triggerUpdateData(rows);
},
[setTableData, triggerUpdateData],
);
const handleUpdateData = useCallback((rows) => {}, []);
// Detarmines the right no results message before selecting vendor and aftering
// selecting vendor id.
const noResultsMessage = vendorId
const noResultsMessage = paymentVendorId
? '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.';
return (
<CloudLoadingIndicator isLoading={fetchVendorDueBills.isFetching}>
<PaymentMadeItemsTableEditor
noResultsMessage={noResultsMessage}
data={tableData}
errors={errors}
onUpdateData={handleUpdateData}
onClickClearAllLines={onClickClearAllLines}
<CloudLoadingIndicator isLoading={isDueBillsFetching}>
<DataTableEditable
progressBarLoading={isDueBillsFetching}
className={classNames(CLASSES.DATATABLE_EDITOR_ITEMS_ENTRIES)}
columns={columns}
data={[]}
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>
);
}
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 PaymentReceiveHeader from './PaymentReceiveFormHeader';
// import PaymentReceiveItemsTable from './PaymentReceiveItemsTable';
import PaymentReceiveItemsTable from './PaymentReceiveItemsTable';
import PaymentReceiveFloatingActions from './PaymentReceiveFloatingActions';
import PaymentReceiveFormFooter from './PaymentReceiveFormFooter';
@@ -167,48 +167,16 @@ function PaymentReceiveForm({
>
<Form>
<PaymentReceiveHeader />
<div className={classNames(CLASSES.PAGE_FORM_BODY)}>
<PaymentReceiveItemsTable />
</div>
<PaymentReceiveFormFooter />
<PaymentReceiveFloatingActions />
</Form>
</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> */}
</div>
);

View File

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

View File

@@ -37,7 +37,12 @@ import { compose } from 'utils';
*/
function PaymentReceiveHeaderFields({ baseCurrency }) {
// Payment receive form context.
const { customers, accounts, isNewMode } = usePaymentReceiveFormContext();
const {
customers,
accounts,
isNewMode,
setPaymentCustomerId,
} = usePaymentReceiveFormContext();
// Formik form context.
const { values } = useFormikContext();
@@ -66,8 +71,9 @@ function PaymentReceiveHeaderFields({ baseCurrency }) {
contactsList={customers}
selectedContactId={value}
defaultSelectText={<T id={'select_customer_account'} />}
onContactSelected={(value) => {
form.setFieldValue('customer_id', value);
onContactSelected={(customer) => {
form.setFieldValue('customer_id', customer);
setPaymentCustomerId(customer.id);
}}
popoverFill={true}
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 { useQuery } from 'react-query';
import { omit } from 'lodash';
import classNames from 'classnames';
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';
import withInvoiceActions from 'containers/Sales/Invoice/withInvoiceActions';
/**
* Payment receive items table.
*/
export default function PaymentReceiveItemsTable() {
// Payment receive form context.
const {
isNewMode,
isDueInvoicesFetching,
paymentCustomerId,
dueInvoices,
} = usePaymentReceiveFormContext();
function PaymentReceiveItemsTable({
// #ownProps
paymentReceiveId,
customerId,
fullAmount,
onUpdateData,
paymentReceiveEntries = [],
errors,
onClickClearAllLines,
onFetchEntriesSuccess,
// #withInvoices
customerInvoiceEntries,
// #withPaymentReceiveActions
requestFetchDueInvoices
}) {
const isNewMode = !paymentReceiveId;
// Payment receive entries form context.
const columns = usePaymentReceiveEntriesColumns();
// Detarmines takes payment receive invoices entries from customer receivable
// invoices or associated payment receive invoices.
const computedTableData = useMemo(() => [
...(!isNewMode ? (paymentReceiveEntries || []) : []),
...(isNewMode ? (customerInvoiceEntries || []) : []),
], [
isNewMode,
paymentReceiveEntries,
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 },
const computedTableData = useMemo(
() => [
...(!isNewMode ? [] || [] : []),
...(isNewMode ? dueInvoices || [] : []),
],
[isNewMode, dueInvoices],
);
// 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) => {
onFetchEntriesSuccess && onFetchEntriesSuccess(bills);
}, [onFetchEntriesSuccess])
// No results message.
const noResultsMessage = paymentCustomerId
? '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);
setTableData(rows);
}, [triggerUpdateData]);
const handleUpdateData = useCallback((rows) => {}, []);
useEffect(() => {
const enabled = isNewMode && customerId;
if (!fetchCustomerDueInvoices.isFetching && enabled) {
triggerOnFetchInvoicesSuccess(computedTableData);
}
}, [
isNewMode,
customerId,
fetchCustomerDueInvoices.isFetching,
computedTableData,
triggerOnFetchInvoicesSuccess,
]);
// Handle click clear all lines button.
const handleClickClearAllLines = () => {
};
return (
<CloudLoadingIndicator isLoading={fetchCustomerDueInvoices.isFetching}>
<PaymentReceiveItemsTableEditor
noResultsMessage={noResultsMessage}
data={tableData}
errors={errors}
onUpdateData={handleUpdateData}
onClickClearAllLines={onClickClearAllLines}
<CloudLoadingIndicator isLoading={isDueInvoicesFetching}>
<DataTableEditable
progressBarLoading={isDueInvoicesFetching}
className={classNames(CLASSES.DATATABLE_EDITOR_ITEMS_ENTRIES)}
columns={columns}
data={[]}
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>
);
}
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],
)
}