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',
}

View File

@@ -110,6 +110,14 @@ export default [
text: <T id={'profit_loss_sheet'}/>,
href: '/profit-loss-sheet',
},
{
text: 'Receivable Aging Summary',
href: '/receivable-aging-summary'
},
{
text: 'Payable Aging Summary',
href: '/payable-aging-summary'
}
],
},
{

View File

@@ -58,9 +58,9 @@ function AccountsActionsBar({
const viewsMenuItems = accountsViews.map((view) => {
return <MenuItem onClick={() => onClickViewItem(view)} text={view.name} />;
});
const hasSelectedRows = useMemo(() => selectedRows.length > 0, [
selectedRows,
]);
const hasSelectedRows = useMemo(
() => selectedRows.length > 0,
[selectedRows]);
const filterDropdown = FilterDropdown({
fields: resourceFields,
@@ -73,10 +73,6 @@ function AccountsActionsBar({
},
});
// const handleBulkArchive = useCallback(() => {
// onBulkArchive && onBulkArchive(selectedRows.map(r => r.id));
// }, [onBulkArchive, selectedRows]);
const handleBulkDelete = useCallback(() => {
onBulkDelete && onBulkDelete(selectedRows.map((r) => r.id));
}, [onBulkDelete, selectedRows]);
@@ -100,8 +96,8 @@ function AccountsActionsBar({
>
<Button
className={classNames(Classes.MINIMAL, 'button--table-views')}
icon={<Icon icon='table-16' iconSize={16} />}
text={<T id={'table_views'}/>}
icon={<Icon icon="table-16" iconSize={16} />}
text={<T id={'table_views'} />}
rightIcon={'caret-down'}
/>
</Popover>
@@ -119,30 +115,40 @@ function AccountsActionsBar({
content={filterDropdown}
interactionKind={PopoverInteractionKind.CLICK}
position={Position.BOTTOM_LEFT}
canOutsideClickClose={true}
>
<Button
className={classNames(Classes.MINIMAL, 'button--filter')}
text={filterCount <= 0 ? <T id={'filter'}/> : `${filterCount} filters applied`}
icon={ <Icon icon="filter-16" iconSize={16} /> }/>
className={classNames(Classes.MINIMAL, 'button--filter', {
'has-active-filters': filterCount > 0,
})}
text={
filterCount <= 0 ? (
<T id={'filter'} />
) : (
`${filterCount} filters applied`
)
}
icon={<Icon icon="filter-16" iconSize={16} />}
/>
</Popover>
<If condition={hasSelectedRows}>
<Button
className={Classes.MINIMAL}
icon={<Icon icon='play-16' iconSize={16} />}
text={<T id={'activate'}/>}
icon={<Icon icon="play-16" iconSize={16} />}
text={<T id={'activate'} />}
onClick={handelBulkActivate}
/>
<Button
className={Classes.MINIMAL}
icon={<Icon icon='pause-16' iconSize={16} />}
text={<T id={'inactivate'}/>}
icon={<Icon icon="pause-16" iconSize={16} />}
text={<T id={'inactivate'} />}
onClick={handelBulkInactive}
/>
<Button
className={Classes.MINIMAL}
icon={<Icon icon='trash-16' iconSize={16} />}
text={<T id={'delete'}/>}
icon={<Icon icon="trash-16" iconSize={16} />}
text={<T id={'delete'} />}
intent={Intent.DANGER}
onClick={handleBulkDelete}
/>
@@ -150,18 +156,18 @@ function AccountsActionsBar({
<Button
className={Classes.MINIMAL}
icon={<Icon icon='print-16' iconSize={16} />}
text={<T id={'print'}/>}
icon={<Icon icon="print-16" iconSize={16} />}
text={<T id={'print'} />}
/>
<Button
className={Classes.MINIMAL}
icon={<Icon icon='file-export-16' iconSize={16} />}
text={<T id={'export'}/>}
icon={<Icon icon="file-export-16" iconSize={16} />}
text={<T id={'export'} />}
/>
<Button
className={Classes.MINIMAL}
icon={<Icon icon='file-import-16' iconSize={16} />}
text={<T id={'import'}/>}
icon={<Icon icon="file-import-16" iconSize={16} />}
text={<T id={'import'} />}
/>
</NavbarGroup>
</DashboardActionsBar>

View File

@@ -190,12 +190,7 @@ function AccountFormDialog({
// Account item of select accounts field.
const accountItem = (item, { handleClick, modifiers, query }) => {
return (
<MenuItem
text={item.name}
label={item.code}
key={item.id}
onClick={handleClick}
/>
<MenuItem text={item.name} label={item.code} key={item.id} onClick={handleClick} />
);
};

View File

@@ -71,12 +71,8 @@ function ExpensesList({
requestDeleteExpense(deleteExpense.id).then(() => {
AppToaster.show({
message: formatMessage(
{
id: 'the_expense_has_been_successfully_deleted',
},
{
number: deleteExpense.payment_account_id,
},
{ id: 'the_expense_has_been_successfully_deleted' },
{ number: deleteExpense.payment_account_id },
),
intent: Intent.SUCCESS,
});
@@ -102,8 +98,8 @@ function ExpensesList({
.then(() => {
AppToaster.show({
message: formatMessage(
{ id: 'the_expenses_has_been_successfully_deleted', },
{ count: selectedRowsCount, },
{ id: 'the_expenses_has_been_successfully_deleted' },
{ count: selectedRowsCount },
),
intent: Intent.SUCCESS,
});
@@ -112,8 +108,8 @@ function ExpensesList({
.catch((error) => {
setBulkDelete(false);
});
// @todo
// @todo
}, [requestDeleteBulkExpenses, bulkDelete, formatMessage, selectedRowsCount]);
// Handle cancel bulk delete alert.
@@ -179,11 +175,11 @@ function ExpensesList({
<DashboardPageContent>
<Switch>
<Route
// exact={true}
// path={[
// '/expenses/:custom_view_id/custom_view',
// '/expenses/new',
// ]}
// exact={true}
// path={[
// '/expenses/:custom_view_id/custom_view',
// '/expenses/new',
// ]}
>
<ExpenseViewTabs />

View File

@@ -18,8 +18,6 @@ import withSettings from 'containers/Settings/withSettings';
import withBalanceSheetActions from './withBalanceSheetActions';
import withBalanceSheetDetail from './withBalanceSheetDetail';
function BalanceSheet({
// #withDashboardActions
changePageTitle,

View File

@@ -3,7 +3,6 @@ import {Row, Col} from 'react-grid-system';
import {momentFormatter} from 'utils';
import {DateInput} from '@blueprintjs/datetime';
import { useIntl } from 'react-intl';
import {
HTMLSelect,
FormGroup,
@@ -11,6 +10,7 @@ import {
Position,
} from '@blueprintjs/core';
import Icon from 'components/Icon';
import { FieldHint } from 'components';
import {
parseDateRangeQuery
} from 'utils';
@@ -87,7 +87,7 @@ export default function FinancialStatementDateRange({
<Col sm={3}>
<FormGroup
label={intl.formatMessage({'id': 'to_date'})}
labelInfo={infoIcon}
labelInfo={<FieldHint />}
fill={true}
intent={formik.errors.to_date && Intent.DANGER}>

View File

@@ -76,6 +76,7 @@ function GeneralLedgerHeader({
/>
</FormGroup>
</Col>
<Col sm={3}>
<RadiosAccountingBasis
onChange={handleAccountingBasisChange}

View File

@@ -0,0 +1,79 @@
import React, { useEffect, useState, useCallback } from 'react';
import { useIntl } from 'react-intl';
import { useQuery } from 'react-query';
import moment from 'moment';
import { FinancialStatement } from 'components';
import DashboardInsider from 'components/Dashboard/DashboardInsider';
import ReceivableAgingSummaryActionsBar from './ReceivableAgingSummaryActionsBar';
import DashboardPageContent from 'components/Dashboard/DashboardPageContent';
import ReceivableAgingSummaryHeader from './ReceivableAgingSummaryHeader'
import ReceivableAgingSummaryTable from './ReceivableAgingSummaryTable';
import withDashboardActions from 'containers/Dashboard/withDashboardActions';
import withReceivableAgingSummaryActions from './withReceivableAgingSummaryActions';
import { compose } from 'utils';
function ReceivableAgingSummarySheet({
// #withDashboardActions
changePageTitle,
// #withReceivableAgingSummaryActions
requestReceivableAgingSummary,
}) {
const { formatMessage } = useIntl();
const [query, setQuery] = useState({
as_date: moment().format('YYYY-MM-DD'),
aging_before_days: 30,
aging_periods: 3,
});
useEffect(() => {
changePageTitle(formatMessage({ id: 'receivable_aging_summary' }));
}, []);
const fetchSheet = useQuery(['receivable-aging-summary', query],
(key, q) => requestReceivableAgingSummary(q),
{ manual: true });
// Handle fetch the data of receivable aging summary sheet.
const handleFetchData = useCallback(() => {
fetchSheet.refetch({ force: true });
}, [fetchSheet]);
const handleFilterSubmit = useCallback((filter) => {
const _filter = {
...filter,
as_date: moment(filter.as_date).format('YYYY-MM-DD'),
};
setQuery(_filter);
fetchSheet.refetch({ force: true });
}, [fetchSheet]);
return (
<DashboardInsider>
<ReceivableAgingSummaryActionsBar />
<DashboardPageContent>
<FinancialStatement>
<ReceivableAgingSummaryHeader
onSubmitFilter={handleFilterSubmit} />
<div class="financial-statement__body">
<ReceivableAgingSummaryTable
receivableAgingSummaryQuery={query}
onFetchData={handleFetchData}
/>
</div>
</FinancialStatement>
</DashboardPageContent>
</DashboardInsider>
);
}
export default compose(
withDashboardActions,
withReceivableAgingSummaryActions
)(ReceivableAgingSummarySheet);

View File

@@ -0,0 +1,99 @@
import React from 'react';
import {
NavbarDivider,
NavbarGroup,
Classes,
Button,
Popover,
PopoverInteractionKind,
Position,
} from "@blueprintjs/core";
import { FormattedMessage as T } from 'react-intl';
import classNames from 'classnames';
import DashboardActionsBar from "components/Dashboard/DashboardActionsBar";
import FilterDropdown from 'components/FilterDropdown';
import Icon from 'components/Icon';
import { If } from 'components';
import withReceivableAging from './withReceivableAgingSummary';
import withReceivableAgingActions from './withReceivableAgingSummaryActions';
import { compose } from 'utils';
function ReceivableAgingSummaryActionsBar({
toggleFilterReceivableAgingSummary,
receivableAgingFilter,
}) {
const filterDropdown = FilterDropdown({
fields: [],
onFilterChange: (filterConditions) => {},
});
const handleFilterToggleClick = () => {
toggleFilterReceivableAgingSummary();
};
return (
<DashboardActionsBar>
<NavbarGroup>
<Button
className={classNames(Classes.MINIMAL, 'button--table-views')}
icon={<Icon icon="cog-16" iconSize={16} />}
text={<T id={'customize_report'} />}
/>
<NavbarDivider />
<If condition={receivableAgingFilter}>
<Button
className={Classes.MINIMAL}
text={<T id={'hide_filter'} />}
onClick={handleFilterToggleClick}
icon={<Icon icon="arrow-to-top" />}
/>
</If>
<If condition={!receivableAgingFilter}>
<Button
className={Classes.MINIMAL}
text={<T id={'show_filter'} />}
onClick={handleFilterToggleClick}
icon={<Icon icon="arrow-to-bottom" />}
/>
</If>
<NavbarDivider />
<Popover
content={filterDropdown}
interactionKind={PopoverInteractionKind.CLICK}
position={Position.BOTTOM_LEFT}
>
<Button
className={classNames(Classes.MINIMAL, 'button--filter')}
text={<T id={'filter'} />}
icon={<Icon icon="filter-16" iconSize={16} />}
/>
</Popover>
<Button
className={Classes.MINIMAL}
icon={<Icon icon='print-16' iconSize={16} />}
text={<T id={'print'} />}
/>
<Button
className={Classes.MINIMAL}
icon={<Icon icon="file-export-16" iconSize={16} />}
text={<T id={'export'} />}
/>
</NavbarGroup>
</DashboardActionsBar>
)
}
export default compose(
withReceivableAgingActions,
withReceivableAging(({ receivableAgingSummaryFilter }) => ({
receivableAgingFilter: receivableAgingSummaryFilter,
})),
)(ReceivableAgingSummaryActionsBar)

View File

@@ -0,0 +1,140 @@
import React, { useCallback } from 'react';
import { useIntl, FormattedMessage as T, } from 'react-intl';
import { useFormik } from "formik";
import { Row, Col } from 'react-grid-system';
import * as Yup from 'yup';
import {
Intent,
FormGroup,
InputGroup,
Position,
Button,
} from '@blueprintjs/core';
import { DateInput } from '@blueprintjs/datetime';
import FinancialStatementHeader from 'containers/FinancialStatements/FinancialStatementHeader';
import {
ErrorMessage,
FieldHint,
FieldRequiredHint,
} from 'components';
import {
momentFormatter,
} from 'utils';
import withReceivableAging from './withReceivableAgingSummary';
import { compose } from 'utils';
function ReceivableAgingSummaryHeader({
onSubmitFilter,
receivableAgingFilter,
}) {
const { formatMessage } = useIntl();
const {
values,
errors,
touched,
setFieldValue,
getFieldProps,
submitForm,
isSubmitting
} = useFormik({
enableReinitialize: true,
initialValues: {
// as_date: new Date(),
aging_before_days: 30,
aging_periods: 3,
},
validationSchema: Yup.object().shape({
as_date: Yup.date().required().label('As date'),
aging_before_days: Yup.number().required().integer().positive().label('aging_before_days'),
aging_periods: Yup.number().required().integer().positive().label('aging_periods'),
}),
onSubmit: (values, { setSubmitting }) => {
onSubmitFilter(values);
setSubmitting(false);
}
});
const handleDateChange = useCallback((name) => (date) => {
setFieldValue(name, date);
}, []);
// Handle submit filter submit button.
const handleSubmitClick = useCallback(() => {
submitForm();
}, [submitForm]);
return (
<FinancialStatementHeader show={receivableAgingFilter}>
<Row>
<Col sm={3}>
<FormGroup
label={formatMessage({'id': 'as_date'})}
labelInfo={<FieldHint />}
fill={true}
intent={errors.as_date && Intent.DANGER}>
<DateInput
{...momentFormatter('YYYY/MM/DD')}
// value={values.as_date}
onChange={handleDateChange('as_date')}
popoverProps={{ position: Position.BOTTOM, minimal: true }}
minimal={true}
fill={true} />
</FormGroup>
</Col>
<Col sm={3}>
<FormGroup
label={<T id={'aging_before_days'} />}
labelInfo={<FieldHint />}
className={'form-group--aging-before-days'}
intent={errors.aging_before_days && Intent.DANGER}
helperText={<ErrorMessage name="aging_before_days" {...{ errors, touched }} />}
>
<InputGroup
medium={true}
intent={errors.aging_before_days && Intent.DANGER}
{...getFieldProps('aging_before_days')}
/>
</FormGroup>
</Col>
<Col sm={3}>
<FormGroup
label={<T id={'aging_periods'} />}
labelInfo={<FieldHint />}
className={'form-group--aging-periods'}
intent={errors.aging_before_days && Intent.DANGER}
helperText={<ErrorMessage name="aging_periods" {...{ errors, touched }} />}
>
<InputGroup
medium={true}
intent={errors.aging_before_days && Intent.DANGER}
{...getFieldProps('aging_periods')}
/>
</FormGroup>
</Col>
</Row>
<Row>
<Col sm={3}>
<Button
type="submit"
onClick={handleSubmitClick}
disabled={isSubmitting}
className={'button--submit-filter'}>
<T id={'calculate_report'} />
</Button>
</Col>
</Row>
</FinancialStatementHeader>
);
}
export default compose(
withReceivableAging(({ receivableAgingSummaryFilter }) => ({
receivableAgingFilter: receivableAgingSummaryFilter,
})),
)(ReceivableAgingSummaryHeader);

View File

@@ -0,0 +1,82 @@
import React, { useMemo, useCallback } from 'react';
import { FormattedMessage as T, useIntl } from 'react-intl';
import DataTable from "components/DataTable";
import FinancialSheet from 'components/FinancialSheet';
import Money from 'components/Money';
import withSettings from 'containers/Settings/withSettings';
import { compose } from 'utils';
import withReceivableAgingSummary from './withReceivableAgingSummary';
import withReceivableAgingSummaryTable from './withReceivableAgingSummaryTable';
function ReceivableAgingSummaryTable({
// #withPreferences
organizationSettings,
// #withReceivableAgingSummary
receviableAgingRows = [],
receivableAgingLoading,
receivableAgingColumns,
// #ownProps
receivableAgingSummaryQuery,
onFetchData,
}) {
const { formatMessage } = useIntl();
const agingColumns = useMemo(() => {
return receivableAgingColumns.map((agingColumn) => {
return `${agingColumn.before_days} - ${agingColumn.to_days || '<'}`;
});
}, [receivableAgingColumns]);
const columns = useMemo(() => ([
{
Header: (<T id={'customer_name'} />),
accessor: 'customer_name',
className: 'customer_name',
},
...agingColumns.map((agingColumn, index) => ({
Header: agingColumn,
id: `asd-${index}`,
})),
{
Header: (<T id={'total'} />),
accessor: 'total',
className: 'total',
},
]), [agingColumns]);
const handleFetchData = useCallback((...args) => {
onFetchData && onFetchData(...args);
}, [onFetchData]);
return (
<FinancialSheet
companyName={organizationSettings.name}
sheetType={formatMessage({ id: 'receivable_aging_summary' })}
asDate={new Date()}
loading={receivableAgingLoading}>
<DataTable
className="bigcapital-datatable--financial-report"
columns={columns}
data={receviableAgingRows}
onFetchData={handleFetchData}
sticky={true}
/>
</FinancialSheet>
);
}
export default compose(
withSettings,
withReceivableAgingSummaryTable,
withReceivableAgingSummary(({
receivableAgingSummaryLoading,
receivableAgingSummaryColumns }) => ({
receivableAgingLoading: receivableAgingSummaryLoading,
receivableAgingColumns: receivableAgingSummaryColumns
})),
)(ReceivableAgingSummaryTable);

View File

@@ -0,0 +1,28 @@
import { connect } from 'react-redux';
import {
getFinancialSheet,
getFinancialSheetColumns,
} from 'store/financialStatement/financialStatements.selectors';
export default (mapState) => {
const mapStateToProps = (state, props) => {
const { receivableAgingSummaryIndex } = props;
const mapped = {
receivableAgingSummarySheet: getFinancialSheet(
state.financialStatements.receivableAgingSummary.sheets,
receivableAgingSummaryIndex,
),
receivableAgingSummaryColumns: getFinancialSheetColumns(
state.financialStatements.receivableAgingSummary.sheets,
receivableAgingSummaryIndex,
),
receivableAgingSummaryLoading:
state.financialStatements.receivableAgingSummary.loading,
receivableAgingSummaryFilter:
state.financialStatements.receivableAgingSummary.filter,
};
return mapState ? mapState(mapped, state, props) : mapped;
};
return connect(mapStateToProps);
}

View File

@@ -0,0 +1,12 @@
import { connect } from 'react-redux';
import { fetchReceivableAgingSummary } from 'store/financialStatement/financialStatements.actions';
const mapActionsToProps = (dispatch) => ({
requestReceivableAgingSummary: (query) =>
dispatch(fetchReceivableAgingSummary({ query })),
toggleFilterReceivableAgingSummary: () => dispatch({
type: 'RECEIVABLE_AGING_SUMMARY_FILTER_TOGGLE',
}),
});
export default connect(null, mapActionsToProps);

View File

@@ -0,0 +1,14 @@
import { getFinancialSheetIndexByQuery } from 'store/financialStatement/financialStatements.selectors';
import { connect } from 'react-redux';
const mapStateToProps = (state, props) => {
const { receivableAgingSummaryQuery } = props;
return {
receivableAgingSummaryIndex: getFinancialSheetIndexByQuery(
state.financialStatements.receivableAgingSummary.sheets,
receivableAgingSummaryQuery,
),
};
}
export default connect(mapStateToProps);

View File

@@ -7,6 +7,8 @@ import {
MenuItem,
} from '@blueprintjs/core';
import { FormattedMessage as T } from 'react-intl';
import classNames from 'classnames';
import { MODIFIER } from 'components';
export default function SelectsListColumnsBy(props) {
const { onItemSelect, formGroupProps, selectListProps } = props;
@@ -46,9 +48,10 @@ export default function SelectsListColumnsBy(props) {
noResults={<MenuItem disabled={true} text="No results." />}
filterable={false}
itemRenderer={itemRenderer}
popoverProps={{ minimal: true }}
popoverProps={{ minimal: true, usePortal: false, inline: true }}
buttonLabel={buttonLabel}
onItemSelect={handleItemSelect}
onItemSelect={handleItemSelect}
className={classNames(MODIFIER.SELECT_LIST_FILL_POPOVER)}
{...selectListProps} />
</FormGroup>
);

View File

@@ -3,12 +3,14 @@ import {
getResourceColumns,
getResourceFields,
getResourceMetadata,
getResourceData,
} from 'store/resources/resources.reducer';
export default (mapState) => {
const mapStateToProps = (state, props) => {
const { resourceName } = props;
const mapped = {
resourceData: getResourceData(state, resourceName),
resourceFields: getResourceFields(state, resourceName),
resourceColumns: getResourceColumns(state, resourceName),
resourceMetadata: getResourceMetadata(state, resourceName),

View File

@@ -2,11 +2,13 @@ import {connect} from 'react-redux';
import {
fetchResourceColumns,
fetchResourceFields,
fetchResourceData,
} from 'store/resources/resources.actions';
export const mapDispatchToProps = (dispatch) => ({
requestFetchResourceFields: (resourceSlug) => dispatch(fetchResourceFields({ resourceSlug })),
requestFetchResourceColumns: (resourceSlug) => dispatch(fetchResourceColumns({ resourceSlug })),
requestResourceData: (resourceSlug) => dispatch(fetchResourceData({ resourceSlug })),
});
export default connect(null, mapDispatchToProps);

View File

@@ -231,7 +231,7 @@ export default {
select_expense_account: 'Select Expense Account',
and: 'And',
or: 'OR',
select_a_comparator: 'Select a comparator',
comparator: 'Comparator',
equals: 'Equals',
not_equal: 'Not Equal',
contain: 'Contain',
@@ -359,7 +359,6 @@ export default {
once_delete_this_expense_you_will_able_to_restore_it: `Once you delete this expense, you won\'t be able to restore it later. Are you sure you want to delete this expense?`,
january: 'January',
february: 'February',
march: 'March',
@@ -372,7 +371,6 @@ export default {
october: 'October',
november: 'November',
december: 'December',
// Name Labels
expense_account_id: 'Expense account',
payment_account_id: 'Payment account',
@@ -434,7 +432,6 @@ export default {
new_expense: 'New Expense',
full_amount: 'Full Amount',
payment_date_: 'Payment date',
the_expense_has_been_successfully_created:
'The expense #{number} has been successfully created.',
the_expense_has_been_successfully_edited:
@@ -444,7 +441,14 @@ export default {
the_expenses_has_been_successfully_deleted:
'The expenses has been successfully deleted',
the_expense_id_has_been_published: 'The expense id has been published',
select_beneficiary_account: 'Select Beneficiary Account',
total_amount_equals_zero: 'Total amount equals zero',
value: 'Value',
you_reached_conditions_limit: 'You have reached to conditions limit.',
customer_name: 'Customer Name',
as_date: 'As Date',
aging_before_days: 'Aging before days',
aging_periods: 'Aging periods',
as: 'As',
receivable_aging_summary: 'Receivable Aging Summary'
};

View File

@@ -127,6 +127,16 @@ export default [
breadcrumb: 'Profit Loss Sheet',
}),
},
{
path: '/receivable-aging-summary',
component: LazyLoader({
loader: () =>
import(
'containers/FinancialStatements/ReceivableAgingSummary/ReceivableAgingSummary'
),
breadcrumb: 'Receivable Aging Summary',
}),
},
{
path: `/journal-sheet`,
component: LazyLoader({

View File

@@ -2,7 +2,6 @@ import { createReducer } from '@reduxjs/toolkit';
import { createTableQueryReducers } from 'store/queryReducers';
import t from 'store/types';
import { omit } from 'lodash';
const initialState = {
items: {},
@@ -90,7 +89,5 @@ const reducer = createReducer(initialState, {
export default createTableQueryReducers('expenses', reducer);
export const getExpenseById = (state, id) => {
// debugger;
// state.items = omit(state.items, [id]);
return state.expenses.items[id];
return state.expenses.items[id] || {};
};

View File

@@ -23,9 +23,6 @@ export const fetchGeneralLedger = ({ query }) => {
export const fetchBalanceSheet = ({ query }) => {
return (dispatch) => new Promise((resolve, reject) => {
dispatch({
type: t.SET_DASHBOARD_REQUEST_LOADING,
});
dispatch({
type: t.BALANCE_SHEET_LOADING,
loading: true,
@@ -109,4 +106,37 @@ export const fetchJournalSheet = ({ query }) => {
resolve(response.data);
}).catch(error => { reject(error); });
});
};
};
export const fetchReceivableAgingSummary = ({ query }) => {
return (dispatch) => new Promise((resolve, reject) => {
dispatch({
type: t.RECEIVABLE_AGING_SUMMARY_LOADING,
payload: {
loading: true,
},
});
ApiService
.get('/financial_statements/receivable_aging_summary', { params: query })
.then((response) => {
dispatch({
type: t.RECEIVABLE_AGING_SUMMARY_SET,
payload: {
aging: response.data.aging,
columns: response.data.columns,
query,
},
});
dispatch({
type: t.RECEIVABLE_AGING_SUMMARY_LOADING,
payload: {
loading: false,
},
});
resolve(response);
})
.catch((error) => {
reject(error);
})
});
}

View File

@@ -1,11 +1,9 @@
import { createReducer } from '@reduxjs/toolkit';
import t from 'store/types';
import {
// getBalanceSheetIndexByQuery,
getFinancialSheetIndexByQuery,
// getFinancialSheetIndexByQuery,
} from './financialStatements.selectors';
import {omit} from 'lodash';
import { omit } from 'lodash';
const initialState = {
balanceSheet: {
@@ -34,38 +32,48 @@ const initialState = {
loading: false,
tableRows: [],
filter: true,
}
},
receivableAgingSummary: {
sheets: [],
loading: false,
tableRows: [],
filter: true,
},
};
const mapGeneralLedgerAccountsToRows = (accounts) => {
return accounts.reduce((tableRows, account) => {
const children = [];
children.push({
...account.opening, rowType: 'opening_balance',
...account.opening,
rowType: 'opening_balance',
});
account.transactions.map((transaction) => {
children.push({
...transaction, ...omit(account, ['transactions']),
rowType: 'transaction'
...transaction,
...omit(account, ['transactions']),
rowType: 'transaction',
});
});
children.push({
...account.closing, rowType: 'closing_balance',
...account.closing,
rowType: 'closing_balance',
});
tableRows.push({
...omit(account, ['transactions']), children,
...omit(account, ['transactions']),
children,
rowType: 'account_name',
});
return tableRows;
}, []);
}
};
const mapJournalTableRows = (journal) => {
return journal.reduce((rows, journal) => {
journal.entries.forEach((entry, index) => {
rows.push({
...entry,
rowType: (index === 0) ? 'first_entry' : 'entry'
rowType: index === 0 ? 'first_entry' : 'entry',
});
});
rows.push({
@@ -78,8 +86,7 @@ const mapJournalTableRows = (journal) => {
});
return rows;
}, []);
}
};
const mapProfitLossToTableRows = (profitLoss) => {
return [
@@ -92,7 +99,7 @@ const mapProfitLossToTableRows = (profitLoss) => {
name: 'Total Income',
total: profitLoss.income.total,
rowType: 'income_total',
}
},
],
},
{
@@ -104,15 +111,15 @@ const mapProfitLossToTableRows = (profitLoss) => {
name: 'Total Expenses',
total: profitLoss.expenses.total,
rowType: 'expense_total',
}
},
],
},
{
name: 'Net Income',
total: profitLoss.net_income.total,
total: profitLoss.net_income.total,
rowType: 'net_income',
}
]
},
];
};
const financialStatementFilterToggle = (financialName, statePath) => {
@@ -120,13 +127,16 @@ const financialStatementFilterToggle = (financialName, statePath) => {
[`${financialName}_FILTER_TOGGLE`]: (state, action) => {
state[statePath].filter = !state[statePath].filter;
},
}
};
};
export default createReducer(initialState, {
[t.BALANCE_SHEET_STATEMENT_SET]: (state, action) => {
const index = getFinancialSheetIndexByQuery(state.balanceSheet.sheets, action.query);
const index = getFinancialSheetIndexByQuery(
state.balanceSheet.sheets,
action.query,
);
const balanceSheet = {
accounts: action.data.accounts,
columns: Object.values(action.data.columns),
@@ -145,7 +155,10 @@ export default createReducer(initialState, {
...financialStatementFilterToggle('BALANCE_SHEET', 'balanceSheet'),
[t.TRAIL_BALANCE_STATEMENT_SET]: (state, action) => {
const index = getFinancialSheetIndexByQuery(state.trialBalance.sheets, action.query);
const index = getFinancialSheetIndexByQuery(
state.trialBalance.sheets,
action.query,
);
const trailBalanceSheet = {
accounts: action.data.accounts,
query: action.data.query,
@@ -163,7 +176,10 @@ export default createReducer(initialState, {
...financialStatementFilterToggle('TRIAL_BALANCE', 'trialBalance'),
[t.JOURNAL_SHEET_SET]: (state, action) => {
const index = getFinancialSheetIndexByQuery(state.journal.sheets, action.query);
const index = getFinancialSheetIndexByQuery(
state.journal.sheets,
action.query,
);
const journal = {
query: action.data.query,
@@ -183,8 +199,11 @@ export default createReducer(initialState, {
...financialStatementFilterToggle('JOURNAL', 'journal'),
[t.GENERAL_LEDGER_STATEMENT_SET]: (state, action) => {
const index = getFinancialSheetIndexByQuery(state.generalLedger.sheets, action.query);
const index = getFinancialSheetIndexByQuery(
state.generalLedger.sheets,
action.query,
);
const generalLedger = {
query: action.data.query,
accounts: action.data.accounts,
@@ -203,7 +222,10 @@ export default createReducer(initialState, {
...financialStatementFilterToggle('GENERAL_LEDGER', 'generalLedger'),
[t.PROFIT_LOSS_SHEET_SET]: (state, action) => {
const index = getFinancialSheetIndexByQuery(state.profitLoss.sheets, action.query);
const index = getFinancialSheetIndexByQuery(
state.profitLoss.sheets,
action.query,
);
const profitLossSheet = {
query: action.query,
@@ -212,7 +234,7 @@ export default createReducer(initialState, {
tableRows: mapProfitLossToTableRows(action.profitLoss),
};
if (index !== -1) {
state.profitLoss.sheets[index] = profitLossSheet;
state.profitLoss.sheets[index] = profitLossSheet;
} else {
state.profitLoss.sheets.push(profitLossSheet);
}
@@ -222,4 +244,27 @@ export default createReducer(initialState, {
state.profitLoss.loading = !!action.loading;
},
...financialStatementFilterToggle('PROFIT_LOSS', 'profitLoss'),
});
[t.RECEIVABLE_AGING_SUMMARY_LOADING]: (state, action) => {
const { loading } = action.payload;
state.receivableAgingSummary.loading = loading;
},
[t.RECEIVABLE_AGING_SUMMARY_SET]: (state, action) => {
const { aging, columns, query } = action.payload;
const index = getFinancialSheetIndexByQuery(state.receivableAgingSummary.sheets, query);
const receivableSheet = {
query,
columns,
aging,
tableRows: aging
};
if (index !== -1) {
state.receivableAgingSummary[index] = receivableSheet;
} else {
state.receivableAgingSummary.sheets.push(receivableSheet);
}
},
...financialStatementFilterToggle('RECEIVABLE_AGING_SUMMARY', 'receivableAgingSummary'),
});

View File

@@ -15,4 +15,7 @@ export default {
PROFIT_LOSS_SHEET_SET: 'PROFIT_LOSS_SHEET_SET',
PROFIT_LOSS_SHEET_LOADING: 'PROFIT_LOSS_SHEET_LOADING',
RECEIVABLE_AGING_SUMMARY_LOADING: 'RECEIVABLE_AGING_SUMMARY_LOADING',
RECEIVABLE_AGING_SUMMARY_SET: 'RECEIVABLE_AGING_SUMMARY_SET',
}

View File

@@ -3,4 +3,5 @@
export default {
RESOURCE_COLUMNS_SET: 'RESOURCE_COLUMNS_SET',
RESOURCE_FIELDS_SET: 'RESOURCE_FIELDS_SET',
RESOURCE_DATA_SET: 'RESOURCE_DATA_SET',
};

View File

@@ -3,9 +3,6 @@ import t from 'store/types';
export const fetchResourceColumns = ({ resourceSlug }) => {
return (dispatch) => new Promise((resolve, reject) => {
dispatch({
type: t.SET_DASHBOARD_REQUEST_LOADING,
});
ApiService.get(`resources/${resourceSlug}/columns`).then((response) => {
dispatch({
type: t.RESOURCE_COLUMNS_SET,
@@ -22,9 +19,6 @@ export const fetchResourceColumns = ({ resourceSlug }) => {
export const fetchResourceFields = ({ resourceSlug }) => {
return (dispatch) => new Promise((resolve, reject) => {
dispatch({
type: t.SET_DASHBOARD_REQUEST_LOADING,
});
ApiService.get(`resources/${resourceSlug}/fields`).then((response) => {
dispatch({
type: t.RESOURCE_FIELDS_SET,
@@ -37,4 +31,19 @@ export const fetchResourceFields = ({ resourceSlug }) => {
resolve(response);
}).catch((error) => { reject(error); });
});
};
export const fetchResourceData = ({ resourceSlug }) => {
return (dispatch) => new Promise((resolve, reject) => {
ApiService.get(`/resources/${resourceSlug}/data`).then((response) => {
dispatch({
type: t.RESOURCE_DATA_SET,
payload: {
data: response.data.data,
resource_key: resourceSlug,
},
});
resolve(response);
}).catch(error => { reject(error); });
});
};

View File

@@ -3,6 +3,7 @@ import t from 'store/types';
import { pickItemsFromIds } from 'store/selectors'
const initialState = {
data: {},
fields: {},
columns: {},
resourceFields: {},
@@ -50,6 +51,14 @@ export default createReducer(initialState, {
};
state.resourceFields[action.resource_slug] = action.fields.map(f => f.id);
},
[t.RESOURCE_DATA_SET]: (state, action) => {
const { data, resource_key: resourceKey } = action.payload;
const dataMapped = {};
data.forEach((item) => { dataMapped[item.id] = item; })
state.data[resourceKey] = dataMapped;
},
});
/**
@@ -96,4 +105,9 @@ export const getResourceColumn = (state, columnId) => {
export const getResourceMetadata = (state, resourceSlug) => {
return state.resources.metadata[resourceSlug];
};
export const getResourceData = (state, resourceSlug) => {
return state.resources.data[resourceSlug] || {};
};

View File

@@ -92,4 +92,30 @@ body.authentication {
.bp3-toast{
box-shadow: none;
}
.select-list--fill-popover{
.bp3-transition-container,
.bp3-popover{
min-width: 100%;
}
}
.select-list--fill-button{
.bp3-popover-wrapper,
.bp3-popover-target{
display: block;
width: 100%;
}
.bp3-button{
width: 100%;
justify-content: start;
}
}
.bp3-datepicker-caption .bp3-html-select::after{
margin-right: 6px;
}

View File

@@ -25,7 +25,7 @@
padding: 0.8rem 0.5rem;
background: #F8FAFA;
font-size: 14px;
color: #555;
color: #444;
font-weight: 500;
border-bottom: 1px solid rgb(224, 224, 224);
}

View File

@@ -66,7 +66,7 @@
.bp3-button:not([class*="bp3-intent-"]):not(.bp3-minimal){
padding: 0;
background-size: contain;
background-color: #F6DCFA;
background-color: #EED1F2;
border-radius: 50%;
height: 30px;
width: 30px;
@@ -78,7 +78,7 @@
&,
&:hover,
&:focus{
background-color: #F6DCFA;
background-color: #EED1F2;
border: 0;
box-shadow: none;
}
@@ -131,11 +131,23 @@
background: rgba(167, 182, 194, 0.12);
color: #5C5C5C;
}
&.bp3-minimal:active,
&.bp3-minimal.bp3-active{
background: rgba(167, 182, 194, 0.12);
color: #5C5C5C;
}
&.has-active-filters{
&,
&.bp3-active,
&:active{
background: #eafbe4;
}
}
.#{$ns}-icon{
color: #666;
margin-right: 7px;
}
&.#{$ns}-minimal.#{$ns}-intent-danger{
color: #c23030;

View File

@@ -1,4 +1,6 @@
.form-group-display-columns-by{
position: relative;
}
.financial-statement{

View File

@@ -22,7 +22,7 @@ $sidebar-popover-submenu-bg: rgb(1, 20, 62);
&.ScrollbarsCustom-ThumbX,
&.ScrollbarsCustom-ThumbY {
background: rgba(0, 0, 0, 0);
}
}
}
&:hover {
@@ -37,7 +37,7 @@ $sidebar-popover-submenu-bg: rgb(1, 20, 62);
padding: 16px 12px;
&-logo{
margin-top: 4px;
margin-top: 2px;
svg{
opacity: 0.5;

View File

@@ -1,6 +1,6 @@
.filter-dropdown{
width: 500px;
width: 550px;
&__body{
padding: 12px;
@@ -17,19 +17,36 @@
}
.bp3-form-group{
padding-right: 16px;
padding-right: 4px;
margin-bottom: 0;
&:not(:last-of-type) {
padding-right: 12px;
padding-right: 8px;
}
.bp3-html-select select,
.bp3-select select{
padding: 0 20px 0 6px;
&:after{
margin-right: 10px;
}
}
.bp3-input{
padding: 0 6px;
}
.bp3-html-select select,
.bp3-select select,
.bp3-input-group .bp3-input{
height: 32px;
border-radius: 3px;
border-color: #dbd8d8;
}
.bp3-html-select::after,
.form-group--select-list .bp3-button::after{
border-top-color: #aaa;
margin-right: 10px;
}
}
&__footer{
@@ -37,9 +54,62 @@
padding: 5px 10px;
}
.form-group{
&--condition{ width: 25%; }
&--field{ width: 45%; }
&--compatator{ width: 30%; }
&--value{ width: 25%; }
&--condition{
width: 70px;
min-width: 70px;
}
&--field{
width: 45%;
}
&--compatator{
min-width: 120px;
width: 120px;
max-width: 120px;
}
&--value{
width: 55%;
}
}
}
.list-select--filter-dropdown{
.bp3-button:not([class*="bp3-intent-"]):not(.bp3-minimal),
.bp3-button:not([class*="bp3-intent-"]):not(.bp3-minimal){
&,
&:hover{
background-color: #E6EFFB;
border: 0;
border-radius: 3px;
}
&:after{
border-top-color: #afb9d0;
}
}
}
.popover--list-select-filter-dropdown{
.bp3-popover-content{
max-width: 200px;
}
.bp3-menu{
max-height: 250px;
overflow: auto;
}
.bp3-input-group{
padding: 8px;
padding-bottom: 4px;
.bp3-input:not(:first-child){
padding-left: 10px;
}
.bp3-icon-search{
display: none;
}
}
}

3190
server/dist/bundle.js vendored

File diff suppressed because one or more lines are too long

View File

@@ -29,8 +29,6 @@ export default class ReceivableAgingSummary extends AgingReport {
static async validateCustomersIds(req, res, next) {
const { Customer } = req.models;
console.log(req.query);
const filter = {
customer_ids: [],
...req.query,
@@ -100,6 +98,7 @@ export default class ReceivableAgingSummary extends AgingReport {
divide_1000: false,
},
customer_ids: [],
none_zero: false,
...req.query,
};
if (!Array.isArray(filter.customer_ids)) {

View File

@@ -1,5 +1,5 @@
import { Model, mixin } from 'objection';
import { snakeCase } from 'lodash';
import { snakeCase, each } from 'lodash';
import { mapKeysDeep } from '@/utils';
import PaginationQueryBuilder from '@/models/Pagination';
import DateSession from '@/models/DateSession';

View File

@@ -1,18 +1,25 @@
import bcrypt from 'bcryptjs';
import moment from 'moment';
import _ from 'lodash';
const { map, isArray, isPlainObject, mapKeys, mapValues } = require('lodash')
const { map, isArray, isPlainObject, mapKeys, mapValues } = require('lodash');
const hashPassword = (password) => new Promise((resolve) => {
bcrypt.genSalt(10, (error, salt) => {
bcrypt.hash(password, salt, (err, hash) => { resolve(hash); });
const hashPassword = (password) =>
new Promise((resolve) => {
bcrypt.genSalt(10, (error, salt) => {
bcrypt.hash(password, salt, (err, hash) => {
resolve(hash);
});
});
});
});
const origin = (request) => `${request.protocol}://${request.hostname}`;
const dateRangeCollection = (fromDate, toDate, addType = 'day', increment = 1) => {
const dateRangeCollection = (
fromDate,
toDate,
addType = 'day',
increment = 1
) => {
const collection = [];
const momentFromDate = moment(fromDate);
let dateFormat = '';
@@ -20,16 +27,21 @@ const dateRangeCollection = (fromDate, toDate, addType = 'day', increment = 1) =
switch (addType) {
case 'day':
default:
dateFormat = 'YYYY-MM-DD'; break;
dateFormat = 'YYYY-MM-DD';
break;
case 'month':
case 'quarter':
dateFormat = 'YYYY-MM'; break;
dateFormat = 'YYYY-MM';
break;
case 'year':
dateFormat = 'YYYY'; break;
dateFormat = 'YYYY';
break;
}
for (let i = momentFromDate;
(i.isBefore(toDate, addType) || i.isSame(toDate, addType));
i.add(increment, `${addType}s`)) {
for (
let i = momentFromDate;
i.isBefore(toDate, addType) || i.isSame(toDate, addType);
i.add(increment, `${addType}s`)
) {
collection.push(i.endOf(addType).format(dateFormat));
}
return collection;
@@ -46,55 +58,69 @@ const dateRangeFormat = (rangeType) => {
}
};
const mapKeysDeep = (obj, cb) => {
if (_.isArray(obj)) {
return obj.map(innerObj => mapKeysDeep(innerObj, cb));
function mapKeysDeep(obj, cb, isRecursive) {
if (!obj && !isRecursive) {
return {};
}
else if (_.isObject(obj)) {
return _.mapValues(
_.mapKeys(obj, cb),
val => mapKeysDeep(val, cb),
)
} else {
return obj;
if (!isRecursive) {
if (
typeof obj === 'string' ||
typeof obj === 'number' ||
typeof obj === 'boolean'
) {
return {};
}
}
if (Array.isArray(obj)) {
return obj.map((item) => mapKeysDeep(item, cb, true));
}
if (!_.isPlainObject(obj)) {
return obj;
}
const result = _.mapKeys(obj, cb);
return _.mapValues(result, (value) => mapKeysDeep(value, cb, true));
}
const mapValuesDeep = (v, callback) => (
const mapValuesDeep = (v, callback) =>
_.isObject(v)
? _.mapValues(v, v => mapValuesDeep(v, callback))
: callback(v));
? _.mapValues(v, (v) => mapValuesDeep(v, callback))
: callback(v);
const promiseSerial = (funcs) => {
return funcs.reduce((promise, func) => promise.then((result) => func().then(Array.prototype.concat.bind(result))),
Promise.resolve([]));
}
return funcs.reduce(
(promise, func) =>
promise.then((result) =>
func().then(Array.prototype.concat.bind(result))
),
Promise.resolve([])
);
};
const flatToNestedArray = (data, config = { id: 'id', parentId: 'parent_id' }) => {
const flatToNestedArray = (
data,
config = { id: 'id', parentId: 'parent_id' }
) => {
const map = {};
const nestedArray = [];
data.forEach((item) => {
map[item[config.id]] = item;
map[item[config.id]].children = [];
});
data.forEach((item) => {
const parentItemId = item[config.parentId];
if (!item[config.parentId]) {
nestedArray.push(item);
}
if(parentItemId) {
if (parentItemId) {
map[parentItemId].children.push(item);
}
});
return nestedArray;
}
return nestedArray;
};
export {
hashPassword,