feat: fix a bunch of bugs.

This commit is contained in:
Ahmed Bouhuolia
2020-04-28 04:01:10 +02:00
parent 6d0ad42582
commit 0cfa1126c5
41 changed files with 591 additions and 187 deletions

View File

@@ -8,10 +8,11 @@
"@blueprintjs/datetime": "^3.15.2", "@blueprintjs/datetime": "^3.15.2",
"@blueprintjs/select": "^3.11.2", "@blueprintjs/select": "^3.11.2",
"@blueprintjs/table": "^3.8.3", "@blueprintjs/table": "^3.8.3",
"@blueprintjs/timezone": "^3.6.1", "@blueprintjs/timezone": "^3.6.2",
"@reduxjs/toolkit": "^1.2.5", "@reduxjs/toolkit": "^1.2.5",
"@svgr/webpack": "4.3.3", "@svgr/webpack": "4.3.3",
"@syncfusion/ej2-react-grids": "^17.4.50", "@syncfusion/ej2-react-grids": "^17.4.50",
"@tanem/react-nprogress": "^3.0.24",
"@testing-library/jest-dom": "^4.2.4", "@testing-library/jest-dom": "^4.2.4",
"@testing-library/react": "^9.4.0", "@testing-library/react": "^9.4.0",
"@testing-library/user-event": "^7.2.1", "@testing-library/user-event": "^7.2.1",

View File

@@ -1,4 +1,4 @@
import React, { useMemo, useState } from 'react'; import React, { useMemo, useState, useCallback } from 'react';
import Icon from 'components/Icon'; import Icon from 'components/Icon';
import { import {
Button, Button,
@@ -29,6 +29,8 @@ function AccountsActionsBar({
getResourceFields, getResourceFields,
addAccountsTableQueries, addAccountsTableQueries,
onFilterChanged, onFilterChanged,
onBulkDelete,
onBulkArchive,
}) { }) {
const {path} = useRouteMatch(); const {path} = useRouteMatch();
const onClickNewAccount = () => { openDialog('account-form', {}); }; const onClickNewAccount = () => { openDialog('account-form', {}); };
@@ -51,6 +53,15 @@ function AccountsActionsBar({
onFilterChanged && onFilterChanged(filterConditions); onFilterChanged && onFilterChanged(filterConditions);
}, },
}); });
const handleBulkArchive = useCallback(() => {
onBulkArchive && onBulkArchive(selectedRows.map(r => r.id));
}, [onBulkArchive, selectedRows]);
const handleBulkDelete = useCallback(() => {
onBulkDelete && onBulkDelete(selectedRows.map(r => r.id));
}, [onBulkDelete, selectedRows]);
return ( return (
<DashboardActionsBar> <DashboardActionsBar>
<NavbarGroup> <NavbarGroup>
@@ -77,6 +88,7 @@ function AccountsActionsBar({
onClick={onClickNewAccount} onClick={onClickNewAccount}
/> />
<Popover <Popover
minimal={true}
content={filterDropdown} content={filterDropdown}
interactionKind={PopoverInteractionKind.CLICK} interactionKind={PopoverInteractionKind.CLICK}
position={Position.BOTTOM_LEFT}> position={Position.BOTTOM_LEFT}>
@@ -92,6 +104,7 @@ function AccountsActionsBar({
className={Classes.MINIMAL} className={Classes.MINIMAL}
icon={<Icon icon='archive' iconSize={15} />} icon={<Icon icon='archive' iconSize={15} />}
text='Archive' text='Archive'
onClick={handleBulkArchive}
/> />
)} )}
{hasSelectedRows && ( {hasSelectedRows && (
@@ -100,6 +113,7 @@ function AccountsActionsBar({
icon={<Icon icon='trash' iconSize={15} />} icon={<Icon icon='trash' iconSize={15} />}
text='Delete' text='Delete'
intent={Intent.DANGER} intent={Intent.DANGER}
onClick={handleBulkDelete}
/> />
)} )}
<Button <Button

View File

@@ -19,6 +19,7 @@ import ViewConnect from 'connectors/View.connector';
import LoadingIndicator from 'components/LoadingIndicator'; import LoadingIndicator from 'components/LoadingIndicator';
import DataTable from 'components/DataTable'; import DataTable from 'components/DataTable';
import Money from 'components/Money'; import Money from 'components/Money';
import { useUpdateEffect } from 'hooks';
function AccountsDataTable({ function AccountsDataTable({
accounts, accounts,
@@ -31,9 +32,16 @@ function AccountsDataTable({
setTopbarEditView, setTopbarEditView,
accountsLoading, accountsLoading,
onFetchData, onFetchData,
setSelectedRowsAccounts, onSelectedRowsChange
}) { }) {
const { custom_view_id: customViewId } = useParams(); const {custom_view_id: customViewId} = useParams();
const [initialMount, setInitialMount] = useState(false);
useUpdateEffect(() => {
if (!accountsLoading) {
setInitialMount(true);
}
}, [accountsLoading, setInitialMount]);
useEffect(() => { useEffect(() => {
const viewMeta = getViewItem(customViewId); const viewMeta = getViewItem(customViewId);
@@ -52,12 +60,20 @@ function AccountsDataTable({
openDialog('account-form', { action: 'edit', id: account.id }); openDialog('account-form', { action: 'edit', id: account.id });
}, [openDialog]); }, [openDialog]);
const actionMenuList = useCallback(account => ( const handleNewParentAccount = useCallback((account) => {
openDialog('account-form', { action: 'new_child', id: account.id });
}, [openDialog]);
const actionMenuList = useCallback((account) => (
<Menu> <Menu>
<MenuItem text='View Details' /> <MenuItem text='View Details' />
<MenuDivider /> <MenuDivider />
<MenuItem text='Edit Account' onClick={handleEditAccount(account)} /> <MenuItem
<MenuItem text='New Account' /> text='Edit Account'
onClick={handleEditAccount(account)} />
<MenuItem
text='New Account'
onClick={() => handleNewParentAccount(account)} />
<MenuDivider /> <MenuDivider />
<MenuItem <MenuItem
text='Inactivate Account' text='Inactivate Account'
@@ -152,17 +168,22 @@ function AccountsDataTable({
onFetchData && onFetchData(...params); onFetchData && onFetchData(...params);
}, []); }, []);
const handleSelectedRowsChange = useCallback((selectedRows) => {
onSelectedRowsChange && onSelectedRowsChange(selectedRows.map(s => s.original));
}, [onSelectedRowsChange]);
return ( return (
<LoadingIndicator loading={accountsLoading} spinnerSize={30}> <DataTable
<DataTable columns={columns}
columns={columns} data={accounts}
data={accounts} onFetchData={handleDatatableFetchData}
onFetchData={handleDatatableFetchData} manualSortBy={true}
manualSortBy={true} selectionColumn={selectionColumn}
selectionColumn={selectionColumn} expandable={true}
expandable={true} treeGraph={true}
treeGraph={true} /> onSelectedRowsChange={handleSelectedRowsChange}
</LoadingIndicator> loading={accountsLoading && !initialMount}
spinnerProps={{size: 30}} />
); );
} }

View File

@@ -8,6 +8,7 @@ import PrivateRoute from 'components/PrivateRoute';
import Authentication from 'components/Authentication'; import Authentication from 'components/Authentication';
import Dashboard from 'components/Dashboard/Dashboard'; import Dashboard from 'components/Dashboard/Dashboard';
import { isAuthenticated } from 'store/authentication/authentication.reducer' import { isAuthenticated } from 'store/authentication/authentication.reducer'
import Progress from 'components/NProgress/Progress';
import messages from 'lang/en'; import messages from 'lang/en';
import 'style/App.scss'; import 'style/App.scss';
@@ -22,6 +23,8 @@ function App(props) {
<PrivateRoute isAuthenticated={props.isAuthorized} component={Dashboard} /> <PrivateRoute isAuthenticated={props.isAuthorized} component={Dashboard} />
</Router> </Router>
</div> </div>
<Progress isAnimating={props.isLoading} />
</IntlProvider> </IntlProvider>
); );
} }
@@ -37,5 +40,6 @@ App.propTypes = {
const mapStateToProps = (state) => ({ const mapStateToProps = (state) => ({
isAuthorized: isAuthenticated(state), isAuthorized: isAuthenticated(state),
isLoading: !!state.dashboard.requestsLoading,
}); });
export default connect(mapStateToProps)(App); export default connect(mapStateToProps)(App);

View File

@@ -6,7 +6,7 @@ export default function DashboardInsider({
loading, loading,
children, children,
name, name,
mount = true, mount = false,
}) { }) {
return ( return (
<div className={classnames({ <div className={classnames({

View File

@@ -1,18 +1,18 @@
import React, {useEffect, useMemo, useCallback} from 'react'; import React, {useEffect, useRef, useCallback} from 'react';
import { import {
useTable, useTable,
useExpanded, useExpanded,
useRowSelect, useRowSelect,
usePagination, usePagination,
useResizeColumns, useResizeColumns,
useAsyncDebounce,
useSortBy, useSortBy,
useFlexLayout useFlexLayout
} from 'react-table' } from 'react-table';
import {Checkbox} from '@blueprintjs/core'; import { Checkbox, Spinner } from '@blueprintjs/core';
import classnames from 'classnames'; import classnames from 'classnames';
import { FixedSizeList } from 'react-window' import { FixedSizeList } from 'react-window'
import { ConditionalWrapper } from 'utils'; import { ConditionalWrapper } from 'utils';
import { useUpdateEffect } from 'hooks';
const IndeterminateCheckbox = React.forwardRef( const IndeterminateCheckbox = React.forwardRef(
({ indeterminate, ...rest }, ref) => { ({ indeterminate, ...rest }, ref) => {
@@ -40,6 +40,8 @@ export default function DataTable({
payload, payload,
expandable = false, expandable = false,
expandToggleColumn = 2, expandToggleColumn = 2,
noInitialFetch = false,
spinnerProps = { size: 40 },
}) { }) {
const { const {
getTableProps, getTableProps,
@@ -62,7 +64,7 @@ export default function DataTable({
isAllRowsExpanded, isAllRowsExpanded,
// Get the state from the instance // Get the state from the instance
state: { pageIndex, pageSize, sortBy, selectedRowIds }, state: { pageIndex, pageSize, sortBy, selectedRowIds, selectedRows },
} = useTable( } = useTable(
{ {
columns, columns,
@@ -116,11 +118,21 @@ export default function DataTable({
} }
); );
const isInitialMount = useRef(noInitialFetch);
// When these table states change, fetch new data! // When these table states change, fetch new data!
useEffect(() => { useEffect(() => {
onFetchData && onFetchData({ pageIndex, pageSize, sortBy }) if (isInitialMount.current) {
isInitialMount.current = false;
} else {
onFetchData && onFetchData({ pageIndex, pageSize, sortBy })
}
}, [pageIndex, pageSize, sortBy]); }, [pageIndex, pageSize, sortBy]);
useUpdateEffect(() => {
onSelectedRowsChange && onSelectedRowsChange(selectedFlatRows);
}, [selectedRowIds, onSelectedRowsChange]);
// Renders table cell. // Renders table cell.
const RenderCell = useCallback(({ row, cell, index }) => ( const RenderCell = useCallback(({ row, cell, index }) => (
<ConditionalWrapper <ConditionalWrapper
@@ -188,13 +200,18 @@ export default function DataTable({
{RenderVirtualizedRows} {RenderVirtualizedRows}
</FixedSizeList> </FixedSizeList>
) : RenderPage(); ) : RenderPage();
}, [fixedSizeHeight, rows, fixedItemSize, virtualizedRows, RenderVirtualizedRows, RenderPage]); }, [fixedSizeHeight, rows, fixedItemSize, virtualizedRows,
RenderVirtualizedRows, RenderPage]);
return ( return (
<div className={classnames( <div className={classnames(
'bigcapital-datatable', 'bigcapital-datatable',
className, className,
{'has-sticky-header': stickyHeader, 'is-expandable': expandable})}> {
'has-sticky-header': stickyHeader,
'is-expandable': expandable,
'has-virtualized-rows': virtualizedRows,
})}>
<div {...getTableProps()} className="table"> <div {...getTableProps()} className="table">
<div className="thead"> <div className="thead">
{headerGroups.map(headerGroup => ( {headerGroups.map(headerGroup => (
@@ -240,13 +257,16 @@ export default function DataTable({
))} ))}
</div> </div>
<div {...getTableBodyProps()} className="tbody"> <div {...getTableBodyProps()} className="tbody">
{ RenderTBody() } { !loading && RenderTBody() }
{ (page.length === 0) && ( { !loading && (page.length === 0) && (
<div className={'tr no-results'}> <div className={'tr no-results'}>
<div class="td">{ noResults }</div> <div class="td">{ noResults }</div>
</div> </div>
)} )}
{ loading && (
<div class="loading"><Spinner size={spinnerProps.size} /></div>
) }
</div> </div>
</div> </div>
</div> </div>

View File

@@ -1,4 +1,4 @@
import React, {useEffect, useMemo} from 'react'; import React, {useEffect, useMemo, useCallback, useRef} from 'react';
import { import {
FormGroup, FormGroup,
InputGroup, InputGroup,
@@ -10,67 +10,94 @@ import {
import { useFormik } from 'formik'; import { useFormik } from 'formik';
import { isEqual } from 'lodash'; import { isEqual } from 'lodash';
import { usePrevious } from 'react-use'; import { usePrevious } from 'react-use';
import { debounce } from 'lodash';
import Icon from 'components/Icon'; import Icon from 'components/Icon';
import { checkRequiredProperties } from 'utils';
export default function FilterDropdown({ export default function FilterDropdown({
fields, fields,
onFilterChange, onFilterChange,
}) { }) {
const conditionalsItems = [ const conditionalsItems = useMemo(() => [
{ value: 'and', label: 'AND' }, { value: 'and', label: 'AND' },
{ value: 'or', label: 'OR' }, { value: 'or', label: 'OR' },
]; ], []);
const resourceFields = [
const resourceFields = useMemo(() => [
...fields.map((field) => ({ value: field.key, label: field.label_name, })), ...fields.map((field) => ({ value: field.key, label: field.label_name, })),
]; ], [fields]);
const compatatorsItems = [
const compatatorsItems = useMemo(() => [
{value: '', label: 'Select a compatator'}, {value: '', label: 'Select a compatator'},
{value: 'equals', label: 'Equals'}, {value: 'equals', label: 'Equals'},
{value: 'not_equal', label: 'Not Equal'}, {value: 'not_equal', label: 'Not Equal'},
{value: 'contain', label: 'Contain'}, {value: 'contain', label: 'Contain'},
{value: 'not_contain', label: 'Not Contain'}, {value: 'not_contain', label: 'Not Contain'},
]; ], []);
const defaultFilterCondition = {
const defaultFilterCondition = useMemo(() => ({
condition: 'and', condition: 'and',
field_key: fields.length > 0 ? fields[0].key : '', field_key: fields.length > 0 ? fields[0].key : '',
compatator: 'equals', compatator: 'equals',
value: '', value: '',
}; }), [fields]);
const formik = useFormik({
const {
setFieldValue,
getFieldProps,
values,
errors,
touched,
} = useFormik({
enableReinitialize: true,
initialValues: { initialValues: {
conditions: [ defaultFilterCondition ], conditions: [ defaultFilterCondition ],
}, },
}); });
const onClickNewFilter = () => { const onClickNewFilter = useCallback(() => {
formik.setFieldValue('conditions', [ setFieldValue('conditions', [
...formik.values.conditions, defaultFilterCondition, ...values.conditions, defaultFilterCondition,
]); ]);
}; }, [values, defaultFilterCondition, setFieldValue]);
const filteredFilterConditions = useMemo(() => { const filteredFilterConditions = useMemo(() => {
return formik.values.conditions.filter(condition => !!condition.value); const requiredProps = ['field_key', 'condition', 'compatator', 'value'];
}, [formik.values.conditions]);
return values.conditions
.filter((condition) =>
!checkRequiredProperties(condition, requiredProps));
}, [values.conditions]);
const prevConditions = usePrevious(filteredFilterConditions); const prevConditions = usePrevious(filteredFilterConditions);
const onClickRemoveCondition = (index) => () => { const onFilterChangeThrottled = useRef(debounce((conditions) => {
if (formik.values.conditions.length === 1) { return; } onFilterChange && onFilterChange(conditions);
}, 1000));
const conditions = [ ...formik.values.conditions ];
conditions.splice(index, 1);
formik.setFieldValue('conditions', [ ...conditions ]);
}
useEffect(() => { useEffect(() => {
if (!isEqual(filteredFilterConditions, prevConditions)) { if (!isEqual(prevConditions, filteredFilterConditions) && prevConditions) {
onFilterChange(filteredFilterConditions); onFilterChangeThrottled.current(filteredFilterConditions);
} }
}, [filteredFilterConditions]) }, [filteredFilterConditions]);
// Handle click remove condition.
const onClickRemoveCondition = (index) => () => {
if (values.conditions.length === 1) {
setFieldValue('conditions', [
defaultFilterCondition,
]);
return;
}
const conditions = [ ...values.conditions ];
conditions.splice(index, 1);
setFieldValue('conditions', [ ...conditions ]);
};
return ( return (
<div class="filter-dropdown"> <div class="filter-dropdown">
<div class="filter-dropdown__body"> <div class="filter-dropdown__body">
{formik.values.conditions.map((condition, index) => ( {values.conditions.map((condition, index) => (
<div class="filter-dropdown__condition"> <div class="filter-dropdown__condition">
<FormGroup <FormGroup
className={'form-group--condition'}> className={'form-group--condition'}>
@@ -78,7 +105,7 @@ export default function FilterDropdown({
options={conditionalsItems} options={conditionalsItems}
className={Classes.FILL} className={Classes.FILL}
disabled={index > 1} disabled={index > 1}
{...formik.getFieldProps(`conditions[${index}].condition`)} /> {...getFieldProps(`conditions[${index}].condition`)} />
</FormGroup> </FormGroup>
<FormGroup <FormGroup
@@ -87,7 +114,7 @@ export default function FilterDropdown({
options={resourceFields} options={resourceFields}
value={1} value={1}
className={Classes.FILL} className={Classes.FILL}
{...formik.getFieldProps(`conditions[${index}].field_key`)} /> {...getFieldProps(`conditions[${index}].field_key`)} />
</FormGroup> </FormGroup>
<FormGroup <FormGroup
@@ -95,14 +122,14 @@ export default function FilterDropdown({
<HTMLSelect <HTMLSelect
options={compatatorsItems} options={compatatorsItems}
className={Classes.FILL} className={Classes.FILL}
{...formik.getFieldProps(`conditions[${index}].compatator`)} /> {...getFieldProps(`conditions[${index}].compatator`)} />
</FormGroup> </FormGroup>
<FormGroup <FormGroup
className={'form-group--value'}> className={'form-group--value'}>
<InputGroup <InputGroup
placeholder="Value" placeholder="Value"
{...formik.getFieldProps(`conditions[${index}].value`)} /> {...getFieldProps(`conditions[${index}].value`)} />
</FormGroup> </FormGroup>
<Button <Button

View File

@@ -39,6 +39,8 @@ export default class Icon extends React.Component{
color, color,
htmlTitle, htmlTitle,
iconSize = Icon.SIZE_STANDARD, iconSize = Icon.SIZE_STANDARD,
height,
width,
intent, intent,
title = icon, title = icon,
tagName = "span", tagName = "span",
@@ -57,6 +59,9 @@ export default class Icon extends React.Component{
const classes = classNames(Classes.ICON, Classes.iconClass(icon), Classes.intentClass(intent), className); const classes = classNames(Classes.ICON, Classes.iconClass(icon), Classes.intentClass(intent), className);
const viewBox = iconPath.viewBox; const viewBox = iconPath.viewBox;
const computedHeight = height || iconSize;
const computedWidth = width || iconSize;
return React.createElement( return React.createElement(
tagName, tagName,
{ {
@@ -64,7 +69,7 @@ export default class Icon extends React.Component{
className: classes, className: classes,
title: htmlTitle, title: htmlTitle,
}, },
<svg fill={color} data-icon={icon} width={iconSize} height={iconSize} viewBox={viewBox}> <svg fill={color} data-icon={icon} width={computedWidth} height={computedHeight} viewBox={viewBox}>
{title && <desc>{title}</desc>} {title && <desc>{title}</desc>}
{paths} {paths}
</svg>, </svg>,

View File

@@ -0,0 +1,38 @@
import PropTypes from 'prop-types'
import * as React from 'react'
const Bar = ({ progress, animationDuration }) => (
<div
style={{
background: '#79b8ff',
height: 3,
left: 0,
marginLeft: `${(-1 + progress) * 100}%`,
position: 'fixed',
top: 0,
transition: `margin-left ${animationDuration}ms linear`,
width: '100%',
zIndex: 1031,
}}
>
<div
style={{
boxShadow: '0 0 10px #79b8ff, 0 0 5px #79b8ff',
display: 'block',
height: '100%',
opacity: 1,
position: 'absolute',
right: 0,
transform: 'rotate(3deg) translate(0px, -4px)',
width: 100,
}}
/>
</div>
)
Bar.propTypes = {
animationDuration: PropTypes.number.isRequired,
progress: PropTypes.number.isRequired,
}
export default Bar;

View File

@@ -0,0 +1,22 @@
import PropTypes from 'prop-types'
import * as React from 'react'
const Container = ({ children, isFinished, animationDuration }) => (
<div
style={{
opacity: isFinished ? 0 : 1,
pointerEvents: 'none',
transition: `opacity ${animationDuration}ms linear`,
}}
>
{children}
</div>
)
Container.propTypes = {
animationDuration: PropTypes.number.isRequired,
children: PropTypes.node.isRequired,
isFinished: PropTypes.bool.isRequired,
}
export default Container;

View File

@@ -0,0 +1,27 @@
import { useNProgress } from '@tanem/react-nprogress'
import PropTypes from 'prop-types'
import React from 'react'
import Bar from './Bar'
import Container from './Container'
import Spinner from './Spinner'
const Progress = ({
isAnimating,
minimum = 0.2
}) => {
const { animationDuration, isFinished, progress } = useNProgress({
isAnimating, minimum,
});
return (
<Container isFinished={isFinished} animationDuration={animationDuration}>
<Bar progress={progress} animationDuration={animationDuration} />
</Container>
)
}
Progress.propTypes = {
isAnimating: PropTypes.bool.isRequired,
};
export default Progress;

View File

@@ -0,0 +1,29 @@
import * as React from 'react'
const Spinner = () => (
<div
style={{
display: 'block',
position: 'fixed',
right: 15,
top: 15,
zIndex: 1031,
}}
>
<div
style={{
animation: '400ms linear infinite spinner',
borderBottom: '2px solid transparent',
borderLeft: '2px solid #29d',
borderRadius: '50%',
borderRight: '2px solid transparent',
borderTop: '2px solid #29d',
boxSizing: 'border-box',
height: 18,
width: 18,
}}
/>
</div>
)
export default Spinner;

View File

@@ -1,23 +1,12 @@
import React from 'react'; import React from 'react';
import appMeta from 'config/app'; import appMeta from 'config/app';
import Icon from 'components/Icon';
export default function() { export default function() {
return ( return (
<div className="sidebar__head"> <div className="sidebar__head">
<div className="sidebar__head-logo"> <div className="sidebar__head-logo">
<Icon icon={'bigcapital'} width={140} height={28} className="bigcapital--alt" />
</div>
<div className="sidebar__head-company-meta">
<div className="company-name">
{ appMeta.app_name }
</div>
<div className="company-meta">
<span class="version">
{ appMeta.app_version }
</span>
</div>
</div> </div>
</div> </div>
); );

View File

@@ -8,6 +8,7 @@ import {
fetchAccountsTable, fetchAccountsTable,
submitAccount, submitAccount,
fetchAccount, fetchAccount,
deleteBulkAccounts,
} from 'store/accounts/accounts.actions'; } from 'store/accounts/accounts.actions';
import { import {
getAccountsItems, getAccountsItems,
@@ -26,6 +27,7 @@ const mapStateToProps = (state, props) => ({
tableQuery: state.accounts.tableQuery, tableQuery: state.accounts.tableQuery,
accountsLoading: state.accounts.loading, accountsLoading: state.accounts.loading,
accountErrors: state.accounts.errors,
getAccountById: (id) => getItemById(state.accounts.items, id), getAccountById: (id) => getItemById(state.accounts.items, id),
}); });
@@ -38,6 +40,7 @@ const mapActionsToProps = (dispatch) => ({
requestInactiveAccount: (id) => dispatch(inactiveAccount({ id })), requestInactiveAccount: (id) => dispatch(inactiveAccount({ id })),
requestFetchAccount: (id) => dispatch(fetchAccount({ id })), requestFetchAccount: (id) => dispatch(fetchAccount({ id })),
requestFetchAccountsTable: (query = {}) => dispatch(fetchAccountsTable({ query: { ...query } })), requestFetchAccountsTable: (query = {}) => dispatch(fetchAccountsTable({ query: { ...query } })),
requestDeleteBulkAccounts: (ids) => dispatch(deleteBulkAccounts({ ids })),
changeCurrentView: (id) => dispatch({ changeCurrentView: (id) => dispatch({
type: t.ACCOUNTS_SET_CURRENT_VIEW, type: t.ACCOUNTS_SET_CURRENT_VIEW,
@@ -50,7 +53,7 @@ const mapActionsToProps = (dispatch) => ({
type: 'ACCOUNTS_TABLE_QUERIES_ADD', queries, type: 'ACCOUNTS_TABLE_QUERIES_ADD', queries,
}), }),
setSelectedRowsAccounts: (ids) => dispatch({ setSelectedRowsAccounts: (ids) => dispatch({
type: t.ACCOUNTS_SELECTED_ROWS_SET, ids, type: t.ACCOUNTS_SELECTED_ROWS_SET, payload: { ids },
}), }),
}); });

View File

@@ -18,6 +18,14 @@ const mapActionsToProps = (dispatch) => ({
setTopbarEditView: (id) => dispatch({ setTopbarEditView: (id) => dispatch({
type: t.SET_TOPBAR_EDIT_VIEW, id, type: t.SET_TOPBAR_EDIT_VIEW, id,
}), }),
setDashboardRequestLoading: () => dispatch({
type: t.SET_DASHBOARD_REQUEST_LOADING,
}),
setDashboardRequestCompleted: () => dispatch({
type: t.SET_DASHBOARD_REQUEST_COMPLETED,
}),
}); });
export default connect(mapStateToProps, mapActionsToProps); export default connect(mapStateToProps, mapActionsToProps);

View File

@@ -12,7 +12,7 @@ import {
import RegisterFromConnect from 'connectors/RegisterForm.connect'; import RegisterFromConnect from 'connectors/RegisterForm.connect';
import ErrorMessage from 'components/ErrorMessage'; import ErrorMessage from 'components/ErrorMessage';
import AppToaster from 'components/AppToaster'; import AppToaster from 'components/AppToaster';
import { compose, regExpCollection } from 'utils'; import { compose } from 'utils';
function Register({ function Register({
requestSubmitRegister, requestSubmitRegister,
@@ -29,7 +29,6 @@ function Register({
last_name: Yup.string().required(), last_name: Yup.string().required(),
email: Yup.string().email().required(), email: Yup.string().email().required(),
phone_number: Yup.string() phone_number: Yup.string()
.matches(regExpCollection.phoneNumber)
.required(intl.formatMessage({ id: 'required' })), .required(intl.formatMessage({ id: 'required' })),
password: Yup.string() password: Yup.string()
.min(4, 'Password has to be longer than 8 characters!') .min(4, 'Password has to be longer than 8 characters!')

View File

@@ -2,8 +2,6 @@ import React, { useEffect, useState, useCallback } from 'react';
import { import {
Route, Route,
Switch, Switch,
useParams,
useRouteMatch
} from 'react-router-dom'; } from 'react-router-dom';
import useAsync from 'hooks/async'; import useAsync from 'hooks/async';
import { Alert, Intent } from '@blueprintjs/core'; import { Alert, Intent } from '@blueprintjs/core';
@@ -21,56 +19,71 @@ import { compose } from 'utils';
function AccountsChart({ function AccountsChart({
changePageTitle, changePageTitle,
requestFetchAccounts,
requestDeleteAccount, requestDeleteAccount,
requestInactiveAccount, requestInactiveAccount,
fetchResourceViews, fetchResourceViews,
fetchResourceFields, fetchResourceFields,
getResourceFields,
requestFetchAccountsTable, requestFetchAccountsTable,
addAccountsTableQueries addAccountsTableQueries,
requestDeleteBulkAccounts,
setDashboardRequestLoading,
setDashboardRequestCompleted,
}) { }) {
const [state, setState] = useState({
deleteAlertActive: false,
restoreAlertActive: false,
inactiveAlertActive: false,
targetAccount: {},
});
const [deleteAccount, setDeleteAccount] = useState(false); const [deleteAccount, setDeleteAccount] = useState(false);
const [inactiveAccount, setInactiveAccount] = useState(false); const [inactiveAccount, setInactiveAccount] = useState(false);
const [bulkDelete, setBulkDelete] = useState(false);
const [selectedRows, setSelectedRows] = useState([]);
// Fetch accounts resource views and fields.
const fetchHook = useAsync(async () => { const fetchHook = useAsync(async () => {
setDashboardRequestLoading();
await Promise.all([ await Promise.all([
fetchResourceViews('accounts'), fetchResourceViews('accounts'),
fetchResourceFields('accounts'), fetchResourceFields('accounts'),
]); ]);
setDashboardRequestCompleted();
}); });
// Fetch accounts list according to the given custom view id. // Fetch accounts list according to the given custom view id.
const fetchAccountsHook = useAsync(async () => { const fetchAccountsHook = useAsync(async () => {
setDashboardRequestLoading();
await Promise.all([ await Promise.all([
requestFetchAccountsTable(), requestFetchAccountsTable(),
]); ]);
setDashboardRequestCompleted();
}, false); }, false);
useEffect(() => { useEffect(() => {
changePageTitle('Chart of Accounts'); changePageTitle('Chart of Accounts');
}, []); }, [changePageTitle]);
// Handle click and cancel/confirm account delete // Handle click and cancel/confirm account delete
const handleDeleteAccount = (account) => { setDeleteAccount(account); }; const handleDeleteAccount = (account) => { setDeleteAccount(account); };
// handle cancel delete account alert. // handle cancel delete account alert.
const handleCancelAccountDelete = () => { setDeleteAccount(false); }; const handleCancelAccountDelete = useCallback(() => { setDeleteAccount(false); }, []);
// Handle confirm account delete // Handle confirm account delete
const handleConfirmAccountDelete = useCallback(() => { const handleConfirmAccountDelete = useCallback(() => {
requestDeleteAccount(deleteAccount.id).then(() => { requestDeleteAccount(deleteAccount.id).then(() => {
setDeleteAccount(false); setDeleteAccount(false);
fetchAccountsHook.execute();
AppToaster.show({ message: 'the_account_has_been_deleted' }); AppToaster.show({ message: 'the_account_has_been_deleted' });
}).catch(errors => {
setDeleteAccount(false);
if (errors.find((e) => e.type === 'ACCOUNT.PREDEFINED')) {
AppToaster.show({
message: 'cannot_delete_predefined_account'
});
}
if (errors.find((e) => e.type === 'ACCOUNT.HAS.ASSOCIATED.TRANSACTIONS')) {
AppToaster.show({
message: 'cannot_delete_account_has_associated_transactions'
});
}
}); });
}, [deleteAccount]); }, [deleteAccount, requestDeleteAccount]);
// Handle cancel/confirm account inactive. // Handle cancel/confirm account inactive.
const handleInactiveAccount = useCallback((account) => { const handleInactiveAccount = useCallback((account) => {
@@ -91,12 +104,6 @@ function AccountsChart({
}); });
}, [inactiveAccount]); }, [inactiveAccount]);
/**
* Handle cancel/confirm account restore.
*/
const handleCancelAccountRestore = () => {
};
const handleEditAccount = (account) => { const handleEditAccount = (account) => {
@@ -106,22 +113,43 @@ function AccountsChart({
}; };
const handleConfirmAccountRestore = (account) => { const handleBulkDelete = useCallback((accountsIds) => {
setBulkDelete(accountsIds);
}, [setBulkDelete]);
}; const handleConfirmBulkDelete = useCallback(() => {
const handleDeleteBulkAccounts = (accounts) => { requestDeleteBulkAccounts(bulkDelete).then(() => {
setBulkDelete(false);
AppToaster.show({ message: 'the_accounts_have_been_deleted' });
}).catch((error) => {
setBulkDelete(false);
});
}, [requestDeleteBulkAccounts, bulkDelete]);
}; const handleCancelBulkDelete = useCallback(() => {
setBulkDelete(false);
}, []);
const handleSelectedRowsChange = (accounts) => { const handleBulkArchive = useCallback((accounts) => {
console.log(accounts);
};
}, []);
// Handle selected rows change.
const handleSelectedRowsChange = useCallback((accounts) => {
setSelectedRows(accounts);
}, [setSelectedRows]);
// Refetches accounts data table when current custom view changed.
const handleFilterChanged = useCallback(() => { const handleFilterChanged = useCallback(() => {
fetchAccountsHook.execute(); fetchAccountsHook.execute();
}, [fetchAccountsHook]); }, [fetchAccountsHook]);
const handleViewChanged = useCallback(() => { fetchAccountsHook.execute(); }, []); // Refetch accounts data table when current custom view changed.
const handleViewChanged = useCallback(() => {
fetchAccountsHook.execute();
}, [fetchAccountsHook]);
// Handle fetch data of accounts datatable.
const handleFetchData = useCallback(({ pageIndex, pageSize, sortBy }) => { const handleFetchData = useCallback(({ pageIndex, pageSize, sortBy }) => {
addAccountsTableQueries({ addAccountsTableQueries({
...(sortBy.length > 0) ? { ...(sortBy.length > 0) ? {
@@ -135,26 +163,29 @@ function AccountsChart({
return ( return (
<DashboardInsider loading={fetchHook.pending} name={'accounts-chart'}> <DashboardInsider loading={fetchHook.pending} name={'accounts-chart'}>
<DashboardActionsBar <DashboardActionsBar
onFilterChanged={handleFilterChanged} /> selectedRows={selectedRows}
onFilterChanged={handleFilterChanged}
onBulkDelete={handleBulkDelete}
onBulkArchive={handleBulkArchive} />
<DashboardPageContent> <DashboardPageContent>
<Switch> <Switch>
<Route <Route
exact={true} exact={true}
path={[ path={[
'/dashboard/accounts/:custom_view_id/custom_view', '/dashboard/accounts/:custom_view_id/custom_view',
'/dashboard/accounts' '/dashboard/accounts',
]}> ]}>
<AccountsViewsTabs <AccountsViewsTabs
onViewChanged={handleViewChanged} onViewChanged={handleViewChanged} />
onDeleteBulkAccounts={handleDeleteBulkAccounts} />
<AccountsDataTable <AccountsDataTable
onSelectedRowsChange={handleSelectedRowsChange}
onDeleteAccount={handleDeleteAccount} onDeleteAccount={handleDeleteAccount}
onInactiveAccount={handleInactiveAccount} onInactiveAccount={handleInactiveAccount}
onRestoreAccount={handleRestoreAccount} onRestoreAccount={handleRestoreAccount}
onEditAccount={handleEditAccount} onEditAccount={handleEditAccount}
onFetchData={handleFetchData} /> onFetchData={handleFetchData}
onSelectedRowsChange={handleSelectedRowsChange} />
</Route> </Route>
</Switch> </Switch>
@@ -185,6 +216,20 @@ function AccountsChart({
but it will become private to you. but it will become private to you.
</p> </p>
</Alert> </Alert>
<Alert
cancelButtonText="Cancel"
confirmButtonText="Delete"
icon="trash"
intent={Intent.DANGER}
isOpen={bulkDelete}
onCancel={handleCancelBulkDelete}
onConfirm={handleConfirmBulkDelete}>
<p>
Are you sure you want to move <b>filename</b> to Trash? You will be able to restore it later,
but it will become private to you.
</p>
</Alert>
</DashboardPageContent> </DashboardPageContent>
</DashboardInsider> </DashboardInsider>
); );

View File

@@ -38,7 +38,7 @@ function AccountFormDialog({
closeDialog, closeDialog,
requestSubmitAccount, requestSubmitAccount,
requestEditAccount, requestEditAccount,
getAccountById getAccountById,
}) { }) {
const intl = useIntl(); const intl = useIntl();
const accountFormValidationSchema = Yup.object().shape({ const accountFormValidationSchema = Yup.object().shape({
@@ -57,12 +57,23 @@ function AccountFormDialog({
}), []); }), []);
const [selectedAccountType, setSelectedAccountType] = useState(null); const [selectedAccountType, setSelectedAccountType] = useState(null);
const [selectedSubaccount, setSelectedSubaccount] = useState(null); const [selectedSubaccount, setSelectedSubaccount] = useState(
payload.action === 'new_child' ?
accounts.find(a => a.id === payload.id) : null,
);
const editAccount = useMemo(() => const editAccount = useMemo(() =>
payload.action === 'edit' ? getAccountById(payload.id) : null, payload.action === 'edit' ? getAccountById(payload.id) : null,
[payload, getAccountById]); [payload, getAccountById]);
const transformApiErrors = (errors) => {
const fields = {};
if (errors.find(e => e.type === 'NOT_UNIQUE_CODE')) {
fields.code = 'Account code is not unqiue.'
}
return fields;
};
// Formik // Formik
const formik = useFormik({ const formik = useFormik({
enableReinitialize: true, enableReinitialize: true,
@@ -71,21 +82,22 @@ function AccountFormDialog({
? editAccount : initialValues, ? editAccount : initialValues,
}, },
validationSchema: accountFormValidationSchema, validationSchema: accountFormValidationSchema,
onSubmit: (values, { setSubmitting }) => { onSubmit: (values, { setSubmitting, setErrors }) => {
const exclude = ['subaccount']; const exclude = ['subaccount'];
if (payload.action === 'edit') { if (payload.action === 'edit') {
requestEditAccount({ requestEditAccount({
payload: payload.id, payload: payload.id,
form: { ...omit(values, exclude) } form: { ...omit(values, [...exclude, 'account_type_id']) }
}).then(response => { }).then(response => {
closeDialog(name); closeDialog(name);
AppToaster.show({ AppToaster.show({
message: 'the_account_has_been_edited' message: 'the_account_has_been_edited'
}); });
setSubmitting(false); setSubmitting(false);
}).catch(() => { }).catch((errors) => {
setSubmitting(false); setSubmitting(false);
setErrors(transformApiErrors(errors));
}); });
} else { } else {
requestSubmitAccount({ form: { ...omit(values, exclude) } }).then(response => { requestSubmitAccount({ form: { ...omit(values, exclude) } }).then(response => {
@@ -94,8 +106,9 @@ function AccountFormDialog({
message: 'the_account_has_been_submit' message: 'the_account_has_been_submit'
}); });
setSubmitting(false); setSubmitting(false);
}).catch(() => { }).catch((errors) => {
setSubmitting(false); setSubmitting(false);
setErrors(transformApiErrors(errors));
}); });
} }
} }
@@ -187,9 +200,7 @@ function AccountFormDialog({
return (<span>{'Sub account?'} <Icon icon="info-circle" iconSize={12} /></span>); return (<span>{'Sub account?'} <Icon icon="info-circle" iconSize={12} /></span>);
}, []); }, []);
const requiredSpan = useMemo(() => ( const requiredSpan = useMemo(() => (<span class="required">*</span>), []);
<span class="required">*</span>
), []);
return ( return (
<Dialog <Dialog
@@ -232,6 +243,7 @@ function AccountFormDialog({
rightIcon='caret-down' rightIcon='caret-down'
text={selectedAccountType ? text={selectedAccountType ?
selectedAccountType.name : 'Select account type'} selectedAccountType.name : 'Select account type'}
disabled={payload.action === 'edit'}
/> />
</Select> </Select>
</FormGroup> </FormGroup>

View File

@@ -1,3 +1,4 @@
import { omit } from 'lodash';
import ApiService from 'services/ApiService'; import ApiService from 'services/ApiService';
import t from 'store/types'; import t from 'store/types';
@@ -41,14 +42,20 @@ export const fetchAccountsList = ({ query } = {}) => {
export const fetchAccountsTable = ({ query } = {}) => { export const fetchAccountsTable = ({ query } = {}) => {
return (dispatch, getState) => return (dispatch, getState) =>
new Promise((resolve, reject) => { new Promise((resolve, reject) => {
const pageQuery = getState().accounts.tableQuery; let pageQuery = getState().accounts.tableQuery;
if (pageQuery.filter_roles) {
pageQuery = {
...omit(pageQuery, ['filter_roles']),
stringified_filter_roles: JSON.stringify(pageQuery.filter_roles) || '',
};
}
dispatch({ dispatch({
type: t.ACCOUNTS_TABLE_LOADING, type: t.ACCOUNTS_TABLE_LOADING,
loading: true, loading: true,
}); });
ApiService.get('accounts', { params: { ...pageQuery, ...query } }) ApiService.get('accounts', { params: { ...pageQuery, ...query } })
.then(response => { .then((response) => {
dispatch({ dispatch({
type: t.ACCOUNTS_PAGE_SET, type: t.ACCOUNTS_PAGE_SET,
accounts: response.data.accounts, accounts: response.data.accounts,
@@ -64,7 +71,7 @@ export const fetchAccountsTable = ({ query } = {}) => {
}); });
resolve(response); resolve(response);
}) })
.catch(error => { .catch((error) => {
reject(error); reject(error);
}); });
}); });
@@ -89,9 +96,12 @@ export const fetchAccountsDataTable = ({ query }) => {
export const submitAccount = ({ form }) => { export const submitAccount = ({ form }) => {
return dispatch => return dispatch =>
new Promise((resolve, reject) => { new Promise((resolve, reject) => {
ApiService.post('accounts', form) ApiService.post('accounts', form)
.then(response => { .then(response => {
dispatch({ type: t.CLEAR_ACCOUNT_FORM_ERRORS }); dispatch({
type: t.ACCOUNT_ERRORS_CLEAR,
});
resolve(response); resolve(response);
}) })
.catch(error => { .catch(error => {
@@ -99,11 +109,16 @@ export const submitAccount = ({ form }) => {
const { data } = response; const { data } = response;
const { errors } = data; const { errors } = data;
dispatch({ type: t.CLEAR_ACCOUNT_FORM_ERRORS }); dispatch({
type: t.ACCOUNT_ERRORS_CLEAR,
});
if (errors) { if (errors) {
dispatch({ type: t.ACCOUNT_FORM_ERRORS, errors }); dispatch({
type: t.ACCOUNT_ERRORS_SET,
payload: { errors },
});
} }
reject(error); reject(errors);
}); });
}); });
}; };
@@ -125,7 +140,7 @@ export const editAccount = ({ id, form }) => {
if (errors) { if (errors) {
dispatch({ type: t.ACCOUNT_FORM_ERRORS, errors }); dispatch({ type: t.ACCOUNT_FORM_ERRORS, errors });
} }
reject(error); reject(errors);
}); });
}); });
}; };
@@ -143,11 +158,25 @@ export const deleteAccount = ({ id }) => {
ApiService.delete(`accounts/${id}`).then((response) => { ApiService.delete(`accounts/${id}`).then((response) => {
dispatch({ type: t.ACCOUNT_DELETE, id }); dispatch({ type: t.ACCOUNT_DELETE, id });
resolve(response); resolve(response);
}).catch(error => { reject(error); }); }).catch((error) => {
reject(error.response.data.errors || []);
});
}); });
}; };
export const deleteBulkAccounts = ({ ids }) => {}; export const deleteBulkAccounts = ({ ids }) => {
return dispatch => new Promise((resolve, reject) => {
ApiService.delete(`accounts`, { params: { ids }}).then((response) => {
dispatch({
type: t.ACCOUNTS_BULK_DELETE,
payload: { ids }
});
resolve(response);
}).catch((error) => {
reject(error.response.data.errors || []);
});
});
};
export const fetchAccount = ({ id }) => { export const fetchAccount = ({ id }) => {
return dispatch => return dispatch =>

View File

@@ -7,11 +7,11 @@ const initialState = {
views: {}, views: {},
accountsTypes: [], accountsTypes: [],
accountsById: {}, accountsById: {},
accountFormErrors: [], tableQuery: {},
datatableQuery: {},
currentViewId: -1, currentViewId: -1,
selectedRows: [], selectedRows: [],
loading: false, loading: false,
errors: [],
}; };
const accountsReducer = createReducer(initialState, { const accountsReducer = createReducer(initialState, {
@@ -35,7 +35,6 @@ const accountsReducer = createReducer(initialState, {
...view, ...view,
ids: action.accounts.map(i => i.id), ids: action.accounts.map(i => i.id),
}; };
state.accounts = action.accounts;
}, },
[t.ACCOUNT_TYPES_LIST_SET]: (state, action) => { [t.ACCOUNT_TYPES_LIST_SET]: (state, action) => {
@@ -53,7 +52,8 @@ const accountsReducer = createReducer(initialState, {
}, },
[t.ACCOUNTS_SELECTED_ROWS_SET]: (state, action) => { [t.ACCOUNTS_SELECTED_ROWS_SET]: (state, action) => {
state.selectedRows.push(...action.ids); const { ids } = action.payload;
state.selectedRows = [];
}, },
[t.ACCOUNTS_SET_CURRENT_VIEW]: (state, action) => { [t.ACCOUNTS_SET_CURRENT_VIEW]: (state, action) => {
@@ -63,6 +63,27 @@ const accountsReducer = createReducer(initialState, {
[t.ACCOUNTS_TABLE_LOADING]: (state, action) => { [t.ACCOUNTS_TABLE_LOADING]: (state, action) => {
state.loading = action.loading; state.loading = action.loading;
}, },
[t.ACCOUNT_ERRORS_SET]: (state, action) => {
const { errors } = action.payload;
state.errors = errors;
},
[t.ACCOUNT_ERRORS_CLEAR]: (state, action) => {
state.errors = [];
},
[t.ACCOUNTS_BULK_DELETE]: (state, action) => {
const { ids } = action.payload;
const items = { ...state.items };
ids.forEach((id) => {
if (typeof items[id] !== 'undefined') {
delete items[id];
}
});
state.items = items;
},
}); });
export default createTableQueryReducers('accounts', accountsReducer); export default createTableQueryReducers('accounts', accountsReducer);

View File

@@ -1,7 +1,6 @@
import { pickItemsFromIds } from 'store/selectors'; import { pickItemsFromIds } from 'store/selectors';
export const getAccountsItems = (state, viewId) => { export const getAccountsItems = (state, viewId) => {
const accountsView = state.accounts.views[viewId || -1]; const accountsView = state.accounts.views[viewId || -1];
const accountsItems = state.accounts.items; const accountsItems = state.accounts.items;

View File

@@ -17,4 +17,8 @@ export default {
ACCOUNTS_TABLE_QUERIES_SET: 'ACCOUNTS_TABLE_QUERIES_SET', ACCOUNTS_TABLE_QUERIES_SET: 'ACCOUNTS_TABLE_QUERIES_SET',
ACCOUNTS_TABLE_LOADING: 'ACCOUNTS_TABLE_LOADING', ACCOUNTS_TABLE_LOADING: 'ACCOUNTS_TABLE_LOADING',
ACCOUNT_ERRORS_SET: 'ACCOUNT_ERRORS_SET',
ACCOUNT_ERRORS_CLEAR: 'ACCOUNT_ERRORS_CLEAR',
ACCOUNTS_BULK_DELETE: 'ACCOUNTS_BULK_DELETE'
}; };

View File

@@ -7,6 +7,7 @@ const initialState = {
preferencesPageTitle: '', preferencesPageTitle: '',
dialogs: {}, dialogs: {},
topbarEditViewId: 1, topbarEditViewId: 1,
requestsLoading: 0,
}; };
export default createReducer(initialState, { export default createReducer(initialState, {
@@ -42,6 +43,15 @@ export default createReducer(initialState, {
[t.SET_TOPBAR_EDIT_VIEW]: (state, action) => { [t.SET_TOPBAR_EDIT_VIEW]: (state, action) => {
state.topbarEditViewId = action.id; state.topbarEditViewId = action.id;
},
[t.SET_DASHBOARD_REQUEST_LOADING]: (state, action) => {
state.requestsLoading = state.requestsLoading + 1;
},
[t.SET_DASHBOARD_REQUEST_COMPLETED]: (state, action) => {
const requestsLoading = state.requestsLoading - 1;
state.requestsLoading = Math.max(requestsLoading, 0);
} }
}); });

View File

@@ -9,4 +9,6 @@ export default {
CHANGE_PREFERENCES_PAGE_TITLE: 'CHANGE_PREFERENCES_PAGE_TITLE', CHANGE_PREFERENCES_PAGE_TITLE: 'CHANGE_PREFERENCES_PAGE_TITLE',
ALTER_DASHBOARD_PAGE_SUBTITLE: 'ALTER_DASHBOARD_PAGE_SUBTITLE', ALTER_DASHBOARD_PAGE_SUBTITLE: 'ALTER_DASHBOARD_PAGE_SUBTITLE',
SET_TOPBAR_EDIT_VIEW: 'SET_TOPBAR_EDIT_VIEW', SET_TOPBAR_EDIT_VIEW: 'SET_TOPBAR_EDIT_VIEW',
SET_DASHBOARD_REQUEST_LOADING: 'SET_DASHBOARD_REQUEST_LOADING',
SET_DASHBOARD_REQUEST_COMPLETED: 'SET_DASHBOARD_REQUEST_COMPLETED',
}; };

View File

@@ -1,11 +1,11 @@
import {pick} from 'lodash'; import {pick, at} from 'lodash';
export const getItemById = (items, id) => { export const getItemById = (items, id) => {
return items[id] || null; return items[id] || null;
}; };
export const pickItemsFromIds = (items, ids) => { export const pickItemsFromIds = (items, ids) => {
return Object.values(pick(items, ids)); return at(items, ids).filter(i => i);
} }
export const getCurrentPageResults = (items, pages, pageNumber) => { export const getCurrentPageResults = (items, pages, pageNumber) => {

View File

@@ -61,3 +61,15 @@ $pt-font-family: Noto Sans, -apple-system, BlinkMacSystemFont, Segoe UI, Roboto,
fill: #2d95fd; fill: #2d95fd;
} }
} }
.bigcapital--alt{
svg{
path,
.path-13,
.path-1{
fill: #fff;
}
}
}

View File

@@ -111,9 +111,6 @@
} }
.tbody{ .tbody{
overflow-y: scroll;
overflow-x: hidden;
.tr .td{ .tr .td{
border-bottom: 1px solid #E8E8E8; border-bottom: 1px solid #E8E8E8;
align-items: center; align-items: center;
@@ -145,6 +142,10 @@
align-items: center; align-items: center;
} }
} }
> .loading{
padding-top: 50px;
}
} }
.tr .th, .tr .th,
@@ -175,8 +176,6 @@
display: block; display: block;
} }
} }
} }
} }
@@ -199,6 +198,13 @@
} }
} }
&.has-virtualized-rows{
.tbody{
overflow-y: scroll;
overflow-x: hidden;
}
}
&--financial-report{ &--financial-report{
.table { .table {

View File

@@ -210,13 +210,13 @@
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
height: 100%;
} }
} }
&__page-content{ &__page-content{
// padding: 22px; // padding: 22px;
.bigcapital-datatable{ .bigcapital-datatable{
.table{ .table{
@@ -229,6 +229,10 @@
} }
} }
} }
.dashboard__loading-indicator{
padding-top: 150px;
}
} }
&__preferences-topbar{ &__preferences-topbar{

View File

@@ -18,22 +18,14 @@ $sidebar-popover-submenu-bg: rgb(1, 20, 62);
overflow-x: hidden; overflow-x: hidden;
height: 100%; height: 100%;
} }
&__head{ &__head{
padding: 16px 10px; padding: 16px 12px;
&-company-meta{ &-logo{
margin-top: 4px;
.company-name{ svg{
font-size: 16px; opacity: 0.35;
font-weight: 200;
margin-bottom: 5px;
color: rgba(255, 255, 255, 0.75);
}
.company-meta{
color: rgba(255, 255, 255, 0.4);
font-size: 12px;
} }
} }
} }

View File

@@ -21,7 +21,14 @@
margin-bottom: 0; margin-bottom: 0;
&:not(:last-of-type) { &:not(:last-of-type) {
padding-right: 15px; padding-right: 12px;
}
.bp3-html-select select,
.bp3-select select{
padding: 0 20px 0 6px;
}
.bp3-input{
padding: 0 6px;
} }
} }

View File

@@ -145,3 +145,10 @@ export function formattedAmount(cents, currency) {
export const ConditionalWrapper = ({ condition, wrapper, children }) => export const ConditionalWrapper = ({ condition, wrapper, children }) =>
condition ? wrapper(children) : children; condition ? wrapper(children) : children;
export const checkRequiredProperties = (obj, properties) => {
return properties.some((prop) => {
const value = obj[prop];
return (value === '' || value === null || value === undefined);
})
}

View File

@@ -33,6 +33,11 @@ export default {
'code': { 'code': {
column: 'code', column: 'code',
}, },
'root_type': {
column: 'account_type_id',
relation: 'account_types.id',
relationColumn: 'account_types.root_type',
},
}, },
'items': { 'items': {

View File

@@ -9,6 +9,7 @@ exports.up = function (knex) {
table.text('description'); table.text('description');
table.boolean('active').defaultTo(true); table.boolean('active').defaultTo(true);
table.integer('index').unsigned(); table.integer('index').unsigned();
table.boolean('predefined').defaultTo(false);
table.timestamps(); table.timestamps();
}).then(() => { }).then(() => {
return knex.seed.run({ return knex.seed.run({

View File

@@ -4,6 +4,7 @@ exports.up = (knex) => {
table.increments(); table.increments();
table.string('name'); table.string('name');
table.string('normal'); table.string('normal');
table.string('root_type');
table.boolean('balance_sheet'); table.boolean('balance_sheet');
table.boolean('income_sheet'); table.boolean('income_sheet');
}).then(() => { }).then(() => {

View File

@@ -9,6 +9,7 @@ exports.seed = (knex) => {
id: 1, id: 1,
name: 'Fixed Asset', name: 'Fixed Asset',
normal: 'debit', normal: 'debit',
root_type: 'asset',
balance_sheet: true, balance_sheet: true,
income_sheet: false, income_sheet: false,
}, },
@@ -16,6 +17,7 @@ exports.seed = (knex) => {
id: 2, id: 2,
name: 'Current Asset', name: 'Current Asset',
normal: 'debit', normal: 'debit',
root_type: 'asset',
balance_sheet: true, balance_sheet: true,
income_sheet: false, income_sheet: false,
}, },
@@ -23,6 +25,7 @@ exports.seed = (knex) => {
id: 3, id: 3,
name: 'Long Term Liability', name: 'Long Term Liability',
normal: 'credit', normal: 'credit',
root_type: 'liability',
balance_sheet: false, balance_sheet: false,
income_sheet: true, income_sheet: true,
}, },
@@ -30,6 +33,7 @@ exports.seed = (knex) => {
id: 4, id: 4,
name: 'Current Liability', name: 'Current Liability',
normal: 'credit', normal: 'credit',
root_type: 'liability',
balance_sheet: false, balance_sheet: false,
income_sheet: true, income_sheet: true,
}, },
@@ -37,6 +41,7 @@ exports.seed = (knex) => {
id: 5, id: 5,
name: 'Equity', name: 'Equity',
normal: 'credit', normal: 'credit',
root_type: 'equity',
balance_sheet: false, balance_sheet: false,
income_sheet: true, income_sheet: true,
}, },
@@ -44,6 +49,7 @@ exports.seed = (knex) => {
id: 6, id: 6,
name: 'Expense', name: 'Expense',
normal: 'debit', normal: 'debit',
root_type: 'expense',
balance_sheet: false, balance_sheet: false,
income_sheet: true, income_sheet: true,
}, },
@@ -51,6 +57,7 @@ exports.seed = (knex) => {
id: 7, id: 7,
name: 'Income', name: 'Income',
normal: 'credit', normal: 'credit',
root_type: 'income',
balance_sheet: false, balance_sheet: false,
income_sheet: true, income_sheet: true,
}, },
@@ -58,6 +65,7 @@ exports.seed = (knex) => {
id: 8, id: 8,
name: 'Accounts Receivable', name: 'Accounts Receivable',
normal: 'debit', normal: 'debit',
root_type: 'asset',
balance_sheet: true, balance_sheet: true,
income_sheet: false, income_sheet: false,
}, },
@@ -65,6 +73,7 @@ exports.seed = (knex) => {
id: 9, id: 9,
name: 'Accounts Payable', name: 'Accounts Payable',
normal: 'credit', normal: 'credit',
root_type: 'liability',
balance_sheet: true, balance_sheet: true,
income_sheet: false, income_sheet: false,
}, },

View File

@@ -10,20 +10,22 @@ exports.seed = (knex) => {
name: 'Petty Cash', name: 'Petty Cash',
account_type_id: 2, account_type_id: 2,
parent_account_id: null, parent_account_id: null,
code: '10000', code: '1000',
description: '', description: '',
active: 1, active: 1,
index: 1, index: 1,
predefined: 1,
}, },
{ {
id: 2, id: 2,
name: 'Bank', name: 'Bank',
account_type_id: 2, account_type_id: 2,
parent_account_id: null, parent_account_id: null,
code: '20000', code: '2000',
description: '', description: '',
active: 1, active: 1,
index: 1, index: 1,
predefined: 1,
}, },
{ {
id: 3, id: 3,
@@ -34,6 +36,7 @@ exports.seed = (knex) => {
description: '', description: '',
active: 1, active: 1,
index: 1, index: 1,
predefined: 1,
}, },
{ {
id: 4, id: 4,
@@ -44,6 +47,7 @@ exports.seed = (knex) => {
description: '', description: '',
active: 1, active: 1,
index: 1, index: 1,
predefined: 1,
}, },
{ {
id: 5, id: 5,
@@ -54,6 +58,7 @@ exports.seed = (knex) => {
description: '', description: '',
active: 1, active: 1,
index: 1, index: 1,
predefined: 1,
}, },
{ {
id: 6, id: 6,
@@ -64,6 +69,7 @@ exports.seed = (knex) => {
description: '', description: '',
active: 1, active: 1,
index: 1, index: 1,
predefined: 1,
}, },
{ {
id: 7, id: 7,
@@ -74,6 +80,7 @@ exports.seed = (knex) => {
description: '', description: '',
active: 1, active: 1,
index: 1, index: 1,
predefined: 1,
}, },
{ {
id: 8, id: 8,
@@ -84,6 +91,7 @@ exports.seed = (knex) => {
description: '', description: '',
active: 1, active: 1,
index: 1, index: 1,
predefined: 1,
}, },
{ {
id: 9, id: 9,
@@ -94,6 +102,7 @@ exports.seed = (knex) => {
description: '', description: '',
active: 1, active: 1,
index: 1, index: 1,
predefined: 1,
} }
]); ]);
}); });

View File

@@ -5,10 +5,19 @@ exports.seed = function(knex) {
.then(() => { .then(() => {
// Inserts seed entries // Inserts seed entries
return knex('resource_fields').insert([ return knex('resource_fields').insert([
{id: 1, label_name: 'Name', key: 'name', data_type: '', 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: 2, label_name: 'Code', key: 'code', data_type: '', active: 1, predefined: 1 },
{id: 3, label_name: 'Account Type', key: 'account_type_id', data_type: '', active: 1, predefined: 1}, { id: 3, label_name: 'Account Type', key: 'account_type_id', data_type: '', active: 1, predefined: 1 },
{id: 4, label_name: 'Description', key: 'description', 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: 6,
label_name: 'Root Account Type',
key: 'root_account_type',
data_type: 'string',
active: 1,
predefined: 1,
},
]); ]);
}); });
}; };

View File

@@ -40,10 +40,19 @@ exports.seed = (knex) => {
predefined: 1, predefined: 1,
columnable: true, columnable: true,
}, },
{
id: 6,
resource_id: 1,
label_name: 'Root type',
data_type: 'textbox',
key: 'root_type',
predefined: 1,
columnable: true,
},
// Expenses // Expenses
{ {
id: 6, id: 7,
resource_id: 3, resource_id: 3,
label_name: 'Date', label_name: 'Date',
data_type: 'date', data_type: 'date',
@@ -51,7 +60,7 @@ exports.seed = (knex) => {
columnable: true, columnable: true,
}, },
{ {
id: 7, id: 8,
resource_id: 3, resource_id: 3,
label_name: 'Expense Account', label_name: 'Expense Account',
data_type: 'options', data_type: 'options',
@@ -59,7 +68,7 @@ exports.seed = (knex) => {
columnable: true, columnable: true,
}, },
{ {
id: 8, id: 9,
resource_id: 3, resource_id: 3,
label_name: 'Payment Account', label_name: 'Payment Account',
data_type: 'options', data_type: 'options',
@@ -67,7 +76,7 @@ exports.seed = (knex) => {
columnable: true, columnable: true,
}, },
{ {
id: 9, id: 10,
resource_id: 3, resource_id: 3,
label_name: 'Amount', label_name: 'Amount',
data_type: 'number', data_type: 'number',
@@ -77,7 +86,7 @@ exports.seed = (knex) => {
// Items // Items
{ {
id: 10, id: 11,
resource_id: 2, resource_id: 2,
label_name: 'Name', label_name: 'Name',
key: 'name', key: 'name',
@@ -86,7 +95,7 @@ exports.seed = (knex) => {
columnable: true, columnable: true,
}, },
{ {
id: 11, id: 12,
resource_id: 2, resource_id: 2,
label_name: 'Type', label_name: 'Type',
key: 'type', key: 'type',

View File

@@ -5,11 +5,11 @@ exports.seed = (knex) => {
.then(() => { .then(() => {
// Inserts seed entries // Inserts seed entries
return knex('views').insert([ return knex('views').insert([
{id: 1, name: 'Assets', roles_logic_expression: '1', resource_id: 1, predefined: true }, { id: 1, name: 'Assets', roles_logic_expression: '1', resource_id: 1, predefined: true },
{id: 2, name: 'Liabilities', roles_logic_expression: '1', resource_id: 1, predefined: true }, { id: 2, name: 'Liabilities', roles_logic_expression: '1', resource_id: 1, predefined: true },
{id: 3, name: 'Equity', roles_logic_expression: '1', resource_id: 1, predefined: true }, { id: 3, name: 'Equity', roles_logic_expression: '1', resource_id: 1, predefined: true },
{id: 4, name: 'Income', roles_logic_expression: '1', resource_id: 1, predefined: true }, { id: 4, name: 'Income', roles_logic_expression: '1', resource_id: 1, predefined: true },
{id: 5, name: 'Expenses', roles_logic_expression: '1', resource_id: 1, predefined: true }, { id: 5, name: 'Expenses', roles_logic_expression: '1', resource_id: 1, predefined: true },
]); ]);
}); });
}; };

View File

@@ -5,11 +5,11 @@ exports.seed = (knex) => {
.then(() => { .then(() => {
// Inserts seed entries // Inserts seed entries
return knex('view_roles').insert([ return knex('view_roles').insert([
{id: 1, field_id: 3, comparator: 'equals', value: '', view_id: 1}, { id: 1, field_id: 6, comparator: 'equals', value: 'asset', view_id: 1 },
{id: 2, field_id: 3, comparator: 'equals', value: '', view_id: 2}, { id: 2, field_id: 6, comparator: 'equals', value: 'liability', view_id: 2 },
{id: 3, field_id: 3, comparator: 'equals', value: '', view_id: 3}, { id: 3, field_id: 6, comparator: 'equals', value: 'equity', view_id: 3 },
{id: 4, field_id: 3, comparator: 'equals', value: '', view_id: 4}, { id: 4, field_id: 6, comparator: 'equals', value: 'income', view_id: 4 },
{id: 5, field_id: 3, comparator: 'equals', value: '', view_id: 5}, { id: 5, field_id: 6, comparator: 'equals', value: 'expense', view_id: 5 },
]); ]);
}); });
}; };

View File

@@ -212,6 +212,11 @@ export default {
if (!account) { if (!account) {
return res.boom.notFound(); return res.boom.notFound();
} }
if (account.predefined) {
return res.boom.badRequest(null, {
errors: [{ type: 'ACCOUNT.PREDEFINED' , code: 200 }],
});
}
const accountTransactions = await AccountTransaction.query() const accountTransactions = await AccountTransaction.query()
.where('account_id', account.id); .where('account_id', account.id);
@@ -289,7 +294,6 @@ export default {
const dynamicFilter = new DynamicFilter(Account.tableName); const dynamicFilter = new DynamicFilter(Account.tableName);
if (filter.column_sort_by) { if (filter.column_sort_by) {
console.log(filter);
if (resourceFieldsKeys.indexOf(filter.column_sort_by) === -1) { if (resourceFieldsKeys.indexOf(filter.column_sort_by) === -1) {
errorReasons.push({ type: 'COLUMN.SORT.ORDER.NOT.FOUND', code: 300 }); errorReasons.push({ type: 'COLUMN.SORT.ORDER.NOT.FOUND', code: 300 });
} }