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],
)
}