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: 'page-form__header',
PAGE_FORM_HEADER_PRIMARY: 'page-form__primary-section', PAGE_FORM_HEADER_PRIMARY: 'page-form__primary-section',
PAGE_FORM_FOOTER: 'page-form__footer', 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_BILL: 'page-form--bill',
PAGE_FORM_ESTIMATE: 'page-form--estimate', PAGE_FORM_ESTIMATE: 'page-form--estimate',
PAGE_FORM_INVOICE: 'page-form--invoice', PAGE_FORM_INVOICE: 'page-form--invoice',
PAGE_FORM_RECEIPT: 'page-form--receipt', PAGE_FORM_RECEIPT: 'page-form--receipt',
PAGE_FORM_PAYMENT_MADE: 'page-form--payment-made',
CLOUD_SPINNER: 'cloud-spinner',
IS_LOADING: 'is-loading',
...Classes, ...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 CategoriesSelectList from './CategoriesSelectList';
import Row from './Grid/Row'; import Row from './Grid/Row';
import Col from './Grid/Col'; import Col from './Grid/Col';
import CloudLoadingIndicator from './CloudLoadingIndicator';
const Hint = FieldHint; const Hint = FieldHint;
@@ -63,4 +64,5 @@ export {
CategoriesSelectList, CategoriesSelectList,
Col, Col,
Row, Row,
CloudLoadingIndicator,
}; };

View File

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

View File

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

View File

@@ -1,7 +1,6 @@
import React, { useState, useEffect, useMemo, useCallback } from 'react'; import React, { useState, useEffect, useMemo, useCallback } from 'react';
import { useQuery } from 'react-query'; import { useQuery } from 'react-query';
import { pick } from 'lodash'; import { CloudLoadingIndicator } from 'components'
import { LoadingIndicator, Choose } from 'components';
import PaymentMadeItemsTableEditor from './PaymentMadeItemsTableEditor'; import PaymentMadeItemsTableEditor from './PaymentMadeItemsTableEditor';
import withPaymentMadeActions from './withPaymentMadeActions'; import withPaymentMadeActions from './withPaymentMadeActions';
@@ -73,7 +72,7 @@ function PaymentMadeItemsTable({
setTableData(computedTableData); setTableData(computedTableData);
}, [computedTableData]); }, [computedTableData]);
// Handle // Handle mapping `fullAmount` prop to `localAmount` state.
useEffect(() => { useEffect(() => {
if (localAmount !== fullAmount) { if (localAmount !== fullAmount) {
let _fullAmount = fullAmount; let _fullAmount = fullAmount;
@@ -111,23 +110,20 @@ function PaymentMadeItemsTable({
triggerUpdateData(rows); triggerUpdateData(rows);
}; };
return ( const noResultsMessage = (vendorId) ?
<div> 'There is no payable bills for this vendor that can be applied for this payment' :
<LoadingIndicator loading={fetchVendorDueBills.isFetching}> 'Please select a vendor to display all open bills for it.';
<Choose>
<Choose.When condition={tableData.length > 0}>
<PaymentMadeItemsTableEditor
data={tableData}
errors={errors}
onUpdateData={handleUpdateData}
onClickClearAllLines={onClickClearAllLines}
/>
</Choose.When>
<Choose.Otherwise>The vendor has no due invoices.</Choose.Otherwise> return (
</Choose> <CloudLoadingIndicator isLoading={fetchVendorDueBills.isFetching}>
</LoadingIndicator> <PaymentMadeItemsTableEditor
</div> 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 { FormattedMessage as T, useIntl } from 'react-intl';
import moment from 'moment'; import moment from 'moment';
import { sumBy } from 'lodash'; import { sumBy } from 'lodash';
import classNames from 'classnames';
import { CLASSES } from 'common/classes';
import { DataTable, Money } from 'components'; import { DataTable, Money } from 'components';
import { transformUpdatedRows } from 'utils'; import { transformUpdatedRows } from 'utils';
import { import {
@@ -37,13 +39,17 @@ export default function PaymentMadeItemsTableEditor({
onClickClearAllLines, onClickClearAllLines,
onUpdateData, onUpdateData,
data, data,
errors errors,
noResultsMessage
}) { }) {
const transformedData = useMemo(() => { const transformedData = useMemo(() => {
return [ ...data, { const rows = data;
const totalRow = {
due_amount: sumBy(data, 'due_amount'), due_amount: sumBy(data, 'due_amount'),
payment_amount: sumBy(data, 'payment_amount'), payment_amount: sumBy(data, 'payment_amount'),
}]; };
if (rows.length > 0) { rows.push(totalRow) }
return rows;
}, [data]); }, [data]);
const [localData, setLocalData] = useState(transformedData); const [localData, setLocalData] = useState(transformedData);
@@ -71,10 +77,7 @@ export default function PaymentMadeItemsTableEditor({
accessor: (r) => moment(r.bill_date).format('YYYY MMM DD'), accessor: (r) => moment(r.bill_date).format('YYYY MMM DD'),
Cell: CellRenderer(EmptyDiv, 'bill_date'), Cell: CellRenderer(EmptyDiv, 'bill_date'),
disableSortBy: true, disableSortBy: true,
disableResizing: true,
width: 250,
}, },
{ {
Header: formatMessage({ id: 'bill_number' }), Header: formatMessage({ id: 'bill_number' }),
accessor: (row) => `#${row.bill_number}`, accessor: (row) => `#${row.bill_number}`,
@@ -87,7 +90,6 @@ export default function PaymentMadeItemsTableEditor({
accessor: 'amount', accessor: 'amount',
Cell: CellRenderer(DivFieldCell, 'amount'), Cell: CellRenderer(DivFieldCell, 'amount'),
disableSortBy: true, disableSortBy: true,
width: 100,
className: '', className: '',
}, },
{ {
@@ -95,7 +97,6 @@ export default function PaymentMadeItemsTableEditor({
accessor: 'due_amount', accessor: 'due_amount',
Cell: TotalCellRederer(DivFieldCell, 'due_amount'), Cell: TotalCellRederer(DivFieldCell, 'due_amount'),
disableSortBy: true, disableSortBy: true,
width: 150,
className: '', className: '',
}, },
{ {
@@ -103,21 +104,19 @@ export default function PaymentMadeItemsTableEditor({
accessor: 'payment_amount', accessor: 'payment_amount',
Cell: TotalCellRederer(MoneyFieldCell, 'payment_amount'), Cell: TotalCellRederer(MoneyFieldCell, 'payment_amount'),
disableSortBy: true, disableSortBy: true,
width: 150,
className: '', className: '',
}, },
], ],
[formatMessage], [formatMessage],
); );
// Handle click clear all lines button.
const handleClickClearAllLines = () => { const handleClickClearAllLines = () => {
onClickClearAllLines && onClickClearAllLines(); onClickClearAllLines && onClickClearAllLines();
}; };
const rowClassNames = useCallback( const rowClassNames = useCallback(
(row) => { (row) => ({ 'row--total': localData.length === row.index + 1 }),
return { 'row--total': localData.length === row.index + 1 };
},
[localData], [localData],
); );
@@ -137,7 +136,10 @@ export default function PaymentMadeItemsTableEditor({
); );
return ( return (
<div className={'estimate-form__table'}> <div className={classNames(
CLASSES.DATATABLE_EDITOR,
CLASSES.DATATABLE_EDITOR_ITEMS_ENTRIES,
)}>
<DataTable <DataTable
columns={columns} columns={columns}
data={localData} data={localData}
@@ -147,11 +149,12 @@ export default function PaymentMadeItemsTableEditor({
errors, errors,
updateData: handleUpdateData, updateData: handleUpdateData,
}} }}
noResults={noResultsMessage}
/> />
<div className={'mt1'}> <div className={classNames(CLASSES.DATATABLE_EDITOR_ACTIONS, 'mt1')}>
<Button <Button
small={true} small={true}
className={'button--secondary button--clear-lines ml1'} className={'button--secondary button--clear-lines'}
onClick={handleClickClearAllLines} onClick={handleClickClearAllLines}
> >
<T id={'clear_all_lines'} /> <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/estimates';
@import 'pages/invoice-form'; @import 'pages/invoice-form';
@import 'pages/receipt-form'; @import 'pages/receipt-form';
@import 'pages/payment-made';
// Views // Views
@import 'views/filter-dropdown'; @import 'views/filter-dropdown';
@@ -219,8 +220,16 @@ body.authentication {
padding: 15px; padding: 15px;
margin: 15px 0 0 0; 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{ .datatable-editor{
padding: 15px 15px 0; 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{
}
}