feat: fix accounts issue.

This commit is contained in:
Ahmed Bouhuolia
2020-06-25 13:43:47 +02:00
parent 6074134a53
commit 111aa83908
46 changed files with 797 additions and 345 deletions

View File

@@ -8,15 +8,10 @@ import DialogsContainer from 'components/DialogsContainer';
import PreferencesContent from 'components/Preferences/PreferencesContent';
import PreferencesSidebar from 'components/Preferences/PreferencesSidebar';
import Search from 'containers/GeneralSearch/Search';
import withDashboard from 'containers/Dashboard/withDashboard';
import { compose } from 'utils';
function Dashboard({ sidebarExpended }) {
export default function Dashboard() {
return (
<div className={classNames('dashboard', {
'has-mini-sidebar': !sidebarExpended,
})}>
<div className={classNames('dashboard')}>
<Switch>
<Route path="/preferences">
<Sidebar />
@@ -35,9 +30,3 @@ function Dashboard({ sidebarExpended }) {
</div>
);
}
export default compose(
withDashboard(({ sidebarExpended }) => ({
sidebarExpended,
})),
)(Dashboard);

View File

@@ -0,0 +1,79 @@
import React, { useState, useMemo } from 'react';
import { FormattedMessage as T } from 'react-intl';
import PropTypes from 'prop-types';
import { Button, Tabs, Tab, Tooltip, Position } from '@blueprintjs/core';
import { If, Icon } from 'components';
export default function DashboardViewsTabs({
tabs,
allTab = true,
newViewTab = true,
onNewViewTabClick,
onChange,
onTabClick,
}) {
const [currentView, setCurrentView] = useState(0);
const handleClickNewView = () => {
onNewViewTabClick && onNewViewTabClick();
};
const handleTabClick = (viewId) => {
onTabClick && onTabClick(viewId);
};
const mappedTabs = useMemo(
() => tabs.map((tab) => ({ ...tab, onTabClick: handleTabClick })),
[tabs],
);
const handleViewLinkClick = () => {
onNewViewTabClick && onNewViewTabClick();
};
const handleTabsChange = (viewId) => {
setCurrentView(viewId);
onChange && onChange(viewId);
};
return (
<Tabs
id="navbar"
large={true}
selectedTabId={currentView}
className="tabs--dashboard-views"
onChange={handleTabsChange}
>
{allTab && (
<Tab id={0} title={<T id={'all'} />} onClick={handleViewLinkClick} />
)}
{mappedTabs.map((tab) => (
<Tab id={tab.id} title={tab.name} onClick={handleTabClick} />
))}
<If condition={newViewTab}>
<Tooltip
content={<T id={'create_a_new_view'} />}
position={Position.RIGHT}
>
<Button
className="button--new-view"
icon={<Icon icon="plus" />}
onClick={handleClickNewView}
minimal={true}
/>
</Tooltip>
</If>
</Tabs>
);
}
DashboardViewsTabs.propTypes = {
tabs: PropTypes.array.isRequired,
allTab: PropTypes.bool,
newViewTab: PropTypes.bool,
onNewViewTabClick: PropTypes.func,
onChange: PropTypes.func,
onTabClick: PropTypes.func,
};

View File

@@ -1,19 +1,17 @@
import React from 'react';
import { connect } from 'react-redux';
import { isDialogOpen, getDialogPayload } from 'store/dashboard/dashboard.selectors';
export default (Dialog) => {
function DialogReduxConnect(props) {
return (<Dialog {...props} />);
};
const mapStateToProps = (state, props) => {
const dialogs = state.dashboard.dialogs;
if (dialogs && dialogs.hasOwnProperty['name'] && dialogs[props.name]) {
const { isOpen, payload } = dialogs[props.name];
return { isOpen, payload };
}
return {
isOpen: isDialogOpen(state, props),
payload: getDialogPayload(state, props),
};
};
return connect(

View File

@@ -0,0 +1,26 @@
import React, { useMemo } from 'react';
import { HTMLSelect, Classes } from '@blueprintjs/core';
import { useIntl } from 'react-intl';
import { getConditionTypeCompatators } from './DynamicFilterCompatators';
export default function DynamicFilterCompatatorField({
dataType,
...restProps
}) {
const { formatMessage } = useIntl();
const options = useMemo(
() => getConditionTypeCompatators(dataType).map(comp => ({
value: comp.value, label: formatMessage({ id: comp.label_id }),
})),
[dataType]
);
return (
<HTMLSelect
options={options}
className={Classes.FILL}
{...{ ...restProps }}
/>
);
}

View File

@@ -0,0 +1,38 @@
export const BooleanCompatators = [
{ value: 'is', label_id: 'is' },
];
export const TextCompatators = [
{ value: 'contain', label_id: 'contain' },
{ value: 'not_contain', label_id: 'not_contain' },
{ value: 'equals', label_id: 'equals' },
{ value: 'not_equal', label_id: 'not_equals' },
];
export const DateCompatators = [
{ value: 'in', label_id: 'in' },
{ value: 'after', label_id: 'after' },
{ value: 'before', label_id: 'before' },
];
export const OptionsCompatators = [
{ value: 'is', label_id: 'is' },
{ value: 'is_not', label_id: 'is_not' },
];
export const getConditionTypeCompatators = (dataType) => {
return [
...(dataType === 'options'
? [...OptionsCompatators]
: dataType === 'date'
? [...DateCompatators]
: dataType === 'boolean'
? [...BooleanCompatators]
: [...TextCompatators]),
];
};
export const getConditionDefaultCompatator = (dataType) => {
const compatators = getConditionTypeCompatators(dataType);
return compatators[0];
};

View File

@@ -1,5 +1,11 @@
import React, { useMemo, useRef, useEffect, useState } from 'react';
import { FormGroup, MenuItem, InputGroup, Position, Spinner } from '@blueprintjs/core';
import {
FormGroup,
MenuItem,
InputGroup,
Position,
Checkbox,
} from '@blueprintjs/core';
import { connect } from 'react-redux';
import { useQuery } from 'react-query';
import { DateInput } from '@blueprintjs/datetime';
@@ -13,35 +19,42 @@ import { If, Choose, ListSelect, MODIFIER } from 'components';
import withResourceDetail from 'containers/Resources/withResourceDetails';
import withResourceActions from 'containers/Resources/withResourcesActions';
import {
getConditionTypeCompatators,
getConditionDefaultCompatator,
} from './DynamicFilterCompatators';
import { compose, momentFormatter } from 'utils';
/**
* Dynamic filter fields.
*/
function DynamicFilterValueField({
fieldMeta,
dataType,
value,
initialValue,
error,
// fieldkey,
// resourceKey,
// #withResourceDetail
resourceName,
resourceData,
requestResourceData,
onChange,
rosourceKey,
inputDebounceWait = 500,
}) {
const { formatMessage } = useIntl();
const [localValue, setLocalValue] = useState();
const fetchResourceData = useQuery(
['resource-data', resourceName],
() => requestResourceData(resourceName),
resourceName && ['resource-data', resourceName],
(k, resName) => requestResourceData(resName),
{ manual: true },
);
@@ -57,7 +70,7 @@ function DynamicFilterValueField({
};
const handleBtnClick = () => {
fetchResourceData.refetch({ force: true });
fetchResourceData.refetch({});
};
const listOptions = useMemo(() => Object.values(resourceData), [
@@ -85,75 +98,85 @@ function DynamicFilterValueField({
onChange && onChange(value);
}, inputDebounceWait),
);
const handleInputChange = (e) => {
setLocalValue(e.currentTarget.value);
if (e.currentTarget.type === 'checkbox') {
setLocalValue(e.currentTarget.checked);
} else {
setLocalValue(e.currentTarget.value);
}
handleInputChangeThrottled.current(e.currentTarget.value);
};
const handleCheckboxChange = (e) => {
const value = !!e.currentTarget.checked;
setLocalValue(value);
onChange && onChange(value);
}
const handleDateChange = (date) => {
setLocalValue(date);
onChange && onChange(date);
};
const transformDateValue = (value) => {
return moment(value || new Date()).toDate();
return value ? moment(value || new Date()).toDate() : null;
};
return (
<FormGroup className={'form-group--value'}>
<Choose>
<Choose.When condition={true}>
<Spinner size={18} />
<Choose.When condition={dataType === '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={dataType === 'date'}>
<DateInput
{...momentFormatter('YYYY/MM/DD')}
value={transformDateValue(localValue)}
onChange={handleDateChange}
popoverProps={{
minimal: true,
position: Position.BOTTOM,
}}
shortcuts={true}
placeholder={'Select date'}
/>
</Choose.When>
<Choose.When condition={dataType === 'boolean'}>
<Checkbox value={localValue} onChange={handleCheckboxChange} />
</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>
<InputGroup
placeholder={formatMessage({ id: 'value' })}
onChange={handleInputChange}
value={localValue}
/>
</Choose.Otherwise>
</Choose>
</FormGroup>
@@ -161,7 +184,7 @@ function DynamicFilterValueField({
}
const mapStateToProps = (state, props) => ({
resourceName: props.fieldMeta.resource_key || 'account_type',
resourceName: props.dataResource,
});
const withResourceFilterValueField = connect(mapStateToProps);

View File

@@ -1,3 +1,4 @@
// @flow
import React, { useEffect, useMemo, useCallback, useRef } from 'react';
import {
FormGroup,
@@ -7,25 +8,39 @@ import {
Intent,
} from '@blueprintjs/core';
import { useFormik } from 'formik';
import { isEqual } from 'lodash';
import { isEqual, last } from 'lodash';
import { usePrevious } from 'react-use';
import { debounce } from 'lodash';
import Icon from 'components/Icon';
import { checkRequiredProperties } from 'utils';
import { checkRequiredProperties, uniqueMultiProps } from 'utils';
import { FormattedMessage as T, useIntl } from 'react-intl';
import { DynamicFilterValueField } from 'components';
import {
DynamicFilterValueField,
DynamicFilterCompatatorField,
} from 'components';
import Toaster from 'components/AppToaster';
import moment from 'moment';
import {
getConditionTypeCompatators,
getConditionDefaultCompatator
} from './DynamicFilter/DynamicFilterCompatators';
let limitToast;
type InitialCondition = {
fieldKey: string,
comparator: string,
value: string,
};
/**
* Filter popover content.
*/
export default function FilterDropdown({
fields,
onFilterChange,
refetchDebounceWait = 250,
refetchDebounceWait = 10,
initialCondition,
}) {
const { formatMessage } = useIntl();
const fieldsKeyMapped = new Map(fields.map((field) => [field.key, field]));
@@ -37,6 +52,7 @@ export default function FilterDropdown({
],
[formatMessage],
);
const resourceFields = useMemo(
() => [
...fields.map((field) => ({
@@ -46,23 +62,13 @@ export default function FilterDropdown({
],
[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 defaultFilterCondition = useMemo(
() => ({
condition: 'and',
field_key: fields.length > 0 ? fields[0].key : '',
compatator: 'equals',
value: '',
field_key: initialCondition.fieldKey,
comparator: initialCondition.comparator,
value: initialCondition.value,
}),
[fields],
);
@@ -86,17 +92,17 @@ export default function FilterDropdown({
} else {
setFieldValue('conditions', [
...values.conditions,
defaultFilterCondition,
last(values.conditions),
]);
}
}, [values, defaultFilterCondition, setFieldValue]);
const filteredFilterConditions = useMemo(() => {
const requiredProps = ['field_key', 'condition', 'compatator', 'value'];
return values.conditions.filter(
const requiredProps = ['field_key', 'condition', 'comparator', 'value'];
const conditions = values.conditions.filter(
(condition) => !checkRequiredProperties(condition, requiredProps),
);
return uniqueMultiProps(conditions, requiredProps);
}, [values.conditions]);
const prevConditions = usePrevious(filteredFilterConditions);
@@ -109,7 +115,7 @@ export default function FilterDropdown({
useEffect(() => {
if (!isEqual(prevConditions, filteredFilterConditions) && prevConditions) {
onFilterChangeThrottled.current(filteredFilterConditions);
onFilterChange && onFilterChange(filteredFilterConditions);
}
}, [filteredFilterConditions, prevConditions]);
@@ -124,7 +130,7 @@ export default function FilterDropdown({
setFieldValue('conditions', [...conditions]);
};
// transform dynamic value field.
// Transform dynamic value field.
const transformValueField = (value) => {
if (value instanceof Date) {
return moment(value).format('YYYY-MM-DD');
@@ -145,27 +151,52 @@ export default function FilterDropdown({
const currentField = fieldsKeyMapped.get(
values.conditions[index].field_key,
);
const prevField = fieldsKeyMapped.get(e.currentTarget.value);
const nextField = fieldsKeyMapped.get(e.currentTarget.value);
if (currentField.data_type !== prevField.data_type) {
if (currentField.data_type !== nextField.data_type) {
setFieldValue(`conditions[${index}].value`, '');
}
const comparatorsObs = getConditionTypeCompatators(nextField.data_type);
const currentCompatator = values.conditions[index].comparator;
if (!currentCompatator || comparatorsObs.map(c => c.value).indexOf(currentCompatator) === -1) {
const defaultCompatator = getConditionDefaultCompatator(nextField.data_type);
setFieldValue(`conditions[${index}].comparator`, defaultCompatator.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);
},
});
// Compatator field props.
const comparatorFieldProps = (name, index) => {
const condition = values.conditions[index];
const field = fieldsKeyMapped.get(condition.field_key);
console.log(values.conditions, 'XX');
return {
...fieldProps(name, index),
dataType: field.data_type,
};
};
// Value field props.
const valueFieldProps = (name, index) => {
const condition = values.conditions[index];
const field = fieldsKeyMapped.get(condition.field_key);
return {
...fieldProps(name, index),
dataType: field.data_type,
resourceKey: field.resource_key,
options: field.options,
dataResource: field.data_resource,
onChange: (value) => {
const transformedValue = transformValueField(value);
setFieldValue(`conditions[${index}].${name}`, transformedValue);
},
};
};
return (
<div class="filter-dropdown">
@@ -190,18 +221,15 @@ export default function FilterDropdown({
/>
</FormGroup>
<FormGroup className={'form-group--compatator'}>
<HTMLSelect
options={compatatorsItems}
<FormGroup className={'form-group--comparator'}>
<DynamicFilterCompatatorField
className={Classes.FILL}
{...fieldProps('compatator', index)}
{...comparatorFieldProps('comparator', index)}
/>
</FormGroup>
<DynamicFilterValueField
fieldMeta={fieldsKeyMapped.get(condition.field_key)}
{...valueFieldProps('value', index)}
/>
<DynamicFilterValueField {...valueFieldProps('value', index)} />
<Button
icon={<Icon icon="times" iconSize={14} />}
minimal={true}

View File

@@ -1,4 +1,4 @@
import * as React from 'react';
import React, { useEffect } from 'react';
import { Scrollbar } from 'react-scrollbars-custom';
import classNames from 'classnames';
@@ -17,6 +17,10 @@ function SidebarContainer({
// #withDashboard
sidebarExpended,
}) {
useEffect(() => {
document.body.classList.toggle('has-mini-sidebar', !sidebarExpended);
}, [sidebarExpended]);
return (
<div
className={classNames('sidebar', {

View File

@@ -6,11 +6,13 @@ import For from './Utils/For';
import ListSelect from './ListSelect';
import FinancialStatement from './FinancialStatement';
import DynamicFilterValueField from './DynamicFilter/DynamicFilterValueField';
import DynamicFilterCompatatorField from './DynamicFilter/DynamicFilterCompatatorField';
import ErrorMessage from './ErrorMessage';
import MODIFIER from './modifiers';
import FieldHint from './FieldHint';
import MenuItemLabel from './MenuItemLabel';
import Pagination from './Pagination';
import DashboardViewsTabs from './Dashboard/DashboardViewsTabs';
const Hint = FieldHint;
@@ -23,11 +25,13 @@ export {
FinancialStatement,
Choose,
DynamicFilterValueField,
DynamicFilterCompatatorField,
MODIFIER,
ErrorMessage,
FieldHint,
Hint,
MenuItemLabel,
Pagination,
DashboardViewsTabs,
// For,
};

View File

@@ -10,11 +10,12 @@ import {
Popover,
PopoverInteractionKind,
Position,
Intent
Intent,
} from '@blueprintjs/core';
import classNames from 'classnames';
import { useRouteMatch, useHistory } from 'react-router-dom';
import { FormattedMessage as T } from 'react-intl';
import { connect } from 'react-redux';
import FilterDropdown from 'components/FilterDropdown';
import DashboardActionsBar from 'components/Dashboard/DashboardActionsBar';
@@ -28,11 +29,9 @@ import withManualJournalsActions from 'containers/Accounting/withManualJournalsA
import { compose } from 'utils';
function ManualJournalActionsBar({
// #withResourceDetail
resourceName = 'manual_journal',
resourceName = 'manual_journals',
resourceFields,
// #withManualJournals
@@ -43,12 +42,12 @@ function ManualJournalActionsBar({
onFilterChanged,
selectedRows,
onBulkDelete
onBulkDelete,
}) {
const { path } = useRouteMatch();
const history = useHistory();
const viewsMenuItems = manualJournalsViews.map(view => {
const viewsMenuItems = manualJournalsViews.map((view) => {
return (
<MenuItem href={`${path}/${view.id}/custom_view`} text={view.name} />
);
@@ -60,18 +59,25 @@ function ManualJournalActionsBar({
const filterDropdown = FilterDropdown({
fields: resourceFields,
onFilterChange: filterConditions => {
initialCondition: {
fieldKey: 'journal_number',
compatator: 'contains',
value: '',
},
onFilterChange: (filterConditions) => {
addManualJournalsTableQueries({
filter_roles: filterConditions || ''
filter_roles: filterConditions || '',
});
onFilterChanged && onFilterChanged(filterConditions);
}
},
});
const hasSelectedRows = useMemo(() => selectedRows.length > 0, [selectedRows]);
const hasSelectedRows = useMemo(() => selectedRows.length > 0, [
selectedRows,
]);
// Handle delete button click.
const handleBulkDelete = useCallback(() => {
onBulkDelete && onBulkDelete(selectedRows.map(r => r.id));
onBulkDelete && onBulkDelete(selectedRows.map((r) => r.id));
}, [onBulkDelete, selectedRows]);
return (
@@ -85,8 +91,8 @@ function ManualJournalActionsBar({
>
<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>
@@ -95,27 +101,28 @@ function ManualJournalActionsBar({
<Button
className={Classes.MINIMAL}
icon={<Icon icon='plus' />}
text={<T id={'new_journal'}/>}
icon={<Icon icon="plus" />}
text={<T id={'new_journal'} />}
onClick={onClickNewManualJournal}
/>
<Popover
minimal={true}
content={filterDropdown}
interactionKind={PopoverInteractionKind.CLICK}
position={Position.BOTTOM_LEFT}
>
<Button
className={classNames(Classes.MINIMAL, 'button--filter')}
text='Filter'
icon={<Icon icon='filter-16' iconSize={16} />}
text="Filter"
icon={<Icon icon="filter-16" iconSize={16} />}
/>
</Popover>
<If condition={hasSelectedRows}>
<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}
/>
@@ -123,20 +130,27 @@ function ManualJournalActionsBar({
<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'} />}
/>
<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'} />}
/>
</NavbarGroup>
</DashboardActionsBar>
);
}
const mapStateToProps = (state, props) => ({
resourceName: 'manual_journals',
});
const withManualJournalsActionsBar = connect(mapStateToProps);
export default compose(
withManualJournalsActionsBar,
withDialogActions,
withResourceDetail(({ resourceFields }) => ({
resourceFields,

View File

@@ -17,6 +17,7 @@ import withManualJournals from 'containers/Accounting/withManualJournals';
import withManualJournalsActions from 'containers/Accounting/withManualJournalsActions';
import withViewsActions from 'containers/Views/withViewsActions';
import withRouteActions from 'containers/Router/withRouteActions';
import withResourceActions from 'containers/Resources/withResourcesActions';
import { compose } from 'utils';
@@ -30,6 +31,9 @@ function ManualJournalsTable({
// #withViewsActions
requestFetchResourceViews,
// #withResourceActions
requestFetchResourceFields,
// #withManualJournals
manualJournalsTableQuery,
@@ -50,10 +54,15 @@ function ManualJournalsTable({
const { formatMessage } = useIntl();
const fetchViews = useQuery('journals-resource-views', () => {
const fetchViews = useQuery('manual-journals-resource-views', () => {
return requestFetchResourceViews('manual_journals');
});
const fetchResourceFields = useQuery(
'manual-journals-resource-fields',
() => requestFetchResourceFields('manual_journals'),
);
const fetchManualJournals = useQuery(
['manual-journals-table', manualJournalsTableQuery],
(key, q) => requestFetchManualJournalsTable(q),
@@ -108,8 +117,8 @@ function ManualJournalsTable({
.then(() => {
AppToaster.show({
message: formatMessage(
{ id: 'the_journals_has_been_successfully_deleted', },
{ count: selectedRowsCount, },
{ id: 'the_journals_has_been_successfully_deleted' },
{ count: selectedRowsCount },
),
intent: Intent.SUCCESS,
});
@@ -189,7 +198,7 @@ function ManualJournalsTable({
return (
<DashboardInsider
loading={fetchViews.isFetching}
loading={fetchViews.isFetching || fetchResourceFields.isFetching}
name={'manual-journals'}
>
<ManualJournalsActionsBar
@@ -265,6 +274,7 @@ export default compose(
withDashboardActions,
withManualJournalsActions,
withViewsActions,
withResourceActions,
withManualJournals(({ manualJournalsTableQuery }) => ({
manualJournalsTableQuery,
})),

View File

@@ -28,7 +28,7 @@ export default (mapState) => {
manualJournalsTableQuery.page,
),
manualJournalsTableQuery,
manualJournalsViews: getResourceViews(state, 'manual_journals'),
manualJournalsViews: getResourceViews(state, props, 'manual_journals'),
manualJournalsItems: state.manualJournals.items,
manualJournalsPagination: state.manualJournals.paginationMeta,

View File

@@ -64,6 +64,11 @@ function AccountsActionsBar({
const filterDropdown = FilterDropdown({
fields: resourceFields,
initialCondition: {
fieldKey: 'name',
compatator: 'contains',
value: '',
},
onFilterChange: (filterConditions) => {
setFilterCount(filterConditions.length || 0);
addAccountsTableQueries({
@@ -125,7 +130,7 @@ function AccountsActionsBar({
filterCount <= 0 ? (
<T id={'filter'} />
) : (
`${filterCount} filters applied`
<T id={'count_filters_applied'} values={{ count: filterCount }} />
)
}
icon={<Icon icon="filter-16" iconSize={16} />}

View File

@@ -333,6 +333,7 @@ function AccountsChart({
<AccountsViewsTabs onViewChanged={handleViewChanged} />
<AccountsDataTable
loading={fetchAccountsHook.isFetching}
onDeleteAccount={handleDeleteAccount}
onInactiveAccount={handleInactiveAccount}
onActivateAccount={handleActivateAccount}

View File

@@ -1,4 +1,4 @@
import React, { useCallback, useState, useMemo } from 'react';
import React, { useCallback, useState, useMemo, useEffect } from 'react';
import {
Button,
Popover,
@@ -10,6 +10,7 @@ import {
Tooltip,
Intent,
} from '@blueprintjs/core';
import { withRouter } from 'react-router';
import { FormattedMessage as T, useIntl } from 'react-intl';
import Icon from 'components/Icon';
@@ -24,6 +25,7 @@ import withDashboardActions from 'containers/Dashboard/withDashboardActions';
import withAccountsActions from 'containers/Accounts/withAccountsActions';
import withAccounts from 'containers/Accounts/withAccounts';
import withDialogActions from 'containers/Dialog/withDialogActions';
import withCurrentView from 'containers/Views/withCurrentView';
import { If } from 'components';
@@ -35,6 +37,8 @@ function AccountsDataTable({
// #withDialog.
openDialog,
currentViewId,
// own properties
loading,
onFetchData,
@@ -43,14 +47,18 @@ function AccountsDataTable({
onInactiveAccount,
onActivateAccount,
}) {
const [initialMount, setInitialMount] = useState(false);
const [isMounted, setIsMounted] = useState(false);
const { formatMessage } = useIntl();
useEffect(() => {
setIsMounted(false);
}, [currentViewId]);
useUpdateEffect(() => {
if (!accountsLoading) {
setInitialMount(true);
setIsMounted(true);
}
}, [accountsLoading, setInitialMount]);
}, [accountsLoading, setIsMounted]);
const handleEditAccount = useCallback(
(account) => () => {
@@ -132,21 +140,21 @@ function AccountsDataTable({
);
},
className: 'account_name',
width: 300,
width: 220,
},
{
id: 'code',
Header: formatMessage({ id: 'code' }),
accessor: 'code',
className: 'code',
width: 100,
width: 125,
},
{
id: 'type',
Header: formatMessage({ id: 'type' }),
accessor: 'type.name',
className: 'type',
width: 120,
width: 140,
},
{
id: 'normal',
@@ -168,7 +176,7 @@ function AccountsDataTable({
);
},
className: 'normal',
width: 75,
width: 115,
},
{
id: 'balance',
@@ -207,9 +215,9 @@ function AccountsDataTable({
const selectionColumn = useMemo(
() => ({
minWidth: 50,
width: 50,
maxWidth: 50,
minWidth: 40,
width: 40,
maxWidth: 40,
}),
[],
);
@@ -227,7 +235,7 @@ function AccountsDataTable({
);
return (
<LoadingIndicator loading={loading} mount={false}>
<LoadingIndicator loading={loading && !isMounted} mount={false}>
<DataTable
noInitialFetch={true}
columns={columns}
@@ -239,7 +247,7 @@ function AccountsDataTable({
treeGraph={true}
sticky={true}
onSelectedRowsChange={handleSelectedRowsChange}
loading={accountsLoading && !initialMount}
loading={accountsLoading && !isMounted}
spinnerProps={{ size: 30 }}
rowContextMenu={rowContextMenu}
/>
@@ -248,6 +256,8 @@ function AccountsDataTable({
}
export default compose(
withRouter,
withCurrentView,
withDialogActions,
withDashboardActions,
withAccountsActions,

View File

@@ -1,4 +1,4 @@
import React, {useEffect} from 'react';
import React, { useEffect, useRef } from 'react';
import { useHistory } from 'react-router';
import { connect } from 'react-redux';
import {
@@ -7,14 +7,15 @@ import {
NavbarGroup,
Tabs,
Tab,
Button
Button,
} from '@blueprintjs/core';
import { useParams, withRouter } from 'react-router-dom';
import Icon from 'components/Icon';
import { Link } from 'react-router-dom';
import { pick, debounce } from 'lodash';
import Icon from 'components/Icon';
import { FormattedMessage as T } from 'react-intl';
import {useUpdateEffect} from 'hooks';
import { useUpdateEffect } from 'hooks';
import { DashboardViewsTabs } from 'components';
import withDashboardActions from 'containers/Dashboard/withDashboardActions';
import withAccounts from 'containers/Accounts/withAccounts';
import withAccountsTableActions from 'containers/Accounts/withAccountsTableActions';
@@ -48,7 +49,7 @@ function AccountsViewsTabs({
useEffect(() => {
changeAccountsCurrentView(customViewId || -1);
setTopbarEditView(customViewId);
changePageSubtitle((customViewId && viewItem) ? viewItem.name : '');
changePageSubtitle(customViewId && viewItem ? viewItem.name : '');
addAccountsTableQueries({
custom_view_id: customViewId,
@@ -57,7 +58,7 @@ function AccountsViewsTabs({
return () => {
setTopbarEditView(null);
changePageSubtitle('');
changeAccountsCurrentView(null)
changeAccountsCurrentView(null);
};
}, [customViewId]);
@@ -71,52 +72,37 @@ function AccountsViewsTabs({
history.push('/custom_views/accounts/new');
};
// Handle view tab link click.
const handleViewLinkClick = () => {
setTopbarEditView(customViewId);
const tabs = accountsViews.map((view) => ({
...pick(view, ['name', 'id']),
}));
const debounceChangeHistory = useRef(
debounce((toUrl) => {
history.push(toUrl);
}, 250),
);
const handleTabsChange = (viewId) => {
const toPath = viewId ? `${viewId}/custom_view` : '';
debounceChangeHistory.current(`/accounts/${toPath}`);
setTopbarEditView(viewId);
};
const tabs = accountsViews.map((view) => {
const baseUrl = '/accounts';
const link = (
<Link
to={`${baseUrl}/${view.id}/custom_view`}
onClick={handleViewLinkClick}
>{ view.name }</Link>
);
return <Tab id={`custom_view_${view.id}`} title={link} />;
});
return (
<Navbar className='navbar--dashboard-views'>
<Navbar className="navbar--dashboard-views">
<NavbarGroup align={Alignment.LEFT}>
<Tabs
id='navbar'
large={true}
selectedTabId={customViewId ? `custom_view_${customViewId}` : 'all'}
className='tabs--dashboard-views'
>
<Tab
id={'all'}
title={<Link to={`/accounts`}><T id={'all'}/></Link>}
onClick={handleViewLinkClick}
/>
{ tabs }
<Button
className='button--new-view'
icon={<Icon icon='plus' />}
onClick={handleClickNewView}
minimal={true}
/>
</Tabs>
<DashboardViewsTabs
baseUrl={'/accounts'}
tabs={tabs}
onNewViewTabClick={handleClickNewView}
onChange={handleTabsChange}
/>
</NavbarGroup>
</Navbar>
);
}
const mapStateToProps = (state, ownProps) => ({
// Mapping view id from matched route params.
viewId: ownProps.match.params.custom_view_id,
});
@@ -130,5 +116,5 @@ export default compose(
accountsViews,
})),
withAccountsTableActions,
withViewDetail
withViewDetail,
)(AccountsViewsTabs);

View File

@@ -10,8 +10,8 @@ import {
export default (mapState) => {
const mapStateToProps = (state, props) => {
const mapped = {
accountsViews: getResourceViews(state, 'accounts'),
accounts: getAccountsItems(state, state.accounts.currentViewId),
accountsViews: getResourceViews(state, props, 'accounts'),
accounts: getAccountsItems(state, props),
accountsTypes: state.accounts.accountsTypes,
accountsTableQuery: state.accounts.tableQuery,

View File

@@ -1,10 +1,11 @@
import { connect } from 'react-redux';
import { getCurrenciesList } from 'store/currencies/currencies.selector';
export default (mapState) => {
const mapStateToProps = (state, props) => {
const mapped = {
currencies: state.currencies.data,
currenciesList: Object.values(state.currencies.data),
currenciesList: getCurrenciesList(state, props),
currenciesLoading: state.currencies.loading,
};
return mapState ? mapState(mapped, state, props) : mapped;

View File

@@ -6,7 +6,7 @@ export default (mapState) => {
const mapStateToProps = (state, props) => {
const mapped = {
customersViews: getResourceViews(state, 'customers'),
customersViews: getResourceViews(state, props, 'customers'),
customersItems: Object.values(state.customers.items),
customers: getCustomersItems(state, state.customers.currentViewId),
customersLoading: state.customers.loading,

View File

@@ -6,7 +6,7 @@ export default (mapState) => {
const mapStateToProps = (state, props) => {
const mapped = {
expenses: getExpensesItems(state, state.expenses.currentViewId),
expensesViews: getResourceViews(state, 'expenses'),
expensesViews: getResourceViews(state, props, 'expenses'),
expensesItems: state.expenses.items,
expensesTableQuery: state.expenses.tableQuery,
expensesLoading: state.expenses.loading,

View File

@@ -11,7 +11,7 @@ export default (mapState) => {
const mapStateToProps = (state, props) => {
const viewPages = getViewPages(state.items.views, state.items.currentViewId);
const mapped = {
itemsViews: getResourceViews(state, 'items'),
itemsViews: getResourceViews(state, props, 'items'),
itemsCurrentPage: getCurrentPageResults(
state.items.items,
viewPages,

View File

@@ -11,7 +11,7 @@ export default (mapState) => {
const { resourceName } = props;
const mapped = {
resourceData: getResourceData(state, resourceName),
resourceFields: getResourceFields(state, resourceName),
resourceFields: getResourceFields(state, props),
resourceColumns: getResourceColumns(state, resourceName),
resourceMetadata: getResourceMetadata(state, resourceName),
};

View File

@@ -0,0 +1,7 @@
import { connect } from 'react-redux';
const mapStateToProps = (state, props) => ({
currentViewId: props.match.params.custom_view_id,
});
export default connect(mapStateToProps);

View File

@@ -6,11 +6,9 @@ import {
export const mapStateToProps = (state, props) => {
const { viewId } = props;
return {
viewMeta: getViewMeta(state, viewId),
viewItem: getViewItem(state, viewId),
viewMeta: getViewMeta(state, props),
viewItem: getViewItem(state, props),
};
};

View File

@@ -501,5 +501,13 @@ export default {
once_delete_this_customer_you_will_able_to_restore_it: `Once you delete this customer, you won\'t be able to restore it later. Are you sure you want to delete this cusomter?`,
once_delete_these_customers_you_will_not_able_restore_them:
"Once you delete these customers, you won't be able to retrieve them later. Are you sure you want to delete them?",
financial_accounting: 'Financial accounting'
financial_accounting: 'Financial accounting',
after: 'After',
before: 'Before',
count_filters_applied: '{count} filters applied',
is: 'Is',
is_not: 'Is Not',
create_a_new_view: 'Create a new view',
in: 'In',
not_equals: 'Not Equals',
};

View File

@@ -1,10 +1,19 @@
import { createSelector } from 'reselect';
import { pickItemsFromIds } from 'store/selectors';
export const getAccountsItems = (state, viewId) => {
const accountsView = state.accounts.views[viewId || -1];
const accountsItems = state.accounts.items;
const accountsViewsSelector = (state) => state.accounts.views;
const accountsDataSelector = (state) => state.accounts.items;
const accountsCurrentViewSelector = (state) => state.accounts.currentViewId;
return typeof accountsView === 'object'
? pickItemsFromIds(accountsItems, accountsView.ids) || []
: [];
};
export const getAccountsItems = createSelector(
accountsViewsSelector,
accountsDataSelector,
accountsCurrentViewSelector,
(accountsViews, accountsItems, viewId) => {
const accountsView = accountsViews[viewId || -1];
return typeof accountsView === 'object'
? pickItemsFromIds(accountsItems, accountsView.ids) || []
: [];
},
);

View File

@@ -1,4 +1,14 @@
// @flow
import { createSelector } from 'reselect';
const currenciesItemsSelector = state => state.currencies.data;
export const getCurrenciesList = createSelector(
currenciesItemsSelector,
(currencies) => {
return Object.values(currencies);
}
);
export const getCurrencyById = (currencies: Object, id: Integer) => {
return Object.values(currencies).find(c => c.id == id) || null;

View File

@@ -1,14 +1,24 @@
import {pickItemsFromIds} from 'store/selectors';
import {getResourceColumn } from 'store/resources/resources.reducer';
import { createSelector } from 'reselect';
import { pickItemsFromIds } from 'store/selectors';
import { getResourceColumn } from 'store/resources/resources.reducer';
export const getResourceViews = (state, resourceName) => {
const resourceViewsIds = state.views.resourceViews[resourceName] || [];
return pickItemsFromIds(state.views.views, resourceViewsIds);
};
const resourceViewsIdsSelector = (state, props, resourceName) =>
state.views.resourceViews[resourceName] || [];
const viewsSelector = (state) => state.views.views;
const viewByIdSelector = (state, props) => state.views.viewsMeta[props.viewId] || {};
export const getResourceViews = createSelector(
resourceViewsIdsSelector,
viewsSelector,
(resourceViewsIds, views) => {
return pickItemsFromIds(views, resourceViewsIds);
},
);
export const getViewMeta = (state, viewId) => {
const view = { ...state.views.viewsMeta[viewId] } || {};
if (view.columns) {
view.columns = view.columns.map((column) => {
return {
@@ -24,6 +34,7 @@ export const getViewItem = (state, viewId) => {
};
export const getViewPages = (resourceViews, viewId) => {
return (typeof resourceViews[viewId] === 'undefined') ?
{} : resourceViews[viewId].pages;
};
return typeof resourceViews[viewId] === 'undefined'
? {}
: resourceViews[viewId].pages;
};

View File

@@ -1,10 +1,19 @@
import { createSelector } from 'reselect';
import { pickItemsFromIds } from 'store/selectors';
export const getCustomersItems = (state, viewId) => {
const customersView = state.customers.views[viewId || -1];
const customersItems = state.customers.items;
const customersViewsSelector = state => state.customers.views;
const customersItemsSelector = state => state.customers.items;
const customersCurrentViewSelector = state => state.customers.currentViewId;
return typeof customersView === 'object'
? pickItemsFromIds(customersItems, customersView.ids) || []
: [];
};
export const getCustomersItems = createSelector(
customersViewsSelector,
customersItemsSelector,
customersCurrentViewSelector,
(customersViews, customersItems, currentViewId) => {
const customersView = customersViews[currentViewId || -1];
return (typeof customersView === 'object')
? pickItemsFromIds(customersItems, customersView.ids) || []
: [];
},
);

View File

@@ -0,0 +1,18 @@
import { createSelector } from "@reduxjs/toolkit";
const dialogByNameSelector = (state, props) => state.dashboard.dialogs[props.name];
export const isDialogOpen = createSelector(
dialogByNameSelector,
(dialog) => {
return dialog && dialog.isOpen;
},
);
export const getDialogPayload = createSelector(
dialogByNameSelector,
(dialog) => {
return dialog?.payload;
},
);

View File

@@ -1,10 +1,19 @@
import { createSelector } from '@reduxjs/toolkit';
import { pickItemsFromIds } from 'store/selectors';
export const getExpensesItems = (state, viewId) => {
const accountsView = state.expenses.views[viewId || -1];
const accountsItems = state.expenses.items;
const expensesViewsSelector = state => state.expenses.views;
const expensesItemsSelector = state => state.expenses.items;
const expensesCurrentViewSelector = state => state.expenses.currentViewId;
return typeof accountsView === 'object'
? pickItemsFromIds(accountsItems, accountsView.ids) || []
: [];
};
export const getExpensesItems = createSelector(
expensesViewsSelector,
expensesItemsSelector,
expensesCurrentViewSelector,
(expensesViews, expensesItems, currentViewId) => {
const expensesView = expensesViews[currentViewId || -1];
return (typeof expensesView === 'object')
? (pickItemsFromIds(expensesItems, expensesView.ids) || [])
: [];
},
);

View File

@@ -1,4 +1,5 @@
import { createReducer } from "@reduxjs/toolkit";
import { createSelector } from 'reselect';
import t from 'store/types';
import { pickItemsFromIds } from 'store/selectors'
@@ -61,17 +62,23 @@ export default createReducer(initialState, {
},
});
const resourceFieldsIdsSelector = (state, props) => state.resources.resourceFields[props.resourceName];
const resourceFieldsItemsSelector = (state) => state.resources.fields;
/**
* Retrieve resource fields of the given resource slug.
* @param {Object} state
* @param {String} resourceSlug
* @return {Array}
*/
export const getResourceFields = (state, resourceSlug) => {
const resourceIds = state.resources.resourceFields[resourceSlug];
const items = state.resources.fields;
return pickItemsFromIds(items, resourceIds);
};
export const getResourceFields = createSelector(
resourceFieldsIdsSelector,
resourceFieldsItemsSelector,
(fieldsIds, fieldsItems) => {
return pickItemsFromIds(fieldsItems, fieldsIds);
}
);
/**
* Retrieve resource columns of the given resource slug.

View File

@@ -22,7 +22,7 @@
overflow-x: hidden;
.th{
padding: 0.8rem 0.5rem;
padding: 0.75rem 0.5rem;
background: #F8FAFA;
font-size: 14px;
color: #444;
@@ -41,12 +41,12 @@
&--desc{
border-left: 4px solid transparent;
border-right: 4px solid transparent;
border-bottom: 6px solid #888;
border-bottom: 6px solid #666;
}
&--asc{
border-left: 4px solid transparent;
border-right: 4px solid transparent;
border-top: 6px solid #888;
border-top: 6px solid #666;
}
}
}
@@ -115,6 +115,7 @@
.tr .td{
border-bottom: 1px solid #E8E8E8;
align-items: center;
color: #252833;
.placeholder{
color: #999;

View File

@@ -1,7 +1,7 @@
body{
color: #333;
color: #1f3255;
}
.#{$ns}-heading{

View File

@@ -4,7 +4,7 @@
.bigcapital-datatable{
.normal{
.#{$ns}-icon{
color: #aaa;
color: #838d9b;
padding-left: 15px;
}
}
@@ -22,9 +22,6 @@
border-bottom-color: #c6c6c6;
}
}
.code{
color: #333;
}
.normal{
.bp3-popover-wrapper{
width: 100%;

View File

@@ -21,10 +21,10 @@
&-sidebar-toggle{
display: flex;
align-items: center;
margin-left: 4px;
margin-left: 2px;
.#{$ns}-button{
color: #8E8E8E;
color: #6B8193;
&,
&:hover,
@@ -116,7 +116,7 @@
&,
&-group{
height: 44px;
height: 42px;
}
.#{$ns}-navbar-divider{
@@ -124,17 +124,17 @@
margin-right: 0;
}
.#{$ns}-button{
color: #5C5C5C;
color: #4d4d4d;
padding: 8px 12px;
&:hover{
background: rgba(167, 182, 194, 0.12);
color: #5C5C5C;
color: #4d4d4d;
}
&.bp3-minimal:active,
&.bp3-minimal.bp3-active{
background: rgba(167, 182, 194, 0.12);
color: #5C5C5C;
color: #4d4d4d;
}
&.has-active-filters{
@@ -145,7 +145,7 @@
}
}
.#{$ns}-icon{
color: #666;
color: #4d4d4d;
margin-right: 7px;
}
&.#{$ns}-minimal.#{$ns}-intent-danger{
@@ -193,12 +193,12 @@
&__title{
align-items: center;;
display: flex;
margin-left: 6px;
margin-left: 4px;
h1{
font-size: 26px;
font-weight: 300;
color: #4d4c4c;
color: #393939;
margin: 0;
}
h3{
@@ -262,7 +262,7 @@
.tbody{
.th.selection,
.td.selection{
padding-left: 18px;
padding-left: 14px;
}
}
}
@@ -294,16 +294,17 @@
.tabs--dashboard-views{
.#{$ns}-tab{
color: #5C5C5C;
color: #666;
font-size: 14px;
line-height: 50px;
font-weight: 400;
padding: 0;
margin-right: 0;
padding-left: 14px;
padding-right: 14px;
> a{
padding-left: 14px;
padding-right: 14px;
&[aria-selected='true'] {
color: #0052cc;
}
}

View File

@@ -60,7 +60,7 @@ $sidebar-popover-submenu-bg: rgb(1, 20, 62);
.#{$ns}-menu-item {
color: $sidebar-menu-item-color;
border-radius: 0;
padding: 9px 16px;
padding: 8px 16px;
font-size: 15px;
font-weight: 400;
@@ -86,10 +86,10 @@ $sidebar-popover-submenu-bg: rgb(1, 20, 62);
}
&-label{
display: block;
color: rgba(255, 255, 255, 0.45);
color: rgba(255, 255, 255, 0.5);
font-size: 12px;
padding: 4px 16px;
margin-top: 6px;
padding: 6px 16px;
margin-top: 4px;
}
}
@@ -127,7 +127,7 @@ $sidebar-popover-submenu-bg: rgb(1, 20, 62);
color: $sidebar-menu-item-color;
}
.#{$ns}-menu-divider {
border-top-color: rgba(255, 255, 255, 0.1);
border-top-color: rgba(255, 255, 255, 0.15);
color: #6b708c;
margin: 4px 0;
}
@@ -135,7 +135,6 @@ $sidebar-popover-submenu-bg: rgb(1, 20, 62);
margin: 4px 0;
height: 1px;
}
}
&--mini-sidebar{

View File

@@ -23,12 +23,13 @@
&:not(:last-of-type) {
padding-right: 8px;
}
.bp3-html-select select,
.bp3-select select{
padding: 0 20px 0 6px;
padding: 0 15px 0 4px;
&:after{
margin-right: 10px;
margin-right: 8px;
}
}
.bp3-input{
@@ -45,7 +46,7 @@
.bp3-html-select::after,
.form-group--select-list .bp3-button::after{
border-top-color: #aaa;
margin-right: 10px;
margin-right: 8px;
}
}
@@ -55,19 +56,24 @@
}
.form-group{
&--condition{
width: 70px;
min-width: 70px;
width: 65px;
min-width: 65px;
}
&--field{
width: 45%;
width: 40%;
}
&--compatator{
min-width: 120px;
width: 120px;
max-width: 120px;
&--comparator{
min-width: 100px;
width: 100px;
max-width: 100px;
}
&--value{
width: 55%;
width: 50%;
.bp3-control{
display: inline-block;
margin-left: 1.8rem;
}
}
}
}
@@ -101,7 +107,7 @@
}
.bp3-input-group{
padding: 8px;
margin: 8px 8px 0;
padding-bottom: 4px;
.bp3-input:not(:first-child){

View File

@@ -181,4 +181,11 @@ export const firstLettersArgs = (...args) => {
}
});
return letters.join('').toUpperCase();
}
export const uniqueMultiProps = (items, props) => {
return _.uniqBy(items, (item) => {
return JSON.stringify(_.pick(item, props));
});
}

View File

@@ -40,6 +40,11 @@ export default {
},
'created_at': {
column: 'created_at',
columnType: 'date',
},
active: {
column: 'active',
},
},

View File

@@ -12,6 +12,7 @@ exports.up = function (knex) {
table.boolean('builtin').defaultTo(false);
table.boolean('columnable');
table.integer('index');
table.string('data_resource');
table.json('options');
table.integer('resource_id').unsigned();
}).raw('ALTER TABLE `RESOURCE_FIELDS` AUTO_INCREMENT = 1000').then(() => {

View File

@@ -1,15 +1,51 @@
exports.seed = function(knex) {
exports.seed = function (knex) {
// Deletes ALL existing entries
return knex('resource_fields').del()
return knex('resource_fields')
.del()
.then(() => {
// Inserts seed entries
return knex('resource_fields').insert([
{ id: 1, label_name: 'Name', key: 'name', data_type: '', active: 1, predefined: 1 },
{ id: 2, label_name: 'Code', key: 'code', data_type: '', active: 1, predefined: 1 },
{ id: 3, label_name: 'Account Type', key: 'type', data_type: '', active: 1, predefined: 1 },
{ id: 4, label_name: 'Description', key: 'description', data_type: '', active: 1, predefined: 1 },
{ id: 5, label_name: 'Account Normal', key: 'normal', data_type: 'string', active: 1, predefined: 1 },
{
id: 1,
label_name: 'Name',
key: 'name',
data_type: '',
active: 1,
predefined: 1,
},
{
id: 2,
label_name: 'Code',
key: 'code',
data_type: '',
active: 1,
predefined: 1,
},
{
id: 3,
label_name: 'Account Type',
key: 'type',
data_type: '',
active: 1,
predefined: 1,
data_resource_id: 8,
},
{
id: 4,
label_name: 'Description',
key: 'description',
data_type: '',
active: 1,
predefined: 1,
},
{
id: 5,
label_name: 'Account Normal',
key: 'normal',
data_type: 'string',
active: 1,
predefined: 1,
},
{
id: 6,
label_name: 'Root Account Type',
@@ -18,6 +54,14 @@ exports.seed = function(knex) {
active: 1,
predefined: 1,
},
{
id: 7,
label_name: 'Active',
key: 'active',
data_type: 'boolean',
active: 1,
predefined: 1,
},
]);
});
};

View File

@@ -6,6 +6,8 @@ exports.seed = (knex) => {
// Inserts seed entries
return knex('resources').insert([
{ id: 1, name: 'accounts' },
{ id: 8, name: 'accounts_types' },
{ id: 2, name: 'items' },
{ id: 3, name: 'expenses' },
{ id: 4, name: 'manual_journals' },

View File

@@ -30,6 +30,7 @@ exports.seed = (knex) => {
data_type: 'options',
predefined: 1,
columnable: true,
data_resource: 'accounts_types',
},
{
id: 5,
@@ -58,6 +59,15 @@ exports.seed = (knex) => {
predefined: 1,
columnable: true,
},
{
id: 17,
resource_id: 1,
data_type: 'boolean',
label_name: 'Active',
key: 'active',
predefined: 1,
columnable: true,
},
// Expenses
{

View File

@@ -120,9 +120,9 @@ export default {
errors: [{ type: 'NOT_EXIST_ACCOUNT_TYPE', code: 200 }],
});
}
await Account.query().insert({ ...form });
const insertedAccount = await Account.query().insertAndFetch({ ...form });
return res.status(200).send({ item: { } });
return res.status(200).send({ account: { ...insertedAccount } });
},
},
@@ -135,7 +135,7 @@ export default {
check('name').exists().isLength({ min: 3 })
.trim()
.escape(),
check('code').exists().isLength({ max: 10 })
check('code').optional().isLength({ max: 10 })
.trim()
.escape(),
check('account_type_id').exists().isNumeric().toInt(),
@@ -157,28 +157,30 @@ export default {
if (!account) {
return res.boom.notFound();
}
const foundAccountCodePromise = (form.code && form.code !== account.code)
? Account.query().where('code', form.code).whereNot('id', account.id) : null;
const errorReasons = [];
const foundAccountTypePromise = (form.account_type_id !== account.account_type_id)
? AccountType.query().where('id', form.account_type_id) : null;
const [foundAccountCode, foundAccountType] = await Promise.all([
foundAccountCodePromise, foundAccountTypePromise,
]);
if (foundAccountCode.length > 0 && foundAccountCodePromise) {
return res.boom.badRequest(null, {
errors: [{ type: 'NOT_UNIQUE_CODE', code: 100 }],
// Validate the account type is not changed.
if (account.account_type_id != form.accountTypeId) {
errorReasons.push({
type: 'NOT.ALLOWED.TO.CHANGE.ACCOUNT.TYPE', code: 100,
});
}
if (foundAccountType.length <= 0 && foundAccountTypePromise) {
return res.boom.badRequest(null, {
errors: [{ type: 'NOT_EXIST_ACCOUNT_TYPE', code: 110 }],
});
}
await account.patch({ ...form });
// Validate the account code not exists on the storage.
if (form.code && form.code !== account.code) {
const foundAccountCode = await Account.query().where('code', form.code).whereNot('id', account.id);
return res.status(200).send();
if (foundAccountCode.length > 0) {
errorReasons.push({ type: 'NOT_UNIQUE_CODE', code: 200 });
}
}
if (errorReasons.length > 0) {
return res.status(400).send({ error: errorReasons });
}
// Update the account on the storage.
const updatedAccount = await Account.query().patchAndFetchById(account.id, { ...form });
return res.status(200).send({ account: { ...updatedAccount } });
},
},
@@ -268,7 +270,6 @@ export default {
if (filter.stringified_filter_roles) {
filter.filter_roles = JSON.parse(filter.stringified_filter_roles);
}
const { Resource, Account, View } = req.models;
const errorReasons = [];
@@ -338,14 +339,31 @@ export default {
if (errorReasons.length > 0) {
return res.status(400).send({ errors: errorReasons });
}
const query = Account.query()
// .remember()
.onBuild((builder) => {
builder.modify('filterAccountTypes', filter.account_types);
builder.withGraphFetched('type');
builder.withGraphFetched('balance');
dynamicFilter.buildQuery()(builder);
// console.log(builder.toKnexQuery().toSQL());
}).toKnexQuery().toSQL();
console.log(query);
const accounts = await Account.query()
.remember()
// .remember()
.onBuild((builder) => {
builder.modify('filterAccountTypes', filter.account_types);
builder.withGraphFetched('type');
builder.withGraphFetched('balance');
dynamicFilter.buildQuery()(builder);
// console.log(builder.toKnexQuery().toSQL());
});
const nestedAccounts = Account.toNestedArray(accounts);

View File

@@ -1,4 +1,5 @@
import { difference } from 'lodash';
import moment from 'moment';
import { Lexer } from '@/lib/LogicEvaluation/Lexer';
import Parser from '@/lib/LogicEvaluation/Parser';
import QueryParser from '@/lib/LogicEvaluation/QueryParser';
@@ -12,25 +13,8 @@ import resourceFieldsKeys from '@/data/ResourceFieldsKeys';
// index: Number,
// }
/**
* Get field column metadata and its relation with other tables.
* @param {String} tableName - Table name of target column.
* @param {String} columnKey - Target column key that stored in resource field.
*/
export function getRoleFieldColumn(tableName, columnKey) {
const tableFields = resourceFieldsKeys[tableName];
return (tableFields[columnKey]) ? tableFields[columnKey] : null;
}
/**
* Builds roles queries.
* @param {String} tableName -
* @param {Object} role -
*/
export function buildRoleQuery(tableName, role) {
const fieldRelation = getRoleFieldColumn(tableName, role.columnKey);
const comparatorColumn = fieldRelation.relationColumn || `${tableName}.${fieldRelation.column}`;
const textRoleQueryBuilder = (role, comparatorColumn) => {
switch (role.comparator) {
case 'equals':
default:
@@ -47,7 +31,82 @@ export function buildRoleQuery(tableName, role) {
return (builder) => {
builder.where(comparatorColumn, 'LIKE', `%${role.value}%`);
};
case 'not_contain':
case 'not_contains':
return (builder) => {
builder.whereNot(comparatorColumn, 'LIKE', `%${role.value}%`);
};
}
};
const dateQueryBuilder = (role, comparatorColumn) => {
switch(role.comparator) {
case 'after':
case 'before':
return (builder) => {
const comparator = role.comparator === 'before' ? '<' : '>';
const hasTimeFormat = moment(role.value, 'YYYY-MM-DD HH:MM', true).isValid();
const targetDate = moment(role.value);
const dateFormat = 'YYYY-MM-DD HH:MM:SS';
if (!hasTimeFormat) {
if (role.comparator === 'before') {
targetDate.startOf('day');
} else {
targetDate.endOf('day');
}
}
const comparatorValue = targetDate.format(dateFormat);
builder.where(comparatorColumn, comparator, comparatorValue);
};
case 'in':
return (builder) => {
const hasTimeFormat = moment(role.value, 'YYYY-MM-DD HH:MM', true).isValid();
const dateFormat = 'YYYY-MM-DD HH:MM:SS';
if (hasTimeFormat) {
const targetDateTime = moment(role.value).format(dateFormat);
builder.where(comparatorColumn, '=', targetDateTime);
} else {
const startDate = moment(role.value).startOf('day');
const endDate = moment(role.value).endOf('day');
builder.where(comparatorColumn, '>=', startDate.format(dateFormat));
builder.where(comparatorColumn, '<=', endDate.format(dateFormat));
}
};
}
};
/**
* Get field column metadata and its relation with other tables.
* @param {String} tableName - Table name of target column.
* @param {String} columnKey - Target column key that stored in resource field.
*/
export function getRoleFieldColumn(tableName, columnKey) {
const tableFields = resourceFieldsKeys[tableName];
return (tableFields[columnKey]) ? tableFields[columnKey] : null;
}
/**
* Builds roles queries.
* @param {String} tableName -
* @param {Object} role -
*/
export function buildRoleQuery(tableName, role) {
const fieldRelation = getRoleFieldColumn(tableName, role.columnKey);
const comparatorColumn = fieldRelation.relationColumn || `${tableName}.${fieldRelation.column}`;
switch (fieldRelation.columnType) {
case 'date':
return dateQueryBuilder(role, comparatorColumn);
case 'text':
case 'varchar':
default:
return textRoleQueryBuilder(role, comparatorColumn);
}
}
/**