refactoring: payment made form.

This commit is contained in:
a.bouhuolia
2021-02-21 13:00:06 +02:00
parent 265198103d
commit df2d215071
25 changed files with 542 additions and 145 deletions

View File

@@ -4,7 +4,7 @@ import { Intent } from '@blueprintjs/core';
import classNames from 'classnames'; import classNames from 'classnames';
import { useIntl } from 'react-intl'; import { useIntl } from 'react-intl';
import { useHistory } from 'react-router-dom'; import { useHistory } from 'react-router-dom';
import { sumBy, isEmpty, omit } from 'lodash'; import { isEmpty, omit } from 'lodash';
import { CLASSES } from 'common/classes'; import { CLASSES } from 'common/classes';
import { EditBillFormSchema, CreateBillFormSchema } from './BillForm.schema'; import { EditBillFormSchema, CreateBillFormSchema } from './BillForm.schema';
@@ -68,7 +68,7 @@ export default function BillForm() {
const entries = values.entries.filter( const entries = values.entries.filter(
(item) => item.item_id && item.quantity, (item) => item.item_id && item.quantity,
); );
const totalQuantity = safeSumBy(entries, (entry) => entry.quantity); const totalQuantity = safeSumBy(entries, 'quantity');
if (totalQuantity === 0) { if (totalQuantity === 0) {
AppToaster.show({ AppToaster.show({

View File

@@ -1,6 +1,8 @@
import React from 'react'; import React from 'react';
import { DashboardContentTable, DashboardPageContent } from 'components'; import { DashboardContentTable, DashboardPageContent } from 'components';
import 'style/pages/Bills/List.scss';
import { BillsListProvider } from './BillsListProvider'; import { BillsListProvider } from './BillsListProvider';
import BillsActionsBar from './BillsActionsBar'; import BillsActionsBar from './BillsActionsBar';

View File

@@ -9,31 +9,34 @@ import { DataTableEditable } from 'components';
import { usePaymentMadeEntriesTableColumns } from './components'; import { usePaymentMadeEntriesTableColumns } from './components';
import { usePaymentMadeFormContext } from './PaymentMadeFormProvider'; import { usePaymentMadeFormContext } from './PaymentMadeFormProvider';
import { compose, updateTableRow, safeSumBy } from 'utils';
import withAlertActions from 'containers/Alert/withAlertActions';
/** /**
* Payment made items table. * Payment made items table.
*/ */
export default function PaymentMadeItemsTable() { function PaymentMadeEntriesTable({
onUpdateData,
entries,
// #withAlertsActions
openAlert
}) {
const { const {
paymentVendorId, paymentVendorId,
dueBills,
isDueBillsFetching, isDueBillsFetching,
isNewMode,
} = usePaymentMadeFormContext(); } = usePaymentMadeFormContext();
const columns = usePaymentMadeEntriesTableColumns(); const columns = usePaymentMadeEntriesTableColumns();
// Detarmines takes vendor payable bills entries in create mode
// or payment made entries in edit mode.
const computedTableEntries = useMemo(() => [], []);
// Triggers `onUpdateData` event that passes changed entries.
const triggerUpdateData = useCallback((entries) => {}, []);
const triggerOnFetchBillsSuccess = useCallback((bills) => {}, []);
// Handle update data. // Handle update data.
const handleUpdateData = useCallback((rows) => {}, []); const handleUpdateData = useCallback((rowIndex, columnId, value) => {
const newRows = compose(
updateTableRow(rowIndex, columnId, value),
)(entries);
onUpdateData(newRows);
}, [onUpdateData, entries]);
// 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.
@@ -41,13 +44,22 @@ export default function PaymentMadeItemsTable() {
? '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.';
// Handle clear all lines action.
const handleClearAllLines = () => {
const fullAmount = safeSumBy(entries, 'payment_amount');
if (fullAmount > 0) {
openAlert('clear-all-lines-payment-made');
}
}
return ( return (
<CloudLoadingIndicator isLoading={isDueBillsFetching}> <CloudLoadingIndicator isLoading={isDueBillsFetching}>
<DataTableEditable <DataTableEditable
progressBarLoading={isDueBillsFetching} progressBarLoading={isDueBillsFetching}
className={classNames(CLASSES.DATATABLE_EDITOR_ITEMS_ENTRIES)} className={classNames(CLASSES.DATATABLE_EDITOR_ITEMS_ENTRIES)}
columns={columns} columns={columns}
data={[]} data={entries}
spinnerProps={false} spinnerProps={false}
payload={{ payload={{
errors: [], errors: [],
@@ -58,7 +70,7 @@ export default function PaymentMadeItemsTable() {
<Button <Button
small={true} small={true}
className={'button--secondary button--clear-lines'} className={'button--secondary button--clear-lines'}
// onClick={handleClickClearAllLines} onClick={handleClearAllLines}
> >
<T id={'clear_all_lines'} /> <T id={'clear_all_lines'} />
</Button> </Button>
@@ -68,3 +80,7 @@ export default function PaymentMadeItemsTable() {
</CloudLoadingIndicator> </CloudLoadingIndicator>
); );
} }
export default compose(
withAlertActions
)(PaymentMadeEntriesTable);

View File

@@ -2,7 +2,7 @@ import React, { useMemo } from 'react';
import { Formik, Form } from 'formik'; import { Formik, Form } from 'formik';
import { Intent } from '@blueprintjs/core'; import { Intent } from '@blueprintjs/core';
import { useIntl } from 'react-intl'; import { useIntl } from 'react-intl';
import { sumBy, omit } from 'lodash'; import { sumBy, pick } from 'lodash';
import classNames from 'classnames'; import classNames from 'classnames';
import { useHistory } from 'react-router-dom'; import { useHistory } from 'react-router-dom';
@@ -11,7 +11,8 @@ 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 PaymentMadeFormBody from './PaymentMadeFormBody';
import { PaymentMadeInnerProvider } from './PaymentMadeInnerProvider';
import withSettings from 'containers/Settings/withSettings'; import withSettings from 'containers/Settings/withSettings';
import { import {
@@ -32,7 +33,9 @@ function PaymentMadeForm() {
// Payment made form context. // Payment made form context.
const { const {
isNewMode, isNewMode,
paymentMade, paymentMadeId,
paymentMadeEditPage,
paymentEntriesEditPage,
submitPayload, submitPayload,
createPaymentMadeMutate, createPaymentMadeMutate,
editPaymentMadeMutate, editPaymentMadeMutate,
@@ -43,14 +46,14 @@ function PaymentMadeForm() {
() => ({ () => ({
...(!isNewMode ...(!isNewMode
? { ? {
...transformToEditForm(paymentMade, []), ...transformToEditForm(paymentMadeEditPage, paymentEntriesEditPage),
} }
: { : {
...defaultPaymentMade, ...defaultPaymentMade,
entries: orderingLinesIndexes(defaultPaymentMade.entries), entries: orderingLinesIndexes(defaultPaymentMade.entries),
}), }),
}), }),
[isNewMode, paymentMade], [isNewMode, paymentMadeEditPage, paymentEntriesEditPage],
); );
// Handle the form submit. // Handle the form submit.
@@ -62,9 +65,9 @@ function PaymentMadeForm() {
// Filters entries that have no `bill_id` or `payment_amount`. // Filters entries that have no `bill_id` or `payment_amount`.
const entries = values.entries const entries = values.entries
.filter((item) => !item.bill_id || item.payment_amount) .filter((item) => item.bill_id && item.payment_amount)
.map((entry) => ({ .map((entry) => ({
...omit(entry, ['due_amount']), ...pick(entry, ['payment_amount', 'bill_id']),
})); }));
// Total payment amount of entries. // Total payment amount of entries.
const totalPaymentAmount = sumBy(entries, 'payment_amount'); const totalPaymentAmount = sumBy(entries, 'payment_amount');
@@ -96,7 +99,11 @@ function PaymentMadeForm() {
submitPayload.resetForm && resetForm(); submitPayload.resetForm && resetForm();
}; };
const onError = ({ response: { error: { data: errors } } }) => { const onError = ({
response: {
error: { data: errors },
},
}) => {
const getError = (errorType) => errors.find((e) => e.type === errorType); const getError = (errorType) => errors.find((e) => e.type === errorType);
if (getError(ERRORS.PAYMENT_NUMBER_NOT_UNIQUE)) { if (getError(ERRORS.PAYMENT_NUMBER_NOT_UNIQUE)) {
@@ -109,7 +116,7 @@ function PaymentMadeForm() {
}; };
if (!isNewMode) { if (!isNewMode) {
editPaymentMadeMutate([paymentMade.id, form]) editPaymentMadeMutate([paymentMadeId, form])
.then(onSaved) .then(onSaved)
.catch(onError); .catch(onError);
} else { } else {
@@ -133,13 +140,12 @@ function PaymentMadeForm() {
onSubmit={handleSubmitForm} onSubmit={handleSubmitForm}
> >
<Form> <Form>
<PaymentMadeHeader /> <PaymentMadeInnerProvider>
<PaymentMadeHeader />
<div className={classNames(CLASSES.PAGE_FORM_BODY)}> <PaymentMadeFormBody />
<PaymentMadeItemsTable /> <PaymentMadeFooter />
</div> <PaymentMadeFloatingActions />
<PaymentMadeFooter /> </PaymentMadeInnerProvider>
<PaymentMadeFloatingActions />
</Form> </Form>
</Formik> </Formik>
</div> </div>

View File

@@ -0,0 +1,23 @@
import React from 'react';
import { FastField } from 'formik';
import classNames from 'classnames';
import { CLASSES } from 'common/classes';
import PaymentMadeEntriesTable from './PaymentMadeEntriesTable';
export default function PaymentMadeFormBody() {
return (
<div className={classNames(CLASSES.PAGE_FORM_BODY)}>
<FastField name={'entries'}>
{({ form, field: { value }, meta: { error, touched } }) => (
<PaymentMadeEntriesTable
entries={value}
onUpdateData={(newEntries) => {
form.setFieldValue('entries', newEntries);
}}
/>
)}
</FastField>
</div>
)
}

View File

@@ -18,10 +18,10 @@ function PaymentMadeFormHeader({
baseCurrency, baseCurrency,
}) { }) {
// Formik form context. // Formik form context.
const { values } = useFormikContext(); const { values: { entries } } = useFormikContext();
// Calculate the payment amount of the entries. // Calculate the payment amount of the entries.
const amountPaid = useMemo(() => sumBy(values, 'payment_amount'), [values]); const amountPaid = useMemo(() => sumBy(entries, 'payment_amount'), [entries]);
return ( return (
<div className={classNames(CLASSES.PAGE_FORM_HEADER)}> <div className={classNames(CLASSES.PAGE_FORM_HEADER)}>

View File

@@ -1,25 +1,27 @@
import React from 'react'; import React, { useMemo } from 'react';
import { import {
FormGroup, FormGroup,
InputGroup, InputGroup,
Position, Position,
Classes, Classes,
ControlGroup, ControlGroup,
Button
} from '@blueprintjs/core'; } from '@blueprintjs/core';
import { DateInput } from '@blueprintjs/datetime'; import { DateInput } from '@blueprintjs/datetime';
import { FastField } from 'formik'; import { FastField, Field, useFormikContext, ErrorMessage } from 'formik';
import { FormattedMessage as T } from 'react-intl'; import { FormattedMessage as T } from 'react-intl';
import { toSafeInteger } from 'lodash';
import classNames from 'classnames'; import classNames from 'classnames';
import { CLASSES } from 'common/classes'; import { CLASSES } from 'common/classes';
import { import {
AccountsSelectList, AccountsSelectList,
ContactSelecetList, ContactSelecetList,
ErrorMessage,
FieldRequiredHint, FieldRequiredHint,
InputPrependText, InputPrependText,
Money, Money,
Hint, Hint,
Icon, Icon,
MoneyInputGroup
} from 'components'; } from 'components';
import withSettings from 'containers/Settings/withSettings'; import withSettings from 'containers/Settings/withSettings';
import { usePaymentMadeFormContext } from './PaymentMadeFormProvider'; import { usePaymentMadeFormContext } from './PaymentMadeFormProvider';
@@ -28,12 +30,19 @@ import {
tansformDateValue, tansformDateValue,
inputIntent, inputIntent,
compose, compose,
safeSumBy,
fullAmountPaymentEntries,
amountPaymentEntries,
} from 'utils'; } from 'utils';
/** /**
* Payment made form header fields. * Payment made form header fields.
*/ */
function PaymentMadeFormHeaderFields({ baseCurrency }) { function PaymentMadeFormHeaderFields({ baseCurrency }) {
// Formik form context.
const { values: { entries }, setFieldValue } = useFormikContext();
// Payment made form context.
const { const {
vendors, vendors,
accounts, accounts,
@@ -41,7 +50,23 @@ function PaymentMadeFormHeaderFields({ baseCurrency }) {
setPaymentVendorId, setPaymentVendorId,
} = usePaymentMadeFormContext(); } = usePaymentMadeFormContext();
const payableFullAmount = 0; // Sumation of payable full-amount.
const payableFullAmount = useMemo(() => safeSumBy(entries, 'due_amount'), [entries]);
// Handle receive full-amount click.
const handleReceiveFullAmountClick = () => {
const newEntries = fullAmountPaymentEntries(entries);
const fullAmount = safeSumBy(newEntries, 'payment_amount');
setFieldValue('entries', newEntries);
setFieldValue('full_amount', fullAmount);
};
// Handles the full-amount field blur.
const onFullAmountBlur = (value) => {
const newEntries = amountPaymentEntries(toSafeInteger(value), entries);
setFieldValue('entries', newEntries);
};
return ( return (
<div className={classNames(CLASSES.PAGE_FORM_HEADER_FIELDS)}> <div className={classNames(CLASSES.PAGE_FORM_HEADER_FIELDS)}>
@@ -96,8 +121,8 @@ function PaymentMadeFormHeaderFields({ baseCurrency }) {
</FastField> </FastField>
{/* ------------ Full amount ------------ */} {/* ------------ Full amount ------------ */}
<FastField name={'full_amount'}> <Field name={'full_amount'}>
{({ form, field, meta: { error, touched } }) => ( {({ form, field: { value }, meta: { error, touched } }) => (
<FormGroup <FormGroup
label={<T id={'full_amount'} />} label={<T id={'full_amount'} />}
inline={true} inline={true}
@@ -108,24 +133,27 @@ function PaymentMadeFormHeaderFields({ baseCurrency }) {
> >
<ControlGroup> <ControlGroup>
<InputPrependText text={baseCurrency} /> <InputPrependText text={baseCurrency} />
<InputGroup <MoneyInputGroup
intent={inputIntent({ error, touched })} value={value}
minimal={true} onChange={(value) => {
{...field} setFieldValue('full_amount', value);
}}
onBlurValue={onFullAmountBlur}
/> />
</ControlGroup> </ControlGroup>
<a <Button
// onClick={handleReceiveFullAmountClick} onClick={handleReceiveFullAmountClick}
href="#"
className={'receive-full-amount'} className={'receive-full-amount'}
small={true}
minimal={true}
> >
Receive full amount ( Receive full amount (
<Money amount={payableFullAmount} currency={baseCurrency} />) <Money amount={payableFullAmount} currency={baseCurrency} />)
</a> </Button>
</FormGroup> </FormGroup>
)} )}
</FastField> </Field>
{/* ------------ Payment number ------------ */} {/* ------------ Payment number ------------ */}
<FastField name={'payment_number'}> <FastField name={'payment_number'}>

View File

@@ -3,11 +3,10 @@ import {
useAccounts, useAccounts,
useVendors, useVendors,
useItems, useItems,
usePaymentMade, usePaymentMadeEditPage,
useSettings, useSettings,
useCreatePaymentMade, useCreatePaymentMade,
useEditPaymentMade, useEditPaymentMade,
useDueBills,
} from 'hooks/query'; } from 'hooks/query';
import { DashboardInsider } from 'components'; import { DashboardInsider } from 'components';
@@ -39,20 +38,13 @@ function PaymentMadeFormProvider({ paymentMadeId, ...props }) {
// Handle fetch specific payment made details. // Handle fetch specific payment made details.
const { const {
data: { paymentMade, payableBills, paymentBills }, data: { paymentMade: paymentMadeEditPage, entries: paymentEntriesEditPage },
isFetching: isPaymentFetching, isFetching: isPaymentFetching,
isLoading: isPaymentLoading, isLoading: isPaymentLoading,
} = usePaymentMade(paymentMadeId, { } = usePaymentMadeEditPage(paymentMadeId, {
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();
@@ -66,12 +58,10 @@ function PaymentMadeFormProvider({ paymentMadeId, ...props }) {
const provider = { const provider = {
paymentMadeId, paymentMadeId,
accounts, accounts,
paymentMade, paymentEntriesEditPage,
payableBills, paymentMadeEditPage,
paymentBills,
vendors, vendors,
items, items,
dueBills,
submitPayload, submitPayload,
paymentVendorId, paymentVendorId,
@@ -82,8 +72,6 @@ function PaymentMadeFormProvider({ paymentMadeId, ...props }) {
isVendorsFetching, isVendorsFetching,
isPaymentFetching, isPaymentFetching,
isPaymentLoading, isPaymentLoading,
isDueBillsLoading,
isDueBillsFetching,
createPaymentMadeMutate, createPaymentMadeMutate,
editPaymentMadeMutate, editPaymentMadeMutate,

View File

@@ -0,0 +1,47 @@
import { useFormikContext } from 'formik';
import { isEmpty } from 'lodash';
import React, { createContext, useContext, useEffect } from 'react';
import { usePaymentMadeNewPageEntries } from 'hooks/query';
import { usePaymentMadeFormContext } from './PaymentMadeFormProvider';
const PaymentMadeInnerContext = createContext();
/**
* Payment made inner form provider.
*/
function PaymentMadeInnerProvider({ ...props }) {
const { isNewMode } = usePaymentMadeFormContext();
// Formik context.
const {
values: { vendor_id: vendorId },
setFieldValue,
} = useFormikContext();
const {
data: newPageEntries,
isLoading: isNewEntriesLoading,
isFetching: isNewEntriesFetching,
} = usePaymentMadeNewPageEntries(vendorId, {
enabled: !!vendorId && isNewMode,
});
useEffect(() => {
if (!isNewEntriesFetching && !isEmpty(newPageEntries)) {
setFieldValue('entries', newPageEntries)
}
}, [isNewEntriesFetching, newPageEntries, setFieldValue]);
// Provider payload.
const provider = {
newPageEntries,
isNewEntriesLoading,
isNewEntriesFetching
};
return <PaymentMadeInnerContext.Provider value={provider} {...props} />;
}
const usePaymentMadeInnerContext = () => useContext(PaymentMadeInnerContext);
export { PaymentMadeInnerProvider, usePaymentMadeInnerContext };

View File

@@ -3,16 +3,17 @@ import { useIntl } from "react-intl";
import moment from 'moment'; import moment from 'moment';
import { Money } from 'components'; import { Money } from 'components';
import { safeSumBy, formattedAmount } from 'utils'; import { safeSumBy, formattedAmount } from 'utils';
import { MoneyFieldCell } from 'components/DataTableCells';
function BillNumberAccessor(row) { function BillNumberAccessor(row) {
return `#${row?.bill_number || ''}` return row?.bill_no ? row?.bill_no : '-';
} }
function IndexTableCell({ row: { index } }) { function IndexTableCell({ row: { index } }) {
return (<span>{index + 1}</span>); return (<span>{index + 1}</span>);
} }
function BillDateTableCell({ value }) { function BillDateCell({ value }) {
return moment(value).format('YYYY MMM DD'); return moment(value).format('YYYY MMM DD');
} }
/** /**
@@ -39,6 +40,14 @@ function PaymentAmountFooterCell({ rows }) {
return <span>{ formattedAmount(totalPaymentAmount, 'USD') }</span>; return <span>{ formattedAmount(totalPaymentAmount, 'USD') }</span>;
} }
/**
* Mobey table cell.
*/
function MoneyTableCell({ value }) {
return <Money amount={value} currency={"USD"} />
}
/** /**
* Payment made entries table columns * Payment made entries table columns
*/ */
@@ -54,13 +63,15 @@ export function usePaymentMadeEntriesTableColumns() {
width: 40, width: 40,
disableResizing: true, disableResizing: true,
disableSortBy: true, disableSortBy: true,
className: 'index'
}, },
{ {
Header: formatMessage({ id: 'Date' }), Header: formatMessage({ id: 'Date' }),
id: 'bill_date', id: 'bill_date',
accessor: 'bill_date', accessor: 'bill_date',
Cell: BillDateTableCell, Cell: BillDateCell,
disableSortBy: true, disableSortBy: true,
width: 250,
}, },
{ {
Header: formatMessage({ id: 'bill_number' }), Header: formatMessage({ id: 'bill_number' }),
@@ -71,6 +82,7 @@ export function usePaymentMadeEntriesTableColumns() {
{ {
Header: formatMessage({ id: 'bill_amount' }), Header: formatMessage({ id: 'bill_amount' }),
accessor: 'amount', accessor: 'amount',
Cell: MoneyTableCell,
Footer: AmountFooterCell, Footer: AmountFooterCell,
disableSortBy: true, disableSortBy: true,
className: '', className: '',
@@ -78,6 +90,7 @@ export function usePaymentMadeEntriesTableColumns() {
{ {
Header: formatMessage({ id: 'amount_due' }), Header: formatMessage({ id: 'amount_due' }),
accessor: 'due_amount', accessor: 'due_amount',
Cell: MoneyTableCell,
Footer: DueAmountFooterCell, Footer: DueAmountFooterCell,
disableSortBy: true, disableSortBy: true,
className: '', className: '',
@@ -85,6 +98,7 @@ export function usePaymentMadeEntriesTableColumns() {
{ {
Header: formatMessage({ id: 'payment_amount' }), Header: formatMessage({ id: 'payment_amount' }),
accessor: 'payment_amount', accessor: 'payment_amount',
Cell: MoneyFieldCell,
Footer: PaymentAmountFooterCell, Footer: PaymentAmountFooterCell,
disableSortBy: true, disableSortBy: true,
className: '', className: '',

View File

@@ -1,12 +1,10 @@
import moment from 'moment'; import moment from 'moment';
import { sumBy } from 'lodash'; import { safeSumBy, transformToForm } from 'utils';
import { transformToForm } from 'utils';
export const ERRORS = { export const ERRORS = {
PAYMENT_NUMBER_NOT_UNIQUE: 'PAYMENT.NUMBER.NOT.UNIQUE', PAYMENT_NUMBER_NOT_UNIQUE: 'PAYMENT.NUMBER.NOT.UNIQUE',
}; };
// Default payment made entry values. // Default payment made entry values.
export const defaultPaymentMadeEntry = { export const defaultPaymentMadeEntry = {
bill_id: '', bill_id: '',
@@ -30,7 +28,7 @@ export const defaultPaymentMade = {
export const transformToEditForm = (paymentMade, paymentMadeEntries) => { export const transformToEditForm = (paymentMade, paymentMadeEntries) => {
return { return {
...transformToForm(paymentMade, defaultPaymentMade), ...transformToForm(paymentMade, defaultPaymentMade),
full_amount: sumBy(paymentMade.entries, 'payment_amount'), full_amount: safeSumBy(paymentMadeEntries, 'payment_amount'),
entries: [ entries: [
...paymentMadeEntries.map((paymentMadeEntry) => ({ ...paymentMadeEntries.map((paymentMadeEntry) => ({
...transformToForm(paymentMadeEntry, defaultPaymentMadeEntry), ...transformToForm(paymentMadeEntry, defaultPaymentMadeEntry),

View File

@@ -1,4 +1,7 @@
import React from 'react'; import React from 'react';
import 'style/pages/PaymentMade/List.scss';
import { DashboardContentTable, DashboardPageContent } from 'components'; import { DashboardContentTable, DashboardPageContent } from 'components';
import PaymentMadeActionsBar from './PaymentMadeActionsBar'; import PaymentMadeActionsBar from './PaymentMadeActionsBar';
import PaymentMadesAlerts from '../PaymentMadesAlerts'; import PaymentMadesAlerts from '../PaymentMadesAlerts';

View File

@@ -27,7 +27,7 @@ export function usePaymentMades(query, props) {
data: defaultTo(states.data, { data: defaultTo(states.data, {
paymentMades: [], paymentMades: [],
pagination: {}, pagination: {},
filterMeta: {} filterMeta: {},
}), }),
}; };
} }
@@ -42,8 +42,9 @@ export function useCreatePaymentMade(props) {
return useMutation( return useMutation(
(values) => apiRequest.post('purchases/bill_payments', values), (values) => apiRequest.post('purchases/bill_payments', values),
{ {
onSuccess: () => { onSuccess: (res, values) => {
client.invalidateQueries('PAYMENT_MADES'); client.invalidateQueries('PAYMENT_MADES');
client.invalidateQueries(['PAYMENT_MADE_NEW_PAGE_ENTRIES', values.vendor_id]);
}, },
...props, ...props,
}, },
@@ -63,6 +64,8 @@ export function useEditPaymentMade(props) {
onSuccess: (res, [id, values]) => { onSuccess: (res, [id, values]) => {
client.invalidateQueries('PAYMENT_MADES'); client.invalidateQueries('PAYMENT_MADES');
client.invalidateQueries(['PAYMENT_MADE', id]); client.invalidateQueries(['PAYMENT_MADE', id]);
client.invalidateQueries(['PAYMENT_MADE_NEW_PAGE_ENTRIES', values.vendor_id]);
}, },
...props, ...props,
}, },
@@ -82,6 +85,7 @@ export function useDeletePaymentMade(props) {
onSuccess: (res, id) => { onSuccess: (res, id) => {
client.invalidateQueries('PAYMENT_MADES'); client.invalidateQueries('PAYMENT_MADES');
client.invalidateQueries(['PAYMENT_MADE', id]); client.invalidateQueries(['PAYMENT_MADE', id]);
}, },
...props, ...props,
}, },
@@ -91,17 +95,16 @@ export function useDeletePaymentMade(props) {
/** /**
* Retrieve specific payment made. * Retrieve specific payment made.
*/ */
export function usePaymentMade(id, props) { export function usePaymentMadeEditPage(id, props) {
const apiRequest = useApiRequest(); const apiRequest = useApiRequest();
const states = useQuery( const states = useQuery(
['PAYMENT_MADE', id], ['PAYMENT_MADE', id],
() => apiRequest.get(`purchases/bill_payments/${id}`), () => apiRequest.get(`purchases/bill_payments/${id}/edit-page`),
{ {
select: res => ({ select: (res) => ({
paymentMade: res.data.bill_payment, paymentMade: res.data.bill_payment,
payableBills: res.data.payable_bills, entries: res.data.entries,
paymentBills: res.data.payment_bills,
}), }),
...props, ...props,
}, },
@@ -112,3 +115,23 @@ export function usePaymentMade(id, props) {
data: defaultTo(states.data, {}), data: defaultTo(states.data, {}),
}; };
} }
/**
* Retreive payment made new page entries.
* @param {number} vendorId -
*/
export function usePaymentMadeNewPageEntries(vendorId, props) {
const apiRequest = useApiRequest();
return useQuery(
['PAYMENT_MADE_NEW_PAGE_ENTRIES', vendorId],
() =>
apiRequest.get(`purchases/bill_payments/new-page/entries`, {
params: { vendor_id: vendorId },
}),
{
select: (res) => res.data.entries,
...props,
},
);
}

View File

@@ -26,7 +26,7 @@ export default function useApiRequest() {
const locale = 'en'; const locale = 'en';
if (token) { if (token) {
request.headers.common['x-access-token'] = token; request.headers.common['X-Access-Token'] = token;
} }
if (organizationId) { if (organizationId) {
request.headers.common['organization-id'] = organizationId; request.headers.common['organization-id'] = organizationId;

View File

@@ -0,0 +1,18 @@
.dashboard__insider--bills{
.bigcapital-datatable{
.tbody{
.td.amount {
.cell-inner{
> span{
font-weight: 600;
}
}
}
}
}
}

View File

@@ -0,0 +1,18 @@
.dashboard__insider--payment-mades-list{
.bigcapital-datatable{
.tbody{
.td.amount {
.cell-inner{
> span{
font-weight: 600;
}
}
}
}
}
}

View File

@@ -19,10 +19,18 @@
&.bp3-inline{ &.bp3-inline{
max-width: 470px; max-width: 470px;
} }
a.receive-full-amount{ button.receive-full-amount{
width: auto;
padding: 0;
min-height: auto;
font-size: 12px; font-size: 12px;
margin-top: 6px; margin-top: 4px;
display: inline-block; background-color: transparent;
color: #0052cc;
&:hover{
text-decoration: underline;
}
} }
} }
} }

View File

@@ -577,3 +577,27 @@ export function safeSumBy(entries, getter) {
.sum() .sum()
.value(); .value();
} }
export const fullAmountPaymentEntries = (entries) => {
return entries.map((item) => ({
...item,
payment_amount: item.due_amount,
}));
}
export const amountPaymentEntries = (amount, entries) => {
let total = amount;
return entries.map((item) => {
const diff = Math.min(item.due_amount, total);
total -= Math.max(diff, 0);
return {
...item,
payment_amount: diff,
};
});
};

View File

@@ -17,14 +17,32 @@ export default class FinancialStatementsService {
router() { router() {
const router = Router(); const router = Router();
router.use('/balance_sheet', Container.get(BalanceSheetController).router()); router.use(
router.use('/profit_loss_sheet', Container.get(ProfitLossController).router()); '/balance_sheet',
router.use('/general_ledger', Container.get(GeneralLedgerController).router()); Container.get(BalanceSheetController).router()
router.use('/trial_balance_sheet', Container.get(TrialBalanceSheetController).router()); );
router.use(
'/profit_loss_sheet',
Container.get(ProfitLossController).router()
);
router.use(
'/general_ledger',
Container.get(GeneralLedgerController).router()
);
router.use(
'/trial_balance_sheet',
Container.get(TrialBalanceSheetController).router()
);
router.use('/journal', Container.get(JournalSheetController).router()); router.use('/journal', Container.get(JournalSheetController).router());
router.use('/receivable_aging_summary', Container.get(ARAgingSummary).router()); router.use(
router.use('/payable_aging_summary', Container.get(APAgingSummary).router()); '/receivable_aging_summary',
Container.get(ARAgingSummary).router()
);
router.use(
'/payable_aging_summary',
Container.get(APAgingSummary).router()
);
return router; return router;
} }
}; }

View File

@@ -8,6 +8,7 @@ import BillPaymentsService from 'services/Purchases/BillPayments';
import DynamicListingService from 'services/DynamicListing/DynamicListService'; import DynamicListingService from 'services/DynamicListing/DynamicListService';
import AccountsService from 'services/Accounts/AccountsService'; import AccountsService from 'services/Accounts/AccountsService';
import ResourceController from '../Resources'; import ResourceController from '../Resources';
import { Request } from 'express-validator/src/base';
/** /**
* Bills payments controller. * Bills payments controller.
@@ -30,6 +31,20 @@ export default class BillsPayments extends BaseController {
router() { router() {
const router = Router(); const router = Router();
router.get(
'/new-page/entries',
[query('vendor_id').exists()],
this.validationResult,
asyncMiddleware(this.getBillPaymentNewPageEntries.bind(this)),
this.handleServiceError
);
router.get(
'/:id/edit-page',
this.specificBillPaymentValidateSchema,
this.validationResult,
asyncMiddleware(this.getBillPaymentEditPage.bind(this)),
this.handleServiceError
);
router.post( router.post(
'/', '/',
[...this.billPaymentSchemaValidation], [...this.billPaymentSchemaValidation],
@@ -76,6 +91,7 @@ export default class BillsPayments extends BaseController {
this.handleServiceError, this.handleServiceError,
this.dynamicListService.handlerErrorsToResponse this.dynamicListService.handlerErrorsToResponse
); );
return router; return router;
} }
@@ -118,6 +134,53 @@ export default class BillsPayments extends BaseController {
]; ];
} }
/**
* Retrieve bill payment new page entries.
* @param {Request} req -
* @param {Response} res -
*/
async getBillPaymentNewPageEntries(req: Request, res: Response) {
const { tenantId } = req;
const { vendorId } = this.matchedQueryData(req);
try {
const entries = await this.billPaymentService.getNewPageEntries(
tenantId,
vendorId
);
return res.status(200).send({
entries: this.transfromToResponse(entries),
});
} catch (error) {}
}
/**
* Retrieve the bill payment edit page details.
* @param {Request} req
* @param {Response} res
*/
async getBillPaymentEditPage(req: Request, res: Response, next: NextFunction) {
const { tenantId } = req;
const { id: paymentReceiveId } = req.params;
try {
const {
billPayment,
entries,
} = await this.billPaymentService.getBillPaymentEditPage(
tenantId,
paymentReceiveId,
);
return res.status(200).send({
bill_payment: this.transfromToResponse(billPayment),
entries: this.transfromToResponse(entries),
});
} catch (error) {
next(error);
}
}
/** /**
* Creates a bill payment. * Creates a bill payment.
* @async * @async

View File

@@ -54,6 +54,15 @@ export default class PaymentReceivesController extends BaseController {
asyncMiddleware(this.getPaymentReceiveEditPage.bind(this)), asyncMiddleware(this.getPaymentReceiveEditPage.bind(this)),
this.handleServiceErrors this.handleServiceErrors
); );
router.get(
'/new-page/entries',
[
query('customer_id').exists().isNumeric().toInt(),
],
this.validationResult,
asyncMiddleware(this.getPaymentReceiveNewPageEntries.bind(this)),
this.getPaymentReceiveNewPageEntries.bind(this)
);
router.get( router.get(
'/', '/',
this.validatePaymentReceiveList, this.validatePaymentReceiveList,
@@ -303,6 +312,26 @@ export default class PaymentReceivesController extends BaseController {
} }
} }
/**
* Retrieve payment receive new page receivable entries.
* @param {Request} req - Request.
* @param {Response} res - Response.
*/
async getPaymentReceiveNewPageEntries(req, res) {
const { tenantId } = req;
const { customerId } = this.matchedQueryData(req);
try {
const entries = await this.paymentReceiveService.getNewPageEntries(
tenantId,
customerId
);
return res.status(200).send({
entries: this.transfromToResponse(entries),
});
} catch (error) {}
}
/** /**
* Handles service errors. * Handles service errors.
* @param error * @param error

View File

@@ -33,3 +33,14 @@ export interface IBillPaymentDTO {
reference: string, reference: string,
entries: IBillPaymentEntryDTO[], entries: IBillPaymentEntryDTO[],
}; };
export interface IBillReceivePageEntry {
billId: number,
entryType: string,
billNo: string,
dueAmount: number,
amount: number,
totalPaymentAmount: number,
paymentAmount: number,
date: Date|string,
};

View File

@@ -52,7 +52,7 @@ export interface IPaymentReceivesFilter extends IDynamicListFilterDTO {
stringifiedFilterRoles?: string, stringifiedFilterRoles?: string,
} }
export interface IPaymentReceiveEditPageEntry { export interface IPaymentReceivePageEntry {
invoiceId: number, invoiceId: number,
entryType: string, entryType: string,
invoiceNo: string, invoiceNo: string,
@@ -65,5 +65,5 @@ export interface IPaymentReceiveEditPageEntry {
export interface IPaymentReceiveEditPage { export interface IPaymentReceiveEditPage {
paymentReceive: IPaymentReceive, paymentReceive: IPaymentReceive,
entries: IPaymentReceiveEditPageEntry[]; entries: IPaymentReceivePageEntry[];
}; };

View File

@@ -15,6 +15,7 @@ import {
IPaginationMeta, IPaginationMeta,
IFilterMeta, IFilterMeta,
IBillPaymentEntry, IBillPaymentEntry,
IBillReceivePageEntry,
} from 'interfaces'; } from 'interfaces';
import AccountsService from 'services/Accounts/AccountsService'; import AccountsService from 'services/Accounts/AccountsService';
import JournalPoster from 'services/Accounting/JournalPoster'; import JournalPoster from 'services/Accounting/JournalPoster';
@@ -26,6 +27,7 @@ import DynamicListingService from 'services/DynamicListing/DynamicListService';
import { entriesAmountDiff, formatDateFields } from 'utils'; import { entriesAmountDiff, formatDateFields } from 'utils';
import { ServiceError } from 'exceptions'; import { ServiceError } from 'exceptions';
import { ACCOUNT_PARENT_TYPE } from 'data/AccountTypes'; import { ACCOUNT_PARENT_TYPE } from 'data/AccountTypes';
import PayableAgingSummaryService from 'services/FinancialStatements/AgingSummary/APAgingSummaryService';
const ERRORS = { const ERRORS = {
BILL_VENDOR_NOT_FOUND: 'VENDOR_NOT_FOUND', BILL_VENDOR_NOT_FOUND: 'VENDOR_NOT_FOUND',
@@ -598,48 +600,43 @@ export default class BillPaymentsService {
* @param {number} billPaymentId - The bill payment id. * @param {number} billPaymentId - The bill payment id.
* @return {object} * @return {object}
*/ */
public async getBillPayment( public async getBillPaymentEditPage(
tenantId: number, tenantId: number,
billPaymentId: number billPaymentId: number
): Promise<{ ): Promise<{
billPayment: IBillPayment; billPayment: Omit<IBillPayment, "entries">;
payableBills: IBill[]; entries: IBillReceivePageEntry[];
paymentMadeBills: IBill[];
}> { }> {
const { BillPayment, Bill } = this.tenancy.models(tenantId); const { BillPayment, Bill } = this.tenancy.models(tenantId);
const billPayment = await BillPayment.query() const billPayment = await BillPayment.query()
.findById(billPaymentId) .findById(billPaymentId)
.withGraphFetched('entries.bill') .withGraphFetched('entries.bill');
.withGraphFetched('vendor')
.withGraphFetched('paymentAccount');
// Throw not found the bill payment.
if (!billPayment) { if (!billPayment) {
throw new ServiceError(ERRORS.PAYMENT_MADE_NOT_FOUND); throw new ServiceError(ERRORS.PAYMENT_MADE_NOT_FOUND);
} }
const billsIds = billPayment.entries.map((entry) => entry.billId); const paymentEntries = billPayment.entries.map((entry) => ({
...this.mapBillToPageEntry(entry.bill),
// Retrieve all payable bills that assocaited to the payment made transaction. paymentAmount: entry.paymentAmount,
const payableBills = await Bill.query()
.modify('dueBills')
.whereNotIn('id', billsIds)
.where('vendor_id', billPayment.vendorId)
.orderBy('bill_date', 'ASC');
// Retrieve all payment made assocaited bills.
const paymentMadeBills = billPayment.entries.map((entry) => ({
...entry.bill,
dueAmount: entry.bill.dueAmount + entry.paymentAmount,
})); }));
const resPayableBills = await Bill.query()
.modify('dueBills')
.where('vendor_id', billPayment.vendorId)
.whereNotIn(
'id',
billPayment.entries.map((e) => e.billId),
)
.orderBy('bill_date', 'ASC');
// Mapping the payable bills to entries.
const restPayableEntries = resPayableBills.map(this.mapBillToPageEntry);
const entries = [...paymentEntries, ...restPayableEntries];
return { return {
billPayment: { billPayment: omit(billPayment, ['entries']),
...billPayment, entries
entries: billPayment.entries.map((entry) => ({
...omit(entry, ['bill']),
})),
},
payableBills,
paymentMadeBills,
}; };
} }
@@ -678,4 +675,55 @@ export default class BillPaymentsService {
); );
await Promise.all(opers); await Promise.all(opers);
} }
/**
* Retrive edit page invoices entries from the given sale invoices models.
* @param {ISaleInvoice[]} invoices - Invoices.
* @return {IPaymentReceiveEditPageEntry}
*/
public mapBillToPageEntry(bill: IBill): IBillReceivePageEntry {
return {
entryType: 'invoice',
billId: bill.id,
dueAmount: bill.dueAmount + bill.paymentAmount,
amount: bill.amount,
billNo: bill.billNumber,
totalPaymentAmount: bill.paymentAmount,
paymentAmount: bill.paymentAmount,
date: bill.billDate,
};
}
public mapBillToNewPageEntry(bill: IBill): IBillReceivePageEntry {
return {
entryType: 'invoice',
billId: bill.id,
dueAmount: bill.dueAmount,
amount: bill.amount,
billNo: bill.billNumber,
date: bill.billDate,
totalPaymentAmount: bill.paymentAmount,
paymentAmount: 0,
}
}
/**
* Retrieve the payable entries of the new page once vendor be selected.
* @param {number} tenantId
* @param {number} vendorId
*/
async getNewPageEntries(
tenantId: number,
vendorId: number,
): Promise<IBillReceivePageEntry[]> {
const { Bill } = this.tenancy.models(tenantId);
// Retrieve all payable bills that assocaited to the payment made transaction.
const payableBills = await Bill.query()
.modify('dueBills')
.where('vendor_id', vendorId)
.orderBy('bill_date', 'ASC');
return payableBills.map(this.mapBillToNewPageEntry);
}
} }

View File

@@ -18,7 +18,7 @@ import {
ISaleInvoice, ISaleInvoice,
ISystemService, ISystemService,
ISystemUser, ISystemUser,
IPaymentReceiveEditPageEntry, IPaymentReceivePageEntry,
} from 'interfaces'; } from 'interfaces';
import AccountsService from 'services/Accounts/AccountsService'; import AccountsService from 'services/Accounts/AccountsService';
import JournalPoster from 'services/Accounting/JournalPoster'; import JournalPoster from 'services/Accounting/JournalPoster';
@@ -471,7 +471,7 @@ export default class PaymentReceiveService {
* @param {ISaleInvoice[]} invoices - Invoices. * @param {ISaleInvoice[]} invoices - Invoices.
* @return {IPaymentReceiveEditPageEntry} * @return {IPaymentReceiveEditPageEntry}
*/ */
public invoicesToEditPageEntries( public invoiceToPageEntry(
invoice: ISaleInvoice invoice: ISaleInvoice
): IPaymentReceiveEditPageEntry { ): IPaymentReceiveEditPageEntry {
return { return {
@@ -494,9 +494,10 @@ export default class PaymentReceiveService {
public async getPaymentReceiveEditPage( public async getPaymentReceiveEditPage(
tenantId: number, tenantId: number,
paymentReceiveId: number, paymentReceiveId: number,
authorizedUser: ISystemUser
): Promise<{ ): Promise<{
paymentReceive: IPaymentReceive; paymentReceive: Omit<IPaymentReceive, "entries">;
entries: IPaymentReceiveEditPageEntry[]; entries: IPaymentReceivePageEntry[];
}> { }> {
const { PaymentReceive, SaleInvoice } = this.tenancy.models(tenantId); const { PaymentReceive, SaleInvoice } = this.tenancy.models(tenantId);
@@ -509,14 +510,8 @@ export default class PaymentReceiveService {
if (!paymentReceive) { if (!paymentReceive) {
throw new ServiceError(ERRORS.PAYMENT_RECEIVE_NOT_EXISTS); throw new ServiceError(ERRORS.PAYMENT_RECEIVE_NOT_EXISTS);
} }
// Mapping the entries invoices.
const entriesInvoicesIds = paymentReceive.entries.map(
(entry) => entry.invoiceId
);
const paymentEntries = paymentReceive.entries.map((entry) => ({ const paymentEntries = paymentReceive.entries.map((entry) => ({
...this.invoicesToEditPageEntries(entry.invoice), ...this.invoiceToPageEntry(entry.invoice),
paymentAmount: entry.paymentAmount, paymentAmount: entry.paymentAmount,
})); }));
@@ -524,17 +519,16 @@ export default class PaymentReceiveService {
const restReceivableInvoices = await SaleInvoice.query() const restReceivableInvoices = await SaleInvoice.query()
.modify('dueInvoices') .modify('dueInvoices')
.where('customer_id', paymentReceive.customerId) .where('customer_id', paymentReceive.customerId)
.whereNotIn('id', entriesInvoicesIds) .whereNotIn(
'id',
paymentReceive.entries.map((entry) => entry.invoiceId)
)
.orderBy('invoice_date', 'ASC'); .orderBy('invoice_date', 'ASC');
const restReceivableEntries = restReceivableInvoices.map( const restReceivableEntries = restReceivableInvoices.map(
this.invoicesToEditPageEntries this.invoiceToPageEntry
); );
const entries = [...paymentEntries, ...restReceivableEntries];
const entries = [
...paymentEntries,
...restReceivableEntries,
];
return { return {
paymentReceive: omit(paymentReceive, ['entries']), paymentReceive: omit(paymentReceive, ['entries']),
@@ -616,6 +610,7 @@ export default class PaymentReceiveService {
paymentReceiveId: number paymentReceiveId: number
) { ) {
const { PaymentReceive } = this.tenancy.models(tenantId); const { PaymentReceive } = this.tenancy.models(tenantId);
return PaymentReceive.query() return PaymentReceive.query()
.where('id', paymentReceiveId) .where('id', paymentReceiveId)
.withGraphFetched('invoices') .withGraphFetched('invoices')
@@ -739,7 +734,6 @@ export default class PaymentReceiveService {
if (diffEntry.paymentAmount === 0) { if (diffEntry.paymentAmount === 0) {
return; return;
} }
const oper = SaleInvoice.changePaymentAmount( const oper = SaleInvoice.changePaymentAmount(
diffEntry.invoiceId, diffEntry.invoiceId,
diffEntry.paymentAmount diffEntry.paymentAmount
@@ -748,4 +742,22 @@ export default class PaymentReceiveService {
}); });
await Promise.all([...opers]); await Promise.all([...opers]);
} }
/**
* Retrieve payment receive new page receivable entries.
* @param {number} tenantId - Tenant id.
* @param {number} vendorId - Vendor id.
* @return {IPaymentReceivePageEntry[]}
*/
async getNewPageEntries(tenantId: number, customerId: number) {
const { SaleInvoice } = this.tenancy.models(tenantId);
// Retrieve due invoices.
const entries = await SaleInvoice.query()
.modify('dueInvoices')
.where('customer_id', customerId)
.orderBy('invoice_date', 'ASC');
return entries.map(this.invoiceToPageEntry);
}
} }