feat: style payment made form.

This commit is contained in:
Ahmed Bouhuolia
2020-11-01 20:43:10 +02:00
parent 70269d382a
commit 6f44cef5fc
10 changed files with 205 additions and 65 deletions

View File

@@ -9,12 +9,16 @@ const CLASSES = {
PAGE_FORM_HEADER: 'page-form__header',
PAGE_FORM_HEADER_PRIMARY: 'page-form__primary-section',
PAGE_FORM_FOOTER: 'page-form__footer',
PAGE_FORM_FLOATING_ACTIONS: 'page-form__floating-action',
PAGE_FORM_FLOATING_ACTIONS: 'page-form__floating-actions',
PAGE_FORM_BILL: 'page-form--bill',
PAGE_FORM_ESTIMATE: 'page-form--estimate',
PAGE_FORM_INVOICE: 'page-form--invoice',
PAGE_FORM_RECEIPT: 'page-form--receipt',
PAGE_FORM_PAYMENT_MADE: 'page-form--payment-made',
CLOUD_SPINNER: 'cloud-spinner',
IS_LOADING: 'is-loading',
...Classes,
};

View File

@@ -0,0 +1,23 @@
import React from 'react';
import classNames from 'classnames';
import { Spinner } from '@blueprintjs/core';
import { CLASSES } from 'common/classes';
import If from './Utils/If';
export default function CloudLoadingIndicator({
isLoading,
children,
}) {
return (
<div className={classNames(
CLASSES.CLOUD_SPINNER,
{ [CLASSES.IS_LOADING]: isLoading },
)}>
<If condition={isLoading}>
<Spinner size={30} value={null} />
</If>
{ children }
</div>
);
}

View File

@@ -28,6 +28,7 @@ import InputPrependButton from './Forms/InputPrependButton';
import CategoriesSelectList from './CategoriesSelectList';
import Row from './Grid/Row';
import Col from './Grid/Col';
import CloudLoadingIndicator from './CloudLoadingIndicator';
const Hint = FieldHint;
@@ -63,4 +64,5 @@ export {
CategoriesSelectList,
Col,
Row,
CloudLoadingIndicator,
};

View File

@@ -1,7 +1,13 @@
import React from 'react';
import { Intent, Button } from '@blueprintjs/core';
import { FormattedMessage as T } from 'react-intl';
import classNames from 'classnames';
import { CLASSES } from 'common/classes';
/**
* Payment made floating actions bar.
*/
export default function PaymentMadeFloatingActions({
isSubmitting,
onSubmitClick,
@@ -21,7 +27,7 @@ export default function PaymentMadeFloatingActions({
};
return (
<div className={'estimate-form__floating-footer'}>
<div className={classNames(CLASSES.PAGE_FORM_FLOATING_ACTIONS)}>
<Button
disabled={isSubmitting}
intent={Intent.PRIMARY}

View File

@@ -1,11 +1,13 @@
import React, { useMemo, useState, useCallback, useEffect } from 'react';
import React, { useMemo, useState, useCallback } from 'react';
import * as Yup from 'yup';
import { useFormik } from 'formik';
import moment from 'moment';
import { Intent, Alert } from '@blueprintjs/core';
import { FormattedMessage as T, useIntl } from 'react-intl';
import { pick, sumBy } from 'lodash';
import classNames from 'classnames';
import { CLASSES } from 'common/classes';
import PaymentMadeHeader from './PaymentMadeFormHeader';
import PaymentMadeItemsTable from './PaymentMadeItemsTable';
import PaymentMadeFloatingActions from './PaymentMadeFloatingActions';
@@ -16,12 +18,10 @@ import withPaymentMadeDetail from './withPaymentMadeDetail';
import withPaymentMade from './withPaymentMade';
import { AppToaster } from 'components';
import { compose, repeatValue, orderingLinesIndexes } from 'utils';
import { compose, orderingLinesIndexes } from 'utils';
import withSettings from 'containers/Settings/withSettings';
import { useHistory } from 'react-router-dom';
const MIN_LINES_NUMBER = 5;
const ERRORS = {
PAYMENT_NUMBER_NOT_UNIQUE: 'PAYMENT.NUMBER.NOT.UNIQUE',
};
@@ -70,8 +70,7 @@ function PaymentMadeForm({
Yup.object().shape({
id: Yup.number().nullable(),
due_amount: Yup.number().nullable(),
payment_amount: Yup.number().nullable()
.max(Yup.ref("due_amount")),
payment_amount: Yup.number().nullable().max(Yup.ref('due_amount')),
bill_id: Yup.number()
.nullable()
.when(['payment_amount'], {
@@ -124,7 +123,10 @@ function PaymentMadeForm({
[paymentMade, defaultInitialValues, defaultPaymentMadeEntry],
);
const handleSubmitForm = (values, { setSubmitting, resetForm, setFieldError }) => {
const handleSubmitForm = (
values,
{ setSubmitting, resetForm, setFieldError },
) => {
setSubmitting(true);
// Filters entries that have no `bill_id` or `payment_amount`.
@@ -165,7 +167,7 @@ function PaymentMadeForm({
if (getError(ERRORS.PAYMENT_NUMBER_NOT_UNIQUE)) {
setFieldError(
'payment_number',
formatMessage({ id: 'payment_number_is_not_unique' })
formatMessage({ id: 'payment_number_is_not_unique' }),
);
}
setSubmitting(false);
@@ -251,7 +253,7 @@ function PaymentMadeForm({
setClearFormAlert(true);
}, []);
//
//
const handleCancelClearFormAlert = () => {
setClearFormAlert(false);
};
@@ -270,7 +272,9 @@ function PaymentMadeForm({
};
return (
<div className={'payment_made_form'}>
<div
className={classNames(CLASSES.PAGE_FORM, CLASSES.PAGE_FORM_PAYMENT_MADE)}
>
<form onSubmit={handleSubmit}>
<PaymentMadeHeader
paymentMadeId={paymentMadeId}
@@ -282,6 +286,7 @@ function PaymentMadeForm({
values={values}
onFullAmountChanged={handleFullAmountChange}
/>
<PaymentMadeItemsTable
fullAmount={fullAmount}
paymentEntries={values.entries}

View File

@@ -12,6 +12,7 @@ import { DateInput } from '@blueprintjs/datetime';
import { FormattedMessage as T } from 'react-intl';
import moment from 'moment';
import classNames from 'classnames';
import { CLASSES } from 'common/classes';
import { momentFormatter, compose, tansformDateValue } from 'utils';
import {
AccountsSelectList,
@@ -19,6 +20,7 @@ import {
ErrorMessage,
FieldRequiredHint,
Money,
Hint,
} from 'components';
import withBills from '../Bill/withBills';
@@ -118,8 +120,8 @@ function PaymentMadeFormHeader({
};
return (
<div>
<div>
<div className={classNames(CLASSES.PAGE_FORM_HEADER)}>
<div className={classNames(CLASSES.PAGE_FORM_HEADER_PRIMARY)}>
{/* ------------ Vendor name ------------ */}
<FormGroup
label={<T id={'vendor_name'} />}
@@ -171,10 +173,10 @@ function PaymentMadeFormHeader({
label={<T id={'full_amount'} />}
inline={true}
className={('form-group--full-amount', Classes.FILL)}
labelInfo={<FieldRequiredHint />}
intent={
errors.full_amount && touched.full_amount && Intent.DANGER
}
labelInfo={<Hint />}
helperText={
<ErrorMessage name="full_amount" {...{ errors, touched }} />
}
@@ -189,7 +191,7 @@ function PaymentMadeFormHeader({
onBlur={handleFullAmountBlur}
/>
<a onClick={handleReceiveFullAmountClick} href="#">
<a onClick={handleReceiveFullAmountClick} href="#" className={'receive-full-amount'}>
Receive full amount (<Money amount={payableFullAmount} currency={'USD'} />)
</a>
</FormGroup>
@@ -246,22 +248,22 @@ function PaymentMadeFormHeader({
selectedAccountId={values.payment_account_id}
/>
</FormGroup>
</div>
{/* ------------ Reference ------------ */}
<FormGroup
label={<T id={'reference'} />}
inline={true}
className={classNames('form-group--reference', Classes.FILL)}
intent={errors.reference && touched.reference && Intent.DANGER}
helperText={<ErrorMessage name="reference" {...{ errors, touched }} />}
>
<InputGroup
{/* ------------ Reference ------------ */}
<FormGroup
label={<T id={'reference'} />}
inline={true}
className={classNames('form-group--reference', Classes.FILL)}
intent={errors.reference && touched.reference && Intent.DANGER}
minimal={true}
{...getFieldProps('reference')}
/>
</FormGroup>
helperText={<ErrorMessage name="reference" {...{ errors, touched }} />}
>
<InputGroup
intent={errors.reference && touched.reference && Intent.DANGER}
minimal={true}
{...getFieldProps('reference')}
/>
</FormGroup>
</div>
</div>
);
}

View File

@@ -1,7 +1,6 @@
import React, { useState, useEffect, useMemo, useCallback } from 'react';
import { useQuery } from 'react-query';
import { pick } from 'lodash';
import { LoadingIndicator, Choose } from 'components';
import { CloudLoadingIndicator } from 'components'
import PaymentMadeItemsTableEditor from './PaymentMadeItemsTableEditor';
import withPaymentMadeActions from './withPaymentMadeActions';
@@ -73,7 +72,7 @@ function PaymentMadeItemsTable({
setTableData(computedTableData);
}, [computedTableData]);
// Handle
// Handle mapping `fullAmount` prop to `localAmount` state.
useEffect(() => {
if (localAmount !== fullAmount) {
let _fullAmount = fullAmount;
@@ -111,23 +110,20 @@ function PaymentMadeItemsTable({
triggerUpdateData(rows);
};
return (
<div>
<LoadingIndicator loading={fetchVendorDueBills.isFetching}>
<Choose>
<Choose.When condition={tableData.length > 0}>
<PaymentMadeItemsTableEditor
data={tableData}
errors={errors}
onUpdateData={handleUpdateData}
onClickClearAllLines={onClickClearAllLines}
/>
</Choose.When>
const noResultsMessage = (vendorId) ?
'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.';
<Choose.Otherwise>The vendor has no due invoices.</Choose.Otherwise>
</Choose>
</LoadingIndicator>
</div>
return (
<CloudLoadingIndicator isLoading={fetchVendorDueBills.isFetching}>
<PaymentMadeItemsTableEditor
noResultsMessage={noResultsMessage}
data={tableData}
errors={errors}
onUpdateData={handleUpdateData}
onClickClearAllLines={onClickClearAllLines}
/>
</CloudLoadingIndicator>
);
}

View File

@@ -3,7 +3,9 @@ import { Button } from '@blueprintjs/core';
import { FormattedMessage as T, useIntl } from 'react-intl';
import moment from 'moment';
import { sumBy } from 'lodash';
import classNames from 'classnames';
import { CLASSES } from 'common/classes';
import { DataTable, Money } from 'components';
import { transformUpdatedRows } from 'utils';
import {
@@ -37,13 +39,17 @@ export default function PaymentMadeItemsTableEditor({
onClickClearAllLines,
onUpdateData,
data,
errors
errors,
noResultsMessage
}) {
const transformedData = useMemo(() => {
return [ ...data, {
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);
@@ -71,10 +77,7 @@ export default function PaymentMadeItemsTableEditor({
accessor: (r) => moment(r.bill_date).format('YYYY MMM DD'),
Cell: CellRenderer(EmptyDiv, 'bill_date'),
disableSortBy: true,
disableResizing: true,
width: 250,
},
{
Header: formatMessage({ id: 'bill_number' }),
accessor: (row) => `#${row.bill_number}`,
@@ -87,7 +90,6 @@ export default function PaymentMadeItemsTableEditor({
accessor: 'amount',
Cell: CellRenderer(DivFieldCell, 'amount'),
disableSortBy: true,
width: 100,
className: '',
},
{
@@ -95,7 +97,6 @@ export default function PaymentMadeItemsTableEditor({
accessor: 'due_amount',
Cell: TotalCellRederer(DivFieldCell, 'due_amount'),
disableSortBy: true,
width: 150,
className: '',
},
{
@@ -103,21 +104,19 @@ export default function PaymentMadeItemsTableEditor({
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) => {
return { 'row--total': localData.length === row.index + 1 };
},
(row) => ({ 'row--total': localData.length === row.index + 1 }),
[localData],
);
@@ -137,7 +136,10 @@ export default function PaymentMadeItemsTableEditor({
);
return (
<div className={'estimate-form__table'}>
<div className={classNames(
CLASSES.DATATABLE_EDITOR,
CLASSES.DATATABLE_EDITOR_ITEMS_ENTRIES,
)}>
<DataTable
columns={columns}
data={localData}
@@ -147,11 +149,12 @@ export default function PaymentMadeItemsTableEditor({
errors,
updateData: handleUpdateData,
}}
noResults={noResultsMessage}
/>
<div className={'mt1'}>
<div className={classNames(CLASSES.DATATABLE_EDITOR_ACTIONS, 'mt1')}>
<Button
small={true}
className={'button--secondary button--clear-lines ml1'}
className={'button--secondary button--clear-lines'}
onClick={handleClickClearAllLines}
>
<T id={'clear_all_lines'} />

View File

@@ -70,6 +70,7 @@ $pt-font-family: Noto Sans, -apple-system, BlinkMacSystemFont, Segoe UI, Roboto,
@import 'pages/estimates';
@import 'pages/invoice-form';
@import 'pages/receipt-form';
@import 'pages/payment-made';
// Views
@import 'views/filter-dropdown';
@@ -219,8 +220,16 @@ body.authentication {
padding: 15px;
margin: 15px 0 0 0;
}
}
&__floating-actions{
position: fixed;
bottom: 0;
width: 100%;
background: #fff;
padding: 14px 18px;
border-top: 1px solid #ececec;
}
}
.datatable-editor{
padding: 15px 15px 0;
@@ -390,4 +399,33 @@ body.authentication {
}
}
}
}
.cloud-spinner{
position: relative;
&.is-loading:before{
content: "";
position: absolute;
top: 0;
bottom: 0;
right: 0;
left: 0;
background: rgba(255, 255, 255, 0.8);
z-index: 999;
}
.bp3-spinner{
position: absolute;
z-index: 999999;
left: 50%;
top: 50%;
margin-top: -20px;
margin-left: -20px;
}
&:not(.is-loading) .bp3-spinner{
display: none;
}
}

View File

@@ -0,0 +1,61 @@
.page-form--payment-made {
$self: '.page-form';
#{$self}__header{
.bp3-label{
min-width: 160px;
}
.bp3-form-content{
width: 100%;
}
.bp3-form-group{
margin-bottom: 18px;
&.bp3-inline{
max-width: 470px;
}
a.receive-full-amount{
font-size: 12px;
margin-top: 6px;
display: inline-block;
}
}
}
#{$self}__primary-section{
padding-bottom: 2px;
}
.datatable-editor{
.table .tbody{
.tr.no-results{
.td{
border-bottom: 1px solid #e2e2e2;
font-size: 15px;
padding: 26px 0;
color: #5a5a77;
}
}
}
.table{
.tr{
.td:first-of-type,
.th:first-of-type{
span, div{
margin-left: auto;
margin-right: auto;
}
}
}
}
}
#{$self}__footer{
}
}