feat: Receivable aging summary front-end.

This commit is contained in:
Ahmed Bouhuolia
2020-06-14 14:19:18 +02:00
parent ac9c360629
commit f0c1985e43
45 changed files with 4150 additions and 538 deletions

View File

@@ -0,0 +1,173 @@
import React, { useMemo, useRef, useEffect, useState } from 'react';
import { FormGroup, MenuItem, InputGroup, Position, Spinner } from '@blueprintjs/core';
import { connect } from 'react-redux';
import { useQuery } from 'react-query';
import { DateInput } from '@blueprintjs/datetime';
import classNames from 'classnames';
import { FormattedMessage as T, useIntl } from 'react-intl';
import { debounce } from 'lodash';
import moment from 'moment';
import { If, Choose, ListSelect, MODIFIER } from 'components';
import withResourceDetail from 'containers/Resources/withResourceDetails';
import withResourceActions from 'containers/Resources/withResourcesActions';
import { compose, momentFormatter } from 'utils';
/**
* Dynamic filter fields.
*/
function DynamicFilterValueField({
fieldMeta,
value,
initialValue,
error,
// fieldkey,
// resourceKey,
resourceName,
resourceData,
requestResourceData,
onChange,
inputDebounceWait = 500,
}) {
const { formatMessage } = useIntl();
const [localValue, setLocalValue] = useState();
const fetchResourceData = useQuery(
['resource-data', resourceName],
() => requestResourceData(resourceName),
{ manual: true },
);
useEffect(() => {
if (value !== localValue) {
setLocalValue(value);
}
}, [value]);
// Account type item of select filed.
const menuItem = (item, { handleClick, modifiers, query }) => {
return <MenuItem text={item.name} key={item.id} onClick={handleClick} />;
};
const handleBtnClick = () => {
fetchResourceData.refetch({ force: true });
};
const listOptions = useMemo(() => Object.values(resourceData), [
resourceData,
]);
// Filters accounts types items.
const filterItems = (query, item, _index, exactMatch) => {
const normalizedTitle = item.name.toLowerCase();
const normalizedQuery = query.toLowerCase();
if (exactMatch) {
return normalizedTitle === normalizedQuery;
} else {
return normalizedTitle.indexOf(normalizedQuery) >= 0;
}
};
const onItemSelect = (item) => {
onChange && onChange(item);
};
const handleInputChangeThrottled = useRef(
debounce((value) => {
onChange && onChange(value);
}, inputDebounceWait),
);
const handleInputChange = (e) => {
setLocalValue(e.currentTarget.value);
handleInputChangeThrottled.current(e.currentTarget.value);
};
const handleDateChange = (date) => {
setLocalValue(date);
onChange && onChange(date);
};
const transformDateValue = (value) => {
return moment(value || new Date()).toDate();
};
return (
<FormGroup className={'form-group--value'}>
<Choose>
<Choose.When condition={true}>
<Spinner size={18} />
</Choose.When>
<Choose.Otherwise>
<Choose>
<Choose.When condition={fieldMeta.data_type === 'options'}>
<ListSelect
className={classNames(
'list-select--filter-dropdown',
'form-group--select-list',
MODIFIER.SELECT_LIST_FILL_POPOVER,
MODIFIER.SELECT_LIST_FILL_BUTTON,
)}
items={listOptions}
itemRenderer={menuItem}
loading={fetchResourceData.isFetching}
itemPredicate={filterItems}
popoverProps={{
inline: true,
minimal: true,
captureDismiss: true,
popoverClassName: 'popover--list-select-filter-dropdown',
}}
onItemSelect={onItemSelect}
selectedItem={value}
selectedItemProp={'id'}
defaultText={<T id={'select_account_type'} />}
labelProp={'name'}
buttonProps={{ onClick: handleBtnClick }}
/>
</Choose.When>
<Choose.When condition={fieldMeta.data_type === 'date'}>
<DateInput
{...momentFormatter('YYYY/MM/DD')}
value={transformDateValue(localValue)}
onChange={handleDateChange}
popoverProps={{
minimal: true,
position: Position.BOTTOM,
}}
/>
</Choose.When>
<Choose.Otherwise>
<InputGroup
placeholder={formatMessage({ id: 'value' })}
onChange={handleInputChange}
value={localValue}
/>
</Choose.Otherwise>
</Choose>
</Choose.Otherwise>
</Choose>
</FormGroup>
);
}
const mapStateToProps = (state, props) => ({
resourceName: props.fieldMeta.resource_key || 'account_type',
});
const withResourceFilterValueField = connect(mapStateToProps);
export default compose(
withResourceFilterValueField,
withResourceDetail(({ resourceData }) => ({ resourceData })),
withResourceActions,
)(DynamicFilterValueField);

View File

@@ -0,0 +1,8 @@
import React from 'react';
import Icon from './Icon';
export default function FieldHint({ hint }) {
return (
<Icon icon="info-circle" iconSize={12} />
);
}

View File

@@ -0,0 +1,9 @@
import { FieldRequiredHint } from "components"
export default function FieldRequiredHint() {
return (
<span class="required">*</span>
);
}

View File

@@ -1,12 +1,11 @@
import React, {useEffect, useMemo, useCallback, useRef} from 'react';
import React, { useEffect, useMemo, useCallback, useRef } from 'react';
import {
FormGroup,
InputGroup,
Classes,
HTMLSelect,
Button,
Intent,
} from "@blueprintjs/core"
} from '@blueprintjs/core';
import { useFormik } from 'formik';
import { isEqual } from 'lodash';
import { usePrevious } from 'react-use';
@@ -14,130 +13,200 @@ import { debounce } from 'lodash';
import Icon from 'components/Icon';
import { checkRequiredProperties } from 'utils';
import { FormattedMessage as T, useIntl } from 'react-intl';
import { DynamicFilterValueField } from 'components';
import Toaster from 'components/AppToaster';
import moment from 'moment';
let limitToast;
/**
* Filter popover content.
*/
export default function FilterDropdown({
fields,
onFilterChange,
refetchDebounceWait = 250,
}) {
const {formatMessage} =useIntl();
const { formatMessage } = useIntl();
const fieldsKeyMapped = new Map(fields.map((field) => [field.key, field]));
const conditionalsItems = useMemo(() => [
{ value: 'and', label:formatMessage({id:'and'}) },
{ value: 'or', label: formatMessage({id:'or'}) },
], [formatMessage]);
const conditionalsItems = useMemo(
() => [
{ value: 'and', label: formatMessage({ id: 'and' }) },
{ value: 'or', label: formatMessage({ id: 'or' }) },
],
[formatMessage],
);
const resourceFields = useMemo(
() => [
...fields.map((field) => ({
value: field.key,
label: field.label_name,
})),
],
[fields],
);
const compatatorsItems = useMemo(
() => [
{ value: '', label: formatMessage({ id: 'comparator' }) },
{ value: 'equals', label: formatMessage({ id: 'equals' }) },
{ value: 'not_equal', label: formatMessage({ id: 'not_equal' }) },
{ value: 'contain', label: formatMessage({ id: 'contain' }) },
{ value: 'not_contain', label: formatMessage({ id: 'not_contain' }) },
],
[formatMessage],
);
const resourceFields = useMemo(() => [
...fields.map((field) => ({ value: field.key, label: field.label_name, })),
], [fields]);
const defaultFilterCondition = useMemo(
() => ({
condition: 'and',
field_key: fields.length > 0 ? fields[0].key : '',
compatator: 'equals',
value: '',
}),
[fields],
);
const compatatorsItems = useMemo(() => [
{value: '', label:formatMessage({id:'select_a_comparator'})},
{value: 'equals', label: formatMessage({id:'equals'})},
{value: 'not_equal', label: formatMessage({id:'not_equal'})},
{value: 'contain', label: formatMessage({id:'contain'})},
{value: 'not_contain', label: formatMessage({id:'not_contain'})},
], [formatMessage]);
const defaultFilterCondition = useMemo(() => ({
condition: 'and',
field_key: fields.length > 0 ? fields[0].key : '',
compatator: 'equals',
value: '',
}), [fields]);
const {
setFieldValue,
getFieldProps,
values,
} = useFormik({
const { setFieldValue, getFieldProps, values } = useFormik({
enableReinitialize: true,
initialValues: {
conditions: [ defaultFilterCondition ],
conditions: [defaultFilterCondition],
},
});
const onClickNewFilter = useCallback(() => {
setFieldValue('conditions', [
...values.conditions, defaultFilterCondition,
]);
if (values.conditions.length >= 12) {
limitToast = Toaster.show(
{
message: formatMessage({ id: 'you_reached_conditions_limit' }),
intent: Intent.WARNING,
},
limitToast,
);
} else {
setFieldValue('conditions', [
...values.conditions,
defaultFilterCondition,
]);
}
}, [values, defaultFilterCondition, setFieldValue]);
const filteredFilterConditions = useMemo(() => {
const requiredProps = ['field_key', 'condition', 'compatator', 'value'];
return values.conditions
.filter((condition) =>
!checkRequiredProperties(condition, requiredProps));
return values.conditions.filter(
(condition) => !checkRequiredProperties(condition, requiredProps),
);
}, [values.conditions]);
const prevConditions = usePrevious(filteredFilterConditions);
const onFilterChangeThrottled = useRef(debounce((conditions) => {
onFilterChange && onFilterChange(conditions);
}, 1000));
const onFilterChangeThrottled = useRef(
debounce((conditions) => {
onFilterChange && onFilterChange(conditions);
}, refetchDebounceWait),
);
useEffect(() => {
if (!isEqual(prevConditions, filteredFilterConditions) && prevConditions) {
onFilterChangeThrottled.current(filteredFilterConditions);
}
}, [filteredFilterConditions,prevConditions]);
}, [filteredFilterConditions, prevConditions]);
// Handle click remove condition.
const onClickRemoveCondition = (index) => () => {
if (values.conditions.length === 1) {
setFieldValue('conditions', [
defaultFilterCondition,
]);
setFieldValue('conditions', [defaultFilterCondition]);
return;
}
const conditions = [ ...values.conditions ];
const conditions = [...values.conditions];
conditions.splice(index, 1);
setFieldValue('conditions', [ ...conditions ]);
setFieldValue('conditions', [...conditions]);
};
// transform dynamic value field.
const transformValueField = (value) => {
if (value instanceof Date) {
return moment(value).format('YYYY-MM-DD');
} else if (typeof value === 'object') {
return value.id;
}
return value;
};
// Override getFieldProps for conditions fields.
const fieldProps = (name, index) => {
const override = {
...getFieldProps(`conditions[${index}].${name}`),
};
return {
...override,
onChange: (e) => {
if (name === 'field_key') {
const currentField = fieldsKeyMapped.get(
values.conditions[index].field_key,
);
const prevField = fieldsKeyMapped.get(e.currentTarget.value);
if (currentField.data_type !== prevField.data_type) {
setFieldValue(`conditions[${index}].value`, '');
}
}
override.onChange(e);
},
};
};
// Value field props.
const valueFieldProps = (name, index) => ({
...fieldProps(name, index),
onChange: (value) => {
const transformedValue = transformValueField(value);
setFieldValue(`conditions[${index}].${name}`, transformedValue);
},
});
console.log(values.conditions, 'XX');
return (
<div class="filter-dropdown">
<div class="filter-dropdown__body">
{values.conditions.map((condition, index) => (
<div class="filter-dropdown__condition">
<FormGroup
className={'form-group--condition'}>
<FormGroup className={'form-group--condition'}>
<HTMLSelect
options={conditionalsItems}
className={Classes.FILL}
disabled={index > 1}
{...getFieldProps(`conditions[${index}].condition`)} />
{...fieldProps('condition', index)}
/>
</FormGroup>
<FormGroup
className={'form-group--field'}>
<FormGroup className={'form-group--field'}>
<HTMLSelect
options={resourceFields}
value={1}
className={Classes.FILL}
{...getFieldProps(`conditions[${index}].field_key`)} />
{...fieldProps('field_key', index)}
/>
</FormGroup>
<FormGroup
className={'form-group--compatator'}>
<FormGroup className={'form-group--compatator'}>
<HTMLSelect
options={compatatorsItems}
className={Classes.FILL}
{...getFieldProps(`conditions[${index}].compatator`)} />
{...fieldProps('compatator', index)}
/>
</FormGroup>
<FormGroup
className={'form-group--value'}>
<InputGroup
placeholder="Value"
{...getFieldProps(`conditions[${index}].value`)} />
</FormGroup>
<Button
icon={<Icon icon="times" />}
iconSize={14}
<DynamicFilterValueField
fieldMeta={fieldsKeyMapped.get(condition.field_key)}
{...valueFieldProps('value', index)}
/>
<Button
icon={<Icon icon="times" iconSize={14} />}
minimal={true}
onClick={onClickRemoveCondition(index)} />
onClick={onClickRemoveCondition(index)}
/>
</div>
))}
</div>
@@ -146,10 +215,11 @@ export default function FilterDropdown({
<Button
minimal={true}
intent={Intent.PRIMARY}
onClick={onClickNewFilter}>
<T id={'new_conditional'}/>
onClick={onClickNewFilter}
>
<T id={'new_conditional'} />
</Button>
</div>
</div>
)
}
);
}

View File

@@ -3,12 +3,14 @@ import moment from 'moment';
import classnames from 'classnames';
import LoadingIndicator from 'components/LoadingIndicator';
import { FormattedMessage as T, useIntl } from 'react-intl';
import { If } from 'components';
export default function FinancialSheet({
companyName,
sheetType,
fromDate,
toDate,
asDate,
children,
accountingBasis,
name,
@@ -16,16 +18,25 @@ export default function FinancialSheet({
className,
basis,
}) {
const formattedFromDate = moment(fromDate).format('DD MMMM YYYY');
const formattedToDate = moment(toDate).format('DD MMMM YYYY');
const nameModifer = name ? `financial-sheet--${name}` : '';
const { formatMessage } = useIntl();
const format = 'DD MMMM YYYY';
const formattedFromDate = useMemo(() => moment(fromDate).format(format), [
fromDate,
]);
const formattedToDate = useMemo(() => moment(toDate).format(format), [
toDate,
]);
const formattedAsDate = useMemo(() => moment(asDate).format(format), [
asDate,
]);
const nameModifer = name ? `financial-sheet--${name}` : '';
const methodsLabels = useMemo(
() => ({
cash: formatMessage({id:'cash'}),
accrual: formatMessage({id:'accrual'}),
cash: formatMessage({ id: 'cash' }),
accrual: formatMessage({ id: 'accrual' }),
}),
[formatMessage]
[formatMessage],
);
const getBasisLabel = useCallback((b) => methodsLabels[b], [methodsLabels]);
const basisLabel = useMemo(() => getBasisLabel(basis), [
@@ -42,18 +53,25 @@ export default function FinancialSheet({
'is-loading': loading,
})}
>
<h1 class='financial-sheet__title'>{companyName}</h1>
<h6 class='financial-sheet__sheet-type'>{sheetType}</h6>
<div class='financial-sheet__date'>
<T id={'from'}/> {formattedFromDate} | <T id={'to'}/> {formattedToDate}
<h1 class="financial-sheet__title">{companyName}</h1>
<h6 class="financial-sheet__sheet-type">{sheetType}</h6>
<div class="financial-sheet__date">
<If condition={asDate}>
<T id={'as'} /> {formattedAsDate}
</If>
<If condition={fromDate && toDate}>
<T id={'from'} /> {formattedFromDate} | <T id={'to'} />{' '}
{formattedToDate}
</If>
</div>
<div class='financial-sheet__table'>{children}</div>
<div class='financial-sheet__accounting-basis'>{accountingBasis}</div>
<div class="financial-sheet__table">{children}</div>
<div class="financial-sheet__accounting-basis">{accountingBasis}</div>
{basisLabel && (
<div class='financial-sheet__basis'>
<T id={'accounting_basis'}/> {basisLabel}
<div class="financial-sheet__basis">
<T id={'accounting_basis'} /> {basisLabel}
</div>
)}
</div>

View File

@@ -1,12 +1,16 @@
import React, { useState, useEffect } from 'react';
import {
Button,
MenuItem,
} from '@blueprintjs/core';
import { Select } from '@blueprintjs/select';
import { FormattedMessage as T } from 'react-intl';
export default function ListSelect ({
buttonProps,
defaultText,
noResultsText = (<T id="no_results" />),
isLoading = false,
labelProp,
selectedItem,
@@ -22,12 +26,17 @@ export default function ListSelect ({
}
}, [selectedItem, selectedItemProp, selectProps.items]);
const noResults = isLoading ?
('loading') : <MenuItem disabled={true} text={noResultsText} />;
return (
<Select
{...selectProps}
noResults={noResults}
>
<Button
text={currentItem ? currentItem[labelProp] : defaultText}
loading={isLoading}
{...buttonProps}
/>
</Select>

View File

@@ -1,6 +1,6 @@
import React from 'react';
import PropTypes from 'prop-types';
import If from './if';
import If from './If';
const Choose = props => {
let when = null;

View File

@@ -1,10 +1,14 @@
import If from './Utils/If';
import Money from './Money';
import Icon from './Icon';
// import Choose from './Utils/Choose';
import Choose from './Utils/Choose';
// import For from './Utils/For';
import ListSelect from './ListSelect';
import FinancialStatement from './FinancialStatement';
import DynamicFilterValueField from './DynamicFilter/DynamicFilterValueField';
import ErrorMessage from './ErrorMessage';
import MODIFIER from './modifiers';
import FieldHint from './FieldHint';
export {
If,
@@ -12,6 +16,10 @@ export {
Icon,
ListSelect,
FinancialStatement,
// Choose,
Choose,
DynamicFilterValueField,
MODIFIER,
ErrorMessage,
FieldHint,
// For,
};

View File

@@ -0,0 +1,4 @@
export default {
SELECT_LIST_FILL_POPOVER: 'select-list--fill-popover',
SELECT_LIST_FILL_BUTTON: 'select-list--fill-button',
}