feat: remove SET_DASHBOARD_REQUEST_LOADING reducer.

feat: fix dropdown filter.
feat: fix fetch resource data.
This commit is contained in:
Ahmed Bouhuolia
2020-10-20 19:58:24 +02:00
parent 00ba1bb75e
commit 322af97d77
51 changed files with 1160 additions and 1009 deletions

View File

@@ -1,13 +0,0 @@
import React from 'react';
import {Dialog, Spinner, Classes} from '@blueprintjs/core';
export default function DialogComponent(props) {
const loadingContent = (
<div className={Classes.DIALOG_BODY}><Spinner size={30} /></div>
);
return (
<Dialog {...props}>
{props.isLoading ? loadingContent : props.children}
</Dialog>
);
}

View File

@@ -0,0 +1,22 @@
import React from 'react';
import { Dialog } from '@blueprintjs/core';
import withDialogActions from 'containers/Dialog/withDialogActions';
import { compose } from 'utils';
function DialogComponent(props) {
const { name, children, closeDialog, onClose } = props;
const handleClose = (event) => {
closeDialog(name)
onClose && onClose(event);
};
return (
<Dialog {...props} onClose={handleClose}>
{ children }
</Dialog>
);
}
export default compose(
withDialogActions,
)(DialogComponent);

View File

@@ -0,0 +1,15 @@
import React from 'react';
import { Spinner, Classes } from '@blueprintjs/core';
export default function DialogContent(props) {
const { isLoading, children } = props;
const loadingContent = (
<div className={Classes.DIALOG_BODY}><Spinner size={30} /></div>
);
return (
<div>
{isLoading ? loadingContent : children}
</div>
);
}

View File

@@ -0,0 +1,18 @@
import React, { Suspense } from 'react';
import { Classes, Spinner } from '@blueprintjs/core';
function LoadingContent() {
return (<div className={Classes.DIALOG_BODY}><Spinner size={30} /></div>);
}
export default function DialogSuspense({
children
}) {
return (
<Suspense fallback={<LoadingContent /> }>
<div className={'dialog__suspense-wrapper'}>
{ children }
</div>
</Suspense>
);
};

View File

@@ -1,17 +1,15 @@
import React from 'react';
import { connect } from 'react-redux';
import {
isDialogOpenFactory,
getDialogPayloadFactory,
} from 'store/dashboard/dashboard.selectors';
export default (mapState, dialogName) => {
const isDialogOpen = isDialogOpenFactory(dialogName);
const getDialogPayload = getDialogPayloadFactory(dialogName);
export default (mapState) => {
const isDialogOpen = isDialogOpenFactory();
const getDialogPayload = getDialogPayloadFactory();
const mapStateToProps = (state, props) => {
const mapped = {
dialogName,
isOpen: isDialogOpen(state, props),
payload: getDialogPayload(state, props),
};

View File

@@ -1,20 +1,17 @@
import React from 'react';
import React, { lazy } from 'react';
import AccountFormDialog from 'containers/Dialogs/AccountFormDialog';
import UserFormDialog from 'containers/Dialogs/UserFormDialog';
import ItemCategoryDialog from 'containers/Dialogs/ItemCategoryDialog';
import CurrencyDialog from 'containers/Dialogs/CurrencyDialog';
import InviteUserDialog from 'containers/Dialogs/InviteUserDialog';
import ExchangeRateDialog from 'containers/Dialogs/ExchangeRateDialog';
// import UserFormDialog from 'containers/Dialogs/UserFormDialog';
// import ItemCategoryDialog from 'containers/Dialogs/ItemCategoryDialog';
// import CurrencyDialog from 'containers/Dialogs/CurrencyDialog';
// import InviteUserDialog from 'containers/Dialogs/InviteUserDialog';
// import ExchangeRateDialog from 'containers/Dialogs/ExchangeRateDialog';
export default function DialogsContainer() {
return (
<div>
<ExchangeRateDialog />
{/* <InviteUserDialog /> */}
<CurrencyDialog />
<ItemCategoryDialog />
<AccountFormDialog />
{/* <UserFormDialog /> */}
<AccountFormDialog dialogName={'account-form'} />
</div>
);
}

View File

@@ -1,5 +1,6 @@
export const BooleanCompatators = [
{ value: 'is', label_id: 'is' },
{ value: 'is_not', label_id: 'is_not' },
];
export const TextCompatators = [
@@ -20,6 +21,15 @@ export const OptionsCompatators = [
{ value: 'is_not', label_id: 'is_not' },
];
export const NumberCampatators = [
{ value: 'equals', label_id: 'equals' },
{ value: 'not_equal', label_id: 'not_equal' },
{ value: 'bigger_than', label_id: 'bigger_than' },
{ value: 'bigger_or_equals', label_id: 'bigger_or_equals' },
{ value: 'smaller_than', label_id: 'smaller_than' },
{ value: 'smaller_or_equals', label_id: 'smaller_or_equals' },
]
export const getConditionTypeCompatators = (dataType) => {
return [
...(dataType === 'options'
@@ -27,7 +37,9 @@ export const getConditionTypeCompatators = (dataType) => {
: dataType === 'date'
? [...DateCompatators]
: dataType === 'boolean'
? [...BooleanCompatators]
? [...BooleanCompatators]
: dataType === 'number'
? [...NumberCampatators]
: [...TextCompatators]),
];
};

View File

@@ -14,72 +14,80 @@ import { FormattedMessage as T, useIntl } from 'react-intl';
import { debounce } from 'lodash';
import moment from 'moment';
import { If, Choose, ListSelect, MODIFIER } from 'components';
import { 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({
dataType,
value,
initialValue,
error,
// fieldkey,
// resourceKey,
// #withResourceDetail
resourceName,
resourceData,
resourceData = [],
requestResourceData,
// #ownProps
fieldType,
fieldName,
value,
initialValue,
error,
optionsResource,
optionsKey = 'key',
optionsLabel = 'label',
options,
onChange,
rosourceKey,
inputDebounceWait = 500,
inputDebounceWait = 250,
}) {
const { formatMessage } = useIntl();
const [localValue, setLocalValue] = useState();
const fetchResourceData = useQuery(
['resource-data', resourceName && resourceName],
(k, resName) => requestResourceData(resName),
{ manual: true },
);
// Makes `localValue` controlled mode from `value`.
useEffect(() => {
if (value !== localValue) {
setLocalValue(value);
}
}, [value]);
// Fetches resource data.
const fetchResourceData = useQuery(
['resource-data', resourceName],
(key, _resourceName) => requestResourceData(_resourceName),
{
enabled: resourceName,
},
);
// Account type item of select filed.
const menuItem = (item, { handleClick, modifiers, query }) => {
return <MenuItem text={item.name} key={item.id} onClick={handleClick} />;
return (<MenuItem
text={item[optionsLabel]}
key={item[optionsKey]}
onClick={handleClick}
/>);
};
// Handle list button click.
const handleBtnClick = () => {
fetchResourceData.refetch({});
};
const listOptions = useMemo(() => Object.values(resourceData), [
resourceData,
const listOptions = useMemo(() => [
...(resourceData || []),
...(options || []),
], [
resourceData, options,
]);
// Filters accounts types items.
const filterItems = (query, item, _index, exactMatch) => {
const normalizedTitle = item.name.toLowerCase();
const normalizedTitle = item.label.toLowerCase();
const normalizedQuery = query.toLowerCase();
if (exactMatch) {
@@ -89,16 +97,16 @@ function DynamicFilterValueField({
}
};
// Handle list item selected.
const onItemSelect = (item) => {
onChange && onChange(item);
onChange && onChange(item[optionsKey]);
};
const handleInputChangeThrottled = useRef(
debounce((value) => {
onChange && onChange(value);
}, inputDebounceWait),
debounce((value) => { onChange && onChange(value); }, inputDebounceWait),
);
// Handle input change.
const handleInputChange = (e) => {
if (e.currentTarget.type === 'checkbox') {
setLocalValue(e.currentTarget.checked);
@@ -108,12 +116,14 @@ function DynamicFilterValueField({
handleInputChangeThrottled.current(e.currentTarget.value);
};
// Handle checkbox field change.
const handleCheckboxChange = (e) => {
const value = !!e.currentTarget.checked;
setLocalValue(value);
onChange && onChange(value);
}
// Handle date field change.
const handleDateChange = (date) => {
setLocalValue(date);
onChange && onChange(date);
@@ -126,7 +136,7 @@ function DynamicFilterValueField({
return (
<FormGroup className={'form-group--value'}>
<Choose>
<Choose.When condition={dataType === 'options'}>
<Choose.When condition={fieldType === 'options'}>
<ListSelect
className={classNames(
'list-select--filter-dropdown',
@@ -146,14 +156,16 @@ function DynamicFilterValueField({
}}
onItemSelect={onItemSelect}
selectedItem={value}
selectedItemProp={'id'}
defaultText={<T id={'select_account_type'} />}
labelProp={'name'}
buttonProps={{ onClick: handleBtnClick }}
selectedItemProp={optionsKey}
defaultText={`Select an option`}
labelProp={optionsLabel}
buttonProps={{
onClick: handleBtnClick
}}
/>
</Choose.When>
<Choose.When condition={dataType === 'date'}>
<Choose.When condition={fieldType === 'date'}>
<DateInput
{...momentFormatter('YYYY/MM/DD')}
value={transformDateValue(localValue)}
@@ -167,7 +179,7 @@ function DynamicFilterValueField({
/>
</Choose.When>
<Choose.When condition={dataType === 'boolean'}>
<Choose.When condition={fieldType === 'checkbox'}>
<Checkbox value={localValue} onChange={handleCheckboxChange} />
</Choose.When>
@@ -184,7 +196,7 @@ function DynamicFilterValueField({
}
const mapStateToProps = (state, props) => ({
resourceName: props.dataResource,
resourceName: props.optionsResource,
});
const withResourceFilterValueField = connect(mapStateToProps);

View File

@@ -1,5 +1,5 @@
// @flow
import React, { useEffect, useMemo, useCallback, useRef } from 'react';
// @flow
import React, { useEffect, useMemo, useCallback, useState } from 'react';
import {
FormGroup,
Classes,
@@ -10,7 +10,6 @@ import {
import { useFormik } from 'formik';
import { isEqual, last } from 'lodash';
import { usePrevious } from 'react-use';
import { debounce } from 'lodash';
import Icon from 'components/Icon';
import { checkRequiredProperties, uniqueMultiProps } from 'utils';
import { FormattedMessage as T, useIntl } from 'react-intl';
@@ -22,7 +21,7 @@ import Toaster from 'components/AppToaster';
import moment from 'moment';
import {
getConditionTypeCompatators,
getConditionDefaultCompatator
getConditionDefaultCompatator,
} from './DynamicFilter/DynamicFilterCompatators';
let limitToast;
@@ -39,224 +38,238 @@ type InitialCondition = {
export default function FilterDropdown({
fields,
onFilterChange,
refetchDebounceWait = 10,
initialCondition,
initialConditions,
}) {
const { formatMessage } = useIntl();
// Fields key -> metadata table.
const fieldsKeyMapped = useMemo(() =>
new Map(fields.map((field) => [field.key, field])),
[fields]
);
// Conditions options.
const conditionalsOptions = useMemo(
() => [
{ value: '&&', label: formatMessage({ id: 'and' }) },
{ value: '||', label: formatMessage({ id: 'or' }) },
],
[formatMessage],
);
// Resources fileds options for fields options.
const resourceFieldsOptions = useMemo(
() => [
...fields.map((field) => ({
value: field.key,
label: field.label,
})),
],
[fields],
);
// Default filter conition.
const defaultFilterCondition = useMemo(
() => ({
condition: '&&',
fieldKey: initialCondition.fieldKey,
comparator: initialCondition.comparator,
value: initialCondition.value,
}),
[initialCondition],
);
// Formik for validation purposes.
const { setFieldValue, getFieldProps, values } = useFormik({
initialValues: {
conditions: [
...((initialConditions && initialConditions.length) ?
[...initialConditions] : [defaultFilterCondition]),
],
},
});
// Handle click a new filter row.
const onClickNewFilter = useCallback(() => {
if (values.conditions.length >= 12) {
limitToast = Toaster.show(
{
message: formatMessage({ id: 'you_reached_conditions_limit' }),
intent: Intent.WARNING,
},
limitToast,
);
} else {
setFieldValue('conditions', [
...values.conditions,
defaultFilterCondition
]);
}
}, [values, setFieldValue, formatMessage, defaultFilterCondition]);
// Filtered conditions that filters conditions that don't contain atleast
// on required fields or fileds keys that not exists.
const filteredFilterConditions = useMemo(() => {
const requiredProps = ['fieldKey', 'condition', 'comparator', 'value'];
const conditions = values.conditions
.filter(
(condition) => !checkRequiredProperties(condition, requiredProps),
)
.filter(
(condition) => !!fieldsKeyMapped.get(condition.fieldKey),
);
return uniqueMultiProps(conditions, requiredProps);
}, [values.conditions, fieldsKeyMapped]);
// Previous filtered conditions.
const prevConditions = usePrevious(filteredFilterConditions);
useEffect(() => {
// Campare the current conditions with previous conditions, if they were equal
// there is no need to execute `onFilterChange` function.
if (!isEqual(prevConditions, filteredFilterConditions) && prevConditions) {
onFilterChange && onFilterChange(filteredFilterConditions);
}
}, [filteredFilterConditions, prevConditions, onFilterChange]);
// 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]);
};
// Transform dynamic value field.
const transformValueField = (value) => {
if (value instanceof Date) {
return moment(value).format('YYYY-MM-DD');
} else if (typeof value === 'object') {
return value.id;
}
return value;
};
// Override getFieldProps for conditions fields.
const fieldProps = (name, index) => {
const override = {
...getFieldProps(`conditions[${index}].${name}`),
};
return {
...override,
onChange: (e) => {
if (name === 'fieldKey') {
const currentField = fieldsKeyMapped.get(
values.conditions[index].fieldKey,
);
const nextField = fieldsKeyMapped.get(e.currentTarget.value);
if (currentField.field_type !== nextField.field_type) {
setFieldValue(`conditions[${index}].value`, '');
}
const comparatorsObs = getConditionTypeCompatators(
nextField.field_type,
);
const currentCompatator = values.conditions[index].comparator;
if (
!currentCompatator ||
comparatorsObs.map((c) => c.value).indexOf(currentCompatator) === -1
) {
const defaultCompatator = getConditionDefaultCompatator(
nextField.field_type,
);
setFieldValue(
`conditions[${index}].comparator`,
defaultCompatator.value,
);
}
}
override.onChange(e);
},
};
};
// Compatator field props.
const comparatorFieldProps = (name, index) => {
const condition = values.conditions[index];
const field = fieldsKeyMapped.get(condition.fieldKey);
return {
...fieldProps(name, index),
dataType: field.field_type,
};
};
// Value field props.
const valueFieldProps = (name, index) => {
const condition = values.conditions[index];
const field = fieldsKeyMapped.get(condition.fieldKey);
return {
...fieldProps(name, index),
fieldName: field.label,
fieldType: field.field_type,
options: field.options,
optionsResource: field.options_resource,
onChange: (value) => {
const transformedValue = transformValueField(value);
setFieldValue(`conditions[${index}].${name}`, transformedValue);
},
};
};
return (
<div class="filter-dropdown">
<div class="filter-dropdown__body">
{values.conditions.map((condition, index) => (
<div class="filter-dropdown__condition">
<FormGroup className={'form-group--condition'}>
<HTMLSelect
options={conditionalsOptions}
className={Classes.FILL}
disabled={index > 1}
{...fieldProps('condition', index)}
/>
</FormGroup>
<FormGroup className={'form-group--field'}>
<HTMLSelect
options={resourceFieldsOptions}
value={1}
className={Classes.FILL}
{...fieldProps('fieldKey', index)}
/>
</FormGroup>
<FormGroup className={'form-group--comparator'}>
<DynamicFilterCompatatorField
className={Classes.FILL}
{...comparatorFieldProps('comparator', index)}
/>
</FormGroup>
<DynamicFilterValueField
{...valueFieldProps('value', index)} />
<Button
icon={<Icon icon="times" iconSize={14} />}
minimal={true}
onClick={onClickRemoveCondition(index)}
/>
</div>
))}
</div>
<div class="filter-dropdown__footer">
<Button
minimal={true}
intent={Intent.PRIMARY}
onClick={onClickNewFilter}
>
<T id={'new_conditional'} />
</Button>
</div>
</div>
);
// const { formatMessage } = useIntl();
// const fieldsKeyMapped = new Map(fields.map((field) => [field.key, field]));
// const conditionalsItems = useMemo(
// () => [
// { value: 'and', label: formatMessage({ id: 'and' }) },
// { value: 'or', label: formatMessage({ id: 'or' }) },
// ],
// [formatMessage],
// );
// const resourceFields = useMemo(
// () => [
// ...fields.map((field) => ({
// value: field.key,
// label: field.label_name,
// })),
// ],
// [fields],
// );
// const defaultFilterCondition = useMemo(
// () => ({
// condition: 'and',
// field_key: initialCondition.fieldKey,
// comparator: initialCondition.comparator,
// value: initialCondition.value,
// }),
// [fields],
// );
// const { setFieldValue, getFieldProps, values } = useFormik({
// enableReinitialize: true,
// initialValues: {
// conditions: [defaultFilterCondition],
// },
// });
// const onClickNewFilter = useCallback(() => {
// if (values.conditions.length >= 12) {
// limitToast = Toaster.show(
// {
// message: formatMessage({ id: 'you_reached_conditions_limit' }),
// intent: Intent.WARNING,
// },
// limitToast,
// );
// } else {
// setFieldValue('conditions', [
// ...values.conditions,
// last(values.conditions),
// ]);
// }
// }, [values, defaultFilterCondition, setFieldValue]);
// const filteredFilterConditions = useMemo(() => {
// 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);
// const onFilterChangeThrottled = useRef(
// debounce((conditions) => {
// onFilterChange && onFilterChange(conditions);
// }, refetchDebounceWait),
// );
// useEffect(() => {
// if (!isEqual(prevConditions, filteredFilterConditions) && prevConditions) {
// onFilterChange && onFilterChange(filteredFilterConditions);
// }
// }, [filteredFilterConditions, prevConditions]);
// // 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]);
// };
// // Transform dynamic value field.
// const transformValueField = (value) => {
// if (value instanceof Date) {
// return moment(value).format('YYYY-MM-DD');
// } else if (typeof value === 'object') {
// return value.id;
// }
// return value;
// };
// // Override getFieldProps for conditions fields.
// const fieldProps = (name, index) => {
// const override = {
// ...getFieldProps(`conditions[${index}].${name}`),
// };
// return {
// ...override,
// onChange: (e) => {
// if (name === 'field_key') {
// const currentField = fieldsKeyMapped.get(
// values.conditions[index].field_key,
// );
// const nextField = fieldsKeyMapped.get(e.currentTarget.value);
// 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);
// },
// };
// };
// // Compatator field props.
// const comparatorFieldProps = (name, index) => {
// const condition = values.conditions[index];
// const field = fieldsKeyMapped.get(condition.field_key);
// 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">
// <div class="filter-dropdown__body">
// {values.conditions.map((condition, index) => (
// <div class="filter-dropdown__condition">
// <FormGroup className={'form-group--condition'}>
// <HTMLSelect
// options={conditionalsItems}
// className={Classes.FILL}
// disabled={index > 1}
// {...fieldProps('condition', index)}
// />
// </FormGroup>
// <FormGroup className={'form-group--field'}>
// <HTMLSelect
// options={resourceFields}
// value={1}
// className={Classes.FILL}
// {...fieldProps('field_key', index)}
// />
// </FormGroup>
// <FormGroup className={'form-group--comparator'}>
// <DynamicFilterCompatatorField
// className={Classes.FILL}
// {...comparatorFieldProps('comparator', index)}
// />
// </FormGroup>
// <DynamicFilterValueField {...valueFieldProps('value', index)} />
// <Button
// icon={<Icon icon="times" iconSize={14} />}
// minimal={true}
// onClick={onClickRemoveCondition(index)}
// />
// </div>
// ))}
// </div>
// <div class="filter-dropdown__footer">
// <Button
// minimal={true}
// intent={Intent.PRIMARY}
// onClick={onClickNewFilter}
// >
// <T id={'new_conditional'} />
// </Button>
// </div>
// </div>
// );
}

View File

@@ -0,0 +1,9 @@
import React from 'react';
export default function InputPrepend({ children }) {
return (
<div class="input-prepend">
{ children }
</div>
);
}

View File

@@ -5,7 +5,7 @@ const Bar = ({ progress, animationDuration }) => (
<div
style={{
background: '#79b8ff',
height: 3,
height: 4,
left: 0,
marginLeft: `${(-1 + progress) * 100}%`,
position: 'fixed',

View File

@@ -15,13 +15,15 @@ import Pagination from './Pagination';
import DashboardViewsTabs from './Dashboard/DashboardViewsTabs';
import CurrenciesSelectList from './CurrenciesSelectList';
import FieldRequiredHint from './FieldRequiredHint';
import Dialog from './Dialog';
import AppToaster from './AppToaster';
import DataTable from './DataTable';
import AccountsSelectList from './AccountsSelectList';
import AccountsTypesSelect from './AccountsTypesSelect';
import LoadingIndicator from './LoadingIndicator';
import DashboardActionViewsList from './Dashboard/DashboardActionViewsList';
import Dialog from './Dialog/Dialog';
import DialogContent from './Dialog/DialogContent';
import DialogSuspense from './Dialog/DialogSuspense';
const Hint = FieldHint;
export {
@@ -43,11 +45,13 @@ export {
DashboardViewsTabs,
CurrenciesSelectList,
FieldRequiredHint,
Dialog,
AppToaster,
DataTable,
AccountsSelectList,
AccountsTypesSelect,
LoadingIndicator,
DashboardActionViewsList,
AppToaster,
Dialog,
DialogContent,
DialogSuspense
};

View File

@@ -35,6 +35,9 @@ function AccountsActionsBar({
// #withAccountsActions
addAccountsTableQueries,
// #withAccounts
accountsTableQuery,
selectedRows = [],
onFilterChanged,
onBulkDelete,
@@ -42,7 +45,9 @@ function AccountsActionsBar({
onBulkActivate,
onBulkInactive,
}) {
const [filterCount, setFilterCount] = useState(0);
const [filterCount, setFilterCount] = useState(
accountsTableQuery?.filter_roles?.length || 0,
);
const onClickNewAccount = () => {
openDialog('account-form', {});
@@ -54,9 +59,10 @@ function AccountsActionsBar({
const filterDropdown = FilterDropdown({
fields: resourceFields,
initialConditions: accountsTableQuery.filter_roles,
initialCondition: {
fieldKey: 'name',
compatator: 'contains',
comparator: 'contains',
value: '',
},
onFilterChange: (filterConditions) => {
@@ -171,8 +177,9 @@ const withAccountsActionsBar = connect(mapStateToProps);
export default compose(
withAccountsActionsBar,
withDialogActions,
withAccounts(({ accountsViews }) => ({
withAccounts(({ accountsViews, accountsTableQuery }) => ({
accountsViews,
accountsTableQuery,
})),
withResourceDetail(({ resourceFields }) => ({
resourceFields,

View File

@@ -51,11 +51,10 @@ function NormalCell({ cell }) {
function BalanceCell({ cell }) {
const account = cell.row.original;
const { balance = null } = account;
return balance ? (
return (account.amount) ? (
<span>
<Money amount={balance.amount} currency={balance.currency_code} />
<Money amount={account.amount} currency={'USD'} />
</span>
) : (
<span class="placeholder"></span>
@@ -245,7 +244,7 @@ function AccountsDataTable({
{
id: 'balance',
Header: formatMessage({ id: 'balance' }),
accessor: 'balance',
accessor: 'amount',
Cell: BalanceCell,
width: 150,
},
@@ -268,11 +267,7 @@ function AccountsDataTable({
);
const selectionColumn = useMemo(
() => ({
minWidth: 40,
width: 40,
maxWidth: 40,
}),
() => ({ minWidth: 45, width: 45, maxWidth: 45 }),
[],
);

View File

@@ -32,13 +32,13 @@ function CustomerForm({
// #withDashboardActions
changePageTitle,
//#withCustomers
// #withCustomers
customers,
//#withCustomerDetail
// #withCustomerDetail
customer,
//#withCustomersActions
// #withCustomersActions
requestSubmitCustomer,
requestFetchCustomers,
requestEditCustomer,
@@ -47,7 +47,7 @@ function CustomerForm({
requestSubmitMedia,
requestDeleteMedia,
//#Props
// #Props
onFormSubmit,
onCancelForm,
}) {

View File

@@ -6,6 +6,7 @@ import { RadioGroup, Radio } from '@blueprintjs/core';
export default function RadioCustomer(props) {
const { onChange, ...rest } = props;
const { formatMessage } = useIntl();
return (
<RadioGroup
inline={true}

View File

@@ -1,28 +0,0 @@
import { connect } from 'react-redux';
import { compose } from 'utils';
import withDialogActions from 'containers/Dialog/withDialogActions';
import withDialogRedux from 'components/DialogReduxConnect';
import withAccountsActions from 'containers/Accounts/withAccountsActions';
import withAccountDetail from 'containers/Accounts/withAccountDetail';
import withAccounts from 'containers/Accounts/withAccounts';
export const mapStateToProps = (state, props) => ({
dialogName: 'account-form',
accountId:
props.payload.action === 'edit' && props.payload.id
? props.payload.id
: null,
});
const AccountFormDialogConnect = connect(mapStateToProps);
export default compose(
withDialogRedux(null, 'account-form'),
AccountFormDialogConnect,
withAccountsActions,
withAccountDetail,
withAccounts(({ accountsTypes, accountsList }) => ({
accountsTypes,
accounts: accountsList,
})),
withDialogActions,
);

View File

@@ -1,405 +1,45 @@
import React, { useCallback, useMemo, useEffect } from 'react';
import {
Button,
Classes,
FormGroup,
InputGroup,
Intent,
TextArea,
Checkbox,
Position,
} from '@blueprintjs/core';
import { useFormik } from 'formik';
import { FormattedMessage as T, useIntl } from 'react-intl';
import { pick, omit } from 'lodash';
import { useQuery, queryCache } from 'react-query';
import classNames from 'classnames';
import Yup from 'services/yup';
import {
If,
ErrorMessage,
Dialog,
AppToaster,
FieldRequiredHint,
Hint,
AccountsSelectList,
AccountsTypesSelect,
} from 'components';
import AccountFormDialogContainer from 'containers/Dialogs/AccountFormDialog.container';
import React, { lazy } from 'react';
import { FormattedMessage as T } from 'react-intl';
import { Dialog, DialogSuspense } from 'components';
import withDialogRedux from 'components/DialogReduxConnect';
import { compose } from 'utils';
const AccountFormDialogContent = lazy(() => import('./AccountFormDialogContent'));
/**
* Account form dialog.
*/
function AccountFormDialog({
dialogName,
payload = { action: 'new', id: null },
payload = { action: '', id: null },
isOpen,
// #withAccounts
accountsTypes,
accounts,
// #withAccountDetail
account,
// #withAccountsActions
requestFetchAccounts,
requestFetchAccountTypes,
requestFetchAccount,
requestSubmitAccount,
requestEditAccount,
// #withDialog
closeDialog,
}) {
const { formatMessage } = useIntl();
const validationSchema = Yup.object().shape({
name: Yup.string()
.required()
.min(3)
.max(255)
.label(formatMessage({ id: 'account_name_' })),
code: Yup.string().digits().min(3).max(6),
account_type_id: Yup.number()
.required()
.label(formatMessage({ id: 'account_type_id' })),
description: Yup.string().min(3).max(512).nullable().trim(),
parent_account_id: Yup.number().nullable(),
});
const initialValues = useMemo(
() => ({
account_type_id: null,
name: '',
code: '',
description: '',
}),
[],
);
const transformApiErrors = (errors) => {
const fields = {};
if (errors.find((e) => e.type === 'NOT_UNIQUE_CODE')) {
fields.code = formatMessage({ id: 'account_code_is_not_unique' });
}
return fields;
};
// Formik
const {
errors,
values,
touched,
setFieldValue,
resetForm,
handleSubmit,
isSubmitting,
getFieldProps,
} = useFormik({
enableReinitialize: true,
initialValues: {
...initialValues,
...(payload.action === 'edit' &&
pick(account, Object.keys(initialValues))),
},
validationSchema,
onSubmit: (values, { setSubmitting, setErrors }) => {
const form = omit(values, ['subaccount']);
const toastAccountName = values.code
? `${values.code} - ${values.name}`
: values.name;
const afterSubmit = () => {
closeDialog(dialogName);
queryCache.invalidateQueries('accounts-table');
queryCache.invalidateQueries('accounts-list');
};
const afterErrors = (errors) => {
const errorsTransformed = transformApiErrors(errors);
setErrors({ ...errorsTransformed });
setSubmitting(false);
};
if (payload.action === 'edit') {
requestEditAccount(payload.id, form)
.then((response) => {
afterSubmit(response);
AppToaster.show({
message: formatMessage(
{ id: 'service_has_been_successful_edited' },
{
name: toastAccountName,
service: formatMessage({ id: 'account' }),
},
),
intent: Intent.SUCCESS,
});
})
.catch(afterErrors);
} else {
requestSubmitAccount({ form })
.then((response) => {
afterSubmit(response);
AppToaster.show({
message: formatMessage(
{ id: 'service_has_been_successful_created' },
{
name: toastAccountName,
service: formatMessage({ id: 'account' }),
},
),
intent: Intent.SUCCESS,
position: Position.BOTTOM,
});
})
.catch(afterErrors);
}
},
});
useEffect(() => {
if (values.parent_account_id) {
setFieldValue('subaccount', true);
}
}, [values.parent_account_id]);
// Reset `parent account id` after change `account type`.
useEffect(() => {
setFieldValue('parent_account_id', null);
}, [values.account_type_id]);
// Filtered accounts based on the given account type.
const filteredAccounts = useMemo(
() =>
accounts.filter(
(account) =>
account.account_type_id === values.account_type_id ||
!values.account_type_id,
),
[accounts, values.account_type_id],
);
// Handles dialog close.
const handleClose = useCallback(() => {
closeDialog(dialogName);
}, [closeDialog, dialogName]);
// Fetches accounts list.
const fetchAccountsList = useQuery(
'accounts-list',
() => requestFetchAccounts(),
{ enabled: false },
);
// Fetches accounts types.
const fetchAccountsTypes = useQuery(
'accounts-types-list',
async () => {
await requestFetchAccountTypes();
},
{ enabled: false },
);
// Fetch the given account id on edit mode.
const fetchAccount = useQuery(
['account', payload.id],
(key, _id) => requestFetchAccount(_id),
{ enabled: false },
);
const isFetching =
fetchAccountsList.isFetching ||
fetchAccountsTypes.isFetching ||
fetchAccount.isFetching;
// Fetch requests on dialog opening.
const onDialogOpening = useCallback(() => {
fetchAccountsList.refetch();
fetchAccountsTypes.refetch();
if (payload.action === 'edit' && payload.id) {
fetchAccount.refetch();
}
if (payload.action === 'new_child') {
setFieldValue('parent_account_id', payload.parentAccountId);
setFieldValue('account_type_id', payload.accountTypeId);
}
}, [payload, fetchAccount, fetchAccountsList, fetchAccountsTypes]);
// Handle account type change.
const onChangeAccountType = useCallback(
(accountType) => {
setFieldValue('account_type_id', accountType.id);
},
[setFieldValue],
);
// Handles change sub-account.
const onChangeSubaccount = useCallback(
(account) => {
setFieldValue('parent_account_id', account.id);
},
[setFieldValue],
);
// Handle dialog on closed.
const onDialogClosed = useCallback(() => {
resetForm();
}, [resetForm]);
return (
<Dialog
name={dialogName}
title={
payload.action === 'edit' ? (
<T id={'edit_account'} />
) : (
<T id={'new_account'} />
)
(payload.action === 'edit') ?
(<T id={'edit_account'} />) :
(<T id={'new_account'} />)
}
className={{
'dialog--loading': isFetching,
'dialog--account-form': true,
}}
className={'dialog--account-form'}
autoFocus={true}
canEscapeKeyClose={true}
onClosed={onDialogClosed}
onOpening={onDialogOpening}
isOpen={isOpen}
isLoading={isFetching}
onClose={handleClose}
>
<form onSubmit={handleSubmit}>
<div className={Classes.DIALOG_BODY}>
<FormGroup
label={<T id={'account_type'} />}
labelInfo={<FieldRequiredHint />}
className={classNames(
'form-group--account-type',
'form-group--select-list',
Classes.FILL,
)}
inline={true}
helperText={
<ErrorMessage name="account_type_id" {...{ errors, touched }} />
}
intent={
errors.account_type_id && touched.account_type_id && Intent.DANGER
}
>
<AccountsTypesSelect
accountsTypes={accountsTypes}
selectedTypeId={values.account_type_id}
defaultSelectText={<T id={'select_account_type'} />}
onTypeSelected={onChangeAccountType}
buttonProps={{ disabled: payload.action === 'edit' }}
popoverProps={{ minimal: true }}
/>
</FormGroup>
<FormGroup
label={<T id={'account_name'} />}
labelInfo={<FieldRequiredHint />}
className={'form-group--account-name'}
intent={errors.name && touched.name && Intent.DANGER}
helperText={<ErrorMessage name="name" {...{ errors, touched }} />}
inline={true}
>
<InputGroup
medium={true}
intent={errors.name && touched.name && Intent.DANGER}
{...getFieldProps('name')}
/>
</FormGroup>
<FormGroup
label={<T id={'account_code'} />}
className={'form-group--account-code'}
intent={errors.code && touched.code && Intent.DANGER}
helperText={<ErrorMessage name="code" {...{ errors, touched }} />}
inline={true}
labelInfo={<Hint content={<T id="account_code_hint" />} />}
>
<InputGroup
medium={true}
intent={errors.code && touched.code && Intent.DANGER}
{...getFieldProps('code')}
/>
</FormGroup>
<FormGroup
label={' '}
className={classNames('form-group--subaccount')}
inline={true}
>
<Checkbox
inline={true}
label={
<>
<T id={'sub_account'} />
<Hint />
</>
}
{...getFieldProps('subaccount')}
checked={values.subaccount}
/>
</FormGroup>
<If condition={values.subaccount}>
<FormGroup
label={<T id={'parent_account'} />}
className={classNames(
'form-group--parent-account',
'form-group--select-list',
Classes.FILL,
)}
inline={true}
>
<AccountsSelectList
accounts={filteredAccounts}
onAccountSelected={onChangeSubaccount}
defaultSelectText={<T id={'select_parent_account'} />}
selectedAccountId={values.parent_account_id}
/>
</FormGroup>
</If>
<FormGroup
label={<T id={'description'} />}
className={'form-group--description'}
intent={errors.description && Intent.DANGER}
helperText={errors.description && errors.credential}
inline={true}
>
<TextArea
growVertically={true}
height={280}
{...getFieldProps('description')}
/>
</FormGroup>
</div>
<div className={Classes.DIALOG_FOOTER}>
<div className={Classes.DIALOG_FOOTER_ACTIONS}>
<Button onClick={handleClose} style={{ minWidth: '75px' }}>
<T id={'close'} />
</Button>
<Button
intent={Intent.PRIMARY}
disabled={isSubmitting}
style={{ minWidth: '75px' }}
type="submit"
>
{payload.action === 'edit' ? (
<T id={'edit'} />
) : (
<T id={'submit'} />
)}
</Button>
</div>
</div>
</form>
<DialogSuspense>
<AccountFormDialogContent
dialogName={dialogName}
accountId={payload.id}
action={payload.action}
parentAccountId={payload.parentAccountId}
accountTypeId={payload.accountTypeId}
/>
</DialogSuspense>
</Dialog>
);
}
export default AccountFormDialogContainer(AccountFormDialog);
export default compose(
withDialogRedux(),
)(AccountFormDialog);

View File

@@ -0,0 +1,400 @@
import React, { useCallback, useMemo, useEffect } from 'react';
import {
Button,
Classes,
FormGroup,
InputGroup,
Intent,
TextArea,
Checkbox,
Position,
} from '@blueprintjs/core';
import { useFormik } from 'formik';
import { FormattedMessage as T, useIntl } from 'react-intl';
import { pick, omit } from 'lodash';
import { useQuery, queryCache } from 'react-query';
import classNames from 'classnames';
import Yup from 'services/yup';
import {
If,
ErrorMessage,
AppToaster,
FieldRequiredHint,
Hint,
AccountsSelectList,
AccountsTypesSelect,
DialogContent,
} from 'components';
import withAccountsActions from 'containers/Accounts/withAccountsActions';
import withAccountDetail from 'containers/Accounts/withAccountDetail';
import withAccounts from 'containers/Accounts/withAccounts';
import withDialogActions from 'containers/Dialog/withDialogActions';
import { compose } from 'utils';
/**
* Account form dialog content.
*/
function AccountFormDialogContent({
// #withAccounts
accountsTypes,
accounts,
// #withAccountDetail
account,
// #withAccountsActions
requestFetchAccounts,
requestFetchAccountTypes,
requestFetchAccount,
requestSubmitAccount,
requestEditAccount,
closeDialog,
// #ownProp
accountId,
action,
dialogName,
parentAccountId,
accountTypeId,
}) {
const { formatMessage } = useIntl();
const validationSchema = Yup.object().shape({
name: Yup.string()
.required()
.min(3)
.max(255)
.label(formatMessage({ id: 'account_name_' })),
code: Yup.string().digits().min(3).max(6),
account_type_id: Yup.number()
.required()
.label(formatMessage({ id: 'account_type_id' })),
description: Yup.string().min(3).max(512).nullable().trim(),
parent_account_id: Yup.number().nullable(),
});
const initialValues = useMemo(
() => ({
account_type_id: null,
name: '',
code: '',
description: '',
}),
[],
);
const transformApiErrors = (errors) => {
const fields = {};
if (errors.find((e) => e.type === 'NOT_UNIQUE_CODE')) {
fields.code = formatMessage({ id: 'account_code_is_not_unique' });
}
return fields;
};
// Formik
const {
errors,
values,
touched,
setFieldValue,
resetForm,
handleSubmit,
isSubmitting,
getFieldProps,
} = useFormik({
enableReinitialize: true,
initialValues: {
...initialValues,
...(accountId && pick(account, Object.keys(initialValues))),
},
validationSchema,
onSubmit: (values, { setSubmitting, setErrors }) => {
const form = omit(values, ['subaccount']);
const toastAccountName = values.code
? `${values.code} - ${values.name}`
: values.name;
const afterSubmit = () => {
closeDialog(dialogName);
queryCache.invalidateQueries('accounts-table');
queryCache.invalidateQueries('accounts-list');
};
const afterErrors = (errors) => {
const errorsTransformed = transformApiErrors(errors);
setErrors({ ...errorsTransformed });
setSubmitting(false);
};
if (accountId) {
requestEditAccount(accountId, form)
.then((response) => {
afterSubmit(response);
AppToaster.show({
message: formatMessage(
{ id: 'service_has_been_successful_edited' },
{
name: toastAccountName,
service: formatMessage({ id: 'account' }),
},
),
intent: Intent.SUCCESS,
});
})
.catch(afterErrors);
} else {
requestSubmitAccount({ form })
.then((response) => {
afterSubmit(response);
AppToaster.show({
message: formatMessage(
{ id: 'service_has_been_successful_created' },
{
name: toastAccountName,
service: formatMessage({ id: 'account' }),
},
),
intent: Intent.SUCCESS,
position: Position.BOTTOM,
});
})
.catch(afterErrors);
}
},
});
useEffect(() => {
if (values.parent_account_id) {
setFieldValue('subaccount', true);
}
}, [values.parent_account_id]);
// Reset `parent account id` after change `account type`.
useEffect(() => {
setFieldValue('parent_account_id', null);
}, [values.account_type_id]);
// Filtered accounts based on the given account type.
const filteredAccounts = useMemo(
() =>
accounts.filter(
(account) =>
account.account_type_id === values.account_type_id ||
!values.account_type_id,
),
[accounts, values.account_type_id],
);
// Handles dialog close.
const handleClose = useCallback(() => {
closeDialog(dialogName);
}, [closeDialog, dialogName]);
// Fetches accounts list.
const fetchAccountsList = useQuery(
'accounts-list',
() => requestFetchAccounts(),
);
// Fetches accounts types.
const fetchAccountsTypes = useQuery(
'accounts-types-list',
async () => {
await requestFetchAccountTypes();
},
);
// Fetch the given account id on edit mode.
const fetchAccount = useQuery(
['account', accountId],
(key, _id) => requestFetchAccount(_id),
{ enabled: accountId },
);
const isFetching =
fetchAccountsList.isFetching ||
fetchAccountsTypes.isFetching ||
fetchAccount.isFetching;
// Fetch requests on dialog opening.
const onDialogOpening = useCallback(() => {
fetchAccountsList.refetch();
fetchAccountsTypes.refetch();
if (action === 'edit' && accountId) {
fetchAccount.refetch();
}
if (action === 'new_child') {
setFieldValue('parent_account_id', parentAccountId);
setFieldValue('account_type_id', accountTypeId);
}
}, [
parentAccountId,
accountTypeId,
fetchAccount,
fetchAccountsList,
fetchAccountsTypes,
]);
// Handle account type change.
const onChangeAccountType = useCallback(
(accountType) => {
setFieldValue('account_type_id', accountType.id);
},
[setFieldValue],
);
// Handles change sub-account.
const onChangeSubaccount = useCallback(
(account) => {
setFieldValue('parent_account_id', account.id);
},
[setFieldValue],
);
// Handle dialog on closed.
const onDialogClosed = useCallback(() => {
resetForm();
}, [resetForm]);
return (
<DialogContent isLoading={isFetching}>
<form onSubmit={handleSubmit}>
<div className={Classes.DIALOG_BODY}>
<FormGroup
label={<T id={'account_type'} />}
labelInfo={<FieldRequiredHint />}
className={classNames(
'form-group--account-type',
'form-group--select-list',
Classes.FILL,
)}
inline={true}
helperText={
<ErrorMessage name="account_type_id" {...{ errors, touched }} />
}
intent={
errors.account_type_id && touched.account_type_id && Intent.DANGER
}
>
<AccountsTypesSelect
accountsTypes={accountsTypes}
selectedTypeId={values.account_type_id}
defaultSelectText={<T id={'select_account_type'} />}
onTypeSelected={onChangeAccountType}
buttonProps={{ disabled: action === 'edit' }}
popoverProps={{ minimal: true }}
/>
</FormGroup>
<FormGroup
label={<T id={'account_name'} />}
labelInfo={<FieldRequiredHint />}
className={'form-group--account-name'}
intent={errors.name && touched.name && Intent.DANGER}
helperText={<ErrorMessage name="name" {...{ errors, touched }} />}
inline={true}
>
<InputGroup
medium={true}
intent={errors.name && touched.name && Intent.DANGER}
{...getFieldProps('name')}
/>
</FormGroup>
<FormGroup
label={<T id={'account_code'} />}
className={'form-group--account-code'}
intent={errors.code && touched.code && Intent.DANGER}
helperText={<ErrorMessage name="code" {...{ errors, touched }} />}
inline={true}
labelInfo={<Hint content={<T id="account_code_hint" />} />}
>
<InputGroup
medium={true}
intent={errors.code && touched.code && Intent.DANGER}
{...getFieldProps('code')}
/>
</FormGroup>
<FormGroup
label={' '}
className={classNames('form-group--subaccount')}
inline={true}
>
<Checkbox
inline={true}
label={
<>
<T id={'sub_account'} />
<Hint />
</>
}
{...getFieldProps('subaccount')}
checked={values.subaccount}
/>
</FormGroup>
<If condition={values.subaccount}>
<FormGroup
label={<T id={'parent_account'} />}
className={classNames(
'form-group--parent-account',
'form-group--select-list',
Classes.FILL,
)}
inline={true}
>
<AccountsSelectList
accounts={filteredAccounts}
onAccountSelected={onChangeSubaccount}
defaultSelectText={<T id={'select_parent_account'} />}
selectedAccountId={values.parent_account_id}
/>
</FormGroup>
</If>
<FormGroup
label={<T id={'description'} />}
className={'form-group--description'}
intent={errors.description && Intent.DANGER}
helperText={errors.description && errors.credential}
inline={true}
>
<TextArea
growVertically={true}
height={280}
{...getFieldProps('description')}
/>
</FormGroup>
</div>
<div className={Classes.DIALOG_FOOTER}>
<div className={Classes.DIALOG_FOOTER_ACTIONS}>
<Button onClick={handleClose} style={{ minWidth: '75px' }}>
<T id={'close'} />
</Button>
<Button
intent={Intent.PRIMARY}
disabled={isSubmitting}
style={{ minWidth: '75px' }}
type="submit"
>
{action === 'edit' ? <T id={'edit'} /> : <T id={'submit'} />}
</Button>
</div>
</div>
</form>
</DialogContent>
);
}
export default compose(
withAccountsActions,
withAccountDetail,
withAccounts(({ accountsTypes, accountsList }) => ({
accountsTypes,
accounts: accountsList,
})),
withDialogActions,
)(AccountFormDialogContent);

View File

@@ -1,20 +1,26 @@
import React, { useEffect } from 'react';
import { connect } from 'react-redux';
import t from 'store/types';
import DashboardInsider from 'components/Dashboard/DashboardInsider';
import withDashboardActions from 'containers/Dashboard/withDashboardActions';
import withSettings from 'containers/Settings/withSettings';
import { compose } from 'utils';
function DashboardHomepage({ changePageTitle, name }) {
const DashboardHomepage = ({ changePageTitle }) => {
useEffect(() => {
changePageTitle('Craigs Design and Landscaping Services')
});
changePageTitle(name)
}, [name, changePageTitle]);
return (
<div>asdasd</div>
<DashboardInsider name="homepage">
</DashboardInsider>
);
}
const mapActionsToProps = (dispatch) => ({
changePageTitle: pageTitle => dispatch({
type: t.CHANGE_DASHBOARD_PAGE_TITLE, pageTitle
}),
});
export default connect(null, mapActionsToProps)(DashboardHomepage);
export default compose(
withDashboardActions,
withSettings(({ organizationSettings }) => ({
name: organizationSettings.name,
})),
)(DashboardHomepage);

View File

@@ -1,18 +1,20 @@
import {connect} from 'react-redux';
import {
getResourceColumns,
getResourceFieldsFactory,
getResourceMetadata,
getResourceData,
} from 'store/resources/resources.reducer';
getResourceFieldsFactory,
getResourceDataFactory,
} from 'store/resources/resources.selectors';
export default (mapState) => {
const getResourceFields = getResourceFieldsFactory();
const getResourceData = getResourceDataFactory();
const mapStateToProps = (state, props) => {
const { resourceName } = props;
const mapped = {
resourceData: getResourceData(state, resourceName),
resourceData: getResourceData(state, props),
resourceFields: getResourceFields(state, props),
resourceColumns: getResourceColumns(state, resourceName),
resourceMetadata: getResourceMetadata(state, resourceName),

View File

@@ -1,8 +1,13 @@
import { connect } from 'react-redux';
export const mapStateToProps = (state, props) => ({
organizationSettings: state.settings.data.organization,
manualJournalsSettings: state.settings.data.manual_journals,
});
export default (mapState) => {
const mapStateToProps = (state, props) => {
const mapped = {
organizationSettings: state.settings.data.organization,
manualJournalsSettings: state.settings.data.manual_journals,
};
return mapState ? mapState(mapped, state, props) : mapped;
};
export default connect(mapStateToProps);
return connect(mapStateToProps);
}

View File

@@ -765,5 +765,9 @@ export default {
something_wentwrong: 'Something went wrong.',
new_password: 'New password',
license_code_: 'License code',
legal_organization_name: 'Legal Organization Name'
legal_organization_name: 'Legal Organization Name',
smaller_than: 'Smaller than',
smaller_or_equals: 'Smaller or equals',
bigger_than: 'Bigger than',
bigger_or_equals: 'Bigger or equals',
};

View File

@@ -4,9 +4,6 @@ import t from 'store/types';
export const submitEstimate = ({ form }) => {
return (dispatch) =>
new Promise((resolve, reject) => {
dispatch({
type: t.SET_DASHBOARD_REQUEST_LOADING,
});
ApiService.post('sales/estimates', form)
.then((response) => {
dispatch({

View File

@@ -37,9 +37,6 @@ export const deleteInvoice = ({ id }) => {
export const editInvoice = (id, form) => {
return (dispatch) =>
new Promise((resolve, reject) => {
dispatch({
type: t.SET_DASHBOARD_REQUEST_LOADING,
});
ApiService.post(`sales/invoices/${id}`, form)
.then((response) => {
resolve(response);

View File

@@ -60,9 +60,6 @@ export const fetchAccountsTable = ({ query } = {}) => {
type: t.ACCOUNTS_TABLE_LOADING,
loading: true,
});
dispatch({
type: t.SET_DASHBOARD_REQUEST_LOADING,
});
ApiService.get('accounts', { params: { ...pageQuery, ...query } })
.then((response) => {
dispatch({
@@ -105,9 +102,6 @@ export const fetchAccountsDataTable = ({ query }) => {
export const submitAccount = ({ form }) => {
return (dispatch) =>
new Promise((resolve, reject) => {
dispatch({
type: t.SET_DASHBOARD_REQUEST_LOADING,
});
ApiService.post('accounts', form)
.then((response) => {
dispatch({
@@ -136,9 +130,6 @@ export const submitAccount = ({ form }) => {
export const editAccount = (id, form) => {
return (dispatch) =>
new Promise((resolve, reject) => {
dispatch({
type: t.SET_DASHBOARD_REQUEST_LOADING,
});
ApiService.post(`accounts/${id}`, form)
.then((response) => {
dispatch({ type: t.CLEAR_ACCOUNT_FORM_ERRORS });

View File

@@ -10,17 +10,12 @@ export const submitCustomer = ({ form }) => {
ApiService.post('customers', form)
.then((response) => {
dispatch({
type: t.SET_DASHBOARD_REQUEST_COMPLETED,
});
resolve(response);
})
.catch((error) => {
const { response } = error;
const { data } = response;
dispatch({
type: t.SET_DASHBOARD_REQUEST_COMPLETED,
});
reject(data?.errors);
});
});
@@ -35,17 +30,12 @@ export const editCustomer = ({ form, id }) => {
ApiService.post(`customers/${id}`, form)
.then((response) => {
dispatch({
type: t.SET_DASHBOARD_REQUEST_COMPLETED,
});
resolve(response);
})
.catch((error) => {
const { response } = error;
const { data } = response;
dispatch({
type: t.SET_DASHBOARD_REQUEST_COMPLETED,
});
reject(data?.errors);
});
});
@@ -59,9 +49,6 @@ export const fetchCustomers = ({ query }) => {
type: t.ITEMS_TABLE_LOADING,
payload: { loading: true },
});
dispatch({
type: t.SET_DASHBOARD_REQUEST_LOADING,
});
ApiService.get(`customers`, { params: { ...pageQuery, ...query } })
.then((response) => {
dispatch({
@@ -79,15 +66,9 @@ export const fetchCustomers = ({ query }) => {
type: t.CUSTOMERS_TABLE_LOADING,
payload: { loading: false },
});
dispatch({
type: t.SET_DASHBOARD_REQUEST_COMPLETED,
});
resolve(response);
})
.catch((error) => {
dispatch({
type: t.SET_DASHBOARD_REQUEST_COMPLETED,
});
reject(error);
});
});

View File

@@ -3,7 +3,7 @@ import { createReducer } from '@reduxjs/toolkit';
const initialState = {
pageTitle: '',
pageSubtitle: 'Hello World',
pageSubtitle: '',
preferencesPageTitle: '',
sidebarExpended: true,
dialogs: {},

View File

@@ -1,16 +1,16 @@
import { createSelector } from "@reduxjs/toolkit";
const dialogByNameSelector = (dialogName) => (state) => state.dashboard.dialogs?.[dialogName];
const dialogByNameSelector = (state, props) => state.dashboard.dialogs?.[props.dialogName];
export const isDialogOpenFactory = (dialogName) => createSelector(
dialogByNameSelector(dialogName),
export const isDialogOpenFactory = () => createSelector(
dialogByNameSelector,
(dialog) => {
return dialog && dialog.isOpen;
},
);
export const getDialogPayloadFactory = (dialogName) => createSelector(
dialogByNameSelector(dialogName),
export const getDialogPayloadFactory = () => createSelector(
dialogByNameSelector,
(dialog) => {
return { ...dialog?.payload };
},

View File

@@ -18,9 +18,6 @@ export const fetchItems = ({ query }) => {
type: t.ITEMS_TABLE_LOADING,
payload: { loading: true },
});
dispatch({
type: t.SET_DASHBOARD_REQUEST_LOADING,
});
ApiService.get(`items`, { params: { ...pageQuery, ...query } })
.then((response) => {
dispatch({

View File

@@ -4,9 +4,6 @@ import t from 'store/types';
export const submitReceipt = ({ form }) => {
return (dispatch) =>
new Promise((resolve, reject) => {
dispatch({
type: t.SET_DASHBOARD_REQUEST_LOADING,
});
ApiService.post('sales/receipts', form)
.then((response) => {
dispatch({

View File

@@ -20,14 +20,11 @@ export const fetchResourceColumns = ({ resourceSlug }) => {
export const fetchResourceFields = ({ resourceSlug }) => {
return (dispatch) => new Promise((resolve, reject) => {
ApiService.get(`resources/${resourceSlug}/fields`).then((response) => {
// dispatch({
// type: t.RESOURCE_FIELDS_SET,
// fields: response.data.resource_fields,
// resource_slug: resourceSlug,
// });
// dispatch({
// type: t.SET_DASHBOARD_REQUEST_COMPLETED,
// });
dispatch({
type: t.RESOURCE_FIELDS_SET,
fields: response.data.resource_fields,
resource_slug: resourceSlug,
});
resolve(response);
}).catch((error) => { reject(error); });
});
@@ -36,13 +33,13 @@ export const fetchResourceFields = ({ resourceSlug }) => {
export const fetchResourceData = ({ resourceSlug }) => {
return (dispatch) => new Promise((resolve, reject) => {
ApiService.get(`/resources/${resourceSlug}/data`).then((response) => {
// dispatch({
// type: t.RESOURCE_DATA_SET,
// payload: {
// data: response.data.data,
// resource_key: resourceSlug,
// },
// });
dispatch({
type: t.RESOURCE_DATA_SET,
payload: {
data: response.data.resource_data,
resourceKey: resourceSlug,
},
});
resolve(response);
}).catch(error => { reject(error); });
});

View File

@@ -1,10 +1,10 @@
import { createReducer } from "@reduxjs/toolkit";
import { createSelector } from 'reselect';
import t from 'store/types';
import { pickItemsFromIds } from 'store/selectors'
const initialState = {
data: {},
data: {
resources: {},
},
fields: {},
columns: {},
resourceFields: {},
@@ -42,79 +42,30 @@ export default createReducer(initialState, {
[t.RESOURCE_FIELDS_SET]: (state, action) => {
const _fields = {};
action.fields.forEach((field) => {
_fields[field.id] = field;
_fields[field.key] = field;
});
state.fields = {
...state.fields,
..._fields,
};
state.resourceFields[action.resource_slug] = action.fields.map(f => f.id);
state.resourceFields[action.resource_slug] = action.fields.map(f => f.key);
},
[t.RESOURCE_DATA_SET]: (state, action) => {
const { data, resource_key: resourceKey } = action.payload;
const dataMapped = {};
const { data, resourceKey } = action.payload;
const _data = {};
data.forEach((item) => { dataMapped[item.id] = item; })
state.data[resourceKey] = dataMapped;
data.forEach((item) => {
_data[item.id] = item;
});
const order = data.map((item) => item.id);
state.data.resources[resourceKey] = {
...(state.data.resources[resourceKey] || {}),
data: _data,
order,
};
},
});
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 getResourceFieldsFactory = () => createSelector(
resourceFieldsIdsSelector,
resourceFieldsItemsSelector,
(fieldsIds, fieldsItems) => {
return pickItemsFromIds(fieldsItems, fieldsIds);
}
);
/**
* Retrieve resource columns of the given resource slug.
* @param {State} state
* @param {String} resourceSlug -
* @return {Array}
*/
export const getResourceColumns = (state, resourceSlug) => {
const resourceIds = state.resources.resourceColumns[resourceSlug];
const items = state.resources.columns;
return pickItemsFromIds(items, resourceIds);
};
/**
*
* @param {State} state
* @param {Number} fieldId
*/
export const getResourceField = (state, fieldId) => {
return state.resources.fields[fieldId];
};
/**
*
* @param {State} state
* @param {Number} columnId
*/
export const getResourceColumn = (state, columnId) => {
return state.resources.columns[columnId];
};
export const getResourceMetadata = (state, resourceSlug) => {
return state.resources.metadata[resourceSlug];
};
export const getResourceData = (state, resourceSlug) => {
return state.resources.data[resourceSlug] || {};
};
});

View File

@@ -0,0 +1,77 @@
import { createSelector } from 'reselect';
import { pickItemsFromIds } from 'store/selectors';
const resourceDataIdsSelector = (state, props) => {
return state.resources.data.resources[props.resourceName]?.order;
}
const resourceDataSelector = (state, props) => {
return state.resources.data.resources[props.resourceName]?.data;
}
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 getResourceFieldsFactory = () => createSelector(
resourceFieldsIdsSelector,
resourceFieldsItemsSelector,
(fieldsIds, fieldsItems) => {
return pickItemsFromIds(fieldsItems, fieldsIds);
}
);
/**
* Retrieve resource data of the given resource name in component properties.
* @return {Array}
*/
export const getResourceDataFactory = () => createSelector(
resourceDataSelector,
resourceDataIdsSelector,
(resourceData, resourceDataIds) => {
return pickItemsFromIds(resourceData, resourceDataIds);
}
);
/**
* Retrieve resource columns of the given resource slug.
* @param {State} state
* @param {String} resourceSlug -
* @return {Array}
*/
export const getResourceColumns = (state, resourceSlug) => {
const resourceIds = state.resources.resourceColumns[resourceSlug];
const items = state.resources.columns;
return pickItemsFromIds(items, resourceIds);
};
/**
*
* @param {State} state
* @param {Number} fieldId
*/
export const getResourceField = (state, fieldId) => {
return state.resources.fields[fieldId];
};
/**
*
* @param {State} state
* @param {Number} columnId
*/
export const getResourceColumn = (state, columnId) => {
return state.resources.columns[columnId];
};
export const getResourceMetadata = (state, resourceSlug) => {
return state.resources.metadata[resourceSlug];
};
export const getResourceData = (state, resourceSlug) => {
return state.resources.data[resourceSlug] || {};
};

View File

@@ -79,10 +79,6 @@ export const deleteVendor = ({ id }) => {
export const submitVendor = ({ form }) => {
return (dispatch) =>
new Promise((resolve, reject) => {
dispatch({
type: t.SET_DASHBOARD_REQUEST_LOADING,
});
ApiService.post('vendors', form)
.then((response) => {
dispatch({

View File

@@ -6,4 +6,9 @@
&-header{
background: #ebf1f5;
}
.bp3-spinner{
padding-top: 10px;
margin-bottom: -10px;
}
}

View File

@@ -58,6 +58,7 @@
"nodemailer": "^6.3.0",
"nodemon": "^1.19.1",
"objection": "^2.0.10",
"objection-filter": "^4.0.1",
"objection-soft-delete": "^1.0.7",
"pluralize": "^8.0.0",
"reflect-metadata": "^0.1.13",

View File

@@ -24,9 +24,18 @@ export default class ResourceController extends BaseController{
'/:resource_model/fields', [
...this.resourceModelParamSchema,
],
this.validationResult,
asyncMiddleware(this.resourceFields.bind(this)),
this.handleServiceErrors
);
router.get(
'/:resource_model/data', [
...this.resourceModelParamSchema,
],
this.validationResult,
asyncMiddleware(this.resourceData.bind(this)),
this.handleServiceErrors,
)
return router;
}
@@ -57,6 +66,28 @@ export default class ResourceController extends BaseController{
}
}
/**
* Retrieve resource data of the give resource based on the given query.
* @param {Request} req
* @param {Response} res
* @param {NextFunction} next
*/
async resourceData(req: Request, res: Response, next: NextFunction) {
const { tenantId } = req;
const { resource_model: resourceModel } = req.params;
const filter = req.query;
try {
const resourceData = await this.resourcesService.getResourceData(tenantId, resourceModel, filter);
return res.status(200).send({
resource_data: this.transfromToResponse(resourceData),
});
} catch (error) {
next(error);
}
}
/**
* Handles service errors.
* @param {Error} error
@@ -72,5 +103,6 @@ export default class ResourceController extends BaseController{
});
}
}
next(error);
}
};

View File

@@ -113,121 +113,6 @@ export default class SalesReceiptsController extends BaseController{
];
}
/**
* Validate whether sale receipt exists on the storage.
* @param {Request} req
* @param {Response} res
*/
async validateSaleReceiptExistance(req: Request, res: Response, next: Function) {
const { tenantId } = req;
const { id: saleReceiptId } = req.params;
const isSaleReceiptExists = await this.saleReceiptService
.isSaleReceiptExists(
tenantId,
saleReceiptId,
);
if (!isSaleReceiptExists) {
return res.status(404).send({
errors: [{ type: 'SALE.RECEIPT.NOT.FOUND', code: 200 }],
});
}
next();
}
/**
* Validate whether sale receipt customer exists on the storage.
* @param {Request} req
* @param {Response} res
* @param {Function} next
*/
async validateReceiptCustomerExistance(req: Request, res: Response, next: Function) {
const saleReceipt = { ...req.body };
const { Customer } = req.models;
const foundCustomer = await Customer.query().findById(saleReceipt.customer_id);
if (!foundCustomer) {
return res.status(400).send({
errors: [{ type: 'CUSTOMER.ID.NOT.EXISTS', code: 200 }],
});
}
next();
}
/**
* Validate whether sale receipt deposit account exists on the storage.
* @param {Request} req
* @param {Response} res
* @param {Function} next
*/
async validateReceiptDepositAccountExistance(req: Request, res: Response, next: Function) {
const { tenantId } = req;
const saleReceipt = { ...req.body };
const isDepositAccountExists = await this.accountsService.isAccountExists(
tenantId,
saleReceipt.deposit_account_id
);
if (!isDepositAccountExists) {
return res.status(400).send({
errors: [{ type: 'DEPOSIT.ACCOUNT.NOT.EXISTS', code: 300 }],
});
}
next();
}
/**
* Validate whether receipt items ids exist on the storage.
* @param {Request} req
* @param {Response} res
* @param {Function} next
*/
async validateReceiptItemsIdsExistance(req: Request, res: Response, next: Function) {
const { tenantId } = req;
const saleReceipt = { ...req.body };
const estimateItemsIds = saleReceipt.entries.map((e) => e.item_id);
const notFoundItemsIds = await this.itemsService.isItemsIdsExists(
tenantId,
estimateItemsIds
);
if (notFoundItemsIds.length > 0) {
return res.status(400).send({ errors: [{ type: 'ITEMS.IDS.NOT.EXISTS', code: 400 }] });
}
next();
}
/**
* Validate receipt entries ids existance on the storage.
* @param {Request} req
* @param {Response} res
* @param {Function} next
*/
async validateReceiptEntriesIds(req: Request, res: Response, next: Function) {
const { tenantId } = req;
const saleReceipt = { ...req.body };
const { id: saleReceiptId } = req.params;
// Validate the entries IDs that not stored or associated to the sale receipt.
const notExistsEntriesIds = await this.saleReceiptService
.isSaleReceiptEntriesIDsExists(
tenantId,
saleReceiptId,
saleReceipt,
);
if (notExistsEntriesIds.length > 0) {
return res.status(400).send({ errors: [{
type: 'ENTRIES.IDS.NOT.FOUND',
code: 500,
}]
});
}
next();
}
/**
* Creates a new receipt.
* @param {Request} req
@@ -244,7 +129,10 @@ export default class SalesReceiptsController extends BaseController{
tenantId,
saleReceiptDTO,
);
return res.status(200).send({ id: storedSaleReceipt.id });
return res.status(200).send({
id: storedSaleReceipt.id,
message: 'Sale receipt has been created successfully.',
});
} catch (error) {
next(error);
}
@@ -263,7 +151,10 @@ export default class SalesReceiptsController extends BaseController{
// Deletes the sale receipt.
await this.saleReceiptService.deleteSaleReceipt(tenantId, saleReceiptId);
return res.status(200).send({ id: saleReceiptId });
return res.status(200).send({
id: saleReceiptId,
message: 'Sale receipt has been deleted successfully.',
});
} catch (error) {
next(error);
}
@@ -287,7 +178,9 @@ export default class SalesReceiptsController extends BaseController{
saleReceiptId,
saleReceipt,
);
return res.status(200).send();
return res.status(200).send({
message: 'Sale receipt has been edited successfully.',
});
} catch (error) {
next(error);
}

View File

@@ -17,6 +17,10 @@ export default class FilterRoles extends DynamicFilterRoleAbstructor {
this.setResponseMeta();
}
/**
* Builds filter roles logic expression.
* @return {string}
*/
private buildLogicExpression(): string {
let expression = '';
this.filterRoles.forEach((role, index) => {

View File

@@ -42,12 +42,14 @@ const numberRoleQueryBuilder = (role: IFilterRole, comparatorColumn: string) =>
const textRoleQueryBuilder = (role: IFilterRole, comparatorColumn: string) => {
switch (role.comparator) {
case 'equals':
case 'is':
default:
return (builder) => {
builder.where(comparatorColumn, role.value);
};
case 'not_equal':
case 'not_equals':
case 'is_not':
return (builder) => {
builder.whereNot(comparatorColumn, role.value);
};

View File

@@ -41,6 +41,7 @@
"Journal": "Journal",
"Reconciliation": "Reconciliation",
"Credit": "Credit",
"Debit": "Debit",
"Interest": "Interest",
"Depreciation": "Depreciation",
"Payroll": "Payroll",
@@ -67,5 +68,10 @@
"Journal number": "Journal number",
"Status": "Status",
"Journal type": "Journal type",
"Date": "Date"
"Date": "Date",
"Asset": "Asset",
"Liability": "Liability",
"First-in first-out (FIFO)": "First-in first-out (FIFO)",
"Last-in first-out (LIFO)": "Last-in first-out (LIFO)",
"Average rate": "Average rate"
}

View File

@@ -123,50 +123,84 @@ export default class Account extends TenantModel {
name: {
label: 'Account name',
column: 'name',
columnType: 'string',
fieldType: 'text',
},
type: {
label: 'Account type',
column: 'account_type_id',
relation: 'account_types.id',
relationColumn: 'account_types.key',
fieldType: 'options',
optionsResource: 'AccountType',
optionsKey: 'key',
optionsLabel: 'label',
},
description: {
label: 'Description',
column: 'description',
columnType: 'string',
fieldType: 'text',
},
code: {
label: 'Account code',
column: 'code',
columnType: 'string',
fieldType: 'text',
},
root_type: {
label: 'Type',
label: 'Root type',
column: 'account_type_id',
relation: 'account_types.id',
relationColumn: 'account_types.root_type',
options: [
{ key: 'asset', label: 'Asset', },
{ key: 'liability', label: 'Liability' },
{ key: 'equity', label: 'Equity' },
{ key: 'Income', label: 'Income' },
{ key: 'expense', label: 'Expense' },
],
fieldType: 'options',
},
created_at: {
label: 'Created at',
column: 'created_at',
columnType: 'date',
fieldType: 'date',
},
active: {
label: 'Active',
column: 'active',
columnType: 'boolean',
fieldType: 'checkbox',
},
balance: {
label: 'Balance',
column: 'amount',
columnType: 'number'
columnType: 'number',
fieldType: 'number',
},
currency: {
label: 'Currency',
column: 'currency_code',
fieldType: 'options',
optionsResource: 'currency',
optionsKey: 'currency_code',
optionsLabel: 'currency_name',
},
normal: {
label: 'Account normal',
column: 'account_type_id',
fieldType: 'options',
relation: 'account_types.id',
relationColumn: 'account_types.normal'
relationColumn: 'account_types.normal',
options: [
{ key: 'credit', label: 'Credit' },
{ key: 'debit', label: 'Debit' },
],
},
};
}

View File

@@ -17,6 +17,13 @@ export default class AccountType extends TenantModel {
return ['label'];
}
/**
* Allows to mark model as resourceable to viewable and filterable.
*/
static get resourceable() {
return true;
}
/**
* Translatable lable.
*/

View File

@@ -14,4 +14,8 @@ export default class Currency extends TenantModel {
get timestamps() {
return ['createdAt', 'updatedAt'];
}
static get resourceable() {
return true;
}
}

View File

@@ -127,31 +127,38 @@ export default class Expense extends TenantModel {
payment_date: {
label: 'Payment date',
column: 'payment_date',
columnType: 'date',
},
payment_account: {
label: 'Payment account',
column: 'payment_account_id',
relation: 'accounts.id',
optionsResource: 'account',
},
amount: {
label: 'Amount',
column: 'total_amount',
columnType: 'number'
},
currency_code: {
label: 'Currency',
column: 'currency_code',
optionsResource: 'currency',
},
reference_no: {
label: 'Reference No.',
column: 'reference_no'
column: 'reference_no',
columnType: 'string',
},
description: {
label: 'Description',
column: 'description',
columnType: 'string',
},
published: {
label: 'Published',
column: 'published',
},
user: {
label: 'User',
@@ -162,6 +169,7 @@ export default class Expense extends TenantModel {
created_at: {
label: 'Created at',
column: 'created_at',
columnType: 'date',
},
};
}

View File

@@ -50,16 +50,19 @@ export default class ItemCategory extends TenantModel {
name: {
label: 'Name',
column: 'name',
columnType: 'string'
},
description: {
label: 'Description',
column: 'description',
columnType: 'string'
},
parent_category_id: {
label: 'Parent category',
column: 'parent_category_id',
relation: 'items_categories.id',
relationColumn: 'items_categories.id',
optionsResource: 'item_category',
},
user: {
label: 'User',
@@ -71,24 +74,34 @@ export default class ItemCategory extends TenantModel {
label: 'Cost account',
column: 'cost_account_id',
relation: 'accounts.id',
optionsResource: 'account'
},
sell_account: {
label: 'Sell account',
column: 'sell_account_id',
relation: 'accounts.id',
optionsResource: 'account'
},
inventory_account: {
label: 'Inventory account',
column: 'inventory_account_id',
relation: 'accounts.id',
optionsResource: 'account'
},
cost_method: {
label: 'Cost method',
column: 'cost_method',
options: [{
key: 'FIFO', label: 'First-in first-out (FIFO)',
key: 'LIFO', label: 'Last-in first-out (LIFO)',
key: 'average', label: 'Average rate',
}],
columnType: 'string',
},
created_at: {
label: 'Created at',
column: 'created_at',
columnType: 'date',
},
};
}

View File

@@ -66,14 +66,17 @@ export default class ManualJournal extends TenantModel {
date: {
label: 'Date',
column: 'date',
columnType: 'date',
},
journal_number: {
label: 'Journal number',
column: 'journal_number',
columnType: 'string',
},
reference: {
label: 'Reference No.',
column: 'reference',
columnType: 'string',
},
status: {
label: 'Status',
@@ -82,10 +85,12 @@ export default class ManualJournal extends TenantModel {
amount: {
label: 'Amount',
column: 'amount',
columnType: 'number',
},
description: {
label: 'Description',
column: 'description',
columnType: 'string',
},
user: {
label: 'User',

View File

@@ -70,8 +70,6 @@ export default class DynamicListService implements IDynamicListService {
private validateRolesFieldsExistance(model: IModel, filterRoles: IFilterRole[]) {
const invalidFieldsKeys = validateFilterRolesFieldsExistance(model, filterRoles);
console.log(invalidFieldsKeys);
if (invalidFieldsKeys.length > 0) {
throw new ServiceError(ERRORS.FILTER_ROLES_FIELDS_NOT_FOUND);
}
@@ -86,8 +84,9 @@ export default class DynamicListService implements IDynamicListService {
required: true,
type: 'object',
properties: {
condition: { type: 'string' },
fieldKey: { required: true, type: 'string' },
value: { required: true, type: 'string' },
value: { required: true },
},
});
const invalidFields = filterRoles.filter((filterRole) => {
@@ -126,12 +125,16 @@ export default class DynamicListService implements IDynamicListService {
}
// Filter roles.
if (filter.filterRoles.length > 0) {
this.validateFilterRolesSchema(filter.filterRoles);
this.validateRolesFieldsExistance(model, filter.filterRoles);
const filterRoles = filter.filterRoles.map((filterRole, index) => ({
...filterRole,
index: index + 1,
}));
this.validateFilterRolesSchema(filterRoles);
this.validateRolesFieldsExistance(model, filterRoles);
// Validate the model resource fields.
const filterRoles = new DynamicFilterFilterRoles(filter.filterRoles);
dynamicFilter.setFilter(filterRoles);
const dynamicFilterRoles = new DynamicFilterFilterRoles(filterRoles);
dynamicFilter.setFilter(dynamicFilterRoles);
}
return dynamicFilter;
}

View File

@@ -1,6 +1,7 @@
import { Service, Inject } from 'typedi';
import { camelCase, upperFirst } from 'lodash';
import pluralize from 'pluralize';
import { buildFilter } from 'objection-filter';
import { IModel } from 'interfaces';
import {
getModelFields,
@@ -35,18 +36,42 @@ export default class ResourceService {
const fields = getModelFields(Model);
return fields.map((field) => ({
label: __(field.label, field.label),
label: __(field.label),
key: field.key,
dataType: field.columnType,
fieldType: field.fieldType,
...(field.options) ? {
options: field.options.map((option) => ({
...option, label: __(option.label),
})),
} : {},
...(field.optionsResource) ? {
optionsResource: field.optionsResource,
optionsKey: field.optionsKey,
optionsLabel: field.optionsLabel,
} : {},
}));
}
/**
* Should model be resource-able or throw service error.
* @param {IModel} model
*/
private shouldModelBeResourceable(model: IModel) {
if (!model.resourceable) {
throw new ServiceError(ERRORS.RESOURCE_MODEL_NOT_FOUND);
}
}
/**
* Retrieve resource fields from resource model name.
* @param {string} resourceName
*/
public getResourceFields(tenantId: number, modelName: string) {
const resourceModel = this.getResourceModel(tenantId, modelName);
this.shouldModelBeResourceable(resourceModel);
return this.getModelFields(tenantId, resourceModel);
}
@@ -63,9 +88,18 @@ export default class ResourceService {
if (!Models[modelName]) {
throw new ServiceError(ERRORS.RESOURCE_MODEL_NOT_FOUND);
}
if (!Models[modelName].resourceable) {
throw new ServiceError(ERRORS.RESOURCE_MODEL_NOT_FOUND);
}
return Models[modelName];
}
/**
* Retrieve resource data from the storage based on the given query.
* @param {number} tenantId
* @param {string} modelName
*/
public async getResourceData(tenantId: number, modelName: string, filter: any) {
const resourceModel = this.getResourceModel(tenantId, modelName);
this.shouldModelBeResourceable(resourceModel);
return buildFilter(resourceModel).build(filter);
}
}