diff --git a/client/src/components/DynamicFilter/DynamicFilterValueField.js b/client/src/components/DynamicFilter/DynamicFilterValueField.js
new file mode 100644
index 000000000..4311ab46a
--- /dev/null
+++ b/client/src/components/DynamicFilter/DynamicFilterValueField.js
@@ -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 ;
+ };
+
+ 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 (
+
+
+
+
+
+
+
+
+
+ }
+ labelProp={'name'}
+ buttonProps={{ onClick: handleBtnClick }}
+ />
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+}
+
+const mapStateToProps = (state, props) => ({
+ resourceName: props.fieldMeta.resource_key || 'account_type',
+});
+
+const withResourceFilterValueField = connect(mapStateToProps);
+
+export default compose(
+ withResourceFilterValueField,
+ withResourceDetail(({ resourceData }) => ({ resourceData })),
+ withResourceActions,
+)(DynamicFilterValueField);
diff --git a/client/src/components/FieldHint.js b/client/src/components/FieldHint.js
new file mode 100644
index 000000000..34d66f740
--- /dev/null
+++ b/client/src/components/FieldHint.js
@@ -0,0 +1,8 @@
+import React from 'react';
+import Icon from './Icon';
+
+export default function FieldHint({ hint }) {
+ return (
+
+ );
+}
\ No newline at end of file
diff --git a/client/src/components/FieldRequiredHint.js b/client/src/components/FieldRequiredHint.js
new file mode 100644
index 000000000..02a48ff8a
--- /dev/null
+++ b/client/src/components/FieldRequiredHint.js
@@ -0,0 +1,9 @@
+import { FieldRequiredHint } from "components"
+
+
+
+export default function FieldRequiredHint() {
+ return (
+ *
+ );
+}
\ No newline at end of file
diff --git a/client/src/components/FilterDropdown.js b/client/src/components/FilterDropdown.js
index 6009028c4..c4bd9f055 100644
--- a/client/src/components/FilterDropdown.js
+++ b/client/src/components/FilterDropdown.js
@@ -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 (
{values.conditions.map((condition, index) => (
-
+
1}
- {...getFieldProps(`conditions[${index}].condition`)} />
+ {...fieldProps('condition', index)}
+ />
-
+
+ {...fieldProps('field_key', index)}
+ />
-
+
+ {...fieldProps('compatator', index)}
+ />
-
-
-
-
- }
- iconSize={14}
+
+ }
minimal={true}
- onClick={onClickRemoveCondition(index)} />
+ onClick={onClickRemoveCondition(index)}
+ />
))}
@@ -146,10 +215,11 @@ export default function FilterDropdown({
- )
-}
\ No newline at end of file
+ );
+}
diff --git a/client/src/components/FinancialSheet.js b/client/src/components/FinancialSheet.js
index 1456ee218..581f67023 100644
--- a/client/src/components/FinancialSheet.js
+++ b/client/src/components/FinancialSheet.js
@@ -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,
})}
>
- {companyName}
- {sheetType}
-
-
{formattedFromDate} | {formattedToDate}
+ {companyName}
+ {sheetType}
+
+
+ {formattedAsDate}
+
+
+
+ {formattedFromDate} | {' '}
+ {formattedToDate}
+
- {children}
- {accountingBasis}
+ {children}
+ {accountingBasis}
{basisLabel && (
-
-
{basisLabel}
+
+ {basisLabel}
)}
diff --git a/client/src/components/ListSelect.js b/client/src/components/ListSelect.js
index b0b735251..e95b1d4eb 100644
--- a/client/src/components/ListSelect.js
+++ b/client/src/components/ListSelect.js
@@ -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 = (),
+ isLoading = false,
labelProp,
selectedItem,
@@ -22,12 +26,17 @@ export default function ListSelect ({
}
}, [selectedItem, selectedItemProp, selectProps.items]);
+ const noResults = isLoading ?
+ ('loading') : ;
+
return (
diff --git a/client/src/components/Utils/Choose.js b/client/src/components/Utils/Choose.js
index 8ac7ad134..fa13379bb 100644
--- a/client/src/components/Utils/Choose.js
+++ b/client/src/components/Utils/Choose.js
@@ -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;
diff --git a/client/src/components/index.js b/client/src/components/index.js
index 0a1bce130..48fd33882 100644
--- a/client/src/components/index.js
+++ b/client/src/components/index.js
@@ -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,
};
\ No newline at end of file
diff --git a/client/src/components/modifiers.js b/client/src/components/modifiers.js
new file mode 100644
index 000000000..f4f9dbb5a
--- /dev/null
+++ b/client/src/components/modifiers.js
@@ -0,0 +1,4 @@
+export default {
+ SELECT_LIST_FILL_POPOVER: 'select-list--fill-popover',
+ SELECT_LIST_FILL_BUTTON: 'select-list--fill-button',
+}
\ No newline at end of file
diff --git a/client/src/config/sidebarMenu.js b/client/src/config/sidebarMenu.js
index c5957bf37..7c42bc55e 100644
--- a/client/src/config/sidebarMenu.js
+++ b/client/src/config/sidebarMenu.js
@@ -110,6 +110,14 @@ export default [
text: ,
href: '/profit-loss-sheet',
},
+ {
+ text: 'Receivable Aging Summary',
+ href: '/receivable-aging-summary'
+ },
+ {
+ text: 'Payable Aging Summary',
+ href: '/payable-aging-summary'
+ }
],
},
{
diff --git a/client/src/containers/Accounts/AccountsActionsBar.js b/client/src/containers/Accounts/AccountsActionsBar.js
index 4bca5a518..3bfe300db 100644
--- a/client/src/containers/Accounts/AccountsActionsBar.js
+++ b/client/src/containers/Accounts/AccountsActionsBar.js
@@ -58,9 +58,9 @@ function AccountsActionsBar({
const viewsMenuItems = accountsViews.map((view) => {
return